Compare commits

..

3 Commits

Author SHA1 Message Date
dicedtomato c7df4a578b feat: script to add file sizes 2023-03-04 04:36:06 +00:00
dicedtomato 38e30b2525 Merge branch 'trunk' into feature/file-size 2023-03-04 04:22:59 +00:00
dicedtomato 535600edc8 feat: baseline support for file sizes 2023-02-26 05:37:20 +00:00
233 changed files with 7225 additions and 13546 deletions
-12
View File
@@ -1,12 +0,0 @@
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
RUN usermod -l zipline node \
&& groupmod -n zipline node \
&& usermod -d /home/zipline zipline \
&& echo "zipline ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/zipline \
&& chmod 0440 /etc/sudoers.d/zipline \
&& sudo apt-get update && apt-get install gnupg2 -y
EXPOSE 3000
USER zipline
+14 -29
View File
@@ -2,15 +2,12 @@
"name": "Zipline Codespace",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/zipline",
"workspaceFolder": "/workspace",
"forwardPorts": [3000, 5432],
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"username": "zipline"
},
"ghcr.io/devcontainers/features/docker-in-docker:1": {
"dockerDashComposeVersion": "v2",
"installDockerBuildx": true
}
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/node:1": {}
},
"customizations": {
"vscode": {
@@ -23,34 +20,22 @@
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.autoSave": "afterDelay",
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.defaultProfile.linux": "zsh",
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/bin/zsh",
"env": {
"ZSH_THEME": "devcontainers"
}
}
}
"files.autoSave": "afterDelay"
},
"extensions": ["prisma.prisma", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}
},
"remoteUser": "zipline",
"updateRemoteUserUID": true,
"remoteEnv": {
"CORE_DATABASE_URL": "postgres://postgres:postgres@db/zip10"
"CORE_DATABASE_URL": "postgres://postgres:postgres@localhost/zip10"
},
"portsAttributes": {
"3000": {
"label": "Zipline",
"3000": {
"label": "Zipline",
"onAutoForward": "openBrowser"
},
"5432": {
"label": "Postgres"
}
},
"postCreateCommand": "sudo chown -R zipline:zipline /zipline && yarn install"
},
"5432": {
"label": "Postgres"
}
}
}
+5 -8
View File
@@ -1,14 +1,12 @@
version: '3.8'
services:
app:
build:
context: ./
dockerfile: Dockerfile
image: mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
volumes:
- ../:/zipline:cached
- uploads:/zipline/uploads
- node_modules:/zipline/node_modules
- ..:/workspace:cached
network_mode: service:db
command: sleep infinity
user: zipline
db:
image: postgres:latest
restart: unless-stopped
@@ -21,5 +19,4 @@ services:
volumes:
pg_data:
uploads:
node_modules:
-2
View File
@@ -2,8 +2,6 @@ node_modules/
.next/
uploads/
.git/
!.git/refs
!.git/HEAD
.yarn/*
!.yarn/releases
!.yarn/plugins
+21 -18
View File
@@ -1,43 +1,46 @@
# every field in here is optional except, CORE_SECRET and CORE_DATABASE_URL.
# if CORE_SECRET is still "changethis" then zipline will exit and tell you to change it.
# if using s3 make sure to uncomment or comment out the correct lines needed.
# if using s3/supabase make sure to comment out the other datasources
CORE_RETURN_HTTPS=true
CORE_HTTPS=true
CORE_SECRET="changethis"
CORE_HOST=0.0.0.0
CORE_PORT=3000
CORE_DATABASE_URL="postgres://postgres:postgres@db/zip10"
CORE_DATABASE_URL="postgres://postgres:postgres@localhost/zip10"
CORE_LOGGER=false
CORE_STATS_INTERVAL=1800
CORE_INVITES_INTERVAL=1800
CORE_THUMBNAILS_INTERVAL=600
# default
DATASOURCE_TYPE=local
DATASOURCE_LOCAL_DIRECTORY=./uploads
# or you can choose to use s3
# DATASOURCE_TYPE=s3
# DATASOURCE_S3_ACCESS_KEY_ID=key
# DATASOURCE_S3_SECRET_ACCESS_KEY=secret
# DATASOURCE_S3_BUCKET=bucket
# DATASOURCE_S3_ENDPOINT=s3.amazonaws.com
# DATASOURCE_S3_REGION=us-west-2
# DATASOURCE_S3_FORCE_S3_PATH=false
# DATASOURCE_S3_USE_SSL=false
DATASOURCE_TYPE=s3
DATASOURCE_S3_ACCESS_KEY_ID=key
DATASOURCE_S3_SECRET_ACCESS_KEY=secret
DATASOURCE_S3_BUCKET=bucket
DATASOURCE_S3_ENDPOINT=s3.amazonaws.com
DATASOURCE_S3_REGION=us-west-2
DATASOURCE_S3_FORCE_S3_PATH=false
DATASOURCE_S3_USE_SSL=false
# or supabase
DATASOURCE_TYPE=supabase
DATASOURCE_SUPABASE_KEY=xxx
# remember: no leading slash
DATASOURCE_SUPABASE_URL=https://something.supabase.co
DATASOURCE_SUPABASE_BUCKET=zipline
UPLOADER_DEFAULT_FORMAT=RANDOM
UPLOADER_ROUTE=/u
UPLOADER_LENGTH=6
UPLOADER_ADMIN_LIMIT=104900000
UPLOADER_USER_LIMIT=104900000
UPLOADER_DISABLED_EXTENSIONS=someext,anotherext
UPLOADER_DISABLED_EXTENSIONS=someext
URLS_ROUTE=/go
URLS_LENGTH=6
RATELIMIT_USER=5
RATELIMIT_ADMIN=3
# for more variables checkout the docs
RATELIMIT_USER = 5
RATELIMIT_ADMIN = 3
-7
View File
@@ -1,7 +0,0 @@
node_modules
dist
.yarn
.devcontainer
.github
.next
.vscode
+2 -18
View File
@@ -1,13 +1,5 @@
{
"root": true,
"extends": [
"next",
"next/core-web-vitals",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["unused-imports", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": [
@@ -36,14 +28,6 @@
"react/style-prop-object": "warn",
"@next/next/no-img-element": "off",
"jsx-a11y/alt-text": "off",
"react/display-name": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
],
"@typescript-eslint/ban-ts-comment": "off"
"react/display-name": "off"
}
}
-3
View File
@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: diced
+2 -2
View File
@@ -15,10 +15,10 @@ body:
id: version
attributes:
label: Version
description: What version (or docker image) of Zipline are you using?
description: What version of Zipline are you using?
options:
- latest (ghcr.io/diced/zipline or ghcr.io/diced/zipline:latest)
- upstream (ghcr.io/diced/zipline:trunk)
- latest (ghcr.io/diced/zipline:latest)
- other (provide version in additional info)
validations:
required: true
+1 -4
View File
@@ -1,11 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/diced/zipline/discussions/new?category=ideas&title=Your%20brief%20description%20here&labels=feature
about: Ask for a new feature
- name: Zipline Discord
url: https://discord.gg/EAhCRfGxCF
about: Ask for help with anything related to Zipline!
- name: Zipline Docs
url: https://zipline.diced.sh
url: https://zipline.diced.tech
about: Maybe take a look a the docs?
+2 -2
View File
@@ -2,9 +2,9 @@ name: 'Build'
on:
push:
branches: [ v3 ]
branches: [ trunk ]
pull_request:
branches: [ v3 ]
branches: [ trunk ]
workflow_dispatch:
jobs:
+6 -14
View File
@@ -3,7 +3,7 @@ name: 'Push Release Docker Images'
on:
push:
tags:
- 'v3.*.*'
- 'v*.*.*'
paths:
- 'src/**'
- 'server/**'
@@ -13,8 +13,8 @@ on:
workflow_dispatch:
jobs:
push:
name: Push Release Image
push_to_ghcr:
name: Push Release Image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Check out the repo
@@ -32,28 +32,20 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Packages
- name: Login to Github Packages
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Docker Image
- name: Build Docker Image
uses: docker/build-push-action@v3
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/diced/zipline:v3
ghcr.io/diced/zipline:latest
ghcr.io/diced/zipline:${{ steps.version.outputs.zipline_version }}
${{ secrets.DOCKERHUB_USERNAME }}/zipline:v3
${{ secrets.DOCKERHUB_USERNAME }}/zipline:${{ steps.version.outputs.zipline_version }}
cache-from: type=gha
cache-to: type=gha,mode=max
+7 -17
View File
@@ -2,7 +2,7 @@ name: 'Push Docker Images'
on:
push:
branches: [ v3 ]
branches: [ trunk ]
paths:
- 'src/**'
- 'server/**'
@@ -12,8 +12,8 @@ on:
workflow_dispatch:
jobs:
push:
name: Push Commit Image
push_to_ghcr:
name: Push Image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Check out the repo
@@ -22,7 +22,7 @@ jobs:
- name: Get version
id: version
run: |
echo "zipline_commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "zipline_version=$(jq .version package.json -r)" >> $GITHUB_OUTPUT
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
@@ -38,23 +38,13 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Docker Image
- name: Build Docker Image
uses: docker/build-push-action@v3
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/diced/zipline:v3-trunk
ghcr.io/diced/zipline:v3-trunk-${{ steps.version.outputs.zipline_commit }}
${{ secrets.DOCKERHUB_USERNAME }}/zipline:v3-trunk
${{ secrets.DOCKERHUB_USERNAME }}/zipline:v3-trunk-${{ steps.version.outputs.zipline_commit }}
ghcr.io/diced/zipline:trunk
ghcr.io/diced/zipline:trunk-${{ steps.version.outputs.zipline_version }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
-1
View File
@@ -31,7 +31,6 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
-1
View File
@@ -1 +0,0 @@
package-lock=false
Vendored Regular → Executable
View File
+1 -1
View File
@@ -14,7 +14,7 @@ Create an issue on GitHub, please include the following (if one of them is not a
Create an discussion on GitHub, please include the following:
- Brief explanation of the feature in the title (very brief please)
- Breif explanation of the feature in the title (very breif please)
- How it would work (detailed, but optional)
## Pull Requests (contributions to the codebase)
+26 -30
View File
@@ -1,14 +1,22 @@
# Use the Prisma binaries image as the first stage
FROM ghcr.io/diced/prisma-binaries:5.1.x AS prisma
FROM ghcr.io/diced/prisma-binaries:4.10.x as prisma
# Use Alpine Linux as the second stage
FROM node:18-alpine3.16 AS base
FROM node:18-alpine3.16 as base
# Set the working directory
WORKDIR /zipline
# Copy the necessary files from the project
COPY prisma ./prisma
COPY src ./src
COPY next.config.js ./next.config.js
COPY tsup.config.ts ./tsup.config.ts
COPY tsconfig.json ./tsconfig.json
COPY mimes.json ./mimes.json
COPY public ./public
FROM base as builder
COPY .yarn ./.yarn
COPY package*.json ./
@@ -18,62 +26,50 @@ COPY .yarnrc.yml ./
# Copy the prisma binaries from prisma stage
COPY --from=prisma /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_SCHEMA_ENGINE_BINARY=/prisma-engines/schema-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary \
ZIPLINE_DOCKER_BUILD=true \
NEXT_TELEMETRY_DISABLED=1
# Install production dependencies then temporarily save
RUN yarn workspaces focus --production --all
RUN cp -RL node_modules /tmp/node_modules
# Install the dependencies
RUN yarn install --immutable
FROM base AS builder
COPY .git/refs ./.git/refs
COPY .git/HEAD ./.git/HEAD
COPY src ./src
COPY next.config.js ./next.config.js
COPY tsup.config.ts ./tsup.config.ts
COPY tsconfig.json ./tsconfig.json
COPY mimes.json ./mimes.json
COPY public ./public
# Run the build
RUN yarn build
# Use Alpine Linux as the final image
FROM base
# Install the necessary packages
RUN apk add --no-cache perl procps tini
COPY --from=prisma /prisma-engines /prisma-engines
COPY --from=builder /prisma-engines /prisma-engines
ENV PRISMA_QUERY_ENGINE_BINARY=/prisma-engines/query-engine \
PRISMA_SCHEMA_ENGINE_BINARY=/prisma-engines/schema-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary \
ZIPLINE_DOCKER_BUILD=true \
NEXT_TELEMETRY_DISABLED=1
# Copy only the necessary files from the previous stage
COPY --from=builder /zipline/.git/refs ./.git/refs
COPY --from=builder /zipline/.git/HEAD ./.git/HEAD
COPY --from=builder /zipline/dist ./dist
COPY --from=builder /zipline/.next ./.next
COPY --from=builder /zipline/package.json ./package.json
COPY --from=builder /zipline/mimes.json ./mimes.json
COPY --from=builder /zipline/next.config.js ./next.config.js
COPY --from=builder /zipline/public ./public
COPY --from=builder /zipline/node_modules ./node_modules
COPY --from=builder /zipline/node_modules/.prisma/client ./node_modules/.prisma/client
COPY --from=builder /zipline/node_modules/@prisma/client ./node_modules/@prisma/client
# Copy Startup Script
COPY docker-entrypoint.sh /zipline
# Make Startup Script Executable
RUN chmod a+x /zipline/docker-entrypoint.sh && rm -rf /zipline/src
# Clean up
RUN rm -rf /tmp/* /root/*
RUN yarn cache clean --all
RUN chmod a+x /zipline/docker-entrypoint.sh
# Set the entrypoint to the startup script
ENTRYPOINT ["tini", "--", "/zipline/docker-entrypoint.sh"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 dicedtomato
Copyright (c) 2022 dicedtomato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+11 -24
View File
@@ -25,7 +25,7 @@ A ShareX/file upload server that is easy to use, packed with features, and with
- Password Protected Uploads
- URL shortening
- Text uploading
- URL Formats (uuid, dates, random alphanumeric, original name, zws, gfycat -> [animals](https://assets.gfycat.com/animals) [adjectives](https://assets.gfycat.com/adjectives))
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
- Discord embeds (OG metadata)
- Gallery viewer, and multiple file format support
- Code highlighting
@@ -35,8 +35,7 @@ A ShareX/file upload server that is easy to use, packed with features, and with
- User invites
- File Chunking (for large files)
- File deletion once it reaches a certain amount of views
- Automatic video thumbnail generation
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker compose up -d`)
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
<details>
<summary><h2>Screenshots (click)</h2></summary>
@@ -52,13 +51,13 @@ A ShareX/file upload server that is easy to use, packed with features, and with
## Install & run with Docker
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker compose](https://docs.docker.com/compose/install/).
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
```shell
git clone https://github.com/diced/zipline
cd zipline
docker compose up -d
docker-compose up -d
```
### After installing
@@ -68,18 +67,17 @@ Ways you could generate the string could be from a password managers generator,
## Building & running from source
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/).
It is recommended to not use npm, as it can cause issues with the build process.
Before you run `yarn build`, you might want to configure Zipline, as when building from source Zipline will need to read some sort of configuration. The only two variables needed are `CORE_SECRET` and `CORE_DATABASE_URL`.
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
```shell
git clone https://github.com/diced/zipline
cd zipline
# npm install
yarn install
# npm run build
yarn build
# npm start
yarn start
```
@@ -112,7 +110,7 @@ This section requires [ShareX](https://www.getsharex.com/).
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/guides/uploaders/sharex)
# Flameshot (Linux(Xorg/Wayland) and macOS)
# Flameshot (Linux)
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
@@ -121,19 +119,12 @@ This section requires [Flameshot](https://www.flameshot.org/), [jq](https://sted
If using wayland you will need to have [wl-clipboard](https://github.com/bugaevc/wl-clipboard) installed, for the `wl-copy` command.
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based or wlroots-compatible compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
If you are not using GNOME/KDE/Qtile/Sway, and are using something like a wlroots-based compositor (ex. [Hyprland](https://github.com/hyprwm/Hyprland/), [River](https://github.com/riverwm/river), etc), you will need to set the `XDG_CURRENT_DESKTOP` environment variable to `sway`, which will just override it for this script. Adding `export XDG_CURRENT_DESKTOP=sway` to the start of the script will work.
After this, replace the `xsel -ib` with `wl-copy` in the script.
</details>
<details>
<summary>Mac instructions</summary>
If using macOS, you can replace the `xsel -ib` with `pbcopy` in the script.
</details>
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
To upload files using flameshot we will use a script. Replace $TOKEN and $HOST with your own values, you probably know how to do this if you use linux.
@@ -141,7 +132,7 @@ To upload files using flameshot we will use a script. Replace $TOKEN and $HOST w
DATE=$(date '+%h_%Y_%d_%I_%m_%S.png');
flameshot gui -r > ~/Pictures/$DATE;
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r '.files[0]' | xsel -ib
curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@$1 $HOST/api/upload | jq -r 'files[0].url' | xsel -ib
```
# Contributing
@@ -166,7 +157,3 @@ Create a discussion on GitHub, please include the following:
## Pull Requests (contributions to the codebase)
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
# Documentation
Documentation source code is located in [diced/zipline-docs](https://github.com/diced/zipline-docs), and can be accessed [here](https://zipline.diced.sh/docs/get-started).
+3 -2
View File
@@ -1,3 +1,4 @@
version: '3'
services:
postgres:
image: postgres:15
@@ -22,8 +23,8 @@ services:
env_file:
- .env.local
volumes:
- './uploads:/zipline/uploads'
- './public:/zipline/public'
- '$PWD/uploads:/zipline/uploads'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
+2 -1
View File
@@ -1,3 +1,4 @@
version: '3'
services:
postgres:
image: postgres:15
@@ -28,7 +29,7 @@ services:
- CORE_LOGGER=true
volumes:
- './uploads:/zipline/uploads'
- './public:/zipline/public'
- '$PWD/public:/zipline/public'
depends_on:
- 'postgres'
-2
View File
@@ -2,6 +2,4 @@
set -e
unset ZIPLINE_DOCKER_BUILD
node --enable-source-maps dist/index.js
-3
View File
@@ -42,9 +42,6 @@
["afm", ["application/octet-stream"]],
["afp", ["application/vnd.ibm.modcap"]],
["ahead", ["application/vnd.ahead.space"]],
["ahk", ["text/autohotkey"]],
["ahk1", ["text/autohotkey"]],
["ahk2", ["text/autohotkey"]],
["ai", ["application/postscript"]],
["aif", ["audio/aiff"]],
["aifc", ["audio/aiff"]],
+58 -64
View File
@@ -1,6 +1,6 @@
{
"name": "zipline",
"version": "3.7.13",
"version": "3.7.0-rc4",
"license": "MIT",
"scripts": {
"dev": "npm-run-all build:server dev:run",
@@ -14,87 +14,81 @@
"migrate:dev": "prisma migrate dev --create-only",
"start": "node dist",
"lint": "next lint",
"compose:up": "docker compose up",
"compose:down": "docker compose down",
"compose:build-dev": "docker compose --file docker-compose.dev.yml up --build",
"compose:up-dev": "docker compose --file docker-compose.dev.yml up",
"compose:down-dev": "docker compose --file docker-compose.dev.yml down",
"docker:up": "docker-compose up",
"docker:down": "docker-compose down",
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
"docker:up-dev": "docker-compose --file docker-compose.dev.yml up",
"docker:down-dev": "docker-compose --file docker-compose.dev.yml down",
"scripts:read-config": "node --enable-source-maps dist/scripts/read-config",
"scripts:import-dir": "node --enable-source-maps dist/scripts/import-dir",
"scripts:list-users": "node --enable-source-maps dist/scripts/list-users",
"scripts:set-user": "node --enable-source-maps dist/scripts/set-user",
"scripts:clear-zero-byte": "node --enable-source-maps dist/scripts/clear-zero-byte",
"scripts:query-size": "node --enable-source-maps dist/scripts/query-size",
"scripts:clear-temp": "node --enable-source-maps dist/scripts/clear-temp"
"scripts:query-size": "node --enable-source-maps dist/scripts/query-size"
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@mantine/core": "6.x",
"@mantine/dropzone": "6.x",
"@mantine/form": "6.x",
"@mantine/hooks": "6.x",
"@mantine/modals": "6.x",
"@mantine/next": "6.x",
"@mantine/notifications": "6.x",
"@mantine/prism": "6.x",
"@mantine/spotlight": "6.x",
"@prisma/client": "^5.1.1",
"@prisma/internals": "^5.1.1",
"@prisma/migrate": "^5.1.1",
"@sapphire/shapeshift": "^3.9.3",
"@tabler/icons-react": "^2.41.0",
"@tanstack/react-query": "^4.28.0",
"argon2": "^0.31.2",
"cookie": "^0.6.0",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",
"@mantine/core": "^5.10.5",
"@mantine/dropzone": "^5.10.5",
"@mantine/form": "^5.10.5",
"@mantine/hooks": "^5.10.5",
"@mantine/modals": "^5.10.5",
"@mantine/next": "^5.10.5",
"@mantine/notifications": "^5.10.5",
"@mantine/prism": "^5.10.5",
"@prisma/client": "^4.10.1",
"@prisma/internals": "^4.10.1",
"@prisma/migrate": "^4.10.1",
"@sapphire/shapeshift": "^3.8.1",
"@tanstack/react-query": "^4.24.10",
"argon2": "^0.30.3",
"cookie": "^0.5.0",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"dotenv-expand": "^10.0.0",
"exiftool-vendored": "^23.4.0",
"fastify": "^4.24.3",
"fastify-plugin": "^4.5.1",
"fflate": "^0.8.1",
"ffmpeg-static": "^5.2.0",
"find-my-way": "^7.7.0",
"katex": "^0.16.9",
"mantine-datatable": "^2.9.14",
"minio": "^7.1.3",
"exiftool-vendored": "^21.2.0",
"fastify": "^4.13.0",
"fastify-plugin": "^4.5.0",
"fflate": "^0.7.4",
"find-my-way": "^7.5.0",
"katex": "^0.16.4",
"mantine-datatable": "^1.8.6",
"minio": "^7.0.32",
"ms": "canary",
"multer": "^1.4.5-lts.1",
"next": "^14.0.3",
"next": "^13.2.1",
"otplib": "^12.0.1",
"prisma": "^5.1.1",
"prisma": "^4.10.1",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"qrcode": "^1.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.3",
"recharts": "^2.10.1",
"recoil": "^0.7.7",
"remark-gfm": "^4.0.1",
"sharp": "^0.32.6"
"react-feather": "^2.0.10",
"react-markdown": "^8.0.5",
"recharts": "^2.4.3",
"recoil": "^0.7.6",
"remark-gfm": "^3.0.1",
"sharp": "^0.31.3"
},
"devDependencies": {
"@types/cookie": "^0.5.4",
"@types/katex": "^0.16.6",
"@types/minio": "^7.1.1",
"@types/multer": "^1.4.10",
"@types/node": "18",
"@types/qrcode": "^1.5.5",
"@types/react": "^18.2.37",
"@types/sharp": "^0.32.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@types/cookie": "^0.5.1",
"@types/katex": "^0.16.0",
"@types/minio": "^7.0.16",
"@types/multer": "^1.4.7",
"@types/node": "^18.14.2",
"@types/qrcode": "^1.5.0",
"@types/react": "^18.0.28",
"@types/sharp": "^0.31.1",
"cross-env": "^7.0.3",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"eslint": "^8.35.0",
"eslint-config-next": "^13.2.1",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.1.0",
"tsup": "^8.0.0",
"typescript": "^5.2.2"
"prettier": "^2.8.4",
"tsup": "^6.6.3",
"typescript": "^4.9.5"
},
"repository": {
"type": "git",
@@ -1,11 +0,0 @@
/*
Warnings:
- You are about to drop the column `format` on the `File` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "File" DROP COLUMN "format";
-- DropEnum
DROP TYPE "FileNameFormat";
@@ -1,18 +0,0 @@
-- CreateEnum
CREATE TYPE "ProcessingStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETE');
-- CreateTable
CREATE TABLE "IncompleteFile" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"status" "ProcessingStatus" NOT NULL,
"chunks" INTEGER NOT NULL,
"chunksComplete" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"data" JSONB NOT NULL,
CONSTRAINT "IncompleteFile_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "IncompleteFile" ADD CONSTRAINT "IncompleteFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,53 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[uuid]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- PRISMA GENERATED BELOW
-- -- DropForeignKey
-- ALTER TABLE "OAuth" DROP CONSTRAINT "OAuth_userId_fkey";
--
-- -- AlterTable
-- ALTER TABLE "OAuth" ALTER COLUMN "userId" SET DATA TYPE TEXT;
--
-- -- AlterTable
-- ALTER TABLE "User" ADD COLUMN "uuid" UUID NOT NULL DEFAULT gen_random_uuid();
--
-- -- CreateIndex
-- CREATE UNIQUE INDEX "User_uuid_key" ON "User"("uuid");
--
-- -- AddForeignKey
-- ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
-- User made changes below
-- Rename old foreign key
ALTER TABLE "OAuth" RENAME CONSTRAINT "OAuth_userId_fkey" TO "OAuth_userId_old_fkey";
-- Rename old column
ALTER TABLE "OAuth" RENAME COLUMN "userId" TO "userId_old";
-- Add new column
ALTER TABLE "OAuth" ADD COLUMN "userId" UUID;
-- Add user uuid
ALTER TABLE "User" ADD COLUMN "uuid" UUID NOT NULL DEFAULT gen_random_uuid();
-- Update table "OAuth" with uuid
UPDATE "OAuth" SET "userId" = "User"."uuid" FROM "User" WHERE "OAuth"."userId_old" = "User"."id";
-- Alter table "OAuth" to make "userId" required
ALTER TABLE "OAuth" ALTER COLUMN "userId" SET NOT NULL;
-- Create index
CREATE UNIQUE INDEX "User_uuid_key" ON "User"("uuid");
-- Add new foreign key
ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
-- Drop old foreign key
ALTER TABLE "OAuth" DROP CONSTRAINT "OAuth_userId_old_fkey";
-- Drop old column
ALTER TABLE "OAuth" DROP COLUMN "userId_old";
@@ -1,16 +0,0 @@
-- CreateTable
CREATE TABLE "Thumbnail" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"fileId" INTEGER NOT NULL,
CONSTRAINT "Thumbnail_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Thumbnail_fileId_key" ON "Thumbnail"("fileId");
-- AddForeignKey
ALTER TABLE "Thumbnail" ADD CONSTRAINT "Thumbnail_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "File" ALTER COLUMN "size" SET DATA TYPE BIGINT;
@@ -1,14 +0,0 @@
-- CreateTable
CREATE TABLE "Export" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"complete" BOOLEAN NOT NULL DEFAULT false,
"path" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "Export_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Export" ADD CONSTRAINT "Export_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+28 -67
View File
@@ -8,39 +8,23 @@ generator client {
}
model User {
id Int @id @default(autoincrement())
uuid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
username String
password String?
avatar String?
token String
administrator Boolean @default(false)
superAdmin Boolean @default(false)
systemTheme String @default("system")
embed Json @default("{}")
ratelimit DateTime?
totpSecret String?
domains String[]
oauth OAuth[]
files File[]
urls Url[]
Invite Invite[]
Folder Folder[]
IncompleteFile IncompleteFile[]
Exports Export[]
}
model Export {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
complete Boolean @default(false)
path String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
id Int @id @default(autoincrement())
username String
password String?
avatar String?
token String
administrator Boolean @default(false)
superAdmin Boolean @default(false)
systemTheme String @default("system")
embed Json @default("{}")
ratelimit DateTime?
totpSecret String?
domains String[]
oauth OAuth[]
files File[]
urls Url[]
Invite Invite[]
Folder Folder[]
}
model Folder {
@@ -56,13 +40,20 @@ model Folder {
files File[]
}
enum FileNameFormat {
UUID
DATE
RANDOM
NAME
}
model File {
id Int @id @default(autoincrement())
name String
originalName String?
mimetype String @default("image/png")
createdAt DateTime @default(now())
size BigInt @default(0)
size Int @default(0)
expiresAt DateTime?
maxViews Int?
views Int @default(0)
@@ -70,23 +61,13 @@ model File {
embed Boolean @default(false)
password String?
invisible InvisibleFile?
format FileNameFormat @default(RANDOM)
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId Int?
folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
folderId Int?
thumbnail Thumbnail?
}
model Thumbnail {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
name String
fileId Int @unique
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
}
model InvisibleFile {
@@ -138,8 +119,8 @@ model Invite {
model OAuth {
id Int @id @default(autoincrement())
provider OauthProviders
user User @relation(fields: [userId], references: [uuid], onDelete: Cascade)
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
username String
oauthId String?
token String
@@ -153,23 +134,3 @@ enum OauthProviders {
GITHUB
GOOGLE
}
model IncompleteFile {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
status ProcessingStatus
chunks Int
chunksComplete Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
data Json
}
enum ProcessingStatus {
PENDING
PROCESSING
COMPLETE
}
File diff suppressed because it is too large Load Diff
-1750
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
import { Anchor } from '@mantine/core';
import Link from 'next/link';
export default function AnchorNext({ href, ...others }) {
return <Anchor component={Link} href={href} {...others} />;
}
+4 -3
View File
@@ -1,15 +1,16 @@
import { createStyles, Textarea } from '@mantine/core';
import { createStyles, MantineSize, Textarea } from '@mantine/core';
import { useEffect } from 'react';
const useStyles = createStyles(() => ({
const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
input: {
fontFamily: 'monospace',
fontSize: theme.fn.size({ size, sizes: theme.fontSizes }) - 2,
height: '80vh',
},
}));
export default function CodeInput({ ...props }) {
const { classes } = useStyles(null, { name: 'CodeInput' });
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
+398
View File
@@ -0,0 +1,398 @@
import {
ActionIcon,
Card,
Group,
LoadingOverlay,
Modal,
Select,
SimpleGrid,
Stack,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { bytesToHuman } from 'lib/utils/bytes';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DownloadIcon,
ExternalLinkIcon,
EyeIcon,
HardDriveIcon,
FileIcon,
FolderMinusIcon,
FolderPlusIcon,
HashIcon,
ImageIcon,
InfoIcon,
StarIcon,
} from './icons';
import MutedText from './MutedText';
import Type from './Type';
export function FileMeta({ Icon, title, subtitle, ...other }) {
return other.tooltip ? (
<Group>
<Icon size={24} />
<Tooltip label={other.tooltip}>
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Tooltip>
</Group>
) : (
<Group>
<Icon size={24} />
<Stack spacing={1}>
<Text>{title}</Text>
<MutedText size='md'>{subtitle}</MutedText>
</Stack>
</Group>
);
}
export default function File({
image,
disableMediaPreview,
exifEnabled,
refreshImages,
reducedActions = false,
}) {
const [open, setOpen] = useState(false);
const [overrideRender, setOverrideRender] = useState(false);
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
const clipboard = useClipboard();
const folders = useFolders();
const loading = deleteFile.isLoading || favoriteFile.isLoading;
const handleDelete = async () => {
deleteFile.mutate(image.id, {
onSuccess: () => {
showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
onSettled: () => {
setOpen(false);
},
});
};
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
favoriteFile.mutate(
{ id: image.id, favorite: !image.favorite },
{
onSuccess: () => {
showNotification({
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
});
},
onError: (res: any) => {
showNotification({
title: 'Failed to favorite file',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
},
}
);
};
const inFolder = image.folderId;
const refresh = () => {
refreshImages();
folders.refetch();
};
const removeFromFolder = async () => {
const res = await useFetch('/api/user/folders/' + image.folderId, 'DELETE', {
file: Number(image.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Removed from folder',
message: res.name,
color: 'green',
icon: <FolderMinusIcon />,
});
} else {
showNotification({
title: 'Failed to remove from folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
};
const addToFolder = async (t) => {
const res = await useFetch('/api/user/folders/' + t, 'POST', {
file: Number(image.id),
});
refresh();
if (!res.error) {
showNotification({
title: 'Added to folder',
message: res.name,
color: 'green',
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to add to folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
};
const createFolder = (t) => {
useFetch('/api/user/folders', 'POST', {
name: t,
add: [Number(image.id)],
}).then((res) => {
refresh();
if (!res.error) {
showNotification({
title: 'Created & added to folder',
message: res.name,
color: 'green',
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to create folder',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
}
});
return { value: t, label: t };
};
return (
<>
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.name}</Title>} size='xl'>
<LoadingOverlay visible={loading} />
<Stack>
<Type
file={image}
src={`/r/${encodeURI(image.name)}`}
alt={image.name}
popup
sx={{ minHeight: 200 }}
style={{ minHeight: 200 }}
disableMediaPreview={false}
overrideRender={overrideRender}
setOverrideRender={setOverrideRender}
/>
<SimpleGrid
my='md'
cols={3}
breakpoints={[
{ maxWidth: 600, cols: 1 },
{ maxWidth: 900, cols: 2 },
{ maxWidth: 1200, cols: 3 },
]}
>
<FileMeta Icon={FileIcon} title='Name' subtitle={image.name} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
<FileMeta Icon={HardDriveIcon} title='Size' subtitle={bytesToHuman(image.size || 0)} />
<FileMeta Icon={EyeIcon} title='Views' subtitle={image?.views?.toLocaleString()} />
{image.maxViews && (
<FileMeta
Icon={EyeIcon}
title='Max views'
subtitle={image?.maxViews?.toLocaleString()}
tooltip={`This file will be deleted after being viewed ${image?.maxViews?.toLocaleString()} times.`}
/>
)}
<FileMeta
Icon={CalendarIcon}
title='Uploaded'
subtitle={relativeTime(new Date(image.createdAt))}
tooltip={new Date(image?.createdAt).toLocaleString()}
/>
{image.expiresAt && !reducedActions && (
<FileMeta
Icon={ClockIcon}
title='Expires'
subtitle={relativeTime(new Date(image.expiresAt))}
tooltip={new Date(image.expiresAt).toLocaleString()}
/>
)}
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
</SimpleGrid>
</Stack>
<Group position='apart' my='md'>
<Group position='left'>
{exifEnabled && !reducedActions && (
<Tooltip label='View Metadata'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/dashboard/metadata/${image.id}`, '_blank')}
>
<InfoIcon />
</ActionIcon>
</Tooltip>
)}
{reducedActions ? null : inFolder && !folders.isLoading ? (
<Tooltip
label={`Remove from folder "${
folders.data.find((f) => f.id === image.folderId)?.name ?? ''
}"`}
>
<ActionIcon
color='red'
variant='filled'
onClick={removeFromFolder}
loading={folders.isLoading}
>
<FolderMinusIcon />
</ActionIcon>
</Tooltip>
) : (
<Tooltip label='Add to folder'>
<Select
onChange={addToFolder}
placeholder='Add to folder'
data={[
...(folders.data ? folders.data : []).map((folder) => ({
value: String(folder.id),
label: `${folder.id}: ${folder.name}`,
})),
]}
searchable
creatable
getCreateLabel={(query) => `Create folder "${query}"`}
onCreate={createFolder}
/>
</Tooltip>
)}
</Group>
<Group position='right'>
{reducedActions ? null : (
<>
<Tooltip label='Delete file'>
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
<DeleteIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={image.favorite ? 'Unfavorite' : 'Favorite'}>
<ActionIcon
color={image.favorite ? 'yellow' : 'gray'}
variant='filled'
onClick={handleFavorite}
>
<StarIcon />
</ActionIcon>
</Tooltip>
</>
)}
<Tooltip label='Open in new tab'>
<ActionIcon color='blue' variant='filled' onClick={() => window.open(image.url, '_blank')}>
<ExternalLinkIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy URL'>
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
<CopyIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Download'>
<ActionIcon
color='blue'
variant='filled'
onClick={() => window.open(`/r/${encodeURI(image.name)}?download=true`, '_blank')}
>
<DownloadIcon />
</ActionIcon>
</Tooltip>
</Group>
</Group>
</Modal>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<LoadingOverlay visible={loading} />
<Type
file={image}
sx={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
style={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
src={`/r/${encodeURI(image.name)}`}
alt={image.name}
onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview}
/>
</Card.Section>
</Card>
</>
);
}
+63 -72
View File
@@ -9,35 +9,31 @@ import {
Title,
Tooltip,
} from '@mantine/core';
import { useClipboard, useMediaQuery } from '@mantine/hooks';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import {
IconAlarm,
IconCalendarPlus,
IconClipboardCopy,
IconDeviceSdCard,
IconExternalLink,
IconEye,
IconEyeglass,
IconFile,
IconFileDownload,
IconFolderCancel,
IconFolderMinus,
IconFolderPlus,
IconHash,
IconInfoCircle,
IconPhoto,
IconPhotoCancel,
IconPhotoMinus,
IconPhotoStar,
} from '@tabler/icons-react';
import useFetch, { ApiError } from 'hooks/useFetch';
import { useFileDelete, useFileFavorite, UserFilesResponse } from 'lib/queries/files';
import useFetch from 'hooks/useFetch';
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useFolders } from 'lib/queries/folders';
import { bytesToHuman } from 'lib/utils/bytes';
import { relativeTime } from 'lib/utils/client';
import { useState } from 'react';
import { FileMeta } from '.';
import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DownloadIcon,
ExternalLinkIcon,
EyeIcon,
FileIcon,
FolderMinusIcon,
FolderPlusIcon,
HashIcon,
ImageIcon,
InfoIcon,
StarIcon,
} from '../icons';
import Type from '../Type';
export default function FileModal({
@@ -48,18 +44,14 @@ export default function FileModal({
refresh,
reducedActions = false,
exifEnabled,
compress,
otherUser = false,
}: {
open: boolean;
setOpen: (open: boolean) => void;
file: UserFilesResponse;
file: any;
loading: boolean;
refresh: () => void;
reducedActions?: boolean;
exifEnabled?: boolean;
compress: boolean;
otherUser: boolean;
}) {
const deleteFile = useFileDelete();
const favoriteFile = useFileFavorite();
@@ -75,16 +67,16 @@ export default function FileModal({
title: 'File Deleted',
message: '',
color: 'green',
icon: <IconPhotoMinus size='1rem' />,
icon: <DeleteIcon />,
});
},
onError: (res: ApiError) => {
onError: (res: any) => {
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
icon: <CrossIcon />,
});
},
@@ -97,12 +89,18 @@ export default function FileModal({
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
setOpen(false);
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <IconClipboardCopy size='1rem' />,
});
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
});
};
const handleFavorite = async () => {
@@ -113,19 +111,19 @@ export default function FileModal({
showNotification({
title: 'The file is now ' + (!file.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <IconPhotoStar size='1rem' />,
icon: <StarIcon />,
});
},
onError: (res: { error: string }) => {
onError: (res: any) => {
showNotification({
title: 'Failed to favorite file',
message: res.error,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
icon: <CrossIcon />,
});
},
},
}
);
};
@@ -143,14 +141,14 @@ export default function FileModal({
title: 'Removed from folder',
message: res.name,
color: 'green',
icon: <IconFolderMinus size='1rem' />,
icon: <FolderMinusIcon />,
});
} else {
showNotification({
title: 'Failed to remove from folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
icon: <CrossIcon />,
});
}
};
@@ -167,14 +165,14 @@ export default function FileModal({
title: 'Added to folder',
message: res.name,
color: 'green',
icon: <IconFolderPlus size='1rem' />,
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to add to folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
icon: <CrossIcon />,
});
}
};
@@ -191,14 +189,14 @@ export default function FileModal({
title: 'Created & added to folder',
message: res.name,
color: 'green',
icon: <IconFolderPlus size='1rem' />,
icon: <FolderPlusIcon />,
});
} else {
showNotification({
title: 'Failed to create folder',
message: res.error,
color: 'red',
icon: <IconFolderCancel size='1rem' />,
icon: <CrossIcon />,
});
}
});
@@ -206,18 +204,12 @@ export default function FileModal({
};
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
title={<Title>{file.name}</Title>}
size='auto'
fullScreen={useMediaQuery('(max-width: 600px)')}
>
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{file.name}</Title>} size='xl'>
<LoadingOverlay visible={loading} />
<Stack>
<Type
file={file}
src={`/r/${encodeURI(file.name)}?compress=${compress}`}
src={`/r/${encodeURI(file.name)}`}
alt={file.name}
popup
sx={{ minHeight: 200 }}
@@ -235,33 +227,32 @@ export default function FileModal({
{ maxWidth: 1200, cols: 3 },
]}
>
<FileMeta Icon={IconFile} title='Name' subtitle={file.name} />
<FileMeta Icon={IconPhoto} title='Type' subtitle={file.mimetype} />
<FileMeta Icon={IconDeviceSdCard} title='Size' subtitle={bytesToHuman(file.size || 0)} />
<FileMeta Icon={IconEye} title='Views' subtitle={file?.views?.toLocaleString()} />
<FileMeta Icon={FileIcon} title='Name' subtitle={file.name} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={file.mimetype} />
<FileMeta Icon={EyeIcon} title='Views' subtitle={file?.views?.toLocaleString()} />
{file.maxViews && (
<FileMeta
Icon={IconEyeglass}
Icon={EyeIcon}
title='Max views'
subtitle={file?.maxViews?.toLocaleString()}
tooltip={`This file will be deleted after being viewed ${file?.maxViews?.toLocaleString()} times.`}
/>
)}
<FileMeta
Icon={IconCalendarPlus}
Icon={CalendarIcon}
title='Uploaded'
subtitle={relativeTime(new Date(file.createdAt))}
tooltip={new Date(file?.createdAt).toLocaleString()}
/>
{file.expiresAt && !reducedActions && (
<FileMeta
Icon={IconAlarm}
Icon={ClockIcon}
title='Expires'
subtitle={relativeTime(new Date(file.expiresAt))}
tooltip={new Date(file.expiresAt).toLocaleString()}
/>
)}
<FileMeta Icon={IconHash} title='ID' subtitle={file.id} />
<FileMeta Icon={HashIcon} title='ID' subtitle={file.id} />
</SimpleGrid>
</Stack>
@@ -274,16 +265,16 @@ export default function FileModal({
variant='filled'
onClick={() => window.open(`/dashboard/metadata/${file.id}`, '_blank')}
>
<IconInfoCircle size='1rem' />
<InfoIcon />
</ActionIcon>
</Tooltip>
)}
{reducedActions || otherUser ? null : inFolder && !folders.isLoading ? (
{reducedActions ? null : inFolder && !folders.isLoading ? (
<Tooltip
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
>
<ActionIcon color='red' variant='filled' onClick={removeFromFolder} loading={folders.isLoading}>
<IconFolderMinus size='1rem' />
<FolderMinusIcon />
</ActionIcon>
</Tooltip>
) : (
@@ -310,7 +301,7 @@ export default function FileModal({
<>
<Tooltip label='Delete file'>
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
<IconPhotoMinus size='1rem' />
<DeleteIcon />
</ActionIcon>
</Tooltip>
@@ -320,7 +311,7 @@ export default function FileModal({
variant='filled'
onClick={handleFavorite}
>
<IconPhotoStar size='1rem' />
<StarIcon />
</ActionIcon>
</Tooltip>
</>
@@ -328,13 +319,13 @@ export default function FileModal({
<Tooltip label='Open in new tab'>
<ActionIcon color='blue' variant='filled' onClick={() => window.open(file.url, '_blank')}>
<IconExternalLink size='1rem' />
<ExternalLinkIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Copy URL'>
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
<IconClipboardCopy size='1rem' />
<CopyIcon />
</ActionIcon>
</Tooltip>
@@ -344,7 +335,7 @@ export default function FileModal({
variant='filled'
onClick={() => window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')}
>
<IconFileDownload size='1rem' />
<DownloadIcon />
</ActionIcon>
</Tooltip>
</Group>
+5 -23
View File
@@ -32,10 +32,8 @@ export default function File({
image,
disableMediaPreview,
exifEnabled,
refreshImages = undefined,
refreshImages,
reducedActions = false,
onDash,
otherUser = false,
}) {
const [open, setOpen] = useState(false);
const deleteFile = useFileDelete();
@@ -45,7 +43,7 @@ export default function File({
const folders = useFolders();
const refresh = () => {
if (!otherUser) refreshImages();
refreshImages();
folders.refetch();
};
@@ -59,26 +57,9 @@ export default function File({
refresh={refresh}
reducedActions={reducedActions}
exifEnabled={exifEnabled}
compress={onDash}
otherUser={otherUser}
/>
<Card
sx={{
maxWidth: '100%',
height: '100%',
'&:hover': {
filter: 'brightness(0.75)',
},
transition: 'filter 0.2s ease-in-out',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
shadow='md'
onClick={() => setOpen(true)}
>
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
<Card.Section>
<LoadingOverlay visible={loading} />
<Type
@@ -97,8 +78,9 @@ export default function File({
width: '100%',
cursor: 'pointer',
}}
src={`/r/${encodeURI(image.name)}?compress=${onDash}`}
src={`/r/${encodeURI(image.name)}`}
alt={image.name}
onClick={() => setOpen(true)}
disableMediaPreview={disableMediaPreview}
/>
</Card.Section>
+108 -147
View File
@@ -4,14 +4,15 @@ import {
Box,
Burger,
Button,
Group,
Header,
Image,
Input,
MediaQuery,
Menu,
Navbar,
NavLink,
Paper,
rem,
ScrollArea,
Select,
Text,
@@ -22,39 +23,36 @@ import {
import { useClipboard, useMediaQuery } from '@mantine/hooks';
import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications';
import {
IconBackspace,
IconBrandDiscordFilled,
IconBrandGithubFilled,
IconBrandGoogle,
IconBrush,
IconClipboardCopy,
IconExternalLink,
IconFiles,
IconFileText,
IconFileUpload,
IconFolders,
IconGraph,
IconHome,
IconLink,
IconLogout,
IconReload,
IconSettings,
IconTag,
IconUpload,
IconUser,
IconUserCog,
IconUsers,
} from '@tabler/icons-react';
import useFetch from 'hooks/useFetch';
import { useVersion } from 'lib/queries/version';
import { userSelector } from 'lib/recoil/user';
import { capitalize } from 'lib/utils/client';
import { UserExtended } from 'middleware/withZipline';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import {
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
DiscordIcon,
ExternalLinkIcon,
FileIcon,
FolderIcon,
GitHubIcon,
GoogleIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TagIcon,
TypeIcon,
UploadIcon,
UserIcon,
} from './icons';
import { friendlyThemeName, themes } from './Theming';
export type NavbarItems = {
@@ -62,67 +60,67 @@ export type NavbarItems = {
text: string;
link?: string;
children?: NavbarItems[];
if?: (user: UserExtended, props: unknown) => boolean;
if?: (user: any, props: any) => boolean;
};
const items: NavbarItems[] = [
{
icon: <IconHome size={18} />,
icon: <HomeIcon size={18} />,
text: 'Home',
link: '/dashboard',
},
{
icon: <IconFiles size={18} />,
icon: <FileIcon size={18} />,
text: 'Files',
link: '/dashboard/files',
},
{
icon: <IconFolders size={18} />,
icon: <FolderIcon size={18} />,
text: 'Folders',
link: '/dashboard/folders',
},
{
icon: <IconGraph size={18} />,
icon: <ActivityIcon size={18} />,
text: 'Stats',
link: '/dashboard/stats',
},
{
icon: <IconLink size={18} />,
icon: <LinkIcon size={18} />,
text: 'URLs',
link: '/dashboard/urls',
},
{
icon: <IconUpload size={18} />,
icon: <UploadIcon size={18} />,
text: 'Upload',
children: [
{
icon: <IconFileUpload size={18} />,
icon: <UploadIcon size={18} />,
text: 'File',
link: '/dashboard/upload/file',
},
{
icon: <IconFileText size={18} />,
icon: <TypeIcon size={18} />,
text: 'Text',
link: '/dashboard/upload/text',
},
],
},
{
icon: <IconUser size={18} />,
icon: <UserIcon size={18} />,
text: 'Administration',
if: (user, _) => user.administrator as boolean,
children: [
{
icon: <IconUsers size={18} />,
icon: <UserIcon size={18} />,
text: 'Users',
link: '/dashboard/users',
if: () => true,
},
{
icon: <IconTag size={18} />,
icon: <TagIcon size={18} />,
text: 'Invites',
link: '/dashboard/invites',
if: (_, props: { invites: boolean }) => props.invites,
if: (_, props) => props.invites,
},
],
},
@@ -134,9 +132,9 @@ export default function Layout({ children, props }) {
const { title, oauth_providers: unparsed } = props;
const oauth_providers = JSON.parse(unparsed);
const icons = {
GitHub: IconBrandGithubFilled,
Discord: IconBrandDiscordFilled,
Google: IconBrandGoogle,
GitHub: GitHubIcon,
Discord: DiscordIcon,
Google: GoogleIcon,
};
for (const provider of oauth_providers) {
@@ -169,7 +167,7 @@ export default function Layout({ children, props }) {
title: `Theme changed to ${friendlyThemeName[value]}`,
message: '',
color: 'green',
icon: <IconBrush size='1rem' />,
icon: <PencilIcon />,
});
};
@@ -190,7 +188,7 @@ export default function Layout({ children, props }) {
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <IconReload size='1rem' />,
icon: <CrossIcon />,
});
} else {
showNotification({
@@ -198,7 +196,7 @@ export default function Layout({ children, props }) {
message:
'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
icon: <IconReload size='1rem' />,
icon: <CheckIcon />,
});
}
@@ -218,21 +216,28 @@ export default function Layout({ children, props }) {
labels: { confirm: 'Copy', cancel: 'Cancel' },
onConfirm: async () => {
clipboard.copy(token);
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy token',
message:
"Zipline couldn't copy to your clipboard. Please copy the token manually from the settings page.",
title: 'Unable to copy to clipboard',
message: (
<Text size='sm'>
Zipline is unable to copy to clipboard due to security reasons. However, you can still copy
the token manually.
<br />
<Group position='left' spacing='sm'>
<Text>Your token is:</Text>
<Input size='sm' onFocus={(e) => e.target.select()} type='text' value={token} />
</Group>
</Text>
),
color: 'red',
icon: <IconClipboardCopy size='1rem' />,
});
else
showNotification({
title: 'Token Copied',
message: 'Your token has been copied to your clipboard.',
color: 'green',
icon: <IconClipboardCopy size='1rem' />,
icon: <CheckIcon />,
});
modals.closeAll();
@@ -259,42 +264,42 @@ export default function Layout({ children, props }) {
{children
.filter((x) => (x.if ? x.if(user, props) : true))
.map(({ icon, text, link }) => (
<NavLink
key={text}
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
component={Link}
href={link}
/>
<Link href={link} key={text} passHref legacyBehavior>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
</Link>
))}
</NavLink>
) : (
<NavLink
key={text}
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
component={Link}
href={link}
/>
),
<Link href={link} key={text} passHref legacyBehavior>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
</Link>
)
)}
</Navbar.Section>
<Navbar.Section>
{external_links.length
? external_links.map(({ label, link }, i: number) => (
<NavLink
key={i}
label={label}
target='_blank'
variant='light'
icon={<IconExternalLink size={18} />}
component={Link}
href={link}
/>
? external_links.map(({ label, link }, i) => (
<Link href={link} passHref key={i} legacyBehavior>
<NavLink
label={label}
component='a'
target='_blank'
variant='light'
icon={<ExternalLinkIcon />}
/>
</Link>
))
: null}
</Navbar.Section>
@@ -316,9 +321,7 @@ export default function Layout({ children, props }) {
variant='dot'
color={version.data.update ? 'red' : 'primary'}
>
{version.data.isUpstream
? version.data.versions.current.slice(0, 7)
: version.data.versions.current}
{version.data.versions.current}
</Badge>
</Tooltip>
</Navbar.Section>
@@ -350,23 +353,16 @@ export default function Layout({ children, props }) {
>
<Menu.Target>
<Button
leftIcon={
avatar ? (
<Image src={avatar} height={32} width={32} fit='cover' radius='md' />
) : (
<IconUserCog size='1rem' />
)
}
variant='subtle'
color={theme.colorScheme === 'dark' ? 'dark' : 'gray'}
compact
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
sx={(t) => ({
backgroundColor: 'inherit',
'&:hover': {
backgroundColor: t.other.hover,
},
color: t.colorScheme === 'dark' ? 'white' : 'black',
})}
size='xl'
p='sm'
styles={{
label: {
overflow: 'unset',
},
}}
>
{user.username}
</Button>
@@ -376,39 +372,19 @@ export default function Layout({ children, props }) {
{user.username} ({user.id}){' '}
{user.administrator && user.username !== 'administrator' ? '(Administrator)' : ''}
</Menu.Label>
<Menu.Item component={Link} icon={<IconFiles size='1rem' />} href='/dashboard/files'>
Files
</Menu.Item>
<Menu.Item
component={Link}
icon={<IconFileUpload size='1rem' />}
href='/dashboard/upload/file'
>
Upload File
</Menu.Item>
<Menu.Item component={Link} icon={<IconLink size='1rem' />} href='/dashboard/urls'>
Shorten URL
</Menu.Item>
<Menu.Label>Settings</Menu.Label>
<Menu.Item component={Link} icon={<IconSettings size='1rem' />} href='/dashboard/manage'>
<Menu.Item component={Link} icon={<SettingsIcon />} href='/dashboard/manage'>
Manage Account
</Menu.Item>
<Menu.Item
icon={<IconClipboardCopy size='1rem' />}
icon={<CopyIcon />}
onClick={() => {
openCopyToken();
}}
>
Copy Token
</Menu.Item>
<Menu.Item icon={<IconLogout size='1rem' />} component={Link} href='/auth/logout'>
Logout
</Menu.Item>
<Menu.Label>Danger</Menu.Label>
<Menu.Item
icon={<IconBackspace size='1rem' />}
icon={<DeleteIcon />}
onClick={() => {
openResetToken();
}}
@@ -416,22 +392,16 @@ export default function Layout({ children, props }) {
>
Reset Token
</Menu.Item>
<Menu.Item component={Link} icon={<LogoutIcon />} href='/auth/logout' color='red'>
Logout
</Menu.Item>
<Menu.Divider />
<>
{oauth_providers.filter(
(x) =>
{oauth_providers
.filter((x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase()),
).length ? (
<Menu.Label>Connected Accounts</Menu.Label>
) : null}
{oauth_providers
.filter(
(x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase()),
.includes(x.name.toLowerCase())
)
.map(({ name, Icon }, i) => (
<>
@@ -444,16 +414,13 @@ export default function Layout({ children, props }) {
</Menu.Item>
</>
))}
{oauth_providers.filter(
(x) =>
user.oauth
?.map(({ provider }) => provider.toLowerCase())
.includes(x.name.toLowerCase()),
{oauth_providers.filter((x) =>
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
).length ? (
<Menu.Divider />
) : null}
</>
<Menu.Item closeMenuOnClick={false} icon={<IconBrush size='1rem' />}>
<Menu.Item closeMenuOnClick={false} icon={<PencilIcon />}>
<Select
size={useMediaQuery('(max-width: 768px)') ? 'md' : 'xs'}
data={Object.keys(themes).map((t) => ({
@@ -474,15 +441,9 @@ export default function Layout({ children, props }) {
<Paper
withBorder
p='md'
mr='md'
mb='md'
shadow='xs'
sx={(theme) => ({
'&[data-with-border]': {
border: `${rem(1)} solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0]
}`,
},
sx={(t) => ({
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
})}
>
{children}
+5
View File
@@ -0,0 +1,5 @@
import { NextLink } from '@mantine/next';
export default function Link(props) {
return <NextLink legacyBehavior {...props} />;
}
+2 -2
View File
@@ -1,13 +1,13 @@
// https://mantine.dev/core/password-input/
import { Box, PasswordInput, Popover, Progress, Text } from '@mantine/core';
import { IconCheck, IconCross } from '@tabler/icons-react';
import { useState } from 'react';
import { CheckIcon, CrossIcon } from './icons';
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
return (
<Text color={meets ? 'teal' : 'red'} sx={{ display: 'flex', alignItems: 'center' }} mt='sm' size='sm'>
{meets ? <IconCheck size='1rem' /> : <IconCross size='1rem' />} <Box ml='md'>{label}</Box>
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
</Text>
);
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { Card, createStyles, Group, Text } from '@mantine/core';
import { IconArrowDownRight, IconArrowUpRight } from '@tabler/icons-react';
import { ArrowDownRight, ArrowUpRight } from 'react-feather';
const useStyles = createStyles((theme) => ({
root: {
padding: `calc(${theme.spacing.xl} * 1.5)`,
padding: theme.spacing.xl * 1.5,
},
value: {
@@ -57,7 +57,7 @@ export default function StatCard({ stat }: StatsGridProps) {
<>
<Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
{stat.diff >= 0 ? <IconArrowUpRight size={16} /> : <IconArrowDownRight size={16} />}
{stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
</Text>
</>
)}
+8 -36
View File
@@ -4,10 +4,6 @@ import { useEffect } from 'react';
import ayu_dark from 'lib/themes/ayu_dark';
import ayu_light from 'lib/themes/ayu_light';
import ayu_mirage from 'lib/themes/ayu_mirage';
import catppuccin_mocha from 'lib/themes/catppuccin_mocha';
import catppuccin_macchiato from 'lib/themes/catppuccin_macchiato';
import catppuccin_frappe from 'lib/themes/catppuccin_frappe';
import catppuccin_latte from 'lib/themes/catppuccin_latte';
import dark from 'lib/themes/dark';
import dark_blue from 'lib/themes/dark_blue';
import dracula from 'lib/themes/dracula';
@@ -19,15 +15,10 @@ import qogir_dark from 'lib/themes/qogir_dark';
import { createEmotionCache, MantineProvider, MantineThemeOverride } from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
import { SpotlightProvider } from '@mantine/spotlight';
import { NotificationsProvider } from '@mantine/notifications';
import { userSelector } from 'lib/recoil/user';
import { useRecoilValue } from 'recoil';
import { createSpotlightActions } from 'lib/spotlight';
import { useRouter } from 'next/router';
import { IconSearch } from '@tabler/icons-react';
export const themes = {
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
dark_blue,
@@ -36,10 +27,6 @@ export const themes = {
ayu_dark,
ayu_mirage,
ayu_light,
catppuccin_mocha,
catppuccin_macchiato,
catppuccin_frappe,
catppuccin_latte,
nord,
dracula,
matcha_dark_azul,
@@ -54,10 +41,6 @@ export const friendlyThemeName = {
ayu_dark: 'Ayu Dark',
ayu_mirage: 'Ayu Mirage',
ayu_light: 'Ayu Light',
catppuccin_mocha: 'Catppuccin Mocha',
catppuccin_macchiato: 'Catppuccin Macchiato',
catppuccin_frappe: 'Catppuccin Frappé',
catppuccin_latte: 'Catppuccin Latte',
nord: 'Nord',
dracula: 'Dracula',
matcha_dark_azul: 'Matcha Dark Azul',
@@ -69,7 +52,6 @@ const cache = createEmotionCache({ key: 'zipline' });
export default function ZiplineTheming({ Component, pageProps, ...props }) {
const user = useRecoilValue(userSelector);
const colorScheme = useColorScheme();
const router = useRouter();
let theme: MantineThemeOverride;
@@ -96,7 +78,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
components: {
AppShell: {
styles: (t) => ({
main: {
root: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
@@ -110,15 +92,10 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
},
Modal: {
defaultProps: {
closeButtonProps: { size: 'lg' },
centered: true,
transitionProps: {
exitDuration: 100,
},
overlayProps: {
blur: 6,
color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
},
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
exitTransitionDuration: 100,
},
},
Popover: {
@@ -129,8 +106,8 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
},
LoadingOverlay: {
defaultProps: {
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
overlayOpacity: 0.3,
},
},
Loader: {
@@ -156,14 +133,9 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
}}
>
<ModalsProvider>
<SpotlightProvider
searchIcon={<IconSearch size='1rem' />}
shortcut={['mod + k', '/']}
actions={createSpotlightActions(router)}
>
<Notifications position='top-center' style={{ marginTop: -10 }} />
<NotificationsProvider position='top-center' style={{ marginTop: -10 }}>
{props.children ? props.children : <Component {...pageProps} />}
</SpotlightProvider>
</NotificationsProvider>
</ModalsProvider>
</MantineProvider>
);
+23 -68
View File
@@ -1,3 +1,4 @@
import exts from 'lib/exts';
import {
Alert,
Box,
@@ -10,24 +11,15 @@ import {
Text,
UnstyledButton,
} from '@mantine/core';
import {
IconFile,
IconFileAlert,
IconFileText,
IconFileUnknown,
IconHeadphones,
IconPhotoCancel,
IconPlayerPlay,
} from '@tabler/icons-react';
import exts from 'lib/exts';
import { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons';
import KaTeX from './render/KaTeX';
import Markdown from './render/Markdown';
import PrismCode from './render/PrismCode';
function PlaceholderContent({ text, Icon }) {
return (
<Group sx={(t) => ({ color: t.colors.dark[2], padding: 3, justifyContent: 'center' })}>
<Group sx={(t) => ({ color: t.colors.dark[2] })}>
<Icon size={48} />
<Text size='md'>{text}</Text>
</Group>
@@ -45,43 +37,14 @@ function Placeholder({ text, Icon, ...props }) {
);
return (
<Box sx={{ height: 320 }} {...props}>
<Center sx={{ height: 320 }}>
<Box sx={{ height: 200 }} {...props}>
<Center sx={{ height: 200 }}>
<PlaceholderContent text={text} Icon={Icon} />
</Center>
</Box>
);
}
function VideoThumbnailPlaceholder({ file, mediaPreview, ...props }) {
if (!file.thumbnail || !mediaPreview)
return <Placeholder Icon={IconPlayerPlay} text={`Click to view video (${file.name})`} {...props} />;
return (
<Box sx={{ position: 'relative' }}>
<Image
src={typeof file.thumbnail === 'string' ? file.thumbnail : `/r/${file.thumbnail.name}`}
sx={{
width: '100%',
height: 'auto',
}}
/>
<Center
sx={{
position: 'absolute',
height: '100%',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<IconPlayerPlay size={48} />
</Center>
</Box>
);
}
export default function Type({ file, popup = false, disableMediaPreview, ...props }) {
const type =
(file.type ?? file.mimetype) === ''
@@ -125,17 +88,6 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
);
};
if (file.password) {
return (
<Placeholder
Icon={IconFileAlert}
text={`This file is password protected. Click to view file (${file.name})`}
onClick={() => window.open(file.url)}
{...props}
/>
);
}
if ((shouldRenderMarkdown || shouldRenderTex || shouldRenderCode) && !props.overrideRender && popup)
return (
<>
@@ -151,7 +103,18 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
);
if (media && disableMediaPreview) {
return <Placeholder Icon={IconFile} text={`Click to view file (${file.name})`} {...props} />;
return <Placeholder Icon={FileIcon} text={`Click to view file (${file.name})`} {...props} />;
}
if (file.password) {
return (
<Placeholder
Icon={FileIcon}
text={`This file is password protected. Click to view file (${file.name})`}
onClick={() => window.open(file.url)}
{...props}
/>
);
}
return popup ? (
@@ -160,12 +123,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
video: <video width='100%' autoPlay muted controls {...props} />,
image: (
<Image
styles={{
imageWrapper: {
position: 'inherit',
},
}}
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
placeholder={<PlaceholderContent Icon={FileIcon} text={'Image failed to load...'} />}
{...props}
/>
),
@@ -188,20 +146,17 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
)
) : media ? (
{
// video: <Placeholder Icon={IconPlayerPlay} text={`Click to view video (${file.name})`} {...props} />,
video: <VideoThumbnailPlaceholder file={file} mediaPreview={!disableMediaPreview} />,
video: <Placeholder Icon={PlayIcon} text={`Click to view video (${file.name})`} {...props} />,
image: (
<Image
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
height={320}
fit='contain'
placeholder={<PlaceholderContent Icon={ImageIcon} text={'Image failed to load...'} />}
{...props}
/>
),
audio: <Placeholder Icon={IconHeadphones} text={`Click to view audio (${file.name})`} {...props} />,
text: <Placeholder Icon={IconFileText} text={`Click to view text file (${file.name})`} {...props} />,
audio: <Placeholder Icon={AudioIcon} text={`Click to view audio (${file.name})`} {...props} />,
text: <Placeholder Icon={FileIcon} text={`Click to view text file (${file.name})`} {...props} />,
}[type]
) : (
<Placeholder Icon={IconFileUnknown} text={`Click to view file (${file.name})`} {...props} />
<Placeholder Icon={FileIcon} text={`Click to view file (${file.name})`} {...props} />
);
}
+7 -5
View File
@@ -1,8 +1,10 @@
import { Box, Group, SimpleGrid, Text } from '@mantine/core';
import { Box, Group, SimpleGrid, Text, useMantineTheme } from '@mantine/core';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
import { IconPhoto } from '@tabler/icons-react';
import { ImageIcon } from 'components/icons';
export default function Dropzone({ loading, onDrop, children }) {
const theme = useMantineTheme();
return (
<SimpleGrid
cols={2}
@@ -11,9 +13,9 @@ export default function Dropzone({ loading, onDrop, children }) {
{ maxWidth: 'xs', cols: 1 },
]}
>
<MantineDropzone loading={loading} onDrop={onDrop} styles={{ inner: { pointerEvents: 'none' } }}>
<Group position='center' spacing='xl' style={{ minHeight: 440, flexDirection: 'column' }}>
<IconPhoto size={80} />
<MantineDropzone onDrop={onDrop} styles={{ inner: { pointerEvents: 'none' } }}>
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
<ImageIcon size={80} />
<Text size='xl' inline>
Drag files here or click to select files
+5 -3
View File
@@ -1,6 +1,6 @@
import { ActionIcon, Box, Card, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
import { IconX } from '@tabler/icons-react';
import { ActionIcon, Badge, Box, Card, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
import Type from 'components/Type';
import { X } from 'react-feather';
export function FilePreview({ file }: { file: File }) {
return (
@@ -23,6 +23,7 @@ export default function FileDropzone({ file, onRemove }: { file: File; onRemove:
return (
<HoverCard shadow='md'>
<HoverCard.Target>
{/* <Badge size='lg'>{file.name}</Badge> */}
<Card shadow='sm' radius='sm' p='sm'>
<Group position='center' spacing='xl'>
{file.name}
@@ -30,6 +31,7 @@ export default function FileDropzone({ file, onRemove }: { file: File; onRemove:
</Card>
</HoverCard.Target>
<HoverCard.Dropdown>
{/* x button that will remove file */}
<Box
sx={{
position: 'absolute',
@@ -41,7 +43,7 @@ export default function FileDropzone({ file, onRemove }: { file: File; onRemove:
m='xs'
>
<ActionIcon onClick={onRemove} size='sm' color='red' variant='filled'>
<IconX />
<X />
</ActionIcon>
</Box>
+5
View File
@@ -0,0 +1,5 @@
import { Activity } from 'react-feather';
export default function ActivityIcon({ ...props }) {
return <Activity size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Disc } from 'react-feather';
export default function AudioIcon({ ...props }) {
return <Disc size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Calendar } from 'react-feather';
export default function CalendarIcon({ ...props }) {
return <Calendar size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Check } from 'react-feather';
export default function CheckIcon({ ...props }) {
return <Check size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Clock } from 'react-feather';
export default function ClockIcon({ ...props }) {
return <Clock size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Copy } from 'react-feather';
export default function CopyIcon({ ...props }) {
return <Copy size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { X } from 'react-feather';
export default function CrossIcon({ ...props }) {
return <X size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Database } from 'react-feather';
export default function DatabaseIcon({ ...props }) {
return <Database size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Delete } from 'react-feather';
export default function DeleteIcon({ ...props }) {
return <Delete size={15} {...props} />;
}
+19
View File
@@ -0,0 +1,19 @@
// https://discord.com/branding
export default function DiscordIcon({ ...props }) {
return (
<svg width='24' height='24' viewBox='0 0 71 55' xmlns='http://www.w3.org/2000/svg'>
<g clipPath='url(#clip0)'>
<path
fill={props.colorScheme === 'manage' ? '#ffffff' : '#5865F2'}
d='M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z'
/>
</g>
<defs>
<clipPath id='clip0'>
<rect width='71' height='55' fill='white' />
</clipPath>
</defs>
</svg>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { Download } from 'react-feather';
export default function DownloadIcon({ ...props }) {
return <Download size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { LogIn } from 'react-feather';
export default function EnterIcon({ ...props }) {
return <LogIn size={15} {...props} />;
}
@@ -0,0 +1,5 @@
import { ExternalLink } from 'react-feather';
export default function ExternalLinkIcon({ ...props }) {
return <ExternalLink size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Eye } from 'react-feather';
export default function EyeIcon({ ...props }) {
return <Eye size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { File } from 'react-feather';
export default function FileIcon({ ...props }) {
return <File size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Folder } from 'react-feather';
export default function FolderIcon({ ...props }) {
return <Folder size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { FolderMinus } from 'react-feather';
export default function FolderMinusIcon({ ...props }) {
return <FolderMinus size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { FolderPlus } from 'react-feather';
export default function FolderPlusIcon({ ...props }) {
return <FolderPlus size={15} {...props} />;
}
+17
View File
@@ -0,0 +1,17 @@
import { GitHub } from 'react-feather';
import Image from 'next/image';
// https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg
export default function GitHubIcon({ colorScheme, ...props }) {
return (
<svg width={24} height={24} viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' {...props}>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z'
transform='scale(64)'
fill={colorScheme === 'dark' ? '#FFFFFF' : '#1B1F23'}
/>
</svg>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { Globe } from 'react-feather';
export default function GlobeIcon({ ...props }) {
return <Globe size={15} {...props} />;
}
+15
View File
@@ -0,0 +1,15 @@
// https://developers.google.com/identity/branding-guidelines
import Image from 'next/image';
export default function GoogleIcon({ colorScheme, ...props }) {
return (
<Image
alt='google'
src='https://madeby.google.com/static/images/google_g_logo.svg'
width={24}
height={24}
{...props}
/>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { HardDrive } from 'react-feather';
export default function HardDriveIcon({ ...props }) {
return <HardDrive size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Hash } from 'react-feather';
export default function HashIcon({ ...props }) {
return <Hash size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Home } from 'react-feather';
export default function HomeIcon({ ...props }) {
return <Home size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Image as FeatherImage } from 'react-feather';
export default function ImageIcon({ ...props }) {
return <FeatherImage size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Info } from 'react-feather';
export default function InfoIcon({ ...props }) {
return <Info size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Key } from 'react-feather';
export default function KeyIcon({ ...props }) {
return <Key size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Link } from 'react-feather';
export default function LinkIcon({ ...props }) {
return <Link size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Lock } from 'react-feather';
export default function LockIcon({ ...props }) {
return <Lock size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { LogOut } from 'react-feather';
export default function LogoutIcon({ ...props }) {
return <LogOut size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Edit2 } from 'react-feather';
export default function PencilIcon({ ...props }) {
return <Edit2 size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Play } from 'react-feather';
export default function PlayIcon({ ...props }) {
return <Play size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Plus } from 'react-feather';
export default function PlusIcon({ ...props }) {
return <Plus size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { RefreshCw } from 'react-feather';
export default function RefreshIcon({ ...props }) {
return <RefreshCw size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Settings } from 'react-feather';
export default function SettingsIcon({ ...props }) {
return <Settings size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Star } from 'react-feather';
export default function StarIcon({ ...props }) {
return <Star size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Tag } from 'react-feather';
export default function TagIcon({ ...props }) {
return <Tag size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Trash2 } from 'react-feather';
export default function TrashIcon({ ...props }) {
return <Trash2 size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Type } from 'react-feather';
export default function TypeIcon({ ...props }) {
return <Type size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Unlock } from 'react-feather';
export default function UnlockIcon({ ...props }) {
return <Unlock size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Upload } from 'react-feather';
export default function UploadIcon({ ...props }) {
return <Upload size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { User } from 'react-feather';
export default function UserIcon({ ...props }) {
return <User size={15} {...props} />;
}
+5
View File
@@ -0,0 +1,5 @@
import { Video } from 'react-feather';
export default function VideoIcon({ ...props }) {
return <Video size={15} {...props} />;
}
+88 -1
View File
@@ -1,4 +1,91 @@
import ActivityIcon from './ActivityIcon';
import CheckIcon from './CheckIcon';
import CopyIcon from './CopyIcon';
import CrossIcon from './CrossIcon';
import DeleteIcon from './DeleteIcon';
import FileIcon from './FileIcon';
import HomeIcon from './HomeIcon';
import LinkIcon from './LinkIcon';
import LogoutIcon from './LogoutIcon';
import PencilIcon from './PencilIcon';
import SettingsIcon from './SettingsIcon';
import TypeIcon from './TypeIcon';
import UploadIcon from './UploadIcon';
import UserIcon from './UserIcon';
import EnterIcon from './EnterIcon';
import PlusIcon from './PlusIcon';
import ImageIcon from './ImageIcon';
import StarIcon from './StarIcon';
import AudioIcon from './AudioIcon';
import VideoIcon from './VideoIcon';
import PlayIcon from './PlayIcon';
import CalendarIcon from './CalendarIcon';
import HashIcon from './HashIcon';
import TagIcon from './TagIcon';
import ClockIcon from './ClockIcon';
import ExternalLinkIcon from './ExternalLinkIcon';
import ShareXIcon from './ShareXIcon';
import DownloadIcon from './DownloadIcon';
import FlameshotIcon from './FlameshotIcon';
import GitHubIcon from './GitHubIcon';
import DiscordIcon from './DiscordIcon';
import GoogleIcon from './GoogleIcon';
import EyeIcon from './EyeIcon';
import RefreshIcon from './RefreshIcon';
import KeyIcon from './KeyIcon';
import DatabaseIcon from './DatabaseIcon';
import InfoIcon from './InfoIcon';
import FolderIcon from './FolderIcon';
import FolderMinusIcon from './FolderMinusIcon';
import FolderPlusIcon from './FolderPlusIcon';
import GlobeIcon from './GlobeIcon';
import LockIcon from './LockIcon';
import UnlockIcon from './UnlockIcon';
import HardDriveIcon from './HardDriveIcon';
export { ShareXIcon, FlameshotIcon };
export {
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
FileIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TypeIcon,
UploadIcon,
UserIcon,
EnterIcon,
PlusIcon,
ImageIcon,
StarIcon,
AudioIcon,
VideoIcon,
PlayIcon,
CalendarIcon,
HashIcon,
TagIcon,
ClockIcon,
ExternalLinkIcon,
ShareXIcon,
DownloadIcon,
FlameshotIcon,
GitHubIcon,
DiscordIcon,
GoogleIcon,
EyeIcon,
RefreshIcon,
KeyIcon,
DatabaseIcon,
InfoIcon,
FolderIcon,
FolderMinusIcon,
FolderPlusIcon,
GlobeIcon,
LockIcon,
UnlockIcon,
HardDriveIcon,
};
@@ -1,11 +1,11 @@
import { Card as MantineCard, Center, Group, SimpleGrid, Skeleton, Title } from '@mantine/core';
import { randomId } from '@mantine/hooks';
import { IconCloudUpload } from '@tabler/icons-react';
import File from 'components/File';
import MutedText from 'components/MutedText';
import { useRecent } from 'lib/queries/files';
import { UploadCloud } from 'react-feather';
export default function RecentFiles({ disableMediaPreview, exifEnabled, compress }) {
export default function RecentFiles({ disableMediaPreview, exifEnabled }) {
const recent = useRecent('media');
return (
@@ -25,7 +25,6 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled, compress
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
refreshImages={recent.refetch}
onDash={compress}
/>
))
) : (
@@ -33,7 +32,7 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled, compress
<Center>
<Group>
<div>
<IconCloudUpload size={48} />
<UploadCloud size={48} />
</div>
<div>
<Title>Nothing here</Title>
+5 -5
View File
@@ -1,8 +1,8 @@
import { SimpleGrid } from '@mantine/core';
import { IconDatabase, IconEye, IconFile, IconUsers } from '@tabler/icons-react';
import StatCard from 'components/StatCard';
import { useStats } from 'lib/queries/stats';
import { percentChange } from 'lib/utils/client';
import { EyeIcon, DatabaseIcon, UserIcon, FileIcon } from 'components/icons';
export function StatCards() {
const stats = useStats();
@@ -23,7 +23,7 @@ export function StatCards() {
title: 'FILES',
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
desc: 'files have been uploaded',
icon: <IconFile />,
icon: <FileIcon />,
diff:
stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
}}
@@ -34,7 +34,7 @@ export function StatCards() {
title: 'STORAGE',
value: stats.isSuccess ? latest.data.size : '...',
desc: 'used',
icon: <IconDatabase />,
icon: <DatabaseIcon />,
diff:
stats.isSuccess && before?.data
? percentChange(before.data.size_num, latest.data.size_num)
@@ -47,7 +47,7 @@ export function StatCards() {
title: 'VIEWS',
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
desc: 'total file views',
icon: <IconEye />,
icon: <EyeIcon />,
diff:
stats.isSuccess && before?.data
? percentChange(before.data.views_count, latest.data.views_count)
@@ -60,7 +60,7 @@ export function StatCards() {
title: 'USERS',
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
desc: 'users',
icon: <IconUsers />,
icon: <UserIcon />,
}}
/>
</SimpleGrid>
@@ -1,56 +0,0 @@
import { Alert, Stack, Anchor, Code, Text } from '@mantine/core';
import { IconExclamationCircle } from '@tabler/icons-react';
import { useCallback, useEffect, useState } from 'react';
export default function Version4Notice() {
const key = 'zipline-v4-notice';
const [isClosed, setClosed] = useState<boolean | null>(null);
useEffect(() => {
const dismissed = localStorage.getItem(key) === 'true';
setClosed(dismissed);
}, [key]);
const handleDismiss = useCallback(() => {
setClosed(true);
localStorage.setItem(key, 'true');
}, [key]);
if (isClosed === null) return null;
if (isClosed) return null;
return (
<Alert
withCloseButton
variant='outline'
icon={<IconExclamationCircle size='1rem' />}
title='⚠️ Important! ⚠️'
p='md'
mb='md'
onClose={handleDismiss}
color='red'
>
<Stack spacing='md'>
<Text>
Zipline v4 will be released soon, and is <b>NOT</b> compatible with v3 (the current version). If you
are using external software to automatically update Zipline on new releases, it is{' '}
<b>strongly advised</b> that you stop auto-updates for the time being until v4 is released. For more
information, please visit{' '}
<Anchor target='_blank' href='https://github.com/diced/zipline/tree/v4'>
the <Code>v4</Code> branch
</Anchor>{' '}
on GitHub to view the progress of v4. If you have any questions, feel free to{' '}
<Anchor target='_blank' href='https://zipline.diced.sh/discord'>
join our discord
</Anchor>
.
</Text>
<Text>
If you are not the server administrator, please consider notifying them of this important message.
</Text>
</Stack>
</Alert>
);
}
+56 -64
View File
@@ -1,30 +1,21 @@
import { ActionIcon, Box, Group, Title, Tooltip } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { showNotification } from '@mantine/notifications';
import {
IconClipboardCopy,
IconExternalLink,
IconGridDots,
IconPhotoCancel,
IconPhotoMinus,
IconPhotoUp,
} from '@tabler/icons-react';
import FileModal from 'components/File/FileModal';
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon, FileIcon } from 'components/icons';
import Link from 'components/Link';
import MutedText from 'components/MutedText';
import useFetch from 'lib/hooks/useFetch';
import { PaginatedFilesOptions, usePaginatedFiles, useRecent } from 'lib/queries/files';
import { usePaginatedFiles, useRecent } from 'lib/queries/files';
import { useStats } from 'lib/queries/stats';
import { userSelector } from 'lib/recoil/user';
import { bytesToHuman } from 'lib/utils/bytes';
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import RecentFiles from './RecentFiles';
import { StatCards } from './StatCards';
import Version4Notice from './Version4Notice';
export default function Dashboard({ disableMediaPreview, exifEnabled, compress }) {
export default function Dashboard({ disableMediaPreview, exifEnabled }) {
const user = useRecoilValue(userSelector);
const recent = useRecent('media');
@@ -46,24 +37,32 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
})();
}, [page]);
const files = usePaginatedFiles(page);
// sorting
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
columnAccessor: 'createdAt',
columnAccessor: 'date',
direction: 'asc',
});
const [records, setRecords] = useState(files.data);
const files = usePaginatedFiles(page, {
filter: 'none',
useEffect(() => {
setRecords(files.data);
}, [files.data]);
// only query for correct results if there is more than one page
// otherwise, querying has no effect
...(numFiles > 1
? {
sortBy: sortStatus.columnAccessor as PaginatedFilesOptions['sortBy'],
order: sortStatus.direction,
}
: {}),
});
useEffect(() => {
if (!records || records.length === 0) return;
const sortedRecords = [...records].sort((a, b) => {
if (sortStatus.direction === 'asc') {
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
}
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
});
setRecords(sortedRecords);
}, [sortStatus]);
// file modal on click
const [open, setOpen] = useState(false);
@@ -85,38 +84,42 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
title: 'File Deleted',
message: `${file.name}`,
color: 'green',
icon: <IconPhotoMinus size='1rem' />,
icon: <DeleteIcon />,
});
} else {
showNotification({
title: 'Failed to Delete File',
message: res.error,
color: 'red',
icon: <IconPhotoCancel size='1rem' />,
icon: <CrossIcon />,
});
}
};
const copyFile = async (file) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${file.url}`);
showNotification({
title: 'Copied to clipboard',
message: (
<a
href={`${window.location.protocol}//${window.location.host}${file.url}`}
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
),
icon: <IconClipboardCopy size='1rem' />,
});
if (!navigator.clipboard)
showNotification({
title: 'Unable to copy to clipboard',
message: 'Zipline is unable to copy to clipboard due to security reasons.',
color: 'red',
});
else
showNotification({
title: 'Copied to clipboard',
message: (
<a
href={`${window.location.protocol}//${window.location.host}${file.url}`}
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
),
icon: <CopyIcon />,
});
};
const viewFile = async (file) => {
window.open(`${window.location.protocol}//${window.location.host}${file.url}`);
};
// local storage to whether to show alert or not
return (
<div>
{selectedFile && (
@@ -128,13 +131,9 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
refresh={() => files.refetch()}
reducedActions={false}
exifEnabled={exifEnabled}
compress={compress}
otherUser={false}
/>
)}
<Version4Notice />
<Title>Welcome back, {user?.username}</Title>
<MutedText size='md'>
You have <b>{numFiles === 0 ? '...' : numFiles}</b> files
@@ -142,17 +141,13 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
<StatCards />
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} compress={compress} />
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} />
<Box my='sm'>
<Group mb='md'>
<Title>Files</Title>
<Tooltip label='View Gallery'>
<ActionIcon variant='filled' color='primary' component={Link} href='/dashboard/files'>
<IconGridDots size='1rem' />
</ActionIcon>
</Tooltip>
</Group>
<Title>Files</Title>
<MutedText size='md'>
View your gallery <Link href='/dashboard/files'>here</Link>.
</MutedText>
<DataTable
withBorder
@@ -162,7 +157,6 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
columns={[
{ accessor: 'name', sortable: true },
{ accessor: 'mimetype', sortable: true },
{ accessor: 'size', sortable: true, render: (file) => bytesToHuman(file.size) },
{
accessor: 'createdAt',
sortable: true,
@@ -181,27 +175,27 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
}}
color='blue'
>
<IconPhotoUp size='1rem' />
<FileIcon />
</ActionIcon>
</Tooltip>
<Tooltip label='Open file in new tab'>
<ActionIcon onClick={() => viewFile(file)} color='blue'>
<IconExternalLink size='1rem' />
<EnterIcon />
</ActionIcon>
</Tooltip>
<ActionIcon onClick={() => copyFile(file)} color='green'>
<IconClipboardCopy size='1rem' />
<CopyIcon />
</ActionIcon>
<ActionIcon onClick={() => deleteFile(file)} color='red'>
<IconPhotoMinus size='1rem' />
<DeleteIcon />
</ActionIcon>
</Group>
),
},
]}
records={files.data ?? []}
records={records ?? []}
fetching={files.isLoading}
loaderBackgroundBlur={5}
loaderVariant='dots'
@@ -218,27 +212,25 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
items: (file) => [
{
key: 'view',
icon: <IconExternalLink size='1rem' />,
icon: <EnterIcon />,
title: `View ${file.name}`,
onClick: () => viewFile(file),
},
{
key: 'copy',
icon: <IconClipboardCopy size='1rem' />,
icon: <CopyIcon />,
title: `Copy ${file.name}`,
onClick: () => copyFile(file),
},
{
key: 'delete',
icon: <IconPhotoMinus size='1rem' />,
icon: <DeleteIcon />,
title: `Delete ${file.name}`,
onClick: () => deleteFile(file),
},
],
}}
onCellClick={({ column, record: file }) => {
if (column.accessor === 'actions') return;
onCellClick={({ record: file }) => {
setSelectedFile(file);
setOpen(true);
}}
+6 -15
View File
@@ -1,7 +1,7 @@
import { Box, Button, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { IconFile } from '@tabler/icons-react';
import File from 'components/File';
import { FileIcon } from 'components/icons';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { usePaginatedFiles } from 'lib/queries/files';
@@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage, compress }) {
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage }) {
const [checked, setChecked] = useRecoilState(showNonMediaSelector);
const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet
const [page, setPage] = useState(Number(queryPage));
@@ -29,7 +29,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
},
},
undefined,
{ shallow: true },
{ shallow: true }
);
const { count } = await useFetch(`/api/user/paged?count=true${!checked ? '&filter=media' : ''}`);
@@ -37,22 +37,14 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
})();
}, [page]);
const pages = usePaginatedFiles(page, {
filter: !checked ? 'media' : 'none',
});
const pages = usePaginatedFiles(page, !checked ? 'media' : null);
if (pages.isSuccess && pages.data.length === 0) {
if (page > 1 && numPages > 0) {
setPage(page - 1);
return null;
}
return (
<Center sx={{ flexDirection: 'column' }}>
<Group>
<div>
<IconFile size={48} />
<FileIcon size={48} />
</div>
<div>
<Title>Nothing here</Title>
@@ -83,7 +75,6 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
refreshImages={pages.refetch}
onDash={compress}
/>
</div>
))
@@ -105,7 +96,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
}}
>
{!isMobile && <div></div>}
<Pagination total={numPages} value={page} onChange={setPage} withEdges />
<Pagination total={numPages} page={page} onChange={setPage} withEdges />
{!isMobile && (
<Checkbox
label='Show non-media files'
@@ -1,118 +0,0 @@
import { Button, Modal, Title, Tooltip } from '@mantine/core';
import { IconTrash } from '@tabler/icons-react';
import AnchorNext from 'components/AnchorNext';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { DataTable } from 'mantine-datatable';
import { useEffect, useState } from 'react';
export type PendingFiles = {
id: number;
createdAt: string;
status: string;
chunks: number;
chunksComplete: number;
userId: number;
data: {
file: {
filename: string;
mimetype: string;
lastchunk: boolean;
identifier: string;
totalBytes: number;
};
code?: number;
message?: string;
};
};
export default function PendingFilesModal({ open, onClose }) {
const [incFiles, setIncFiles] = useState<PendingFiles[]>([]);
const [loading, setLoading] = useState(true);
const [selectedFiles, setSelectedFiles] = useState<PendingFiles[]>([]);
async function updateIncFiles() {
setLoading(true);
const files = await useFetch('/api/user/pending');
setIncFiles(files);
setLoading(false);
}
async function deleteIncFiles() {
await useFetch('/api/user/pending', 'DELETE', {
id: selectedFiles.map((file) => file.id),
});
updateIncFiles();
setSelectedFiles([]);
}
useEffect(() => {
updateIncFiles();
}, []);
useEffect(() => {
const interval = setInterval(() => {
if (open) updateIncFiles();
}, 5000);
return () => clearInterval(interval);
}, [open]);
return (
<Modal title={<Title>Pending Files</Title>} size='auto' opened={open} onClose={onClose}>
<MutedText size='xs'>Refreshing every 5 seconds...</MutedText>
<DataTable
withBorder
borderRadius='md'
highlightOnHover
verticalSpacing='sm'
minHeight={200}
records={incFiles ?? []}
columns={[
{ accessor: 'id', title: 'ID' },
{ accessor: 'createdAt', render: (file) => new Date(file.createdAt).toLocaleString() },
{ accessor: 'status', render: (file) => file.status.toLowerCase() },
{
accessor: 'progress',
title: 'Progress',
render: (file) => `${file.chunksComplete}/${file.chunks} chunks`,
},
{
accessor: 'message',
render: (file) =>
file.data.code === 200 ? (
<AnchorNext href={file.data.message} target='_blank'>
view file
</AnchorNext>
) : (
file.data.message
),
},
]}
fetching={loading}
loaderBackgroundBlur={5}
loaderVariant='dots'
onSelectedRecordsChange={setSelectedFiles}
selectedRecords={selectedFiles}
/>
{selectedFiles.length ? (
<Tooltip label='Clearing pending files will still leave the final file on the server.'>
<Button
variant='filled'
my='md'
color='red'
onClick={deleteIncFiles}
leftIcon={<IconTrash size='1rem' />}
fullWidth
>
Clear {selectedFiles.length} pending file{selectedFiles.length > 1 ? 's' : ''}
</Button>
</Tooltip>
) : null}
</Modal>
);
}
+9 -27
View File
@@ -1,26 +1,16 @@
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title, Tooltip } from '@mantine/core';
import { IconFileUpload, IconPhotoUp } from '@tabler/icons-react';
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title } from '@mantine/core';
import File from 'components/File';
import { PlusIcon } from 'components/icons';
import useFetch from 'hooks/useFetch';
import { usePaginatedFiles } from 'lib/queries/files';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import FilePagation from './FilePagation';
import PendingFilesModal from './PendingFilesModal';
import { showNonMediaSelector } from 'lib/recoil/settings';
import { useRecoilState } from 'recoil';
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
const [checked] = useRecoilState(showNonMediaSelector);
export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
const [favoritePage, setFavoritePage] = useState(1);
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
const favoritePages = usePaginatedFiles(favoritePage, {
filter: checked ? 'none' : 'media',
favorite: true,
});
const [open, setOpen] = useState(false);
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
useEffect(() => {
(async () => {
@@ -31,19 +21,13 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
return (
<>
<PendingFilesModal open={open} onClose={() => setOpen(false)} />
<Group mb='md'>
<Title>Files</Title>
<ActionIcon component={Link} href='/dashboard/upload/file' variant='filled' color='primary'>
<IconFileUpload size='1rem' />
</ActionIcon>
<Tooltip label='View pending uploads'>
<ActionIcon onClick={() => setOpen(true)} variant='filled' color='primary'>
<IconPhotoUp size='1rem' />
<Link href='/dashboard/upload/file' passHref legacyBehavior>
<ActionIcon component='a' variant='filled' color='primary'>
<PlusIcon />
</ActionIcon>
</Tooltip>
</Link>
</Group>
{favoritePages.isSuccess && favoritePages.data.length ? (
<Accordion
@@ -66,7 +50,6 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
refreshImages={favoritePages.refetch}
onDash={compress}
/>
</div>
))
@@ -81,7 +64,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
paddingBottom: 3,
}}
>
<Pagination total={favoriteNumPages} value={favoritePage} onChange={setFavoritePage} />
<Pagination total={favoriteNumPages} page={favoritePage} onChange={setFavoritePage} />
</Box>
</Accordion.Panel>
</Accordion.Item>
@@ -92,7 +75,6 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
disableMediaPreview={disableMediaPreview}
exifEnabled={exifEnabled}
queryPage={queryPage}
compress={compress}
/>
</>
);
@@ -1,7 +1,7 @@
import { Button, Group, Modal, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import { IconFolderPlus, IconFolderX } from '@tabler/icons-react';
import { CrossIcon, FolderIcon } from 'components/icons';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { useRouter } from 'next/router';
@@ -25,14 +25,14 @@ export default function CreateFolderModal({ open, setOpen, updateFolders, create
showNotification({
title: 'Failed to create folder',
message: res.error,
icon: <IconFolderX size='1rem' />,
icon: <CrossIcon />,
color: 'red',
});
} else {
showNotification({
title: 'Created folder ' + res.name,
message: createWithFile ? 'Added file to folder' : undefined,
icon: <IconFolderPlus size='1rem' />,
icon: <FolderIcon />,
color: 'green',
});
@@ -43,7 +43,6 @@ export default function CreateFolderModal({ open, setOpen, updateFolders, create
setOpen(false);
updateFolders();
form.setValues({ name: '' });
};
return (
@@ -3,14 +3,7 @@ import File from 'components/File';
import MutedText from 'components/MutedText';
import { useFolder } from 'lib/queries/folders';
export default function ViewFolderFilesModal({
open,
setOpen,
folderId,
disableMediaPreview,
exifEnabled,
compress,
}) {
export default function ViewFolderFilesModal({ open, setOpen, folderId, disableMediaPreview, exifEnabled }) {
if (!folderId) return null;
const folder = useFolder(folderId, true);
@@ -33,7 +26,6 @@ export default function ViewFolderFilesModal({
image={file}
exifEnabled={exifEnabled}
refreshImages={folder.refetch}
onDash={compress}
/>
))}
</SimpleGrid>

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