Compare commits

..

49 Commits

Author SHA1 Message Date
diced cb2590aae5 feat(v4.5.2): version 2026-03-28 23:58:39 -07:00
diced 93ff18a120 fix: reformat routes to include catch-all 2026-03-28 23:57:51 -07:00
diced 4343f130fb fix: refine batch uploads 2026-03-28 23:36:52 -07:00
diced 5e9778d18a fix: mfa showing when disabled 2026-03-28 20:35:49 -07:00
diced 9bcccbc8aa fix: #1031 2026-03-28 19:41:02 -07:00
diced 00ddf86ea8 feat(v4.5.1): version 2026-03-27 23:13:22 -07:00
diced cc582f6d20 fix: clean up tampered settings scroller 2026-03-27 23:11:49 -07:00
diced 318b09feae fix: use query for file modals 2026-03-27 23:11:39 -07:00
diced d55e36375d fix: tags overflow 2026-03-27 23:11:25 -07:00
diced 40b917df30 refactor: put client stuff in lib/client 2026-03-27 13:58:35 -07:00
diced 053a50d1bc refactor: uploading files
may fix: #1019
2026-03-27 13:46:32 -07:00
diced 78b554cbe8 fix: #1021 2026-03-27 12:41:50 -07:00
diced bf7a4e92e3 chore: update packages 2026-03-25 20:56:18 -07:00
diced 3b56e7f1ce fix: maybe fix #1021 again 2026-03-24 15:52:20 -07:00
diced 1d91a008e1 fix: linting 2026-03-24 12:45:43 -07:00
diced ff1fc0eb75 fix: limit copying jpeg/webp qr codes 2026-03-24 12:43:48 -07:00
diced 430774082c fix: maybe fix #1021 2026-03-24 12:40:29 -07:00
Benjamin Jørgensen ef67fdd553 fix: encode password #1007 (#1023)
* Url Encode Password Query Parameter

Signed-off-by: Benjamin Jørgensen <me@benmi.me>
Signed-off-by: Benjamin Jørgensen <me@benmi.dev>

* Fix lint error

Signed-off-by: Benjamin Jørgensen <me@benmi.me>
Signed-off-by: Benjamin Jørgensen <me@benmi.dev>

* fix: use appendPassword

---------

Signed-off-by: Benjamin Jørgensen <me@benmi.me>
Signed-off-by: Benjamin Jørgensen <me@benmi.dev>
Co-authored-by: dicedtomato <git@diced.sh>
2026-03-24 12:27:29 -07:00
dicedtomato 73b5528586 Merge commit from fork 2026-03-24 12:19:02 -07:00
diced 3f65029464 fix: use title in folder page 2026-03-23 17:34:17 -07:00
diced 6da1719fda refactor: fix up DashboardFileType 2026-03-23 17:33:14 -07:00
diced 4d85b41ec3 feat: add qrcode gen to invites 2026-03-22 22:47:18 -07:00
diced ac5c0a1cb3 fix: table hydration error 2026-03-22 22:42:41 -07:00
diced eb22598f20 feat: add qrcode for urls (#812) 2026-03-22 22:37:54 -07:00
diced 7a4c29d9d4 fix: mutate folders on update 2026-03-22 21:58:55 -07:00
diced 255336d74f fix: version badge 2026-03-22 21:51:33 -07:00
diced dc625fc682 fix: add Domain setting (#1009) 2026-03-22 12:46:02 -07:00
diced d457cb8693 fix: don't render when not opened 2026-03-22 12:45:53 -07:00
diced 331c4b4a4e chore: update flake.nix 2026-03-21 22:39:56 -07:00
diced d8ca9dc9b5 feat(v4.5.0): version 2026-03-21 11:50:36 -07:00
diced 0eee082035 fix: #1017 2026-03-20 15:01:12 -07:00
diced 3e287e8ad7 feat: identify anon uploads
- anon uploads will be identifiable now
- still anonymous but will show that they were uploaded anonymously
- reworked table edit modal to support new fields and merge local
  state
2026-03-19 17:11:21 -07:00
diced 5ec471050e fix: #1018 2026-03-18 10:22:22 -07:00
dicedtomato 842dac2660 Merge commit from fork
* fix: advisory

* fix: typo
2026-03-16 13:13:06 -07:00
diced dee86aaa86 fix: once and for all fix #907 2026-03-10 21:37:26 -07:00
diced 13e3a58035 fix: #1014 2026-03-10 20:50:30 -07:00
diced f4382d5bd9 fix: #947 2026-03-10 15:55:31 -07:00
diced 8990801268 fix: build errors 2026-03-07 16:17:17 -08:00
diced 01b9c06513 fix: clean up stuff 2026-03-07 16:16:20 -08:00
diced fc180de616 fix: build errors 2026-03-07 14:30:36 -08:00
diced f907133d3a fix: mobile nav not closing on change 2026-03-07 14:25:22 -08:00
diced 9ae9734a3d feat: add reload page when cached modules 2026-03-07 14:14:14 -08:00
diced 770b5cf706 fix: validation errors 2026-03-07 14:14:06 -08:00
diced 56625c664d fix: lint 2026-03-03 23:54:28 -08:00
diced 056a19b946 feat: max files per upload (#991) 2026-03-03 23:52:53 -08:00
diced 281ab666c1 feat: add tags to api routes 2026-03-03 23:40:23 -08:00
dicedtomato 31df5341b5 Merge commit from fork 2026-03-03 22:19:32 -08:00
diced ec7024242f fix: remove use of union types in response for now 2026-03-03 21:30:59 -08:00
dicedtomato ef6e0e00a0 feat: response validation (#1012)
* feat: add response schemas (WIP, hella unstable!!)

* refactor: models to zod

* feat: descriptions for api routes

* fix: finish up api refactor

* refactor: generalized error codes

* fix: responses + add descriptions

* fix: more

* fix: lint

* fix: settings errors

* fix: add errors to spec
2026-03-03 16:32:50 -08:00
183 changed files with 3922 additions and 2674 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ Visit [the docs](https://zipline.diced.sh/docs/get-started/docker) for a more in
This is the recommended way to run Zipline:
```yml
```yaml
services:
postgresql:
image: postgres:16
Generated
+645 -39
View File
@@ -6,7 +6,8 @@
"devenv"
],
"flake-compat": [
"devenv"
"devenv",
"flake-compat"
],
"git-hooks": [
"devenv",
@@ -18,11 +19,11 @@
]
},
"locked": {
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"lastModified": 1767714506,
"narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
"owner": "cachix",
"repo": "cachix",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
"type": "github"
},
"original": {
@@ -32,22 +33,142 @@
"type": "github"
}
},
"cachix_2": {
"inputs": {
"devenv": [
"devenv",
"crate2nix"
],
"flake-compat": [
"devenv",
"crate2nix"
],
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1767714506,
"narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
"owner": "cachix",
"repo": "cachix",
"rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"cachix_3": {
"inputs": {
"devenv": [
"devenv",
"crate2nix",
"crate2nix_stable"
],
"flake-compat": [
"devenv",
"crate2nix",
"crate2nix_stable"
],
"git-hooks": "git-hooks_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1767714506,
"narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
"owner": "cachix",
"repo": "cachix",
"rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
},
"crate2nix": {
"inputs": {
"cachix": "cachix_2",
"crate2nix_stable": "crate2nix_stable",
"devshell": "devshell_2",
"flake-compat": "flake-compat_2",
"flake-parts": "flake-parts_2",
"nix-test-runner": "nix-test-runner_2",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"pre-commit-hooks": "pre-commit-hooks_2"
},
"locked": {
"lastModified": 1773440526,
"narHash": "sha256-OcX1MYqUdoalY3/vU67PEx8m6RvqGxX0LwKonjzXn7I=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "e697d3049c909580128caa856ab8eb709556a97b",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "crate2nix",
"type": "github"
}
},
"crate2nix_stable": {
"inputs": {
"cachix": "cachix_3",
"crate2nix_stable": [
"devenv",
"crate2nix",
"crate2nix_stable"
],
"devshell": "devshell",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"nix-test-runner": "nix-test-runner",
"nixpkgs": "nixpkgs_3",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1769627083,
"narHash": "sha256-SUuruvw1/moNzCZosHaa60QMTL+L9huWdsCBN6XZIic=",
"owner": "nix-community",
"repo": "crate2nix",
"rev": "7c33e664668faecf7655fa53861d7a80c9e464a2",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "0.15.0",
"repo": "crate2nix",
"type": "github"
}
},
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"crate2nix": "crate2nix",
"flake-compat": "flake-compat_3",
"flake-parts": "flake-parts_3",
"git-hooks": "git-hooks_3",
"nix": "nix",
"nixd": "nixd",
"nixpkgs": [
"nixpkgs"
]
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1753888869,
"narHash": "sha256-VRYrrUmvXnBzfzuJVoI3os1H/0l8cJQ2KnrrxWkTB3E=",
"lastModified": 1774134162,
"narHash": "sha256-pGjE0Agjnh8FmymDi3hiOy/pflcnbS8kpkfkL5/QKAc=",
"owner": "cachix",
"repo": "devenv",
"rev": "bdf26a4453eff6bae835f33d519a36f77e0ca257",
"rev": "b24c9b58457396a9a6fe275b87555ba6e8f0a5fb",
"type": "github"
},
"original": {
@@ -68,14 +189,87 @@
"url": "file:///dev/null"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"devenv",
"crate2nix",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768818222,
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide",
"repo": "devshell",
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"devshell_2": {
"inputs": {
"nixpkgs": [
"devenv",
"crate2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768818222,
"narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
"owner": "numtide",
"repo": "devshell",
"rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"revCount": 69,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_2": {
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"revCount": 69,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_3": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
@@ -88,16 +282,17 @@
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"crate2nix",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"lastModified": 1768135262,
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github"
},
"original": {
@@ -107,15 +302,58 @@
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"crate2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768135262,
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1772408722,
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_4": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1753121425,
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
"lastModified": 1772408722,
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
"type": "github"
},
"original": {
@@ -128,20 +366,82 @@
"inputs": {
"flake-compat": [
"devenv",
"crate2nix",
"cachix",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"crate2nix",
"cachix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1765404074,
"narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"git-hooks_2": {
"inputs": {
"flake-compat": [
"devenv",
"crate2nix",
"crate2nix_stable",
"cachix",
"flake-compat"
],
"gitignore": "gitignore_2",
"nixpkgs": [
"devenv",
"crate2nix",
"crate2nix_stable",
"cachix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1765404074,
"narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"git-hooks_3": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"gitignore": "gitignore_5",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1750779888,
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"lastModified": 1772893680,
"narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"rev": "8baab586afc9c9b57645a734c820e4ac0a604af9",
"type": "github"
},
"original": {
@@ -151,6 +451,102 @@
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"devenv",
"crate2nix",
"cachix",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"devenv",
"crate2nix",
"crate2nix_stable",
"cachix",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_3": {
"inputs": {
"nixpkgs": [
"devenv",
"crate2nix",
"crate2nix_stable",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_4": {
"inputs": {
"nixpkgs": [
"devenv",
"crate2nix",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_5": {
"inputs": {
"nixpkgs": [
"devenv",
@@ -178,7 +574,10 @@
"devenv",
"flake-compat"
],
"flake-parts": "flake-parts",
"flake-parts": [
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"devenv",
"git-hooks"
@@ -195,43 +594,101 @@
]
},
"locked": {
"lastModified": 1752773918,
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
"lastModified": 1774103430,
"narHash": "sha256-MRNVInSmvhKIg3y0UdogQJXe+omvKijGszFtYpd5r9k=",
"owner": "cachix",
"repo": "nix",
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
"rev": "e127c1c94cefe02d8ca4cca79ef66be4c527510e",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "devenv-2.30",
"ref": "devenv-2.32",
"repo": "nix",
"type": "github"
}
},
"nix-test-runner": {
"flake": false,
"locked": {
"lastModified": 1588761593,
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
"owner": "stoeffel",
"repo": "nix-test-runner",
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
"type": "github"
},
"original": {
"owner": "stoeffel",
"repo": "nix-test-runner",
"type": "github"
}
},
"nix-test-runner_2": {
"flake": false,
"locked": {
"lastModified": 1588761593,
"narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
"owner": "stoeffel",
"repo": "nix-test-runner",
"rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
"type": "github"
},
"original": {
"owner": "stoeffel",
"repo": "nix-test-runner",
"type": "github"
}
},
"nixd": {
"inputs": {
"flake-parts": [
"devenv",
"flake-parts"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1773634079,
"narHash": "sha256-49qb4QNMv77VOeEux+sMd0uBhPvvHgVc0r938Bulvbo=",
"owner": "nix-community",
"repo": "nixd",
"rev": "8ecf93d4d93745e05ea53534e8b94f5e9506e6bd",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixd",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1752827260,
"narHash": "sha256-noFjJbm/uWRcd2Lotr7ovedfhKVZT+LeJs9rU416lKQ=",
"owner": "nixos",
"lastModified": 1765186076,
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b527e89270879aaaf584c41f26b2796be634bc9d",
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
"type": "github"
},
"original": {
"owner": "nixos",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"rev": "b527e89270879aaaf584c41f26b2796be634bc9d",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1751159883,
"narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=",
"lastModified": 1772328832,
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab",
"rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742",
"type": "github"
},
"original": {
@@ -240,12 +697,161 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1765186076,
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1769433173,
"narHash": "sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1773964973,
"narHash": "sha256-NV/J+tTER0P5iJhUDL/8HO5MDjDceLQPRUYgdmy5wXw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "812b3986fd1568f7a858f97fcf425ad996ba7d25",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"rev": "812b3986fd1568f7a858f97fcf425ad996ba7d25",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"crate2nix",
"crate2nix_stable",
"flake-compat"
],
"gitignore": "gitignore_3",
"nixpkgs": [
"devenv",
"crate2nix",
"crate2nix_stable",
"nixpkgs"
]
},
"locked": {
"lastModified": 1769069492,
"narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"pre-commit-hooks_2": {
"inputs": {
"flake-compat": [
"devenv",
"crate2nix",
"flake-compat"
],
"gitignore": "gitignore_4",
"nixpkgs": [
"devenv",
"crate2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1769069492,
"narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"devenv-root": "devenv-root",
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs"
"flake-parts": "flake-parts_4",
"nixpkgs": "nixpkgs_4"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1773630837,
"narHash": "sha256-zJhgAGnbVKeBMJOb9ctZm4BGS/Rnrz+5lfSXTVah4HQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "f600ea449c7b5bb596fa1cf21c871cc5b9e31316",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"devenv",
"nixd",
"nixpkgs"
]
},
"locked": {
"lastModified": 1772660329,
"narHash": "sha256-IjU1FxYqm+VDe5qIOxoW+pISBlGvVApRjiw/Y/ttJzY=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "3710e0e1218041bbad640352a0440114b1e10428",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
+2 -8
View File
@@ -6,8 +6,8 @@
flake = false;
};
# node 24.4.1, postgres 17
nixpkgs.url = "github:nixos/nixpkgs/b527e89270879aaaf584c41f26b2796be634bc9d";
# node 24.14, postgres 17
nixpkgs.url = "github:nixos/nixpkgs/812b3986fd1568f7a858f97fcf425ad996ba7d25";
flake-parts.url = "github:hercules-ci/flake-parts";
devenv.url = "github:cachix/devenv";
@@ -58,7 +58,6 @@
ffmpeg
# for testing docker
colima
docker
docker-compose
];
@@ -75,11 +74,6 @@
downall.exec = ''
process-compose down
'';
# ensure that volumes are mounted with write access for docker containers
start_colima.exec = ''
colima start --mount $PWD/themes:w --mount $PWD/uploads:w --mount $PWD/public:w
'';
};
enterShell = ''
+44 -42
View File
@@ -2,7 +2,7 @@
"name": "zipline",
"private": true,
"license": "MIT",
"version": "4.4.2",
"version": "4.5.2",
"scripts": {
"build": "tsx scripts/build.ts",
"dev": "cross-env NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
@@ -28,83 +28,85 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@fastify/multipart": "^9.3.0",
"@fastify/cors": "^11.2.0",
"@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0",
"@fastify/sensible": "^6.0.4",
"@fastify/static": "^8.3.0",
"@fastify/swagger": "^9.6.1",
"@mantine/charts": "^8.3.9",
"@mantine/code-highlight": "^8.3.9",
"@mantine/core": "^8.3.9",
"@mantine/dates": "^8.3.9",
"@mantine/dropzone": "^8.3.9",
"@mantine/form": "^8.3.9",
"@mantine/hooks": "^8.3.9",
"@mantine/modals": "^8.3.9",
"@mantine/notifications": "^8.3.9",
"@fastify/static": "^9.0.0",
"@fastify/swagger": "^9.7.0",
"@mantine/charts": "^8.3.18",
"@mantine/code-highlight": "^8.3.18",
"@mantine/core": "^8.3.18",
"@mantine/dates": "^8.3.18",
"@mantine/dropzone": "^8.3.18",
"@mantine/form": "^8.3.18",
"@mantine/hooks": "^8.3.18",
"@mantine/modals": "^8.3.18",
"@mantine/notifications": "^8.3.18",
"@prisma/adapter-pg": "6.13.0",
"@prisma/client": "6.13.0",
"@prisma/engines": "6.13.0",
"@prisma/internals": "6.13.0",
"@prisma/migrate": "6.13.0",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.2",
"@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.0",
"@smithy/node-http-handler": "^4.1.1",
"@tabler/icons-react": "^3.35.0",
"@tabler/icons-react": "^3.40.0",
"archiver": "^7.0.1",
"argon2": "^0.44.0",
"asciinema-player": "^3.12.1",
"asciinema-player": "^3.15.1",
"bytes": "^3.1.2",
"clsx": "^2.1.1",
"colorette": "^2.0.20",
"commander": "^14.0.2",
"commander": "^14.0.3",
"cookie": "^1.1.1",
"cross-env": "^10.1.0",
"dayjs": "^1.11.19",
"dayjs": "^1.11.20",
"detect-browser": "^5.3.0",
"dotenv": "^17.2.3",
"dotenv": "^17.3.1",
"fast-glob": "^3.3.3",
"fastify": "^5.6.2",
"fastify": "^5.8.4",
"fastify-plugin": "^5.1.0",
"fastify-type-provider-zod": "^6.1.0",
"fluent-ffmpeg": "^2.1.3",
"he": "^1.2.0",
"highlight.js": "^11.11.1",
"iron-session": "^8.0.4",
"isomorphic-dompurify": "^2.33.0",
"katex": "^0.16.27",
"mantine-datatable": "^8.3.9",
"isomorphic-dompurify": "^3.7.1",
"katex": "^0.16.42",
"mantine-datatable": "^8.3.13",
"ms": "^2.1.3",
"multer": "2.0.2",
"otplib": "^12.0.1",
"multer": "2.1.1",
"otplib": "^13.4.0",
"prisma": "6.13.0",
"qrcode": "^1.5.4",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.10.1",
"react-router-dom": "^7.13.2",
"react-window": "1.8.11",
"remark-gfm": "^4.0.1",
"sharp": "^0.34.5",
"swr": "^2.3.7",
"typescript-eslint": "^8.48.1",
"vite": "^7.2.7",
"zod": "^4.1.13",
"zustand": "^5.0.9"
"swr": "^2.4.1",
"typescript-eslint": "^8.57.2",
"vite": "^8.0.2",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@types/archiver": "^7.0.0",
"@types/bytes": "^3.1.5",
"@types/fluent-ffmpeg": "^2.1.28",
"@types/katex": "^0.16.7",
"@types/he": "^1.2.3",
"@types/katex": "^0.16.8",
"@types/ms": "^2.1.0",
"@types/multer": "^2.0.0",
"@types/multer": "^2.1.0",
"@types/node": "^24.10.1",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.7",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-window": "^1.8.8",
"@vitejs/plugin-react": "^5.1.1",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
@@ -113,11 +115,11 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-unused-imports": "^4.3.0",
"postcss": "^8.5.6",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.7.4",
"sass": "^1.94.2",
"prettier": "^3.8.1",
"sass": "^1.98.0",
"tsc-alias": "^1.8.16",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
+1591 -1553
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."Zipline" ADD COLUMN "filesMaxFilesPerUpload" INTEGER NOT NULL DEFAULT 1000;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."File" ADD COLUMN "anonymous" BOOLEAN NOT NULL DEFAULT false;
+2
View File
@@ -46,6 +46,7 @@ model Zipline {
filesRandomWordsNumAdjectives Int @default(2)
filesRandomWordsSeparator String @default("-")
filesDefaultCompressionFormat String? @default("jpg")
filesMaxFilesPerUpload Int @default(1000)
urlsRoute String @default("/go")
urlsLength Int @default(6)
@@ -282,6 +283,7 @@ model File {
maxViews Int?
favorite Boolean @default(false)
password String?
anonymous Boolean @default(false)
tags Tag[]
@@ -1,6 +1,13 @@
import { useRouteError } from 'react-router-dom';
import GenericError from './GenericError';
import ReloadPage from './ReloadPage';
export default function DashboardErrorBoundary(props: Record<string, any>) {
const error = useRouteError();
if (error instanceof Error && error.message.startsWith('Failed to fetch dynamically imported module:')) {
return <ReloadPage />;
}
return (
<GenericError
title='Dashboard Client Error'
+37
View File
@@ -0,0 +1,37 @@
import { Button, Collapse, Container, Text, Title } from '@mantine/core';
import { IconReload } from '@tabler/icons-react';
import GenericError from './GenericError';
import { useState } from 'react';
export default function ReloadPage() {
const [view, setView] = useState(false);
return (
<Container my='lg'>
<Title order={3}>Update available</Title>
<Text size='lg'>A new version of the app is available. Please reload the page to update.</Text>
<Button
leftSection={<IconReload size='1rem' />}
mr='sm'
mt='md'
onClick={() => window.location.reload()}
>
Reload Page
</Button>
<Button variant='subtle' mt='md' onClick={() => setView((v) => !v)}>
Why am I seeing this?
</Button>
<Collapse in={view}>
<GenericError
title='Failed to fetch dynamically imported module'
message='This error can occur when a new version of the app is deployed while you have the page open. Please reload the page to update to the latest version.'
details={{}}
/>
</Collapse>
</Container>
);
}
+11 -11
View File
@@ -1,14 +1,14 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="manifest.json" />
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="manifest" href="manifest.json">
<title>Zipline</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
<title>Zipline</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
+1 -1
View File
@@ -1,4 +1,4 @@
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
import { Button, Center, Stack, Text, Title } from '@mantine/core';
import { IconArrowLeft } from '@tabler/icons-react';
import { Link } from 'react-router-dom';
+3 -3
View File
@@ -6,9 +6,9 @@ import TotpModal from '@/components/pages/login/TotpModal';
import { getWebClient } from '@/lib/api/detect';
import { ApiError } from '@/lib/api/errors';
import { fetchApi } from '@/lib/fetchApi';
import useLogin from '@/lib/hooks/useLogin';
import useObjectState from '@/lib/hooks/useObjectState';
import { useTitle } from '@/lib/hooks/useTitle';
import useLogin from '@/lib/client/hooks/useLogin';
import useObjectState from '@/lib/client/hooks/useObjectState';
import { useTitle } from '@/lib/client/hooks/useTitle';
import {
Anchor,
Box,
+1 -1
View File
@@ -1,6 +1,6 @@
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
import {
Button,
Center,
+1 -1
View File
@@ -1,6 +1,6 @@
import { type Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
import {
Anchor,
Button,
+1 -1
View File
@@ -3,7 +3,7 @@ import { Response } from '@/lib/api/response';
import { Container, LoadingOverlay } from '@mantine/core';
import useSWR from 'swr';
import GenericError from '../../error/GenericError';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Terms of Service');
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardServerActions from '@/components/pages/serverActions';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Server Actions');
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardInvites from '@/components/pages/invites';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Invites');
@@ -1,5 +1,5 @@
import DashboardServerSettings from '@/components/pages/serverSettings';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Server Settings');
@@ -1,5 +1,5 @@
import ViewUserFiles from '@/components/pages/users/ViewUserFiles';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
import { Params, redirect, useLoaderData } from 'react-router-dom';
export async function loader({ params }: { params: Params<string> }) {
@@ -1,5 +1,5 @@
import DashboardUsers from '@/components/pages/users';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Users');
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardFiles from '@/components/pages/files';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Files');
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardFolders from '@/components/pages/folders';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Folders');
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardHome from '@/components/pages/dashboard';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle();
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardMetrics from '@/components/pages/metrics';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
import { isAdministrator } from '@/lib/role';
import { redirect } from 'react-router-dom';
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardSettings from '@/components/pages/settings';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Settings');
+1 -1
View File
@@ -1,5 +1,5 @@
import UploadFile from '@/components/pages/upload/File';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Upload File');
+1 -1
View File
@@ -1,5 +1,5 @@
import UploadText from '@/components/pages/upload/Text';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('Upload Text');
+1 -1
View File
@@ -1,5 +1,5 @@
import DashboardURLs from '@/components/pages/urls';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
export function Component() {
useTitle('URLs');
+4 -1
View File
@@ -1,6 +1,7 @@
import { type Response } from '@/lib/api/response';
import { Folder } from '@/lib/db/models/folder';
import { FolderBreadcrumb } from '@/lib/folderHierarchy';
import { useTitle } from '@/lib/client/hooks/useTitle';
import {
ActionIcon,
Anchor,
@@ -61,6 +62,8 @@ export function Component() {
const { folder } = useLoaderData<typeof loader>();
const navigate = useNavigate();
useTitle(folder.name);
const buildBreadcrumbs = () => {
const items: FolderBreadcrumb[] = [];
@@ -100,7 +103,7 @@ export function Component() {
<Title order={1}>{folder.name}</Title>
{folder.allowUploads && (
<Link to={`/folder/${folder.id}/upload`}>
<Link to={`/folder/${folder.id}/upload`} reloadDocument>
<ActionIcon variant='outline'>
<IconUpload size='1rem' />
</ActionIcon>
+4 -6
View File
@@ -2,16 +2,14 @@ import ConfigProvider from '@/components/ConfigProvider';
import UploadFile from '@/components/pages/upload/File';
import { type Response } from '@/lib/api/response';
import { SafeConfig } from '@/lib/config/safe';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
import { Anchor, Center, Container, Text } from '@mantine/core';
import { data, Link, Params, useLoaderData } from 'react-router-dom';
import useSWR from 'swr';
export async function loader({ params }: { params: Params<string> }) {
const res = await fetch(`/api/server/folder/${params.id}?upload=true`);
if (!res.ok) {
throw data('Folder not found', { status: 404 });
}
const res = await fetch(`/api/server/folder/${params.id}`);
if (!res.ok) throw data('Folder not found', { status: 404 });
return {
folder: (await res.json()) as Response['/api/server/folder/[id]'],
@@ -40,7 +38,7 @@ export function Component() {
{folder.public ? (
<>
This folder is{' '}
<Anchor component={Link} to={`/folder/${folder.id}`}>
<Anchor component={Link} to={`/folder/${folder.id}`} reloadDocument>
public
</Anchor>
. Anyone with the link can view its contents and upload files.
+11 -5
View File
@@ -26,7 +26,7 @@ import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useSsrData } from '../../../components/ZiplineSSRProvider';
import { getFile } from '../../ssr-view/server';
import { useTitle } from '@/lib/hooks/useTitle';
import { useTitle } from '@/lib/client/hooks/useTitle';
type SsrData = {
file: Partial<NonNullable<Awaited<ReturnType<typeof getFile>>>>;
@@ -112,7 +112,7 @@ export default function ViewFileId() {
size='md'
variant='outline'
component={Link}
to={`/raw/${file.name}?download=true${pw ? `&pw=${pw}` : ''}`}
to={`/raw/${file.name}?download=true${pw ? `&pw=${encodeURIComponent(pw)}` : ''}`}
target='_blank'
>
<IconDownload size='1rem' />
@@ -180,7 +180,13 @@ export default function ViewFileId() {
file.Folder &&
(file.Folder.public ? (
<Tooltip label='View folder'>
<Anchor component={Link} ml='sm' to={`/folder/${file.Folder.id}`}>
<Anchor
component={Link}
ml='sm'
to={`/folder/${file.Folder.id}`}
target='_blank'
reloadDocument
>
{file.Folder.name}
</Anchor>
</Tooltip>
@@ -202,7 +208,7 @@ export default function ViewFileId() {
size='md'
variant='outline'
component={Link}
to={`/raw/${file.name}${pw ? `?pw=${pw}` : ''}`}
to={`/raw/${file.name}${pw ? `?pw=${encodeURIComponent(pw)}` : ''}`}
target='_blank'
>
<IconExternalLink size='1rem' />
@@ -213,7 +219,7 @@ export default function ViewFileId() {
size='md'
variant='outline'
component={Link}
to={`/raw/${file.name}?download=true${pw ? `&pw=${pw}` : ''}`}
to={`/raw/${file.name}?download=true${pw ? `&pw=${encodeURIComponent(pw)}` : ''}`}
target='_blank'
>
<IconDownload size='1rem' />
+22 -28
View File
@@ -8,6 +8,11 @@ import FourOhFour from './pages/404';
import Login from './pages/auth/login';
import Root from './Root';
const fourOhFourCatchall = {
path: '*',
Component: FourOhFour,
};
export async function dashboardLoader() {
try {
const res = await fetch('/api/server/settings/web');
@@ -28,21 +33,21 @@ export const router = createBrowserRouter([
{
Component: Root,
path: '/',
HydrateFallback: () => null,
children: [
{
ErrorBoundary: RootErrorBoundary,
children: [
{ path: '*', Component: FourOhFour },
fourOhFourCatchall,
{
path: '/auth',
children: [
{ path: 'login', Component: Login },
{ path: 'register', lazy: () => import('./pages/auth/register') },
{ path: 'auth/login', Component: Login },
{ path: 'auth/register', lazy: () => import('./pages/auth/register') },
{
path: 'setup',
path: 'auth/setup',
lazy: () => import('./pages/auth/setup'),
},
{ path: 'tos', lazy: () => import('./pages/auth/tos') },
{ path: 'auth/tos', lazy: () => import('./pages/auth/tos') },
],
},
{
@@ -59,37 +64,26 @@ export const router = createBrowserRouter([
{ path: 'files', lazy: () => import('./pages/dashboard/files') },
{ path: 'folders/*', lazy: () => import('./pages/dashboard/folders') },
{ path: 'urls', lazy: () => import('./pages/dashboard/urls') },
{ path: 'upload/file', lazy: () => import('./pages/dashboard/upload/file') },
{ path: 'upload/text', lazy: () => import('./pages/dashboard/upload/text') },
// admin routes
{
path: 'upload',
children: [
{ path: 'file', lazy: () => import('./pages/dashboard/upload/file') },
{ path: 'text', lazy: () => import('./pages/dashboard/upload/text') },
],
},
{
path: 'admin',
loader: async () => {
const res = await fetch('/api/user');
if (!res.ok) {
return redirect('/auth/login');
}
if (!res.ok) return redirect('/auth/login');
const { user } = await res.json();
if (!isAdministrator(user.role)) return redirect('/dashboard');
},
children: [
{ path: 'invites', lazy: () => import('./pages/dashboard/admin/invites') },
{ path: 'settings', lazy: () => import('./pages/dashboard/admin/settings') },
{ path: 'actions', lazy: () => import('./pages/dashboard/admin/actions') },
{ path: 'admin/invites', lazy: () => import('./pages/dashboard/admin/invites') },
{ path: 'admin/settings', lazy: () => import('./pages/dashboard/admin/settings') },
{ path: 'admin/actions', lazy: () => import('./pages/dashboard/admin/actions') },
{ path: 'admin/users', lazy: () => import('./pages/dashboard/admin/users') },
{
path: 'users',
children: [
{ index: true, lazy: () => import('./pages/dashboard/admin/users') },
{
path: ':id/files',
lazy: () => import('./pages/dashboard/admin/users/[id]/files'),
},
],
path: 'admin/users/:id/files',
lazy: () => import('./pages/dashboard/admin/users/[id]/files'),
},
],
},
+32 -27
View File
@@ -22,6 +22,7 @@ import { FastifyRequest } from 'fastify';
import { renderToString } from 'react-dom/server';
import { createStaticHandler, createStaticRouter, StaticRouterProvider } from 'react-router-dom';
import { createRoutes } from './routes';
import { stripHtml } from '@/lib/stripHtml';
export const getFile = async (id: string) =>
prisma.file.findFirst({
@@ -166,49 +167,53 @@ export async function render(
const router = createStaticRouter(routes, context);
const html = renderToString(<StaticRouterProvider context={context} router={router} />);
const safeFilename = stripHtml(file.name);
const safeOriginalName = stripHtml(file.originalName || '');
const safeType = stripHtml(file.type || '');
const meta = `
${
user?.view?.embedTitle && user.view.embed
? `<meta property="og:title" content="${
? `<meta property="og:title" content="${stripHtml(
parseString(user.view.embedTitle, {
file: file as unknown as File,
user: user as User,
...metrics,
}) ?? ''
}" />`
}) ?? '',
)}" />`
: ''
}
${
user?.view?.embedDescription && user.view.embed
? `<meta property="og:description" content="${
? `<meta property="og:description" content="${stripHtml(
parseString(user.view.embedDescription, {
file: file as unknown as File,
user: user as User,
...metrics,
}) ?? ''
}" />`
}) ?? '',
)}" />`
: ''
}
${
user?.view?.embedSiteName && user.view.embed
? `<meta property="og:site_name" content="${
? `<meta property="og:site_name" content="${stripHtml(
parseString(user.view.embedSiteName, {
file: file as unknown as File,
user: user as User,
...metrics,
}) ?? ''
}" />`
}) ?? '',
)}" />`
: ''
}
${
user?.view?.embedColor && user.view.embed
? `<meta property="theme-color" content="${
? `<meta property="theme-color" content="${stripHtml(
parseString(user.view.embedColor, {
file: file as unknown as File,
user: user as User,
...metrics,
}) ?? ''
}" />`
}) ?? '',
)}" />`
: ''
}
@@ -216,11 +221,11 @@ export async function render(
file.type?.startsWith('image')
? `
<meta property="og:type" content="image" />
<meta property="og:image" itemProp="image" content="${host}/raw/${file.name}" />
<meta property="og:url" content="${host}/raw/${file.name}" />
<meta property="og:image" itemProp="image" content="${host}/raw/${safeFilename}" />
<meta property="og:url" content="${host}/raw/${safeFilename}" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="${host}/raw/${file.name}" />
<meta property="twitter:title" content="${file.name}" />
<meta property="twitter:image" content="${host}/raw/${safeFilename}" />
<meta property="twitter:title" content="${safeFilename}" />
`
: ''
}
@@ -230,7 +235,7 @@ export async function render(
? `
${file.thumbnail ? `<meta property="og:image" content="${host}/raw/${file.thumbnail.path}" />` : ''}
<meta property="og:type" content="video.other" />
<meta property="og:video:url" content="${host}/raw/${file.name}" />
<meta property="og:video:url" content="${host}/raw/${safeFilename}" />
<meta property="og:video:width" content="1920" />
<meta property="og:video:height" content="1080" />
`
@@ -241,18 +246,18 @@ export async function render(
file.type?.startsWith('audio')
? `
<meta name="twitter:card" content="player" />
<meta name="twitter:player" content="${host}/raw/${file.name}" />
<meta name="twitter:player:stream" content="${host}/raw/${file.name}" />
<meta name="twitter:player:stream:content_type" content="${file.type}" />
<meta name="twitter:title" content="${file.name}" />
<meta name="twitter:player" content="${host}/raw/${safeFilename}" />
<meta name="twitter:player:stream" content="${host}/raw/${safeFilename}" />
<meta name="twitter:player:stream:content_type" content="${safeType}" />
<meta name="twitter:title" content="${safeFilename}" />
<meta name="twitter:player:width" content="720" />
<meta name="twitter:player:height" content="480" />
<meta property="og:type" content="music.song" />
<meta property="og:url" content="${host}/raw/${file.name}" />
<meta property="og:audio" content="${host}/raw/${file.name}" />
<meta property="og:audio:secure_url" content="${host}/raw/${file.name}" />
<meta property="og:audio:type" content="${file.type}" />
<meta property="og:url" content="${host}/raw/${safeFilename}" />
<meta property="og:audio" content="${host}/raw/${safeFilename}" />
<meta property="og:audio:secure_url" content="${host}/raw/${safeFilename}" />
<meta property="og:audio:type" content="${safeType}" />
`
: ''
}
@@ -260,12 +265,12 @@ export async function render(
${
!file.type?.startsWith('video') && !file.type?.startsWith('image')
? `
<meta property="og:url" content="${host}/raw/${file.name}" />
<meta property="og:url" content="${host}/raw/${safeFilename}" />
`
: ''
}
<title>${file.originalName ?? file.name}</title>
<title>${file.originalName ? safeOriginalName : safeFilename}</title>
`;
return {
+1 -1
View File
@@ -1,4 +1,4 @@
import { ViewStore, ViewType, useViewStore } from '@/lib/store/view';
import { ViewStore, ViewType, useViewStore } from '@/lib/client/store/view';
import { Center, SegmentedControl } from '@mantine/core';
import { IconLayoutGrid, IconLayoutList } from '@tabler/icons-react';
import { useShallow } from 'zustand/shallow';
+14 -8
View File
@@ -1,11 +1,11 @@
import type { Response } from '@/lib/api/response';
import useAvatar from '@/lib/client/hooks/useAvatar';
import useLogin from '@/lib/client/hooks/useLogin';
import { useLogout } from '@/lib/client/hooks/useLogout';
import { useUserStore } from '@/lib/client/store/user';
import type { SafeConfig } from '@/lib/config/safe';
import { fetchApi } from '@/lib/fetchApi';
import useAvatar from '@/lib/hooks/useAvatar';
import useLogin from '@/lib/hooks/useLogin';
import { Outlet, useLocation } from 'react-router-dom';
import { isAdministrator } from '@/lib/role';
import { useUserStore } from '@/lib/store/user';
import {
AppShell,
Avatar,
@@ -47,11 +47,10 @@ import {
IconUsersGroup,
} from '@tabler/icons-react';
import { useState } from 'react';
import { Link, Outlet, useLoaderData, useLocation } from 'react-router-dom';
import { dashboardLoader } from '../client/routes';
import ConfigProvider from './ConfigProvider';
import VersionBadge from './VersionBadge';
import { Link, useLoaderData } from 'react-router-dom';
import { dashboardLoader } from '../client/routes';
import { useLogout } from '@/lib/hooks/useLogout';
type NavLinks = {
label: string;
@@ -167,6 +166,12 @@ export default function Layout() {
const { user, mutate } = useLogin();
const { avatar } = useAvatar();
const [prev, setPrev] = useState(location.pathname);
if (prev !== location.pathname) {
setPrev(location.pathname);
setOpened(false);
}
const copyToken = () => {
modals.openConfirmModal({
title: 'Copy token?',
@@ -241,6 +246,7 @@ export default function Layout() {
color={theme.colors.gray[6]}
mr='xl'
hiddenFrom='sm'
bdrs='md'
/>
{config.website.titleLogo && (
@@ -395,7 +401,7 @@ export default function Layout() {
<AppShell.Main>
<ConfigProvider data={loaderData}>
<Paper m='lg' withBorder p='xs'>
<Paper withBorder m='md' p='xs' radius='md'>
<Outlet />
</Paper>
</ConfigProvider>
+118
View File
@@ -0,0 +1,118 @@
import { getDomain } from '@/lib/client/webDomain';
import { Button, Group, Image, Modal, Select, Text, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconClipboardCheck, IconClipboardX, IconCopy, IconDownload } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
type Type = 'image/png' | 'image/jpeg' | 'image/webp';
const UNSUPPORTED_COPY = ['image/jpeg', 'image/webp'];
export default function QRCodeModal({
opened,
onClose,
url,
}: {
opened: boolean;
onClose: () => void;
url: string;
}) {
const [dataUrl, setDataUrl] = useState<string | null>(null);
const [type, setType] = useState<Type>('image/png');
useEffect(() => {
if (!opened) return;
import('qrcode')
.then((QRCode) => QRCode.toDataURL(getDomain(url), { width: 500, type }))
.then(setDataUrl)
.catch(() => setDataUrl(null));
}, [opened, url, type]);
const copyImageToClipboard = async () => {
if (!dataUrl) return;
try {
const response = await fetch(dataUrl);
const blob = await response.blob();
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
showNotification({
message: 'QR code image copied to clipboard',
color: 'green',
icon: <IconClipboardCheck size='1rem' />,
});
} catch (error) {
showNotification({
title: 'Failed to copy QR code image',
message: error instanceof Error ? error.message : String(error),
color: 'red',
icon: <IconClipboardX size='1rem' />,
});
}
};
const downloadImage = () => {
if (!dataUrl) return;
const link = document.createElement('a');
link.href = dataUrl;
link.style.display = 'none';
link.download = `qr-code.${type.split('/')[1]}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<Modal title='QR Code' opened={opened} onClose={onClose} size='sm' centered>
{dataUrl ? (
<Image src={dataUrl} alt='QR Code' />
) : (
<Text c='red' ta='center'>
Failed to generate QR code.
</Text>
)}
<Select
mt='md'
label='Format'
value={type}
onChange={(value) => setType(value as Type)}
data={[
{ value: 'image/png', label: 'png' },
{ value: 'image/jpeg', label: 'jpeg' },
{ value: 'image/webp', label: 'webp' },
]}
size='xs'
/>
{dataUrl && (
<Group gap='xs' mt='md' grow>
<Tooltip
label={
UNSUPPORTED_COPY.includes(type)
? 'Copying this format is not supported in some browsers. You can copy the image normally via right-click or holding it.'
: ''
}
hidden={!UNSUPPORTED_COPY.includes(type)}
>
<Button
onClick={copyImageToClipboard}
leftSection={<IconCopy size='1rem' />}
disabled={UNSUPPORTED_COPY.includes(type)}
>
Copy Image
</Button>
</Tooltip>
<Button onClick={downloadImage} leftSection={<IconDownload size='1rem' />}>
Download
</Button>
</Group>
)}
</Modal>
);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { Response } from '@/lib/api/response';
import { Config } from '@/lib/config/validate';
import { useSettingsStore } from '@/lib/store/settings';
import { useUserStore } from '@/lib/store/user';
import { useSettingsStore } from '@/lib/client/store/settings';
import { useUserStore } from '@/lib/client/store/user';
import { ZiplineTheme, findTheme, themeComponents } from '@/lib/theme';
import dark_blue from '@/lib/theme/builtins/dark_blue';
import { MantineProvider, createTheme } from '@mantine/core';
+14 -5
View File
@@ -1,4 +1,4 @@
import useVersion from '@/lib/hooks/useVersion';
import useVersion from '@/lib/client/hooks/useVersion';
import {
Anchor,
Badge,
@@ -14,7 +14,11 @@ import {
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function DataDisplay({ items }: { items: { label: string; value: string; href?: string }[] }) {
function DataDisplay({
items,
}: {
items: { label: string; value: string; href?: string; color?: string }[];
}) {
return (
<Paper withBorder p='sm'>
<Stack gap='xs'>
@@ -29,7 +33,7 @@ function DataDisplay({ items }: { items: { label: string; value: string; href?:
{item.value}
</Anchor>
) : (
<Text>{item.value}</Text>
<Text c={item.color ?? undefined}>{item.value}</Text>
)}
</Flex>
))}
@@ -105,10 +109,14 @@ export default function VersionBadge() {
},
{
label: 'Commit',
value: version.version.sha!,
value: version.version.sha!.slice(0, 7)!,
href: `https://github.com/diced/zipline/commit/${version.version.sha}`,
},
{ label: 'Upstream?', value: version.isUpstream ? 'Yes' : 'No' },
{
label: 'Upstream?',
value: version.isUpstream ? 'Yes' : 'No',
color: version.isUpstream ? 'orange' : 'green',
},
]}
/>
@@ -131,6 +139,7 @@ export default function VersionBadge() {
{
label: 'Available to update',
value: version.latest.commit.pull ? 'Yes' : 'No',
color: version.latest.commit.pull ? 'green' : 'red',
},
]}
/>
@@ -1,6 +1,6 @@
import { File } from '@/lib/db/models/file';
import { fetchApi } from '@/lib/fetchApi';
import useObjectState from '@/lib/hooks/useObjectState';
import useObjectState from '@/lib/client/hooks/useObjectState';
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconEye, IconKey, IconPencil, IconPencilOff, IconTrashFilled } from '@tabler/icons-react';
@@ -6,8 +6,8 @@ import { File } from '@/lib/db/models/file';
import { Tag } from '@/lib/db/models/tag';
import { fetchApi } from '@/lib/fetchApi';
import { buildFolderHierarchy } from '@/lib/folderHierarchy';
import { useFolders } from '@/lib/hooks/useFolders';
import { useSettingsStore } from '@/lib/store/settings';
import { useFolders } from '@/lib/client/hooks/useFolders';
import { useSettingsStore } from '@/lib/client/store/settings';
import {
ActionIcon,
Box,
@@ -48,6 +48,7 @@ import {
IconTextRecognition,
IconTrashFilled,
IconUpload,
IconUserQuestion,
} from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import useSWR, { mutate } from 'swr';
@@ -229,6 +230,7 @@ export default function FileModal({
{file.originalName && (
<FileStat Icon={IconTextRecognition} title='Original Name' value={file.originalName} />
)}
{file.anonymous && <FileStat Icon={IconUserQuestion} title='Anonymous' value='Yes' />}
</SimpleGrid>
{!reduce && (
@@ -237,12 +239,7 @@ export default function FileModal({
<Title order={4} mt='lg' mb='xs'>
Tags
</Title>
<Combobox
zIndex={90000}
withinPortal={false}
store={tagsCombobox}
onOptionSubmit={handleValueSelect}
>
<Combobox zIndex={90000} store={tagsCombobox} onOptionSubmit={handleValueSelect}>
<Combobox.DropdownTarget>
<PillsInput
onBlur={() => triggerSave()}
@@ -318,7 +315,7 @@ export default function FileModal({
</Button>
) : (
<Combobox
withinPortal={false}
zIndex={90000}
store={folderCombobox}
onOptionSubmit={(value) => handleAdd(value)}
>
@@ -349,6 +346,12 @@ export default function FileModal({
</Combobox.Target>
<Combobox.Dropdown>
{folders?.length === 0 && (
<Combobox.Empty>
You have no folders. Start typing to create a new folder for this file.
</Combobox.Empty>
)}
<FolderComboboxOptions
folderOptions={folderOptions}
searchValue={search}
+83 -64
View File
@@ -1,5 +1,6 @@
import { useSettingsStore } from '@/lib/client/store/settings';
import { useUserStore } from '@/lib/client/store/user';
import type { File as DbFile } from '@/lib/db/models/file';
import { useSettingsStore } from '@/lib/store/settings';
import {
Box,
Center,
@@ -11,13 +12,23 @@ import {
Text,
} from '@mantine/core';
import { Icon, IconFileUnknown, IconPlayerPlay, IconShieldLockFilled } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { renderMode } from '../pages/upload/renderMode';
import { useCallback, useEffect, useState } from 'react';
import Asciinema from '../render/Asciinema';
import Pdf from '../render/Pdf';
import Render from '../render/Render';
import { renderMode } from '../render/renderMode';
import fileIcon from './fileIcon';
import { useUserStore } from '@/lib/store/user';
const MAX_BYTES = 1 * 1024 * 1024;
const FILE_BIG = '\n...\nThe file is too big to display click the download icon to view/download it.';
function appendPassword(url: string, password?: string | null) {
return `${url}${password ? `?pw=${encodeURIComponent(password)}` : ''}`;
}
function isDbFile(file: DbFile | File): file is DbFile {
return typeof globalThis.File !== 'undefined' ? !(file instanceof globalThis.File) : 'thumbnail' in file;
}
function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
return (
@@ -82,16 +93,37 @@ export default function DashboardFileType({
}) {
const user = useUserStore((state) => state.user);
const disableMediaPreview = useSettingsStore((state) => state.settings.disableMediaPreview);
const fileRoute = user ? `/api/user/files/${(file as DbFile).id}/raw` : `/raw/${file.name}`;
const thumbnailRoute = user
? `/api/user/files/${(file as DbFile).thumbnail?.path}/raw`
: `/raw/${(file as DbFile).thumbnail?.path}`;
const dbFile = 'id' in file;
const renderIn = useMemo(() => renderMode(file.name.split('.').pop() || ''), [file.name]);
const dbFile = isDbFile(file);
const fileRoute = dbFile ? (user ? `/api/user/files/${file.id}/raw` : `/raw/${file.name}`) : '';
const thumbnailRoute = dbFile
? file.thumbnail?.path
? user
? `/api/user/files/${file.thumbnail.path}/raw`
: `/raw/${file.thumbnail.path}`
: null
: null;
const dbFileUrl = dbFile ? appendPassword(fileRoute, password) : '';
const [blobUrl, setBlobUrl] = useState('');
useEffect(() => {
if (dbFile) return setBlobUrl('');
const objectUrl = URL.createObjectURL(file);
setBlobUrl(objectUrl);
return () => URL.revokeObjectURL(objectUrl);
}, [dbFile, file]);
const fileUrl = dbFile ? dbFileUrl : blobUrl;
const extension = file.name.split('.').pop() || '';
const renderIn = renderMode(extension);
const type = code ? 'text' : file.type.split('/')[0];
const [fileContent, setFileContent] = useState('');
const [type, setType] = useState(file.type.split('/')[0]);
const [open, setOpen] = useState(false);
const getText = useCallback(async () => {
@@ -99,52 +131,41 @@ export default function DashboardFileType({
if (!dbFile) {
const reader = new FileReader();
reader.onload = () => {
if ((reader.result! as string).length > 1 * 1024 * 1024) {
setFileContent(
reader.result!.slice(0, 1 * 1024 * 1024) +
'\n...\nThe file is too big to display click the download icon to view/download it.',
);
const content = reader.result as string;
if (content.length > MAX_BYTES) {
setFileContent(content.slice(0, MAX_BYTES) + FILE_BIG);
} else {
setFileContent(reader.result as string);
setFileContent(content);
}
};
reader.readAsText(file);
return;
}
if (file.size > 1 * 1024 * 1024) {
const res = await fetch(`${fileRoute}${password ? `?pw=${password}` : ''}`, {
if (file.size > MAX_BYTES) {
const res = await fetch(fileUrl, {
headers: {
Range: 'bytes=0-' + 1 * 1024 * 1024, // 0 mb to 1 mb
Range: `bytes=0-${MAX_BYTES}`,
},
});
if (!res.ok) throw new Error('Failed to fetch file');
const text = await res.text();
setFileContent(
text + '\n...\nThe file is too big to display click the download icon to view/download it.',
);
setFileContent(text + FILE_BIG);
return;
}
const res = await fetch(`${fileRoute}${password ? `?pw=${password}` : ''}`);
const res = await fetch(fileUrl);
if (!res.ok) throw new Error('Failed to fetch file');
const text = await res.text();
setFileContent(text);
} catch {
setFileContent('Error loading file.');
}
}, [dbFile, file, password]);
}, [dbFile, file, fileUrl]);
useEffect(() => {
if (code) {
setType('text');
getText();
} else if (type === 'text') {
getText();
} else {
return;
}
}, []);
if (type === 'text') getText();
}, [type, getText]);
useEffect(() => {
if (open) {
@@ -152,6 +173,10 @@ export default function DashboardFileType({
} else {
document.body.style.overflow = 'auto';
}
return () => {
document.body.style.overflow = 'auto';
};
}, [open]);
if (disableMediaPreview && !show)
@@ -166,7 +191,7 @@ export default function DashboardFileType({
<Placeholder
text={`Click to view protected ${file.name}`}
Icon={IconShieldLockFilled}
onClick={() => window.open(`/view/${file.name}${password ? `?pw=${password}` : ''}`)}
onClick={() => window.open(appendPassword(`/view/${file.name}`, password))}
/>
</Paper>
);
@@ -175,16 +200,17 @@ export default function DashboardFileType({
switch (true) {
case type === 'video':
if (!fileUrl) return <Loader />;
return show ? (
<video
width='100%'
autoPlay
muted
controls
src={dbFile ? `${fileRoute}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
src={fileUrl}
style={{ cursor: 'pointer', maxWidth: '85vw', maxHeight: '85vh' }}
/>
) : (file as DbFile).thumbnail && dbFile ? (
) : thumbnailRoute ? (
<Box pos='relative'>
<MantineImage src={thumbnailRoute} alt={file.name || 'Video thumbnail'} />
@@ -209,22 +235,23 @@ export default function DashboardFileType({
);
case type === 'image':
if (!fileUrl) return <Loader />;
return show ? (
<Center>
<MantineImage
src={dbFile ? `${fileRoute}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
src={fileUrl}
alt={file.name || 'Image'}
style={{
cursor: allowZoom ? 'zoom-in' : 'default',
maxWidth: '70vw',
maxHeight: '70vw',
}}
onClick={() => setOpen(true)}
onClick={() => allowZoom && setOpen(true)}
/>
{allowZoom && open && (
<FileZoomModal setOpen={setOpen}>
<MantineImage
src={dbFile ? `${fileRoute}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
src={fileUrl}
alt={file.name || 'Image'}
style={{
maxWidth: '95vw',
@@ -238,23 +265,13 @@ export default function DashboardFileType({
)}
</Center>
) : (
<MantineImage
fit='contain'
mah={400}
src={dbFile ? `${fileRoute}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
alt={file.name || 'Image'}
/>
<MantineImage fit='contain' mah={400} src={fileUrl} alt={file.name || 'Image'} />
);
case type === 'audio':
if (!fileUrl) return <Loader />;
return show ? (
<audio
autoPlay
muted
controls
style={{ width: '100%' }}
src={dbFile ? `${fileRoute}${password ? `?pw=${password}` : ''}` : URL.createObjectURL(file)}
/>
<audio autoPlay muted controls style={{ width: '100%' }} src={fileUrl} />
) : (
<Placeholder text={`Click to play audio ${file.name}`} Icon={fileIcon(file.type)} />
);
@@ -278,15 +295,16 @@ export default function DashboardFileType({
}}
/>
) : (
<Render mode={renderIn} language={file.name.split('.').pop() || ''} code={fileContent} />
<Render mode={renderIn} language={extension} code={fileContent} />
)
) : (
<Placeholder text={`Click to view text ${file.name}`} Icon={fileIcon(file.type)} />
);
case isAsciicast === true:
return show && dbFile ? (
<Asciinema src={`${fileRoute}${password ? `?pw=${password}` : ''}`} />
if (!fileUrl) return <Loader />;
return show ? (
<Asciinema src={fileUrl} />
) : (
<Placeholder
text={`Click to download asciinema cast ${file.name}`}
@@ -295,26 +313,27 @@ export default function DashboardFileType({
);
case file.type === 'application/pdf':
return show && dbFile ? (
<Pdf src={`${fileRoute}${password ? `?pw=${password}` : ''}`} />
if (!fileUrl) return <Loader />;
return show ? (
<Pdf src={fileUrl} />
) : (
<Placeholder text={`Click to view PDF ${file.name}`} Icon={fileIcon(file.type)} />
);
default:
if (dbFile && !show)
return <Placeholder text={`Click to view file ${file.name}`} Icon={fileIcon(file.type)} />;
if (!show) return <Placeholder text={`Click to view file ${file.name}`} Icon={fileIcon(file.type)} />;
if (dbFile && show)
if (show)
return (
<Paper withBorder p='xs' style={{ cursor: 'pointer' }}>
<Placeholder
onClick={() => window.open(`${fileRoute}${password ? `?pw=${password}` : ''}`)}
onClick={() => window.open(fileUrl)}
text={`Click to view file ${file.name} in a new tab`}
Icon={fileIcon(file.type)}
/>
</Paper>
);
else return <IconFileUnknown size={48} />;
return <IconFileUnknown size={48} />;
}
}
+10 -15
View File
@@ -3,7 +3,8 @@ import { Response } from '@/lib/api/response';
import type { File } from '@/lib/db/models/file';
import { Folder } from '@/lib/db/models/folder';
import { fetchApi } from '@/lib/fetchApi';
import { conditionalWarning } from '@/lib/warningModal';
import { conditionalWarning } from '@/lib/client/warningModal';
import { getDomain } from '@/lib/client/webDomain';
import { Anchor } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
@@ -29,13 +30,11 @@ export function downloadFile(file: File) {
}
export function copyFile(file: File, clipboard: ReturnType<typeof useClipboard>, raw: boolean = false) {
const domain = `${window.location.protocol}//${window.location.host}`;
const url = raw
? `${domain}/raw/${file.name}`
? getDomain(`/raw/${file.name}`)
: file.url
? `${domain}${file.url}`
: `${domain}/view/${file.name}`;
? getDomain(`${file.url}`)
: getDomain(`/view/${file.name}`);
clipboard.copy(url);
@@ -141,14 +140,10 @@ export async function createFolderAndAdd(file: File, folderName: string | null)
}
export async function removeFromFolder(file: File) {
const { data, error } = await fetchApi<Response['/api/user/files/[id]']>(
`/api/user/folders/${file.folderId}`,
'DELETE',
{
delete: 'file',
id: file.id,
},
);
const { data, error } = await fetchApi<{ folder: Folder }>(`/api/user/folders/${file.folderId}`, 'DELETE', {
delete: 'file',
id: file.id,
});
if (error) {
notifications.show({
@@ -160,7 +155,7 @@ export async function removeFromFolder(file: File) {
} else {
notifications.show({
title: 'File removed from folder',
message: `${file.name} has been removed from ${data!.name}`,
message: `${file.name} has been removed from ${data?.folder.name}`,
color: 'green',
icon: <IconFolderMinus size='1rem' />,
});
+1 -1
View File
@@ -2,7 +2,7 @@ import { useConfig } from '@/components/ConfigProvider';
import Stat from '@/components/Stat';
import type { Response } from '@/lib/api/response';
import { bytes } from '@/lib/bytes';
import useLogin from '@/lib/hooks/useLogin';
import useLogin from '@/lib/client/hooks/useLogin';
import { isAdministrator } from '@/lib/role';
import { Button, Group, Paper, ScrollArea, SimpleGrid, Skeleton, Table, Text, Title } from '@mantine/core';
import {
@@ -1,7 +1,7 @@
import { Response } from '@/lib/api/response';
import { IncompleteFile } from '@/lib/db/models/incompleteFile';
import { fetchApi } from '@/lib/fetchApi';
import { UpdateFn } from '@/lib/hooks/useObjectState';
import { UpdateFn } from '@/lib/client/hooks/useObjectState';
import { IncompleteFileStatus } from '@/prisma/client';
import { Badge, Button, Card, Group, Modal, Paper, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
@@ -72,7 +72,7 @@ export default function PendingFilesModal({
};
return (
<Modal opened={modals.pending} onClose={() => setModals('pending', false)}>
<Modal opened={modals.pending} onClose={() => setModals('pending', false)} title='Pending Files'>
<Stack gap='xs'>
{incompleteFiles?.map((incompleteFile) => (
<Card key={incompleteFile.id} withBorder>
+1 -12
View File
@@ -1,4 +1,4 @@
import { FieldSettings, useFileTableSettingsStore } from '@/lib/store/fileTableSettings';
import { FieldSettings, NAMES, useFileTableSettingsStore } from '@/lib/client/store/fileTableSettings';
import {
closestCenter,
DndContext,
@@ -14,17 +14,6 @@ import { Button, Checkbox, Group, Modal, Paper, Text } from '@mantine/core';
import { IconGripVertical } from '@tabler/icons-react';
import { useShallow } from 'zustand/shallow';
export const NAMES = {
name: 'Name',
originalName: 'Original Name',
tags: 'Tags',
type: 'Type',
size: 'Size',
createdAt: 'Created At',
favorite: 'Favorite',
views: 'Views',
};
function SortableTableField({ item }: { item: FieldSettings }) {
const setVisible = useFileTableSettingsStore((state) => state.setVisible);
+41 -8
View File
@@ -1,6 +1,6 @@
import GridTableSwitcher from '@/components/GridTableSwitcher';
import useObjectState from '@/lib/hooks/useObjectState';
import { useViewStore } from '@/lib/store/view';
import useObjectState, { type UpdateFn } from '@/lib/client/hooks/useObjectState';
import { useViewStore } from '@/lib/client/store/view';
import { ActionIcon, Group, Menu, Title, Tooltip } from '@mantine/core';
import {
IconDots,
@@ -10,7 +10,7 @@ import {
IconTableOptions,
IconTags,
} from '@tabler/icons-react';
import { Link } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router-dom';
import PendingFilesModal from './PendingFilesModal';
import TagsModal from './tags/TagsModal';
import FavoriteFiles from './views/FavoriteFiles';
@@ -26,14 +26,47 @@ export type DashboardFilesModals = {
export default function DashboardFiles() {
const view = useViewStore((state) => state.files);
const [searchParams, setSearchParams] = useSearchParams();
const modalKeys: Array<keyof DashboardFilesModals> = ['table', 'idSearch', 'tags', 'pending'];
const [modals, setModals] = useObjectState<DashboardFilesModals>({
table: false,
idSearch: false,
tags: false,
pending: false,
const modalQS = (key: keyof DashboardFilesModals) => searchParams.get(key) === 'true';
const [modals, setModalState] = useObjectState<DashboardFilesModals>({
table: modalQS('table'),
idSearch: modalQS('idSearch'),
tags: modalQS('tags'),
pending: modalQS('pending'),
});
const updateModalQuery = (updates: Partial<DashboardFilesModals>) => {
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
for (const key of modalKeys) {
if (!(key in updates)) continue;
if (updates[key]) next.set(key, 'true');
else next.delete(key);
}
return next;
},
{ replace: true },
);
};
const setModals: UpdateFn<DashboardFilesModals> = (keyOrObj: any, value?: any) => {
if (typeof keyOrObj === 'object' && value === undefined) {
setModalState(keyOrObj);
updateModalQuery(keyOrObj);
return;
}
setModalState(keyOrObj, value);
updateModalQuery({ [keyOrObj]: value });
};
return (
<>
<TagsModal modals={modals} setModals={setModals} />
+7 -2
View File
@@ -11,8 +11,13 @@ export default function TagPill({
if (!tag) return null;
return (
<Pill bg={tag.color || undefined} c={isLightColor(tag.color) ? 'black' : 'white'} {...other}>
{tag.name}
<Pill
bg={tag.color || undefined}
c={isLightColor(tag.color) ? 'black' : 'white'}
title={tag.name}
{...other}
>
{tag.name.length <= 24 ? tag.name : tag.name.slice(0, 21) + '...'}
</Pill>
);
}
@@ -2,7 +2,7 @@ import { mutateFiles } from '@/components/file/actions';
import { Response } from '@/lib/api/response';
import { Tag } from '@/lib/db/models/tag';
import { fetchApi } from '@/lib/fetchApi';
import { UpdateFn } from '@/lib/hooks/useObjectState';
import { UpdateFn } from '@/lib/client/hooks/useObjectState';
import { ActionIcon, Group, Modal, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconPencil, IconPlus, IconTagOff, IconTrashFilled } from '@tabler/icons-react';
@@ -1,4 +1,4 @@
import { useQueryState } from '@/lib/hooks/useQueryState';
import { useQueryState } from '@/lib/client/hooks/useQueryState';
import {
Accordion,
Button,
@@ -1,4 +1,4 @@
import { useQueryState } from '@/lib/hooks/useQueryState';
import { useQueryState } from '@/lib/client/hooks/useQueryState';
import {
Button,
Center,
@@ -3,13 +3,13 @@ import { addMultipleToFolder, copyFile, deleteFile, downloadFile } from '@/compo
import FolderComboboxOptions from '@/components/folders/FolderComboboxOptions';
import { Response } from '@/lib/api/response';
import { bytes } from '@/lib/bytes';
import { useFolders } from '@/lib/client/hooks/useFolders';
import { useQueryState } from '@/lib/client/hooks/useQueryState';
import { NAMES, useFileTableSettingsStore } from '@/lib/client/store/fileTableSettings';
import { useSettingsStore } from '@/lib/client/store/settings';
import { type File } from '@/lib/db/models/file';
import { Tag } from '@/lib/db/models/tag';
import { buildFolderHierarchy } from '@/lib/folderHierarchy';
import { useFolders } from '@/lib/hooks/useFolders';
import { useQueryState } from '@/lib/hooks/useQueryState';
import { useFileTableSettingsStore } from '@/lib/store/fileTableSettings';
import { useSettingsStore } from '@/lib/store/settings';
import {
ActionIcon,
Box,
@@ -44,9 +44,9 @@ import { lazy, useEffect, useMemo, useReducer, useState } from 'react';
import { Link } from 'react-router-dom';
import useSWR from 'swr';
import { UpdateFn } from '@/lib/hooks/useObjectState';
import { UpdateFn } from '@/lib/client/hooks/useObjectState';
import { DashboardFilesModals } from '..';
import TableEditModal, { NAMES } from '../TableEditModal';
import TableEditModal from '../TableEditModal';
import { bulkDelete, bulkFavorite } from '../bulk';
import TagPill from '../tags/TagPill';
import { useApiPagination } from '../useApiPagination';
@@ -342,6 +342,7 @@ export default function FileTable({
{
accessor: 'favorite',
sortable: true,
title: 'Favorite?',
render: (file: File) => (file.favorite ? <Text c='yellow'>Yes</Text> : 'No'),
},
{
@@ -354,6 +355,12 @@ export default function FileTable({
hidden: searchField !== 'id' || searchQuery.id.trim() === '',
filtering: searchField === 'id' && searchQuery.id.trim() !== '',
},
{
accessor: 'anonymous',
sortable: true,
title: 'Anonymous?',
render: (file: File) => (file.anonymous ? <Text c='green'>Yes</Text> : 'No'),
},
];
const visibleFields = fields.filter((f) => f.visible).map((f) => f.field);
@@ -384,8 +391,8 @@ export default function FileTable({
user={id}
/>
{modals && setModals && modals.table && (
<TableEditModal opened={modals.table} onClose={() => setModals('table', false)} />
{modals && setModals && (
<TableEditModal opened={!!modals.table} onClose={() => setModals('table', false)} />
)}
<Box>
@@ -1,4 +1,4 @@
import { useQueryState } from '@/lib/hooks/useQueryState';
import { useQueryState } from '@/lib/client/hooks/useQueryState';
import {
Accordion,
Button,
+6 -4
View File
@@ -1,6 +1,7 @@
import { Response } from '@/lib/api/response';
import { Folder } from '@/lib/db/models/folder';
import { fetchApi } from '@/lib/fetchApi';
import { getDomain } from '@/lib/client/webDomain';
import { Anchor } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
@@ -9,13 +10,14 @@ import { Link } from 'react-router-dom';
import { mutate } from 'swr';
export function copyFolderUrl(folder: Folder, clipboard: ReturnType<typeof useClipboard>) {
clipboard.copy(`${window.location.protocol}//${window.location.host}/folder/${folder.id}`);
const url = getDomain(`/folder/${folder.id}`);
clipboard.copy(url);
notifications.show({
title: 'Copied link',
message: (
<Anchor component={Link} to={`/folder/${folder.id}`}>
{`${window.location.protocol}//${window.location.host}/folder/${folder.id}`}
{url}
</Anchor>
),
color: 'green',
@@ -48,7 +50,7 @@ export async function editFolderVisibility(folder: Folder, isPublic: boolean) {
});
}
mutateFolder(folder.id);
mutateFolder();
}
export async function editFolderUploads(folder: Folder, allowUploads: boolean) {
@@ -76,7 +78,7 @@ export async function editFolderUploads(folder: Folder, allowUploads: boolean) {
});
}
mutateFolder(folder.id);
mutateFolder();
}
export async function mutateFolder(folderId?: string) {
+2 -2
View File
@@ -3,8 +3,8 @@ import { Response } from '@/lib/api/response';
import { Folder } from '@/lib/db/models/folder';
import { fetchApi } from '@/lib/fetchApi';
import { FolderBreadcrumb } from '@/lib/folderHierarchy';
import { SEPARATOR, useTitle } from '@/lib/hooks/useTitle';
import { useViewStore } from '@/lib/store/view';
import { SEPARATOR, useTitle } from '@/lib/client/hooks/useTitle';
import { useViewStore } from '@/lib/client/store/view';
import {
Alert,
Anchor,
@@ -3,7 +3,7 @@ import { Response } from '@/lib/api/response';
import { Folder } from '@/lib/db/models/folder';
import { fetchApi } from '@/lib/fetchApi';
import { buildFolderHierarchy } from '@/lib/folderHierarchy';
import { useFolders } from '@/lib/hooks/useFolders';
import { useFolders } from '@/lib/client/hooks/useFolders';
import { Button, Combobox, InputBase, Modal, Radio, Stack, Text, useCombobox } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconTrashFilled } from '@tabler/icons-react';
@@ -3,7 +3,7 @@ import { Response } from '@/lib/api/response';
import { Folder } from '@/lib/db/models/folder';
import { fetchApi } from '@/lib/fetchApi';
import { buildFolderHierarchy, getDescendantIds } from '@/lib/folderHierarchy';
import { useFolders } from '@/lib/hooks/useFolders';
import { useFolders } from '@/lib/client/hooks/useFolders';
import { Button, Combobox, InputBase, Modal, Stack, Text, useCombobox } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconFolderSymlink } from '@tabler/icons-react';
+13 -4
View File
@@ -1,12 +1,18 @@
import RelativeDate from '@/components/RelativeDate';
import { Invite } from '@/lib/db/models/invite';
import { useSettingsStore } from '@/lib/client/store/settings';
import { ActionIcon, Anchor, Card, Group, Menu, Stack, Text } from '@mantine/core';
import { IconCopy, IconDots, IconTrashFilled } from '@tabler/icons-react';
import { copyInviteUrl, deleteInvite } from './actions';
import { useClipboard } from '@mantine/hooks';
import { useSettingsStore } from '@/lib/store/settings';
import { IconCopy, IconDots, IconQrcode, IconTrashFilled } from '@tabler/icons-react';
import { copyInviteUrl, deleteInvite } from './actions';
export default function InviteCard({ invite }: { invite: Invite }) {
export default function InviteCard({
invite,
setQrOpen,
}: {
invite: Invite;
setQrOpen: (invite: Invite) => void;
}) {
const clipboard = useClipboard();
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
@@ -36,6 +42,9 @@ export default function InviteCard({ invite }: { invite: Invite }) {
>
Copy URL
</Menu.Item>
<Menu.Item leftSection={<IconQrcode size='1rem' />} onClick={() => setQrOpen(invite)}>
Show QR Code
</Menu.Item>
<Menu.Item
leftSection={<IconTrashFilled size='1rem' />}
color='red'
+5 -3
View File
@@ -1,7 +1,8 @@
import { Response } from '@/lib/api/response';
import { Invite } from '@/lib/db/models/invite';
import { fetchApi } from '@/lib/fetchApi';
import { conditionalWarning } from '@/lib/warningModal';
import { conditionalWarning } from '@/lib/client/warningModal';
import { getDomain } from '@/lib/client/webDomain';
import { Anchor } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
@@ -18,13 +19,14 @@ export async function deleteInvite(warnDeletion: boolean, invite: Invite) {
}
export function copyInviteUrl(invite: Invite, clipboard: ReturnType<typeof useClipboard>) {
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
const url = getDomain(`/invite/${invite.code}`);
clipboard.copy(url);
notifications.show({
title: 'Copied link',
message: (
<Anchor component={Link} to={`/invite/${invite.code}`}>
{`${window.location.protocol}//${window.location.host}/invite/${invite.code}`}
{url}
</Anchor>
),
color: 'green',
+1 -1
View File
@@ -2,7 +2,7 @@ import GridTableSwitcher from '@/components/GridTableSwitcher';
import { Response } from '@/lib/api/response';
import { Invite } from '@/lib/db/models/invite';
import { fetchApi } from '@/lib/fetchApi';
import { useViewStore } from '@/lib/store/view';
import { useViewStore } from '@/lib/client/store/view';
import { Button, Group, Modal, NumberInput, Select, Stack, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
@@ -4,13 +4,23 @@ import { Center, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '
import { IconLink } from '@tabler/icons-react';
import useSWR from 'swr';
import InviteCard from '../InviteCard';
import { useState } from 'react';
import QRCodeModal from '@/components/QRCodeModal';
export default function InviteGridView() {
const { data: folders, isLoading } =
useSWR<Extract<Response['/api/auth/invites'], Invite[]>>('/api/auth/invites');
const [qrOpen, setQrOpen] = useState<Invite | null>(null);
return (
<>
<QRCodeModal
opened={!!qrOpen}
onClose={() => setQrOpen(null)}
url={qrOpen ? `/invite/${qrOpen.code}` : ''}
/>
{isLoading ? (
<SimpleGrid
my='sm'
@@ -38,7 +48,7 @@ export default function InviteGridView() {
pos='relative'
>
{folders?.map((invite) => (
<InviteCard key={invite.id} invite={invite} />
<InviteCard setQrOpen={setQrOpen} key={invite.id} invite={invite} />
))}
</SimpleGrid>
) : (
@@ -1,14 +1,15 @@
import RelativeDate from '@/components/RelativeDate';
import { Response } from '@/lib/api/response';
import { Invite } from '@/lib/db/models/invite';
import { useSettingsStore } from '@/lib/store/settings';
import { useSettingsStore } from '@/lib/client/store/settings';
import { ActionIcon, Anchor, Box, Group, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { IconCopy, IconTrashFilled } from '@tabler/icons-react';
import { IconCopy, IconQrcode, IconTrashFilled } from '@tabler/icons-react';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useMemo, useState } from 'react';
import useSWR from 'swr';
import { copyInviteUrl, deleteInvite } from '../actions';
import QRCodeModal from '@/components/QRCodeModal';
export default function InviteTableView() {
const clipboard = useClipboard();
@@ -36,8 +37,16 @@ export default function InviteTableView() {
});
}, [data, sortStatus]);
const [qrOpen, setQrOpen] = useState<Invite | null>(null);
return (
<>
<QRCodeModal
opened={!!qrOpen}
onClose={() => setQrOpen(null)}
url={qrOpen ? `/invite/${qrOpen.code}` : ''}
/>
<Box my='sm'>
<DataTable
borderRadius='sm'
@@ -101,6 +110,16 @@ export default function InviteTableView() {
<IconCopy size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Show QR code'>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
setQrOpen(invite);
}}
>
<IconQrcode size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Delete invite'>
<ActionIcon
color='red'
@@ -5,14 +5,7 @@ const ICON_SIZE = '1.75rem';
export default function ActionButton({ onClick, Icon }: { onClick: () => void; Icon?: React.FC<any> }) {
return (
<ActionIcon
onClick={onClick}
variant='filled'
color='blue'
radius='md'
size='xl'
className='zip-click-action-button'
>
<ActionIcon onClick={onClick} variant='filled' radius='md' size='xl' className='zip-click-action-button'>
{Icon ? <Icon size={ICON_SIZE} /> : <IconPlayerPlayFilled size={ICON_SIZE} />}
</ActionIcon>
);
@@ -1,5 +1,5 @@
import { Export4 } from '@/lib/import/version4/validateExport';
import { useUserStore } from '@/lib/store/user';
import { useUserStore } from '@/lib/client/store/user';
import { Box, Checkbox, Group, Text } from '@mantine/core';
export function detectSameInstance(export4?: Export4 | null, currentUserId?: string) {
@@ -1,17 +1,17 @@
import { Response } from '@/lib/api/response';
import { useUserStore } from '@/lib/client/store/user';
import { fetchApi } from '@/lib/fetchApi';
import { Export4, validateExport } from '@/lib/import/version4/validateExport';
import { Button, FileButton, Modal, Pill, Text } from '@mantine/core';
import { modals } from '@mantine/modals';
import { showNotification, updateNotification } from '@mantine/notifications';
import { IconDatabaseImport, IconDatabaseOff, IconUpload, IconX } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { mutate } from 'swr';
import Export4Details from './Export4Details';
import Export4ImportSettings from './Export4ImportSettings';
import Export4WarningSameInstance, { detectSameInstance } from './Export4WarningSameInstance';
import Export4UserChoose from './Export4UserChoose';
import { useUserStore } from '@/lib/store/user';
import { modals } from '@mantine/modals';
import { fetchApi } from '@/lib/fetchApi';
import { Response } from '@/lib/api/response';
import { mutate } from 'swr';
import Export4WarningSameInstance, { detectSameInstance } from './Export4WarningSameInstance';
export default function ImportV4Button() {
const [open, setOpen] = useState(false);
+25 -21
View File
@@ -33,28 +33,32 @@ export default function DashboardServerSettings() {
const scrollToSetting = useMemo(() => {
return (setting: string) => {
const input = document.querySelector<HTMLInputElement>(`[data-path="${setting}"]`);
if (input) {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
observer.disconnect();
const parent = input.parentElement?.parentElement;
if (parent) {
parent.style.transition = 'transform 0.35s';
parent.style.transform = 'scale(1.2)';
setTimeout(() => {
parent.style.transform = 'scale(1)';
}, 350);
}
}
},
{ threshold: 1.0 },
);
observer.observe(input);
const parent = input?.parentElement?.parentElement;
if (!input || !parent) return;
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
input.focus();
}
parent.style.transition = 'all 0.4s ease';
parent.style.borderRadius = 'var(--mantine-radius-xs)';
parent.style.outline = '2px solid var(--mantine-primary-color-filled)';
parent.style.outlineOffset = 'var(--mantine-spacing-xs)';
const observer = new IntersectionObserver(
(entries) => {
if (entries.length === 0) return;
if (!entries[0].isIntersecting) return;
observer.disconnect();
setTimeout(() => {
parent.style.outline = '0 solid transparent';
parent.style.outlineOffset = '0';
parent.style.borderRadius = '0';
}, 2000);
},
{ threshold: 1.0 },
);
observer.observe(input);
input.scrollIntoView({ behavior: 'smooth', block: 'center' });
input.focus();
};
}, []);
@@ -37,6 +37,7 @@ export default function Files({
filesRandomWordsNumAdjectives: number;
filesRandomWordsSeparator: string;
filesDefaultCompressionFormat: string;
filesMaxFilesPerUpload: number;
}>({
initialValues: {
filesRoute: '/u',
@@ -52,6 +53,7 @@ export default function Files({
filesRandomWordsNumAdjectives: 3,
filesRandomWordsSeparator: '-',
filesDefaultCompressionFormat: 'jpg',
filesMaxFilesPerUpload: 1000,
},
enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false,
@@ -110,6 +112,7 @@ export default function Files({
filesRandomWordsNumAdjectives: data.settings.filesRandomWordsNumAdjectives ?? 3,
filesRandomWordsSeparator: data.settings.filesRandomWordsSeparator ?? '-',
filesDefaultCompressionFormat: data.settings.filesDefaultCompressionFormat ?? 'jpg',
filesMaxFilesPerUpload: data.settings.filesMaxFilesPerUpload ?? 1000,
});
}, [data]);
@@ -218,6 +221,13 @@ export default function Files({
]}
{...form.getInputProps('filesDefaultCompressionFormat')}
/>
<NumberInput
label='Max Files Per Upload'
description='The maximum number of files allowed per upload. Requires a server restart.'
min={1}
{...form.getInputProps('filesMaxFilesPerUpload')}
/>
</SimpleGrid>
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
@@ -1,5 +1,3 @@
import React from 'react';
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { showNotification } from '@mantine/notifications';
+1 -1
View File
@@ -41,7 +41,7 @@ export default function DashboardSettings() {
config.oauthEnabled.oidc,
) && <SettingsOAuth />}
{eitherTrue(config.mfa.totp.enabled, config.mfa.passkeys) && <SettingsMfa />}
{eitherTrue(config.mfa.totp.enabled, config.mfa.passkeys.enabled) && <SettingsMfa />}
<SettingsExports />
<SettingsGenerators />
@@ -1,8 +1,8 @@
import { Response } from '@/lib/api/response';
import { readToDataURL } from '@/lib/base64';
import { fetchApi } from '@/lib/fetchApi';
import useAvatar from '@/lib/hooks/useAvatar';
import { useUserStore } from '@/lib/store/user';
import useAvatar from '@/lib/client/hooks/useAvatar';
import { useUserStore } from '@/lib/client/store/user';
import {
Avatar,
Button,
@@ -1,5 +1,6 @@
import DomainSelect from '@/components/DomainSelect';
import { useThemes } from '@/components/ThemeProvider';
import { useSettingsStore } from '@/lib/store/settings';
import { useSettingsStore } from '@/lib/client/store/settings';
import { Group, Paper, Select, Stack, Switch, Text, Title } from '@mantine/core';
import { IconMoonFilled, IconPaintFilled, IconSunFilled } from '@tabler/icons-react';
import { useShallow } from 'zustand/shallow';
@@ -52,6 +53,13 @@ export default function SettingsDashboard() {
/>
</Group>
<DomainSelect
label='Default Domain'
description='Set the default domain used for copied links anywhere in the dashboard. Leave blank or select "Default domain" to use the current domain that serves the dashboard.'
value={settings.domain}
onChange={(value) => update('domain', value ?? '')}
/>
<Select
label='Theme'
description='The theme to use for the dashboard. This is only a visual change on your browser and does not change the theme for other users.'
@@ -82,7 +82,6 @@ export default function SettingsExports() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{isLoading && <Table.Tr>Loading...</Table.Tr>}
{data?.map((exportDb) => (
<Table.Tr key={exportDb.id}>
<Table.Td maw={140}>
@@ -1,6 +1,6 @@
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useUserStore } from '@/lib/store/user';
import { useUserStore } from '@/lib/client/store/user';
import {
Anchor,
Button,
@@ -13,7 +13,7 @@ import {
Text,
} from '@mantine/core';
import { IconDownload, IconEyeFilled, IconGlobe, IconPercentage, IconWriting } from '@tabler/icons-react';
import React, { useReducer, useState } from 'react';
import { useReducer, useState } from 'react';
import useSWR from 'swr';
import { flameshot } from './generators/flameshot';
import { sharex } from './generators/sharex';
@@ -1,7 +1,7 @@
import RelativeDate from '@/components/RelativeDate';
import { fetchApi } from '@/lib/fetchApi';
import useObjectState from '@/lib/hooks/useObjectState';
import { useUserStore } from '@/lib/store/user';
import useObjectState from '@/lib/client/hooks/useObjectState';
import { useUserStore } from '@/lib/client/store/user';
import { UserPasskey } from '@/prisma/client';
import { ActionIcon, Button, Group, Modal, Paper, Stack, Text, TextInput } from '@mantine/core';
import { modals } from '@mantine/modals';
@@ -1,7 +1,7 @@
import { Response } from '@/lib/api/response';
import { User } from '@/lib/db/models/user';
import { fetchApi } from '@/lib/fetchApi';
import { useUserStore } from '@/lib/store/user';
import { useUserStore } from '@/lib/client/store/user';
import {
Anchor,
Box,
@@ -1,8 +1,8 @@
import { useConfig } from '@/components/ConfigProvider';
import { Response } from '@/lib/api/response';
import { useUserStore } from '@/lib/client/store/user';
import { fetchApi } from '@/lib/fetchApi';
import { findProvider } from '@/lib/oauth/providerUtil';
import { useUserStore } from '@/lib/store/user';
import { findProvider } from '@/lib/oauth/providers';
import { darken } from '@/lib/theme/color';
import type { OAuthProviderType } from '@/prisma/client';
import { Button, ButtonProps, Paper, SimpleGrid, Text, Title, useMantineTheme } from '@mantine/core';
@@ -68,6 +68,12 @@ function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked
'--z-bol-color': darken(t.colors?.[provider.toLowerCase()]?.[0] ?? '', 0.2, t),
},
className: !linked ? styles.button : undefined,
styles: {
label: {
whiteSpace: 'normal',
textAlign: 'center',
},
},
};
return linked ? (
@@ -1,6 +1,6 @@
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useLogout } from '@/lib/hooks/useLogout';
import { useLogout } from '@/lib/client/hooks/useLogout';
import { ActionIcon, Button, Modal, Paper, SimpleGrid, Skeleton, Table, Text, Title } from '@mantine/core';
import { modals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
@@ -1,7 +1,7 @@
import { ApiError } from '@/lib/api/errors';
import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi';
import { useUserStore } from '@/lib/store/user';
import { useUserStore } from '@/lib/client/store/user';
import {
ActionIcon,
Button,
@@ -14,7 +14,7 @@ import {
} from '@mantine/core';
import { IconFileUpload, IconTrashFilled } from '@tabler/icons-react';
export default function ToUploadFile({
export default function DropzoneFile({
file,
onDelete,
loading,
@@ -43,7 +43,9 @@ export default function ToUploadFile({
<Center h='100%'>
<Group justify='center' gap='xl'>
<IconFileUpload size={48} />
<Text size='md'>{file.name}</Text>
<Text size='md' ff='monospace'>
{file.name}
</Text>
</Group>
</Center>
</Paper>
+56 -17
View File
@@ -1,7 +1,10 @@
import { useConfig } from '@/components/ConfigProvider';
import { bytes } from '@/lib/bytes';
import { uploadFiles } from '@/lib/client/upload/files';
import { uploadPartialFiles } from '@/lib/client/upload/partial';
import { useProgress } from '@/lib/client/upload/useProgress';
import { humanizeDuration } from '@/lib/relativeTime';
import { useUploadOptionsStore } from '@/lib/store/uploadOptions';
import { useUploadOptionsStore } from '@/lib/client/store/uploadOptions';
import {
Button,
Collapse,
@@ -12,6 +15,7 @@ import {
Progress,
Text,
Title,
Tooltip,
rem,
useMantineTheme,
} from '@mantine/core';
@@ -23,9 +27,9 @@ import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useShallow } from 'zustand/shallow';
import UploadOptionsButton from '../UploadOptionsButton';
import { uploadFiles } from '../uploadFiles';
import { uploadPartialFiles } from '../uploadPartialFiles';
import ToUploadFile from './ToUploadFile';
import DropzoneFile from './DropzoneFile';
const initialVisible = 24;
export default function UploadFile({ title, folder }: { title?: string; folder?: string }) {
const theme = useMantineTheme();
@@ -40,13 +44,13 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
);
const [files, setFiles] = useState<File[]>([]);
const [progress, setProgress] = useState<{ percent: number; remaining: number; speed: number }>({
percent: 0,
remaining: 0,
speed: 0,
});
const [visibleCount, setVisibleCount] = useState(initialVisible);
const [progress, setProgress] = useProgress();
const [dropLoading, setLoading] = useState(false);
const visibleFiles = files.slice(0, visibleCount);
const hiddenFiles = Math.max(0, files.length - visibleFiles.length);
const aggSize = useCallback(() => files.reduce((acc, file) => acc + file.size, 0), [files]);
const handlePaste = useCallback((e: ClipboardEvent) => {
@@ -56,11 +60,12 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
const blob = e.clipboardData.items[i].getAsFile();
if (!blob) return;
setFiles((prev) => [...prev, blob]);
setVisibleCount(initialVisible);
showNotification({ message: `Image ${blob.name} pasted from clipboard`, color: 'blue' });
}
}, []);
const upload = () => {
const upload = async () => {
const toPartialFiles: File[] = files.filter(
(file) => config.chunks.enabled && file.size >= bytes(config.chunks.max),
);
@@ -92,7 +97,8 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
),
});
}
uploadFiles(files, {
await uploadFiles(files, {
setFiles,
setLoading,
setProgress,
@@ -118,6 +124,7 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
e.preventDefault();
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
@@ -145,7 +152,10 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
</Group>
<Dropzone
onDrop={(f) => setFiles((prev) => [...f, ...prev])}
onDrop={(f) => {
setFiles((prev) => [...f, ...prev]);
setVisibleCount(initialVisible);
}}
my='sm'
loading={dropLoading}
disabled={dropLoading}
@@ -211,24 +221,53 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
</Collapse>
<Grid grow my='sm'>
{files.map((file, i) => (
{visibleFiles.map((file, i) => (
<Grid.Col span={3} key={i}>
<ToUploadFile
<DropzoneFile
loading={dropLoading}
file={file}
onDelete={() => setFiles(files.filter((_, j) => i !== j))}
onDelete={() => setFiles((prev) => prev.filter((_, j) => i !== j))}
/>
</Grid.Col>
))}
</Grid>
{hiddenFiles > 0 && (
<Group justify='center' gap='xs' my='xs'>
<Text size='sm' c='dimmed'>
{hiddenFiles} more file{hiddenFiles !== 1 && 's'} hidden{' '}
</Text>
<Button
size='compact-sm'
variant='light'
disabled={dropLoading}
onClick={() => setVisibleCount((prev) => Math.min(files.length, prev + initialVisible))}
>
Show more
</Button>
<Tooltip label='This may cause performance issues if there are a lot of files' hidden={dropLoading}>
<Button
size='compact-sm'
variant='subtle'
disabled={dropLoading}
onClick={() => setVisibleCount(files.length)}
>
Show all
</Button>
</Tooltip>
</Group>
)}
<Group justify='right' gap='sm' my='md'>
<Button
variant='outline'
color='red'
leftSection={<IconTrashFilled size='1rem' />}
disabled={files.length === 0 || dropLoading}
onClick={() => setFiles([])}
onClick={() => {
setFiles([]);
setVisibleCount(initialVisible);
}}
>
Clear all
</Button>
@@ -239,7 +278,7 @@ export default function UploadFile({ title, folder }: { title?: string; folder?:
disabled={files.length === 0 || dropLoading}
onClick={upload}
>
Upload {files.length} files ({bytes(aggSize())})
Upload {files.length} file{files.length !== 1 && 's'} ({bytes(aggSize())})
</Button>
</Group>
</>
+13 -7
View File
@@ -1,6 +1,10 @@
import { useCodeMap } from '@/components/ConfigProvider';
import Render from '@/components/render/Render';
import { useUploadOptionsStore } from '@/lib/store/uploadOptions';
import { renderMode } from '@/components/render/renderMode';
import { bytes } from '@/lib/bytes';
import { uploadFiles } from '@/lib/client/upload/files';
import useMultiTextFiles from '@/lib/client/upload/useMultiTextFiles';
import { useUploadOptionsStore } from '@/lib/client/store/uploadOptions';
import { ActionIcon, Button, Group, Select, Tabs, Textarea, Title } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import {
@@ -14,10 +18,7 @@ import {
import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useShallow } from 'zustand/shallow';
import { renderMode } from '../renderMode';
import { uploadFiles } from '../uploadFiles';
import UploadOptionsButton from '../UploadOptionsButton';
import useMultiTextFiles from '../useMultiTextFiles';
import styles from './index.module.css';
export default function UploadText() {
@@ -60,7 +61,12 @@ export default function UploadText() {
[selected, setFile],
);
const upload = () => {
const aggSize = useCallback(
() => files.reduce((acc, file) => acc + new Blob([file.text]).size, 0),
[files],
);
const upload = async () => {
const fileBlobs = files.map((file) => {
const blob = new Blob([file.text], {
type: codeMap.find((meta) => meta.ext === file.lang)?.mime,
@@ -72,7 +78,7 @@ export default function UploadText() {
});
});
uploadFiles(fileBlobs, {
await uploadFiles(fileBlobs, {
clipboard,
setFiles: () => {},
setLoading,
@@ -179,7 +185,7 @@ export default function UploadText() {
disabled={files.some((file) => file.text.length === 0) || loading}
onClick={upload}
>
Upload
Upload {files.length} file{files.length !== 1 && 's'} ({bytes(aggSize())})
</Button>
</Group>
</>
@@ -3,8 +3,8 @@ import DomainSelect from '@/components/DomainSelect';
import FolderComboboxOptions from '@/components/folders/FolderComboboxOptions';
import { Response } from '@/lib/api/response';
import { buildFolderHierarchy } from '@/lib/folderHierarchy';
import { useFolders } from '@/lib/hooks/useFolders';
import { useUploadOptionsStore } from '@/lib/store/uploadOptions';
import { useFolders } from '@/lib/client/hooks/useFolders';
import { useUploadOptionsStore } from '@/lib/client/store/uploadOptions';
import {
Badge,
Button,
-278
View File
@@ -1,278 +0,0 @@
import { Response } from '@/lib/api/response';
import { ErrorBody } from '@/lib/response';
import { UploadOptionsStore } from '@/lib/store/uploadOptions';
import { ActionIcon, Anchor, Button, Group, Stack, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { notifications } from '@mantine/notifications';
import { IconClipboardCopy, IconExternalLink, IconFileUpload, IconFileXFilled } from '@tabler/icons-react';
import { Link } from 'react-router-dom';
export function handleResponse<R = Response['/api/upload']>(
xml: XMLHttpRequest,
): { data: R | null; error: ErrorBody | null } {
if (xml.status < 200 || xml.status >= 300) {
return {
data: null,
error: {
statusCode: xml.status,
error: `Request failed with status code ${xml.status}: ${xml.responseText}`,
},
};
}
try {
const res = JSON.parse(xml.responseText) as R | ErrorBody;
if ((res as ErrorBody).statusCode) {
return { data: null, error: res as ErrorBody };
}
return { data: res as R, error: null };
} catch (e) {
console.error('Failed to parse server response:', e, xml.responseText);
return {
data: null,
error: {
statusCode: 500,
error: 'Failed to parse server response. See browser console for more details.',
},
};
}
}
function progressTracker() {
const alpha = 0.2;
let lastLoaded = 0;
let lastTime = Date.now();
let resSpeed = 0;
return (loaded: number, total: number) => {
const now = Date.now();
const timeDiff = (now - lastTime) / 1000;
if (timeDiff > 0) {
const loadedDiff = loaded - lastLoaded;
const speed = loadedDiff / timeDiff;
// exponential moving average
resSpeed = resSpeed === 0 ? speed : speed * alpha + resSpeed * (1 - alpha);
lastLoaded = loaded;
lastTime = now;
}
const percent = Math.round((loaded / total) * 100);
const remainingBytes = total - loaded;
const remaining = resSpeed > 0 ? remainingBytes / resSpeed : 0;
return {
percent,
speed: resSpeed,
remaining,
};
};
}
export function filesModal(
files: Response['/api/upload']['files'],
{
clipboard,
clearEphemeral,
}: {
clipboard: ReturnType<typeof useClipboard>;
clearEphemeral: () => void;
},
) {
const open = (idx: number) => window.open(files[idx].url, '_blank');
const copy = (idx: number) => {
clipboard.copy(files[idx].url);
notifications.show({
title: 'Copied URL to clipboard',
message: (
<Anchor component={Link} to={files[idx].url} target='_blank'>
{files[idx].url}
</Anchor>
),
color: 'blue',
icon: <IconClipboardCopy size='1rem' />,
});
};
modals.open({
title: 'Uploaded files',
size: 'auto',
children: (
<>
<Stack>
{files.map((file, idx) => (
<Group key={idx} justify='space-between'>
<Group justify='left'>
<Anchor component={Link} to={file.url}>
{file.url}
</Anchor>
</Group>
<Group justify='right'>
<Tooltip label='Open link in a new tab'>
<ActionIcon onClick={() => open(idx)} variant='filled'>
<IconExternalLink size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy link to clipboard'>
<ActionIcon onClick={() => copy(idx)} variant='filled'>
<IconClipboardCopy size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
</Group>
))}
</Stack>
{files.length > 1 && (
<Group justify='right'>
<Tooltip label='Copy all links to clipboard (seperated by a new line)'>
<Button
onClick={() => {
clipboard.copy(files.map((file) => file.url).join('\n'));
notifications.show({
title: 'Copied URLs to clipboard',
message: 'Copied all URLs to clipboard seperated by a new line.',
color: 'blue',
icon: <IconClipboardCopy size='1rem' />,
});
}}
variant='filled'
color='blue'
size='compact-md'
mt='sm'
fullWidth
leftSection={<IconClipboardCopy size='1rem' />}
>
Copy {files.length} URLs to clipboard
</Button>
</Tooltip>
</Group>
)}
</>
),
});
clearEphemeral();
}
export function uploadFiles(
files: File[],
{
setProgress,
setLoading,
setFiles,
clipboard,
clearEphemeral,
options,
ephemeral,
folder,
}: {
setProgress: (o: { percent: number; remaining: number; speed: number }) => void;
setLoading: (loading: boolean) => void;
setFiles: (files: File[]) => void;
clipboard: ReturnType<typeof useClipboard>;
clearEphemeral: () => void;
options: UploadOptionsStore['options'];
ephemeral: UploadOptionsStore['ephemeral'];
folder?: string;
},
) {
setLoading(true);
setProgress({ percent: 0, remaining: 0, speed: 0 });
const body = new FormData();
for (let i = 0; i !== files.length; ++i) {
body.append('file', files[i]);
}
notifications.show({
id: 'upload',
title: 'Uploading files',
message: `Uploading ${files.length} file${files.length === 1 ? '' : 's'}`,
loading: true,
autoClose: false,
});
const tracker = progressTracker();
let lastUpdate = 0;
const req = new XMLHttpRequest();
req.upload.addEventListener('progress', (e) => {
if (!e.lengthComputable) return;
const stats = tracker(e.loaded, e.total);
const now = Date.now();
if (now - lastUpdate > 250 || e.loaded === e.total) {
setProgress(stats);
lastUpdate = now;
}
});
req.addEventListener(
'load',
() => {
const { data: res, error } = handleResponse<Response['/api/upload']>(req);
setLoading(false);
setProgress({ percent: 0, remaining: 0, speed: 0 });
if (error || !res) {
notifications.update({
id: 'upload',
title: 'Error uploading files',
message: error?.error ?? 'An unknown error occurred',
color: 'red',
icon: <IconFileXFilled size='1rem' />,
autoClose: true,
loading: false,
});
return;
}
notifications.update({
id: 'upload',
title: 'Uploaded files',
message: `Uploaded ${files.length} file${files.length === 1 ? '' : 's'}`,
color: 'green',
icon: <IconFileUpload size='1rem' />,
autoClose: true,
loading: false,
});
setFiles([]);
filesModal(res!.files, { clipboard, clearEphemeral });
},
false,
);
req.open('POST', '/api/upload');
options.deletesAt !== 'default' && req.setRequestHeader('x-zipline-deletes-at', options.deletesAt);
options.format !== 'default' && req.setRequestHeader('x-zipline-format', options.format);
options.imageCompressionPercent &&
req.setRequestHeader('x-zipline-image-compression-percent', options.imageCompressionPercent.toString());
options.imageCompressionFormat !== 'default' &&
req.setRequestHeader('x-zipline-image-compression-type', options.imageCompressionFormat);
options.maxViews && req.setRequestHeader('x-zipline-max-views', options.maxViews.toString());
options.addOriginalName && req.setRequestHeader('x-zipline-original-name', 'true');
options.overrides_returnDomain && req.setRequestHeader('x-zipline-domain', options.overrides_returnDomain);
ephemeral.password && req.setRequestHeader('x-zipline-password', ephemeral.password);
ephemeral.filename && req.setRequestHeader('x-zipline-filename', encodeURIComponent(ephemeral.filename));
if (folder) {
req.setRequestHeader('x-zipline-folder', folder);
} else if (ephemeral.folderId) {
req.setRequestHeader('x-zipline-folder', ephemeral.folderId);
}
req.send(body);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Url } from '@/lib/db/models/url';
import { fetchApi } from '@/lib/fetchApi';
import useObjectState from '@/lib/hooks/useObjectState';
import useObjectState from '@/lib/client/hooks/useObjectState';
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, Switch, TextInput } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconEye, IconKey, IconPencil, IconPencilOff, IconTrashFilled } from '@tabler/icons-react';
+26 -16
View File
@@ -4,11 +4,19 @@ import { Url } from '@/lib/db/models/url';
import { formatRootUrl, trimUrl } from '@/lib/url';
import { ActionIcon, Anchor, Card, Group, Menu, Stack, Text, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { IconCopy, IconDots, IconPencil, IconTrashFilled } from '@tabler/icons-react';
import { IconCopy, IconDots, IconPencil, IconQrcode, IconTrashFilled } from '@tabler/icons-react';
import { copyUrl, deleteUrl } from './actions';
import { useSettingsStore } from '@/lib/store/settings';
import { useSettingsStore } from '@/lib/client/store/settings';
export default function UserCard({ url, setSelectedUrl }: { url: Url; setSelectedUrl: (url: Url) => void }) {
export default function UserCard({
url,
setSelectedUrl,
setQrOpen,
}: {
url: Url;
setSelectedUrl: (url: Url) => void;
setQrOpen: (url: Url) => void;
}) {
const config = useConfig();
const clipboard = useClipboard();
@@ -19,19 +27,18 @@ export default function UserCard({ url, setSelectedUrl }: { url: Url; setSelecte
<Card withBorder shadow='sm' radius='sm'>
<Card.Section withBorder inheritPadding py='xs'>
<Group justify='space-between'>
<Text fw={400}>
{url.enabled ? (
<Anchor
href={formatRootUrl(config.urls.route, url.vanity ?? url.code)}
target='_blank'
rel='noopener noreferrer'
>
{url.vanity ?? url.code}
</Anchor>
) : (
<Text>{url.vanity ?? url.code}</Text>
)}
</Text>
{url.enabled ? (
<Anchor
href={formatRootUrl(config.urls.route, url.vanity ?? url.code)}
target='_blank'
rel='noopener noreferrer'
fw={400}
>
{url.vanity ?? url.code}
</Anchor>
) : (
<Text fw={400}>{url.vanity ?? url.code}</Text>
)}
<Menu withinPortal position='bottom-end' shadow='sm'>
<Group gap={2}>
@@ -55,6 +62,9 @@ export default function UserCard({ url, setSelectedUrl }: { url: Url; setSelecte
>
Copy destination
</Menu.Item>
<Menu.Item leftSection={<IconQrcode size='1rem' />} onClick={() => setQrOpen(url)}>
Show QR code
</Menu.Item>
<Menu.Item leftSection={<IconPencil size='1rem' />} onClick={() => setSelectedUrl(url)}>
Edit
</Menu.Item>
+6 -5
View File
@@ -3,7 +3,8 @@ import type { SafeConfig } from '@/lib/config/safe';
import { Url } from '@/lib/db/models/url';
import { fetchApi } from '@/lib/fetchApi';
import { formatRootUrl } from '@/lib/url';
import { conditionalWarning } from '@/lib/warningModal';
import { conditionalWarning } from '@/lib/client/warningModal';
import { getDomain } from '@/lib/client/webDomain';
import { Anchor } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
@@ -20,15 +21,15 @@ export async function deleteUrl(warnDeletion: boolean, url: Url) {
}
export function copyUrl(url: Url, config: SafeConfig, clipboard: ReturnType<typeof useClipboard>) {
const domain = `${window.location.protocol}//${window.location.host}`;
const urlFormatted = getDomain(formatRootUrl(config.urls.route, url.vanity ?? url.code));
clipboard.copy(`${domain}${formatRootUrl(config.urls.route, url.vanity ?? url.code)}`);
clipboard.copy(urlFormatted);
notifications.show({
title: 'Copied link',
message: (
<Anchor component={Link} to={formatRootUrl(config.urls.route, url.vanity ?? url.code)}>
{`${domain}${formatRootUrl(config.urls.route, url.vanity ?? url.code)}`}
<Anchor component={Link} to={urlFormatted}>
{urlFormatted}
</Anchor>
),
color: 'green',
+1 -1
View File
@@ -1,9 +1,9 @@
import DomainSelect from '@/components/DomainSelect';
import GridTableSwitcher from '@/components/GridTableSwitcher';
import { Response } from '@/lib/api/response';
import { useViewStore } from '@/lib/client/store/view';
import { Url } from '@/lib/db/models/url';
import { fetchApi } from '@/lib/fetchApi';
import { useViewStore } from '@/lib/store/view';
import {
ActionIcon,
Anchor,
@@ -6,14 +6,24 @@ import { useState } from 'react';
import useSWR from 'swr';
import EditUrlModal from '../EditUrlModal';
import UrlCard from '../UrlCard';
import QRCodeModal from '@/components/QRCodeModal';
import { formatRootUrl } from '@/lib/url';
import { useConfig } from '@/components/ConfigProvider';
export default function UrlGridView() {
const config = useConfig();
const { data: urls, isLoading } = useSWR<Extract<Response['/api/user/urls'], Url[]>>('/api/user/urls');
const [selectedUrl, setSelectedUrl] = useState<Url | null>(null);
const [qrOpen, setQrOpen] = useState<Url | null>(null);
return (
<>
<EditUrlModal url={selectedUrl} onClose={() => setSelectedUrl(null)} />
<QRCodeModal
url={qrOpen ? formatRootUrl(config.urls.route, qrOpen.vanity ?? qrOpen.code) : ''}
opened={!!qrOpen}
onClose={() => setQrOpen(null)}
/>
{isLoading ? (
<SimpleGrid
@@ -42,7 +52,7 @@ export default function UrlGridView() {
pos='relative'
>
{urls?.map((url) => (
<UrlCard setSelectedUrl={setSelectedUrl} key={url.id} url={url} />
<UrlCard setSelectedUrl={setSelectedUrl} setQrOpen={setQrOpen} key={url.id} url={url} />
))}
</SimpleGrid>
) : (
@@ -6,12 +6,13 @@ import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import { useEffect, useMemo, useReducer, useState } from 'react';
import useSWR from 'swr';
import { copyUrl, deleteUrl } from '../actions';
import { IconCopy, IconPencil, IconTrashFilled } from '@tabler/icons-react';
import { IconCopy, IconPencil, IconQrcode, IconTrashFilled } from '@tabler/icons-react';
import { useConfig } from '@/components/ConfigProvider';
import { useClipboard } from '@mantine/hooks';
import { useSettingsStore } from '@/lib/store/settings';
import { useSettingsStore } from '@/lib/client/store/settings';
import { formatRootUrl, trimUrl } from '@/lib/url';
import EditUrlModal from '../EditUrlModal';
import QRCodeModal from '@/components/QRCodeModal';
const NAMES = {
code: 'Code',
@@ -141,9 +142,16 @@ export default function UrlTableView() {
}
}, [searchField]);
const [qrOpen, setQrOpen] = useState<Url | null>(null);
return (
<>
<EditUrlModal url={selectedUrl} onClose={() => setSelectedUrl(null)} />
<QRCodeModal
url={qrOpen ? formatRootUrl(config.urls.route, qrOpen.vanity ?? qrOpen.code) : ''}
opened={!!qrOpen}
onClose={() => setQrOpen(null)}
/>
<Box my='sm'>
<DataTable
@@ -252,6 +260,16 @@ export default function UrlTableView() {
<IconCopy size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Show QR Code'>
<ActionIcon
onClick={(e) => {
e.stopPropagation();
setQrOpen(url);
}}
>
<IconQrcode size='1rem' />
</ActionIcon>
</Tooltip>
<Tooltip label='Edit URL'>
<ActionIcon
onClick={(e) => {
+1 -1
View File
@@ -4,7 +4,7 @@ import { bytes } from '@/lib/bytes';
import { User } from '@/lib/db/models/user';
import { fetchApi } from '@/lib/fetchApi';
import { canInteract } from '@/lib/role';
import { useUserStore } from '@/lib/store/user';
import { useUserStore } from '@/lib/client/store/user';
import {
ActionIcon,
Button,
+1 -1
View File
@@ -1,6 +1,6 @@
import { User } from '@/lib/db/models/user';
import { ActionIcon, Avatar, Card, Group, Menu, Stack, Text } from '@mantine/core';
import { useUserStore } from '@/lib/store/user';
import { useUserStore } from '@/lib/client/store/user';
import { IconDots, IconFiles, IconTrashFilled, IconUserEdit } from '@tabler/icons-react';
import EditUserModal from './EditUserModal';
import { useState } from 'react';
+2 -2
View File
@@ -1,7 +1,7 @@
import { type loader } from '@/client/pages/dashboard/admin/users/[id]/files';
import GridTableSwitcher from '@/components/GridTableSwitcher';
import useObjectState from '@/lib/hooks/useObjectState';
import { useViewStore } from '@/lib/store/view';
import useObjectState from '@/lib/client/hooks/useObjectState';
import { useViewStore } from '@/lib/client/store/view';
import { ActionIcon, Group, Title, Tooltip } from '@mantine/core';
import { IconArrowBackUp, IconGridPatternFilled, IconTableOptions } from '@tabler/icons-react';
import { Link, useLoaderData } from 'react-router-dom';
+2 -2
View File
@@ -4,8 +4,8 @@ import { readToDataURL } from '@/lib/base64';
import { User } from '@/lib/db/models/user';
import { fetchApi } from '@/lib/fetchApi';
import { canInteract } from '@/lib/role';
import { useUserStore } from '@/lib/store/user';
import { useViewStore } from '@/lib/store/view';
import { useUserStore } from '@/lib/client/store/user';
import { useViewStore } from '@/lib/client/store/view';
import {
ActionIcon,
Button,
@@ -1,8 +1,8 @@
import RelativeDate from '@/components/RelativeDate';
import { Response } from '@/lib/api/response';
import { useUserStore } from '@/lib/client/store/user';
import { User } from '@/lib/db/models/user';
import { canInteract, roleName } from '@/lib/role';
import { useUserStore } from '@/lib/store/user';
import { ActionIcon, Avatar, Box, Group, Tooltip } from '@mantine/core';
import { IconEdit, IconFiles, IconTrashFilled } from '@tabler/icons-react';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
+1 -1
View File
@@ -1,4 +1,4 @@
import { RenderMode } from '@/components/pages/upload/renderMode';
import { RenderMode } from './renderMode';
import { Alert, Button } from '@mantine/core';
import { IconEyeFilled } from '@tabler/icons-react';
import { useState } from 'react';
+4 -1
View File
@@ -6,6 +6,7 @@ import { FixedSizeList as List } from 'react-window';
import { useLocation } from 'react-router-dom';
import './HighlightCode.theme.scss';
import * as sanitize from 'isomorphic-dompurify';
export default function HighlightCode({ language, code }: { language: string; code: string }) {
const { pathname } = useLocation();
@@ -69,7 +70,9 @@ export default function HighlightCode({ language, code }: { language: string; co
<code
className='theme hljs'
style={{ flex: 1, fontSize: '0.8rem' }}
dangerouslySetInnerHTML={{ __html: hlLines[index] }}
dangerouslySetInnerHTML={{
__html: sanitize.sanitize(hlLines[index], { USE_PROFILES: { html: true } }),
}}
/>
</div>
);

Some files were not shown because too many files have changed in this diff Show More