Compare commits

...

12 Commits

Author SHA1 Message Date
Elias Schneider
7a387d86d6 release: 0.20.3 2023-11-17 15:27:31 +01:00
Elias Schneider
330eef51e4 fix: max expiration gets ignored if expiration is set to "never" 2023-11-17 15:27:22 +01:00
Elias Schneider
2e1a2b60c4 release: 0.20.2 2023-11-11 20:29:24 +01:00
Elias Schneider
9896ca0e8c chore(translations): update translations via Crowdin (#313)
* New translations en-us.ts (French)

* New translations en-us.ts (Spanish)

* New translations en-us.ts (Japanese)

* New translations en-us.ts (Japanese)

* New translations en-us.ts (Swedish)

* New translations en-us.ts (Swedish)

* New translations en-us.ts (Swedish)

* New translations en-us.ts (Swedish)

* New translations en-us.ts (Russian)

* New translations en-us.ts (Russian)
2023-11-11 20:27:03 +01:00
Qing Fu
fd44f42f28 fix(oauth): github and discord login error (#323)
* fix(oauth): github and discord login error
fixed #322, fixed #302

* feat(oauth): print log when ErrorPageException occurs

* refactor(oauth): migrate to Logger

* feat(oauth): add logger for OAuthExceptionFilter

* docs(oauth): update oauth login docs
2023-11-11 20:25:05 +01:00
Elias Schneider
966ce261cb fix: reverse shares couldn't be created unauthenticated 2023-11-11 18:57:54 +01:00
Elias Schneider
5503e7a54f chore(translations): add Swedish translation files 2023-11-08 08:18:10 +01:00
Elias Schneider
b49ec93c54 release: 0.20.1 2023-11-05 12:38:13 +01:00
Elias Schneider
e6584322fa chore(translations): update translations via Crowdin (#310)
* New translations en-us.ts (French)

* New translations en-us.ts (Spanish)

* New translations en-us.ts (Danish)

* New translations en-us.ts (German)

* New translations en-us.ts (Finnish)

* New translations en-us.ts (Japanese)

* New translations en-us.ts (Polish)

* New translations en-us.ts (Russian)

* New translations en-us.ts (Serbian (Cyrillic))

* New translations en-us.ts (Portuguese, Brazilian)

* New translations en-us.ts (Thai)

* New translations en-us.ts (Dutch, Belgium)

* New translations en-us.ts (Polish)

* New translations en-us.ts (Portuguese, Brazilian)

* New translations en-us.ts (German)
2023-11-05 12:37:48 +01:00
Elias Schneider
1138cd02b0 fix: share information text color in light mode 2023-11-05 12:36:42 +01:00
Elias Schneider
1ba8d0cbd1 release: 0.20.0 2023-11-04 20:40:20 +01:00
Ivan Li
98380e2d48 feat: ability to add and delete files of existing share (#306)
* feat(share): delete file api, revert complete share api.

* feat(share): share edit page.

* feat(share): Modify the DropZone title of the edit sharing UI.

* feat(share): i18n for edit share. (en, zh)

* feat(share): allow creator get share by id.

* feat(share): add edit button in account/shares.

* style(share): lint.

* chore: some minor adjustments.

* refactor: run formatter

* refactor: remove unused return

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
2023-11-04 20:39:58 +01:00
42 changed files with 1201 additions and 218 deletions

View File

@@ -1,3 +1,32 @@
## [0.20.3](https://github.com/stonith404/pingvin-share/compare/v0.20.2...v0.20.3) (2023-11-17)
### Bug Fixes
* max expiration gets ignored if expiration is set to "never" ([330eef5](https://github.com/stonith404/pingvin-share/commit/330eef51e4f3f3fb29833bc9337e705553340aaa))
## [0.20.2](https://github.com/stonith404/pingvin-share/compare/v0.20.1...v0.20.2) (2023-11-11)
### Bug Fixes
* **oauth:** github and discord login error ([#323](https://github.com/stonith404/pingvin-share/issues/323)) ([fd44f42](https://github.com/stonith404/pingvin-share/commit/fd44f42f28c0fa2091876b138f170202d9fde04e)), closes [#322](https://github.com/stonith404/pingvin-share/issues/322) [#302](https://github.com/stonith404/pingvin-share/issues/302)
* reverse shares couldn't be created unauthenticated ([966ce26](https://github.com/stonith404/pingvin-share/commit/966ce261cb4ad99efaadef5c36564fdfaed0d5c4))
## [0.20.1](https://github.com/stonith404/pingvin-share/compare/v0.20.0...v0.20.1) (2023-11-05)
### Bug Fixes
* share information text color in light mode ([1138cd0](https://github.com/stonith404/pingvin-share/commit/1138cd02b0b6ac1d71c4dbc2808110c672237190))
## [0.20.0](https://github.com/stonith404/pingvin-share/compare/v0.19.2...v0.20.0) (2023-11-04)
### Features
* ability to add and delete files of existing share ([#306](https://github.com/stonith404/pingvin-share/issues/306)) ([98380e2](https://github.com/stonith404/pingvin-share/commit/98380e2d48cc8ffa831d9b69cf5c0e8a40e28862))
## [0.19.2](https://github.com/stonith404/pingvin-share/compare/v0.19.1...v0.19.2) (2023-11-03)

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-backend",
"version": "0.19.2",
"version": "0.20.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pingvin-share-backend",
"version": "0.19.2",
"version": "0.20.3",
"dependencies": {
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.1.2",

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share-backend",
"version": "0.19.2",
"version": "0.20.3",
"scripts": {
"build": "nest build",
"dev": "cross-env NODE_ENV=development nest start --watch",

View File

@@ -1,6 +1,7 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
@@ -81,4 +82,14 @@ export class FileController {
return new StreamableFile(file.file);
}
@Delete(":fileId")
@SkipThrottle()
@UseGuards(ShareOwnerGuard)
async remove(
@Param("fileId") fileId: string,
@Param("shareId") shareId: string,
) {
await this.fileService.remove(shareId, fileId);
}
}

View File

@@ -124,6 +124,18 @@ export class FileService {
};
}
async remove(shareId: string, fileId: string) {
const fileMetaData = await this.prisma.file.findUnique({
where: { id: fileId },
});
if (!fileMetaData) throw new NotFoundException("File not found");
fs.unlinkSync(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
await this.prisma.file.delete({ where: { id: fileId } });
}
async deleteAllFiles(shareId: string) {
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
recursive: true,

View File

@@ -1,12 +1,16 @@
import { ArgumentsHost, Catch, ExceptionFilter } from "@nestjs/common";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { ConfigService } from "../../config/config.service";
import { ErrorPageException } from "../exceptions/errorPage.exception";
@Catch(ErrorPageException)
export class ErrorPageExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(ErrorPageExceptionFilter.name);
constructor(private config: ConfigService) {}
catch(exception: ErrorPageException, host: ArgumentsHost) {
this.logger.error(exception);
const ctx = host.switchToHttp();
const response = ctx.getResponse();

View File

@@ -3,6 +3,7 @@ import {
Catch,
ExceptionFilter,
HttpException,
Logger,
} from "@nestjs/common";
import { ConfigService } from "../../config/config.service";
@@ -12,14 +13,20 @@ export class OAuthExceptionFilter implements ExceptionFilter {
access_denied: "access_denied",
expired_token: "expired_token",
};
private readonly logger = new Logger(OAuthExceptionFilter.name);
constructor(private config: ConfigService) {}
catch(_exception: HttpException, host: ArgumentsHost) {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
this.logger.error(exception.message);
this.logger.error(
"Request query: " + JSON.stringify(request.query, null, 2),
);
const key = this.errorKeys[request.query.error] || "default";
const url = new URL(`${this.config.get("general.appUrl")}/error`);

View File

@@ -60,8 +60,8 @@ export class DiscordProvider implements OAuthProvider<DiscordToken> {
}
async getUserInfo(token: OAuthToken<DiscordToken>): Promise<OAuthSignInDto> {
const res = await fetch("https://discord.com/api/v10/user/@me", {
method: "post",
const res = await fetch("https://discord.com/api/v10/users/@me", {
method: "get",
headers: {
Accept: "application/json",
Authorization: `${token.tokenType || "Bearer"} ${token.accessToken}`,

View File

@@ -41,15 +41,16 @@ export class GitHubProvider implements OAuthProvider<GitHubToken> {
return {
accessToken: token.access_token,
tokenType: token.token_type,
scope: token.scope,
rawToken: token,
};
}
async getUserInfo(token: OAuthToken<GitHubToken>): Promise<OAuthSignInDto> {
const user = await this.getGitHubUser(token);
if (!token.scope.includes("user:email")) {
throw new BadRequestException("No email permission granted");
}
const user = await this.getGitHubUser(token);
const email = await this.getGitHubEmail(token);
if (!email) {
throw new BadRequestException("No email found");

View File

@@ -1,5 +1,4 @@
import {
CanActivate,
ExecutionContext,
Injectable,
NotFoundException,
@@ -7,10 +6,17 @@ import {
import { User } from "@prisma/client";
import { Request } from "express";
import { PrismaService } from "src/prisma/prisma.service";
import { JwtGuard } from "../../auth/guard/jwt.guard";
import { ConfigService } from "src/config/config.service";
@Injectable()
export class ShareOwnerGuard implements CanActivate {
constructor(private prisma: PrismaService) {}
export class ShareOwnerGuard extends JwtGuard {
constructor(
configService: ConfigService,
private prisma: PrismaService,
) {
super(configService);
}
async canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest();
@@ -30,6 +36,8 @@ export class ShareOwnerGuard implements CanActivate {
if (!share.creatorId) return true;
if (!(await super.canActivate(context))) return false;
return share.creatorId == (request.user as User).id;
}
}

View File

@@ -43,6 +43,12 @@ export class ShareController {
return new ShareDTO().from(await this.shareService.get(id));
}
@Get(":id/from-owner")
@UseGuards(ShareOwnerGuard)
async getFromOwner(@Param("id") id: string) {
return new ShareDTO().from(await this.shareService.get(id));
}
@Get(":id/metaData")
@UseGuards(ShareSecurityGuard)
async getMetaData(@Param("id") id: string) {
@@ -62,12 +68,6 @@ export class ShareController {
);
}
@Delete(":id")
@UseGuards(JwtGuard, ShareOwnerGuard)
async remove(@Param("id") id: string) {
await this.shareService.remove(id);
}
@Post(":id/complete")
@HttpCode(202)
@UseGuards(CreateShareGuard, ShareOwnerGuard)
@@ -78,6 +78,18 @@ export class ShareController {
);
}
@Delete(":id/complete")
@UseGuards(ShareOwnerGuard)
async revertComplete(@Param("id") id: string) {
return new ShareDTO().from(await this.shareService.revertComplete(id));
}
@Delete(":id")
@UseGuards(ShareOwnerGuard)
async remove(@Param("id") id: string) {
await this.shareService.remove(id);
}
@Throttle(10, 60)
@Get("isShareIdAvailable/:id")
async isShareIdAvailable(@Param("id") id: string) {

View File

@@ -54,10 +54,15 @@ export class ShareService {
} else {
const parsedExpiration = parseRelativeDateToAbsolute(share.expiration);
const expiresNever = moment(0).toDate() == parsedExpiration;
if (
this.config.get("share.maxExpiration") !== 0 &&
parsedExpiration >
moment().add(this.config.get("share.maxExpiration"), "hours").toDate()
(expiresNever ||
parsedExpiration >
moment()
.add(this.config.get("share.maxExpiration"), "hours")
.toDate())
) {
throw new BadRequestException(
"Expiration date exceeds maximum expiration date",
@@ -182,6 +187,13 @@ export class ShareService {
});
}
async revertComplete(id: string) {
return this.prisma.share.update({
where: { id },
data: { uploadLocked: false, isZipReady: false },
});
}
async getSharesByUser(userId: string) {
const shares = await this.prisma.share.findMany({
where: {

View File

@@ -10,23 +10,22 @@
### GitHub
Please follow the [official guide](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
to create an OAuth app.
Please follow the [official guide](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) to create an OAuth app.
Redirect URL: `https://<your-domain>/api/oauth/callback/github`
### Google
Please follow the [official guide](https://developers.google.com/identity/protocols/oauth2/web-server#prerequisites) to
create an OAuth 2.0 App.
Please follow the [official guide](https://developers.google.com/identity/protocols/oauth2/web-server#prerequisites) to create an OAuth 2.0 App.
Redirect URL: `https://<your-domain>/api/oauth/callback/google`
### Microsoft
Please follow
the [official guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
to register an application.
Please follow the [official guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) to register an application.
> [!IMPORTANT]
> **Microsoft Tenant** you set in the admin panel must match the **supported account types** you set in the Microsoft Entra admin center, otherwise the OAuth login will not work. Refer to the [official documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri) for more details.
Redirect URL: `https://<your-domain>/api/oauth/callback/microsoft`
@@ -38,7 +37,7 @@ Redirect URL: `https://<your-domain>/api/oauth/callback/discord`
### OpenID Connect
Generic OpenID Connect provider is also supported, we have tested it on Keycloak and Authentik.
Generic OpenID Connect provider is also supported, we have tested it on Keycloak, Authentik and Casdoor.
Redirect URL: `https://<your-domain>/api/oauth/callback/oidc`
@@ -74,14 +73,11 @@ const configVariables: ConfigVariables = {
### 2. Create provider class
#### OpenID Connect
#### Generic OpenID Connect
If your provider supports OpenID connect, it's extremely easy to
extend [`GenericOidcProvider`](../backend/src/oauth/provider/genericOidc.provider.ts) to add a new OpenID Connect
provider.
If your provider supports OpenID connect, it's extremely easy to extend [`GenericOidcProvider`](../backend/src/oauth/provider/genericOidc.provider.ts) to add a new OpenID Connect provider.
The [Google provider](../backend/src/oauth/provider/google.provider.ts)
and [Microsoft provider](../backend/src/oauth/provider/microsoft.provider.ts) are good examples.
The [Google provider](../backend/src/oauth/provider/google.provider.ts) and [Microsoft provider](../backend/src/oauth/provider/microsoft.provider.ts) are good examples.
Here are some discovery URIs for popular providers:
@@ -95,17 +91,13 @@ Here are some discovery URIs for popular providers:
#### OAuth 2
If your provider only supports OAuth 2, you can
implement [`OAuthProvider`](../backend/src/oauth/provider/oauthProvider.interface.ts) interface to add a new OAuth 2
provider.
If your provider only supports OAuth 2, you can implement [`OAuthProvider`](../backend/src/oauth/provider/oauthProvider.interface.ts) interface to add a new OAuth 2 provider.
The [GitHub provider](../backend/src/oauth/provider/github.provider.ts)
and [Discord provider](../backend/src/oauth/provider/discord.provider.ts) are good examples.
The [GitHub provider](../backend/src/oauth/provider/github.provider.ts) and [Discord provider](../backend/src/oauth/provider/discord.provider.ts) are good examples.
### 3. Register provider
Register your provider in [`OAuthModule`](../backend/src/oauth/oauth.module.ts)
and [`OAuthSignInDto`](../backend/src/oauth/dto/oauthSignIn.dto.ts):
Register your provider in [`OAuthModule`](../backend/src/oauth/oauth.module.ts) and [`OAuthSignInDto`](../backend/src/oauth/dto/oauthSignIn.dto.ts):
```ts
@Module({
@@ -117,8 +109,7 @@ and [`OAuthSignInDto`](../backend/src/oauth/dto/oauthSignIn.dto.ts):
useFactory(github: GitHubProvider, /* your provider */): Record<string, OAuthProvider<unknown>> {
return {
github,
google,
oidc,
/* your provider */
};
},
inject: [GitHubProvider, /* your provider */],
@@ -131,8 +122,7 @@ export class OAuthModule {
```ts
export interface OAuthSignInDto {
provider: 'github' | 'google' | 'microsoft' | 'discord' | 'oidc' /* your provider*/
;
provider: 'github' | 'google' | 'microsoft' | 'discord' | 'oidc' /* your provider*/;
providerId: string;
providerUsername: string;
email: string;
@@ -161,8 +151,7 @@ Add keys below to your i18n text in [locale file](../frontend/src/i18n/translati
- `admin.config.oauth.YOUR_PROVIDER_NAME-enabled`
- `admin.config.oauth.YOUR_PROVIDER_NAME-client-id`
- `admin.config.oauth.YOUR_PROVIDER_NAME-client-secret`
- `error.param.provider_YOUR_PROVIDER_NAME`
- Other config keys you defined in step 1
Congratulations! 🎉 You have successfully added a new OAuth 2 provider! Pull requests are welcome if you want to share
your provider with others.
Congratulations! 🎉 You have successfully added a new OAuth 2 provider! Pull requests are welcome if you want to share your provider with others.

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-frontend",
"version": "0.19.2",
"version": "0.20.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pingvin-share-frontend",
"version": "0.19.2",
"version": "0.20.3",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share-frontend",
"version": "0.19.2",
"version": "0.20.3",
"scripts": {
"dev": "next dev",
"build": "next build",

View File

@@ -36,28 +36,28 @@ const showShareInformationsModal = (
children: (
<Stack align="stretch" spacing="md">
<Text size="sm" color="lightgray">
<Text size="sm">
<b>
<FormattedMessage id="account.shares.table.id" />:{" "}
</b>
{share.id}
</Text>
<Text size="sm" color="lightgray">
<Text size="sm">
<b>
<FormattedMessage id="account.shares.table.description" />:{" "}
</b>
{share.description || "No description"}
</Text>
<Text size="sm" color="lightgray">
<Text size="sm">
<b>
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
</b>
{formattedCreatedAt}
</Text>
<Text size="sm" color="lightgray">
<Text size="sm">
<b>
<FormattedMessage id="account.shares.table.expiresAt" />:{" "}
</b>
@@ -66,7 +66,7 @@ const showShareInformationsModal = (
<Divider />
<CopyTextField link={link} />
<Divider />
<Text size="sm" color="lightgray">
<Text size="sm">
<b>
<FormattedMessage id="account.shares.table.size" />:{" "}
</b>
@@ -76,7 +76,7 @@ const showShareInformationsModal = (
<Flex align="center" justify="center">
{shareSize / maxShareSize < 0.1 && (
<Text size="xs" color="lightgray" style={{ marginRight: "4px" }}>
<Text size="xs" style={{ marginRight: "4px" }}>
{formattedShareSize}
</Text>
)}
@@ -87,7 +87,7 @@ const showShareInformationsModal = (
size="xl"
radius="xl"
/>
<Text size="xs" color="lightgray" style={{ marginLeft: "4px" }}>
<Text size="xs" style={{ marginLeft: "4px" }}>
{formattedMaxShareSize}
</Text>
</Flex>

View File

@@ -33,10 +33,12 @@ const useStyles = createStyles((theme) => ({
}));
const Dropzone = ({
title,
isUploading,
maxShareSize,
showCreateUploadModalCallback,
}: {
title?: string;
isUploading: boolean;
maxShareSize: number;
showCreateUploadModalCallback: (files: FileUpload[]) => void;
@@ -78,7 +80,7 @@ const Dropzone = ({
<TbCloudUpload size={50} />
</Group>
<Text align="center" weight={700} size="lg" mt="xl">
<FormattedMessage id="upload.dropzone.title" />
{title || <FormattedMessage id="upload.dropzone.title" />}
</Text>
<Text align="center" size="sm" mt="xs" color="dimmed">
<FormattedMessage

View File

@@ -0,0 +1,238 @@
import { Button, Group } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { cleanNotifications } from "@mantine/notifications";
import { AxiosError } from "axios";
import pLimit from "p-limit";
import { useEffect, useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import Dropzone from "../../components/upload/Dropzone";
import FileList from "../../components/upload/FileList";
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type";
import toast from "../../utils/toast.util";
import { useRouter } from "next/router";
const promiseLimit = pLimit(3);
const chunkSize = 10 * 1024 * 1024; // 10MB
let errorToastShown = false;
const EditableUpload = ({
maxShareSize,
shareId,
files: savedFiles = [],
}: {
maxShareSize?: number;
isReverseShare?: boolean;
shareId: string;
files?: FileMetaData[];
}) => {
const t = useTranslate();
const router = useRouter();
const config = useConfig();
const [existingFiles, setExistingFiles] =
useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles);
const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]);
const [isUploading, setIsUploading] = useState(false);
const existingAndUploadedFiles: FileListItem[] = useMemo(
() => [...uploadingFiles, ...existingFiles],
[existingFiles, uploadingFiles],
);
const dirty = useMemo(() => {
return (
existingFiles.some((file) => !!file.deleted) || !!uploadingFiles.length
);
}, [existingFiles, uploadingFiles]);
const setFiles = (files: FileListItem[]) => {
const _uploadFiles = files.filter(
(file) => "uploadingProgress" in file,
) as FileUpload[];
const _existingFiles = files.filter(
(file) => !("uploadingProgress" in file),
) as FileMetaData[];
setUploadingFiles(_uploadFiles);
setExistingFiles(_existingFiles);
};
maxShareSize ??= parseInt(config.get("share.maxSize"));
const uploadFiles = async (files: FileUpload[]) => {
const fileUploadPromises = files.map(async (file, fileIndex) =>
// Limit the number of concurrent uploads to 3
promiseLimit(async () => {
let fileId: string;
const setFileProgress = (progress: number) => {
setUploadingFiles((files) =>
files.map((file, callbackIndex) => {
if (fileIndex == callbackIndex) {
file.uploadingProgress = progress;
}
return file;
}),
);
};
setFileProgress(1);
let chunks = Math.ceil(file.size / chunkSize);
// If the file is 0 bytes, we still need to upload 1 chunk
if (chunks == 0) chunks++;
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
const from = chunkIndex * chunkSize;
const to = from + chunkSize;
const blob = file.slice(from, to);
try {
await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (event) =>
await shareService
.uploadFile(
shareId,
event,
{
id: fileId,
name: file.name,
},
chunkIndex,
chunks,
)
.then((response) => {
fileId = response.id;
resolve(response);
})
.catch(reject);
reader.readAsDataURL(blob);
});
setFileProgress(((chunkIndex + 1) / chunks) * 100);
} catch (e) {
if (
e instanceof AxiosError &&
e.response?.data.error == "unexpected_chunk_index"
) {
// Retry with the expected chunk index
chunkIndex = e.response!.data!.expectedChunkIndex - 1;
continue;
} else {
setFileProgress(-1);
// Retry after 5 seconds
await new Promise((resolve) => setTimeout(resolve, 5000));
chunkIndex = -1;
continue;
}
}
}
}),
);
await Promise.all(fileUploadPromises);
};
const removeFiles = async () => {
const removedFiles = existingFiles.filter((file) => !!file.deleted);
if (removedFiles.length > 0) {
await Promise.all(
removedFiles.map(async (file) => {
await shareService.removeFile(shareId, file.id);
}),
);
setExistingFiles(existingFiles.filter((file) => !file.deleted));
}
};
const revertComplete = async () => {
await shareService.revertComplete(shareId).then();
};
const completeShare = async () => {
return await shareService.completeShare(shareId);
};
const save = async () => {
setIsUploading(true);
try {
await revertComplete();
await uploadFiles(uploadingFiles);
const hasFailed = uploadingFiles.some(
(file) => file.uploadingProgress == -1,
);
if (!hasFailed) {
await removeFiles();
}
await completeShare();
if (!hasFailed) {
toast.success(t("share.edit.notify.save-success"));
router.back();
}
} catch {
toast.error(t("share.edit.notify.generic-error"));
} finally {
setIsUploading(false);
}
};
const appendFiles = (appendingFiles: FileUpload[]) => {
setUploadingFiles([...appendingFiles, ...uploadingFiles]);
};
useEffect(() => {
// Check if there are any files that failed to upload
const fileErrorCount = uploadingFiles.filter(
(file) => file.uploadingProgress == -1,
).length;
if (fileErrorCount > 0) {
if (!errorToastShown) {
toast.error(
t("upload.notify.count-failed", { count: fileErrorCount }),
{
withCloseButton: false,
autoClose: false,
},
);
}
errorToastShown = true;
} else {
cleanNotifications();
errorToastShown = false;
}
}, [uploadingFiles]);
return (
<>
<Group position="right" mb={20}>
<Button loading={isUploading} disabled={!dirty} onClick={() => save()}>
<FormattedMessage id="common.button.save" />
</Button>
</Group>
<Dropzone
title={t("share.edit.append-upload")}
maxShareSize={maxShareSize}
showCreateUploadModalCallback={appendFiles}
isUploading={isUploading}
/>
{existingAndUploadedFiles.length > 0 && (
<FileList files={existingAndUploadedFiles} setFiles={setFiles} />
)}
</>
);
};
export default EditableUpload;

View File

@@ -1,41 +1,106 @@
import { ActionIcon, Table } from "@mantine/core";
import { Dispatch, SetStateAction } from "react";
import { TbTrash } from "react-icons/tb";
import { FileUpload } from "../../types/File.type";
import { GrUndo } from "react-icons/gr";
import { FileListItem } from "../../types/File.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
import UploadProgressIndicator from "./UploadProgressIndicator";
import { FormattedMessage } from "react-intl";
const FileList = ({
const FileListRow = ({
file,
onRemove,
onRestore,
}: {
file: FileListItem;
onRemove?: () => void;
onRestore?: () => void;
}) => {
{
const uploadable = "uploadingProgress" in file;
const uploading = uploadable && file.uploadingProgress !== 0;
const removable = uploadable
? file.uploadingProgress === 0
: onRemove && !file.deleted;
const restorable = onRestore && !uploadable && !!file.deleted; // maybe undefined, force boolean
const deleted = !uploadable && !!file.deleted;
return (
<tr
style={{
color: deleted ? "rgba(120, 120, 120, 0.5)" : "inherit",
textDecoration: deleted ? "line-through" : "none",
}}
>
<td>{file.name}</td>
<td>{byteToHumanSizeString(+file.size)}</td>
<td>
{removable && (
<ActionIcon
color="red"
variant="light"
size={25}
onClick={onRemove}
>
<TbTrash />
</ActionIcon>
)}
{uploading && (
<UploadProgressIndicator progress={file.uploadingProgress} />
)}
{restorable && (
<ActionIcon
color="primary"
variant="light"
size={25}
onClick={onRestore}
>
<GrUndo />
</ActionIcon>
)}
</td>
</tr>
);
}
};
const FileList = <T extends FileListItem = FileListItem>({
files,
setFiles,
}: {
files: FileUpload[];
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
files: T[];
setFiles: (files: T[]) => void;
}) => {
const remove = (index: number) => {
files.splice(index, 1);
const file = files[index];
if ("uploadingProgress" in file) {
files.splice(index, 1);
} else {
files[index] = { ...file, deleted: true };
}
setFiles([...files]);
};
const restore = (index: number) => {
const file = files[index];
if ("uploadingProgress" in file) {
return;
} else {
files[index] = { ...file, deleted: false };
}
setFiles([...files]);
};
const rows = files.map((file, i) => (
<tr key={i}>
<td>{file.name}</td>
<td>{byteToHumanSizeString(file.size)}</td>
<td>
{file.uploadingProgress == 0 ? (
<ActionIcon
color="red"
variant="light"
size={25}
onClick={() => remove(i)}
>
<TbTrash />
</ActionIcon>
) : (
<UploadProgressIndicator progress={file.uploadingProgress} />
)}
</td>
</tr>
<FileListRow
key={i}
file={file}
onRemove={() => remove(i)}
onRestore={() => restore(i)}
/>
));
return (

View File

@@ -125,11 +125,13 @@ const CreateUploadModalBody = ({
"",
) as moment.unitOfTime.DurationConstructor,
);
if (
options.maxExpirationInHours != 0 &&
expirationDate.isAfter(
moment().add(options.maxExpirationInHours, "hours"),
)
(form.values.never_expires ||
expirationDate.isAfter(
moment().add(options.maxExpirationInHours, "hours"),
))
) {
form.setFieldError(
"expiration_num",

View File

@@ -6,12 +6,13 @@ import finnish from "./translations/fi-FI";
import french from "./translations/fr-FR";
import japanese from "./translations/ja-JP";
import dutch from "./translations/nl-BE";
import polish from "./translations/pl-PL";
import portuguese from "./translations/pt-BR";
import russian from "./translations/ru-RU";
import serbian from "./translations/sr-SP";
import swedish from "./translations/sv-SE";
import thai from "./translations/th-TH";
import chineseSimplified from "./translations/zh-CN";
import polish from "./translations/pl-PL";
export const LOCALES = {
ENGLISH: {
@@ -84,4 +85,9 @@ export const LOCALES = {
code: "pl-PL",
messages: polish,
},
SWEDISH: {
name: "Svenska",
code: "sv-SE",
messages: swedish,
},
};

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Forhåndsvisning ikke understøttet",
"share.modal.file-preview.error.not-supported.description": "En forhåndsvisning for thise filtype er ikke understøttet. Download venligst filen for at se den.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Konfiguration",
"admin.config.category.general": "Generelt",

View File

@@ -115,7 +115,7 @@ export default {
"account.shares.title.empty": "Es ist so leer hier 👀",
"account.shares.description.empty": "Du hast keine Freigaben erstellt.",
"account.shares.button.create": "Erstelle eine",
"account.shares.info.title": "Teile deine Information",
"account.shares.info.title": "Freigabe Informationen",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Beschreibung",
@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Vorschau wird nicht unterstützt",
"share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte lade die Datei herunter, um sie anzuzeigen.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Einstellungen",
"admin.config.category.general": "Allgemein",

View File

@@ -358,6 +358,13 @@ export default {
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",

View File

@@ -51,8 +51,8 @@ export default {
"signup.button.submit": "Comencemos",
// END /auth/signup
// /auth/totp
"totp.title": "TOTP Authentication",
"totp.button.signIn": "Sign in",
"totp.title": "Autenticación TOTP",
"totp.button.signIn": "Iniciar sesión",
// END /auth/totp
// /auth/reset-password
"resetPassword.title": "¿Olvidaste tu contraseña?",
@@ -72,20 +72,20 @@ export default {
"account.card.password.title": "Contraseña",
"account.card.password.old": "Anterior contraseña",
"account.card.password.new": "Nueva contraseña",
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
"account.card.password.noPasswordSet": "No tienes una establecida contraseña. Si deseas iniciar sesión con correo electrónico y una contraseña necesitas crear una contraseña.",
"account.notify.password.success": "Contraseña cambiada correctamente",
"account.card.oauth.title": "Social login",
"account.card.oauth.title": "Inicio de sesión con red social",
"account.card.oauth.github": "GitHub",
"account.card.oauth.google": "Google",
"account.card.oauth.microsoft": "Microsoft",
"account.card.oauth.discord": "Discord",
"account.card.oauth.oidc": "OpenID",
"account.card.oauth.link": "Link",
"account.card.oauth.unlink": "Unlink",
"account.card.oauth.unlinked": "Unlinked",
"account.modal.unlink.title": "Unlink account",
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
"account.notify.oauth.unlinked.success": "Unlinked successfully",
"account.card.oauth.link": "Vincular",
"account.card.oauth.unlink": "Desvincular",
"account.card.oauth.unlinked": "Desvinculado",
"account.modal.unlink.title": "Desvincular cuenta",
"account.modal.unlink.description": "Desvincular tus cuentas sociales puede causar que pierdas tu cuenta si no recuerdas tu nombre de usuario y contraseña.",
"account.notify.oauth.unlinked.success": "Desvinculado correctamente",
"account.card.security.title": "Seguridad",
"account.card.security.totp.enable.description": "Ingrese su contraseña actual para habilitar TOTP",
"account.card.security.totp.disable.description": "Ingrese su contraseña actual para deshabilitar TOTP",
@@ -214,7 +214,7 @@ export default {
"upload.modal.not-signed-in-description": "No podrás eliminar tus compartidos manualmente ni ver el número de visitas.",
"upload.modal.expires.never": "nunca",
"upload.modal.expires.never-long": "Nunca Expira",
"upload.modal.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
"upload.modal.expires.error.too-long": "La caducidad excede la fecha de caducidad máxima de {max}.",
"upload.modal.link.label": "Enlace",
"upload.modal.expires.label": "Expiración",
"upload.modal.expires.minute-singular": "Minuto",
@@ -264,13 +264,19 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Vista previa no disponible",
"share.modal.file-preview.error.not-supported.description": "La vista previa para este tipo de archivo no está disponible. Por favor descargue el archivo para verlo.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Editar {shareId}",
"share.edit.append-upload": "Agregar archivo",
"share.edit.notify.generic-error": "Ha ocurrido un error mientras se compartía tu archivo.",
"share.edit.notify.save-success": "Compartir actualizado correctamente",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Configuración",
"admin.config.category.general": "General",
"admin.config.category.share": "Compartido",
"admin.config.category.email": "Correo",
"admin.config.category.smtp": "SMTP",
"admin.config.category.oauth": "Social Login",
"admin.config.category.oauth": "Inicio de sesión social",
"admin.config.general.app-name": "Nombre de la App",
"admin.config.general.app-name.description": "Nombre de la aplicación",
"admin.config.general.app-url": "App URL",
@@ -302,7 +308,7 @@ export default {
"admin.config.share.allow-registration.description": "Si se permite el registro",
"admin.config.share.allow-unauthenticated-shares": "Permitir compartir sin iniciar sesión",
"admin.config.share.allow-unauthenticated-shares.description": "Si los usuarios que no han iniciado sesión pueden compartir",
"admin.config.share.max-expiration": "Max expiration",
"admin.config.share.max-expiration": "Expiración máxima",
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
"admin.config.share.max-size": "Tamaño máximo",
"admin.config.share.max-size.description": "Tamaño máximo de los archivos, en bytes",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Esikatselua ei tuettu",
"share.modal.file-preview.error.not-supported.description": "Esikatselua thise tiedostotyypille ei tueta. Ole hyvä ja lataa tiedosto nähdäksesi sen.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Asetukset",
"admin.config.category.general": "Yleiset",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Aperçu non supporté",
"share.modal.file-preview.error.not-supported.description": "Un aperçu pour ce type de fichier n'est pas pris en charge. Veuillez télécharger le fichier pour le voir.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Modifier {shareId}",
"share.edit.append-upload": "Ajouter un fichier",
"share.edit.notify.generic-error": "Une erreur est survenue durant le traitement de votre partage.",
"share.edit.notify.save-success": "Partage mis à jour avec succès",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Paramètres",
"admin.config.category.general": "Général",

View File

@@ -33,7 +33,7 @@ export default {
"signin.button.submit": "サインイン",
"signIn.notify.totp-required.title": "二段階認証が必要です",
"signIn.notify.totp-required.description": "二段階認証コードを入力してください",
"signIn.oauth.or": "OR",
"signIn.oauth.or": "または",
"signIn.oauth.github": "GitHub",
"signIn.oauth.google": "Google",
"signIn.oauth.microsoft": "Microsoft",
@@ -51,8 +51,8 @@ export default {
"signup.button.submit": "さあ始めましょう",
// END /auth/signup
// /auth/totp
"totp.title": "TOTP Authentication",
"totp.button.signIn": "Sign in",
"totp.title": "二段階認証",
"totp.button.signIn": "サインイン",
// END /auth/totp
// /auth/reset-password
"resetPassword.title": "パスワードを忘れてしまいましたか?",
@@ -72,20 +72,20 @@ export default {
"account.card.password.title": "パスワード",
"account.card.password.old": "現在のパスワード",
"account.card.password.new": "新規パスワード",
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
"account.card.password.noPasswordSet": "パスワードが設定されていません。メールアドレスとパスワードでサインインしたい場合は、パスワードの設定が必要です。",
"account.notify.password.success": "パスワードの変更に成功しました",
"account.card.oauth.title": "Social login",
"account.card.oauth.title": "ソーシャルログイン",
"account.card.oauth.github": "GitHub",
"account.card.oauth.google": "Google",
"account.card.oauth.microsoft": "Microsoft",
"account.card.oauth.discord": "Discord",
"account.card.oauth.oidc": "OpenID",
"account.card.oauth.link": "Link",
"account.card.oauth.unlink": "Unlink",
"account.card.oauth.unlinked": "Unlinked",
"account.modal.unlink.title": "Unlink account",
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
"account.notify.oauth.unlinked.success": "Unlinked successfully",
"account.card.oauth.link": "リンク",
"account.card.oauth.unlink": "リンク解除",
"account.card.oauth.unlinked": "リンクされていません",
"account.modal.unlink.title": "アカウントのリンクを解除",
"account.modal.unlink.description": "ソーシャルアカウントのリンクを解除すると、ユーザー名とパスワードを覚えていない場合にアカウントへのアクセスが失われる可能性があります。",
"account.notify.oauth.unlinked.success": "リンク解除に成功しました",
"account.card.security.title": "セキュリティ",
"account.card.security.totp.enable.description": "2段階認証を有効にするため、現在のパスワードを入力してください",
"account.card.security.totp.disable.description": "2段階認証を無効にするため、現在のパスワードを入力してください",
@@ -214,7 +214,7 @@ export default {
"upload.modal.not-signed-in-description": "共有の手動削除と訪問者カウンターは表示できません。",
"upload.modal.expires.never": "永久",
"upload.modal.expires.never-long": "期限切れにさせない",
"upload.modal.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
"upload.modal.expires.error.too-long": "設定可能な有効期限を越えています。有効期限の上限は{max} です。",
"upload.modal.link.label": "リンク",
"upload.modal.expires.label": "有効期限",
"upload.modal.expires.minute-singular": "分間",
@@ -264,13 +264,19 @@ export default {
"share.modal.file-preview.error.not-supported.title": "プレビューに対応していません",
"share.modal.file-preview.error.not-supported.description": "これらのファイルのプレビューには対応していません。ファイルをダウンロードして、直接確認してください。",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "編集 {shareId}",
"share.edit.append-upload": "ファイルを追加",
"share.edit.notify.generic-error": "共有の最終処理でエラーが発生しました。",
"share.edit.notify.save-success": "共有の更新に成功しました",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "設定",
"admin.config.category.general": "一般",
"admin.config.category.share": "共有",
"admin.config.category.email": "メール",
"admin.config.category.smtp": "SMTP",
"admin.config.category.oauth": "Social Login",
"admin.config.category.oauth": "ソーシャルログイン",
"admin.config.general.app-name": "アプリ名",
"admin.config.general.app-name.description": "アプリの名前",
"admin.config.general.app-url": "アプリ名",
@@ -302,8 +308,8 @@ export default {
"admin.config.share.allow-registration.description": "登録を許可するかどうかを選択してください。",
"admin.config.share.allow-unauthenticated-shares": "ログインしていない状態での共有を許可する",
"admin.config.share.allow-unauthenticated-shares.description": "ログインしていないユーザーに共有の作成を許可するかどうかを選択してください。",
"admin.config.share.max-expiration": "Max expiration",
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
"admin.config.share.max-expiration": "有効期限の上限",
"admin.config.share.max-expiration.description": "共有に設定可能な有効期限の上限を時間単位で設定できます。0を設定すると、有効期限が無制限になります。",
"admin.config.share.max-size": "最大ファイルサイズ",
"admin.config.share.max-size.description": "最大ファイルサイズbyte単位",
"admin.config.share.zip-compression-level": "Zip圧縮レベル",
@@ -321,58 +327,58 @@ export default {
"admin.config.smtp.password": "パスワード",
"admin.config.smtp.password.description": "SMTPサーバーのパスワード",
"admin.config.smtp.button.test": "テストメールを送信",
"admin.config.oauth.allow-registration": "Allow registration",
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
"admin.config.oauth.ignore-totp": "Ignore TOTP",
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
"admin.config.oauth.allow-registration": "登録を許可する",
"admin.config.oauth.allow-registration.description": "ユーザーにソーシャルアカウント経由での登録を許可します",
"admin.config.oauth.ignore-totp": "二段階認証を無視する",
"admin.config.oauth.ignore-totp.description": "ソーシャルログイン時に二段階認証を無視するかどうかを設定します",
"admin.config.oauth.github-enabled": "GitHub",
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
"admin.config.oauth.github-client-id": "GitHub Client ID",
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
"admin.config.oauth.github-client-secret": "GitHub Client secret",
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
"admin.config.oauth.github-enabled.description": "GitHubアカウントを使用したログインを許可するかどうかを設定します",
"admin.config.oauth.github-client-id": "GitHub クライアントID",
"admin.config.oauth.github-client-id.description": "GitHub OAuthアプリのクライアントID",
"admin.config.oauth.github-client-secret": "GitHub クライアントシークレット",
"admin.config.oauth.github-client-secret.description": "GitHub OAuthアプリのクライアントシークレット",
"admin.config.oauth.google-enabled": "Google",
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
"admin.config.oauth.google-client-id": "Google Client ID",
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
"admin.config.oauth.google-client-secret": "Google Client secret",
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
"admin.config.oauth.google-enabled.description": "Googleアカウントを使用したログインを許可するかどうかを設定します",
"admin.config.oauth.google-client-id": "Google クライアントID",
"admin.config.oauth.google-client-id.description": "Google OAuthアプリのクライアントID",
"admin.config.oauth.google-client-secret": "Google クライアントシークレット",
"admin.config.oauth.google-client-secret.description": "Google OAuthアプリのクライアントシークレット",
"admin.config.oauth.microsoft-enabled": "Microsoft",
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
"admin.config.oauth.microsoft-enabled.description": "Microsoftアカウントを使用したログインを許可するかどうかを設定します",
"admin.config.oauth.microsoft-tenant": "Microsoftテナント",
"admin.config.oauth.microsoft-tenant.description": "Microsoft OAuthアプリのテナントID\ncommon: 個人のMicrosoftアカウントとMicrosoft Entra IDの職場または学校のアカウントを持つユーザーは、アプリケーションにサインインできます。 \norganizations: Microsoft Entra IDからの職場または学校のアカウントを持つユーザーのみがアプリケーションにサインインできます。\nconsumers: 個人のMicrosoftアカウントを持つユーザーのみがアプリケーションにサインインできます。\nMicrosoft Entraテナントのドメイン名またはGUID形式のテナントID: 特定のMicrosoft Entraテナント (職場または学校のアカウントを持つディレクトリメンバーまたはパーソナルMicrosoftアカウントを持つディレクトリゲスト) からのユーザーのみがアプリケーションにサインインできます。",
"admin.config.oauth.microsoft-client-id": "Microsoft クライアントID",
"admin.config.oauth.microsoft-client-id.description": "Microsoft OAuthアプリのクライアントID",
"admin.config.oauth.microsoft-client-secret": "Microsoft クライアントシークレット",
"admin.config.oauth.microsoft-client-secret.description": "Microsoft OAuthアプリのクライアントシークレット",
"admin.config.oauth.discord-enabled": "Discord",
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
"admin.config.oauth.discord-client-id": "Discord Client ID",
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
"admin.config.oauth.discord-client-secret": "Discord Client secret",
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
"admin.config.oauth.discord-enabled.description": "Discordアカウントを使用したログインを許可するかどうかを設定します",
"admin.config.oauth.discord-client-id": "Discord クライアントID",
"admin.config.oauth.discord-client-id.description": "Discord OAuthアプリのクライアントID",
"admin.config.oauth.discord-client-secret": "Discord クライアントシークレット",
"admin.config.oauth.discord-client-secret.description": "Discord OAuthアプリのクライアントシークレット",
"admin.config.oauth.oidc-enabled": "OpenID",
"admin.config.oauth.oidc-enabled.description": "Whether OpenID login is enabled",
"admin.config.oauth.oidc-enabled.description": "OpenIDアカウントを使用したログインを許可するかどうかを設定します",
"admin.config.oauth.oidc-discovery-uri": "OpenID Discovery URI",
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID OAuth app",
"admin.config.oauth.oidc-client-id": "OpenID Client ID",
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID OAuth app",
"admin.config.oauth.oidc-client-secret": "OpenID Client secret",
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID OAuth app",
"admin.config.oauth.oidc-discovery-uri.description": "OpenID OAuthアプリのDiscovery URI",
"admin.config.oauth.oidc-client-id": "OpenID クライアントID",
"admin.config.oauth.oidc-client-id.description": "OpenID OAuthアプリのクライアントID",
"admin.config.oauth.oidc-client-secret": "OpenID クライアントシークレット",
"admin.config.oauth.oidc-client-secret.description": "OpenID OAuthアプリのクライアントシークレット",
// 404
"404.description": "ページが見つかりません。",
"404.button.home": "ホームに戻る",
// error
"error.title": "Error",
"error.description": "Oops!",
"error.button.back": "Go back",
"error.msg.default": "Something went wrong.",
"error.msg.access_denied": "You canceled the authentication process, please try again.",
"error.msg.expired_token": "The authentication process took too long, please try again.",
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
"error.msg.no_email": "Can't get email address from this {0} account.",
"error.msg.already_linked": "This {0} account is already linked to another account.",
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
"error.title": "エラー",
"error.description": "申し訳ありません",
"error.button.back": "戻る",
"error.msg.default": "問題が発生しました。",
"error.msg.access_denied": "認証処理を中止しました、後で再度お試しください。",
"error.msg.expired_token": "認証処理に時間がかかりすぎています、後で再度お試しください。",
"error.msg.no_user": "この{0} アカウントにリンクしたユーザーが存在しません。",
"error.msg.no_email": "この{0} アカウントからメールアドレスを取得出来ません。",
"error.msg.already_linked": "この{0} アカウントは、既に別のアカウントにリンクされています。",
"error.msg.not_linked": "この{0} アカウントはどのアカウントにもリンクされていません。",
"error.param.provider_github": "GitHub",
"error.param.provider_google": "Google",
"error.param.provider_microsoft": "Microsoft",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Voorbeeld niet ondersteund",
"share.modal.file-preview.error.not-supported.description": "Een voorbeeld voor dit bestandstype wordt niet ondersteund. Download het bestand om het te bekijken.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Configuratie",
"admin.config.category.general": "Algemeen",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Podgląd nie jest obsługiwany",
"share.modal.file-preview.error.not-supported.description": "Podgląd dla tego typu pliku nie jest obsługiwany. Pobierz plik, aby go zobaczyć.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edytuj {shareId}",
"share.edit.append-upload": "Dołącz plik",
"share.edit.notify.generic-error": "W trakcie zakańczania tworzenia udziału wystąpił błąd.",
"share.edit.notify.save-success": "Udział zaktualizowany pomyślnie",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Konfiguracja",
"admin.config.category.general": "Ogólne",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Visualização não suportada",
"share.modal.file-preview.error.not-supported.description": "Uma visualização para este tipo de arquivo não é suportada. Faça o download do arquivo para visualizá-lo.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Editar {shareId}",
"share.edit.append-upload": "Anexar arquivo",
"share.edit.notify.generic-error": "Ocorreu um erro ao terminar seu compartilhamento.",
"share.edit.notify.save-success": "Compartilhamento atualizado com sucesso",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Configuração",
"admin.config.category.general": "Geral",

View File

@@ -33,7 +33,7 @@ export default {
"signin.button.submit": "Вход",
"signIn.notify.totp-required.title": "Требуется двухфакторная аутентификация",
"signIn.notify.totp-required.description": "Пожалуйста, введите код Вашей 2-х факторной аутентификации",
"signIn.oauth.or": "OR",
"signIn.oauth.or": "ИЛИ",
"signIn.oauth.github": "GitHub",
"signIn.oauth.google": "Google",
"signIn.oauth.microsoft": "Microsoft",
@@ -51,8 +51,8 @@ export default {
"signup.button.submit": "Давайте начнём",
// END /auth/signup
// /auth/totp
"totp.title": "TOTP Authentication",
"totp.button.signIn": "Sign in",
"totp.title": "Авторизация TOTP",
"totp.button.signIn": "Войти",
// END /auth/totp
// /auth/reset-password
"resetPassword.title": "Забыли пароль?",
@@ -72,20 +72,20 @@ export default {
"account.card.password.title": "Пароль",
"account.card.password.old": "Старый пароль",
"account.card.password.new": "Новый пароль",
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
"account.card.password.noPasswordSet": "У вас не установлен пароль. Если вы хотите войти с помощью электронной почты и пароля, вам необходимо установить пароль.",
"account.notify.password.success": "Пароль успешно изменён",
"account.card.oauth.title": "Social login",
"account.card.oauth.title": "Авторизация через социальные сети",
"account.card.oauth.github": "GitHub",
"account.card.oauth.google": "Google",
"account.card.oauth.microsoft": "Microsoft",
"account.card.oauth.discord": "Discord",
"account.card.oauth.oidc": "OpenID",
"account.card.oauth.link": "Link",
"account.card.oauth.unlink": "Unlink",
"account.card.oauth.unlinked": "Unlinked",
"account.modal.unlink.title": "Unlink account",
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
"account.notify.oauth.unlinked.success": "Unlinked successfully",
"account.card.oauth.link": "Подключить",
"account.card.oauth.unlink": "Отключить",
"account.card.oauth.unlinked": "Отключен",
"account.modal.unlink.title": "Отключить связь с учетной записью",
"account.modal.unlink.description": "Отключение связи с учетной записью социальных аккаунтов может привести к потере вашей учетной записи, если вы не помните свое имя пользователя и пароль.",
"account.notify.oauth.unlinked.success": "Отключение прошло успешно",
"account.card.security.title": "Безопасность",
"account.card.security.totp.enable.description": "Введите ваш текущий пароль для начала включения TOTP",
"account.card.security.totp.disable.description": "Введите ваш текущий пароль, чтобы отключить TOTP",
@@ -134,7 +134,7 @@ export default {
"account.reverseShares.title.empty": "Тут пусто 👀",
"account.reverseShares.description.empty": "У вас пока нет обратных загрузок.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.title": "Создать обратную ссылку на файл",
"account.reverseShares.modal.expiration.label": "Истекает",
"account.reverseShares.modal.expiration.minute-singular": "Минута",
"account.reverseShares.modal.expiration.minute-plural": "Минут(ы)",
@@ -214,7 +214,7 @@ export default {
"upload.modal.not-signed-in-description": "Вы не сможете удалить свои файлы вручную и просмотреть количество посетителей.",
"upload.modal.expires.never": "никогда",
"upload.modal.expires.never-long": "Никогда не истекает",
"upload.modal.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
"upload.modal.expires.error.too-long": "Срок действия превышает максимальную дату окончания срока действия {max}.",
"upload.modal.link.label": "Ссылка",
"upload.modal.expires.label": "Истекает",
"upload.modal.expires.minute-singular": "Минута",
@@ -264,13 +264,19 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Предпросмотр не поддерживается",
"share.modal.file-preview.error.not-supported.description": "Предварительный просмотр этого типа файла не поддерживается. Пожалуйста, скачайте файл для его просмотра.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Редактировать {shareId}",
"share.edit.append-upload": "Добавить файл",
"share.edit.notify.generic-error": "Произошла ошибка при завершении вашей загрузки.",
"share.edit.notify.save-success": "Ссылка на ресурс успешна обновлена",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Конфигурация",
"admin.config.category.general": "Общее",
"admin.config.category.share": "Загрузки",
"admin.config.category.email": "Электронная почта",
"admin.config.category.smtp": "SMTP",
"admin.config.category.oauth": "Social Login",
"admin.config.category.oauth": "Авторизация через социальные сети",
"admin.config.general.app-name": "Название приложения",
"admin.config.general.app-name.description": "Видимое название приложения",
"admin.config.general.app-url": "URL-адрес приложения",
@@ -302,8 +308,8 @@ export default {
"admin.config.share.allow-registration.description": "Разрешена ли регистрация",
"admin.config.share.allow-unauthenticated-shares": "Разрешить неавторизованные загрузки",
"admin.config.share.allow-unauthenticated-shares.description": "Могут ли неавторизованные пользователи создавать загрузки",
"admin.config.share.max-expiration": "Max expiration",
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
"admin.config.share.max-expiration": "Максимальная срок действия",
"admin.config.share.max-expiration.description": "Максимальный срок действия общего доступа в часах. Установите значение 0, чтобы разрешить неограниченный срок действия.",
"admin.config.share.max-size": "Максимальный размер",
"admin.config.share.max-size.description": "Максимальный размер файла в байтах",
"admin.config.share.zip-compression-level": "Уровень сжатия Zip",
@@ -321,58 +327,58 @@ export default {
"admin.config.smtp.password": "Пароль",
"admin.config.smtp.password.description": "Пароль SMTP-сервера",
"admin.config.smtp.button.test": "Отправить тестовое письмо",
"admin.config.oauth.allow-registration": "Allow registration",
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
"admin.config.oauth.ignore-totp": "Ignore TOTP",
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
"admin.config.oauth.allow-registration": "Разрешить регистрацию",
"admin.config.oauth.allow-registration.description": "Разрешить пользователям регистрироваться используя учетные записи социальных сетей",
"admin.config.oauth.ignore-totp": "Игнорировать TOTP",
"admin.config.oauth.ignore-totp.description": "Игнорировать TOTP при использовании социальной авторизации",
"admin.config.oauth.github-enabled": "GitHub",
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
"admin.config.oauth.github-client-id": "GitHub Client ID",
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
"admin.config.oauth.github-client-secret": "GitHub Client secret",
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
"admin.config.oauth.github-enabled.description": "Включен ли логин на GitHub",
"admin.config.oauth.github-client-id": "ID клиента GitHub",
"admin.config.oauth.github-client-id.description": "ID клиента в приложении GitHub OAuth",
"admin.config.oauth.github-client-secret": "Секретный ключ клиента GitHub",
"admin.config.oauth.github-client-secret.description": "Секретный ключ клиента в приложении GitHub OAuth",
"admin.config.oauth.google-enabled": "Google",
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
"admin.config.oauth.google-client-id": "Google Client ID",
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
"admin.config.oauth.google-client-secret": "Google Client secret",
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
"admin.config.oauth.google-enabled.description": "Включен ли логин Google на GitHub",
"admin.config.oauth.google-client-id": "ID клиента Google",
"admin.config.oauth.google-client-id.description": "ID клиента в приложении Google OAuth",
"admin.config.oauth.google-client-secret": "Секретный ключ клиента Google",
"admin.config.oauth.google-client-secret.description": "Секретный ключ клиента в приложении Google OAuth",
"admin.config.oauth.microsoft-enabled": "Microsoft",
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
"admin.config.oauth.microsoft-enabled.description": "Включен ли логин Microsoft",
"admin.config.oauth.microsoft-tenant": "Корпоративный аккаунт Microsoft",
"admin.config.oauth.microsoft-tenant.description": "Идентификатор арендатора приложения Microsoft OAuth\ncommon: Пользователи с личным аккаунтом Microsoft и рабочим или учебным аккаунтом от Microsoft Entra ID могут войти в приложение. organizations: Только пользователи с рабочим или учебным аккаунтом от Microsoft Entra ID могут войти в приложение.\nconsumers: Только пользователи с личным аккаунтом Microsoft могут войти в приложение.\nимя домена арендатора Microsoft Entra или идентификатор арендатора в формате GUID: Только пользователи из определенного арендатора Microsoft Entra (участники каталога с рабочим или учебным аккаунтом или гости каталога с личным аккаунтом Microsoft) могут войти в приложение.",
"admin.config.oauth.microsoft-client-id": "Идентификатор клиента Microsoft",
"admin.config.oauth.microsoft-client-id.description": "ID клиента в приложении Microsoft OAuth",
"admin.config.oauth.microsoft-client-secret": "Секретный ключ клиента Microsoft",
"admin.config.oauth.microsoft-client-secret.description": "Секретный ключ клиента в приложении Microsoft OAuth",
"admin.config.oauth.discord-enabled": "Discord",
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
"admin.config.oauth.discord-client-id": "Discord Client ID",
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
"admin.config.oauth.discord-client-secret": "Discord Client secret",
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
"admin.config.oauth.discord-enabled.description": "Включен ли логин Discord",
"admin.config.oauth.discord-client-id": "ID клиента Discord",
"admin.config.oauth.discord-client-id.description": "ID клиента в приложении Discord OAuth",
"admin.config.oauth.discord-client-secret": "Секретный ключ клиента Discord",
"admin.config.oauth.discord-client-secret.description": "Секретный ключ клиента в приложении Discord OAuth",
"admin.config.oauth.oidc-enabled": "OpenID",
"admin.config.oauth.oidc-enabled.description": "Whether OpenID login is enabled",
"admin.config.oauth.oidc-enabled.description": "Включен ли логин OpenID",
"admin.config.oauth.oidc-discovery-uri": "OpenID Discovery URI",
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID OAuth app",
"admin.config.oauth.oidc-client-id": "OpenID Client ID",
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID OAuth app",
"admin.config.oauth.oidc-client-secret": "OpenID Client secret",
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID OAuth app",
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI приложения OpenID OAuth",
"admin.config.oauth.oidc-client-id": "ID клиента OpenID",
"admin.config.oauth.oidc-client-id.description": "ID клиента в приложении OpenID OAuth",
"admin.config.oauth.oidc-client-secret": "Секретный ключ клиента OpenID",
"admin.config.oauth.oidc-client-secret.description": "Секретный ключ клиента в приложении OpenID OAuth",
// 404
"404.description": "Упс, этой страницы не существует.",
"404.button.home": "Верните меня домой",
// error
"error.title": "Error",
"error.description": "Oops!",
"error.button.back": "Go back",
"error.msg.default": "Something went wrong.",
"error.msg.access_denied": "You canceled the authentication process, please try again.",
"error.msg.expired_token": "The authentication process took too long, please try again.",
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
"error.msg.no_email": "Can't get email address from this {0} account.",
"error.msg.already_linked": "This {0} account is already linked to another account.",
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
"error.title": "Ошибка",
"error.description": "Что-то пошло не так!",
"error.button.back": "Назад",
"error.msg.default": "Что-то пошло не так.",
"error.msg.access_denied": "Вы отменили процесс аутентификации, пожалуйста, попробуйте еще раз.",
"error.msg.expired_token": "Процесс аутентификации занял слишком много времени, пожалуйста, попробуйте еще раз.",
"error.msg.no_user": "Пользователь связанный с учетной записью {0} не существует.",
"error.msg.no_email": "Не удается получить адрес электронной почты от учетной записи {0}.",
"error.msg.already_linked": "Эта учетная запись {0} уже привязана к другому аккаунту.",
"error.msg.not_linked": "Эта учетная запись {0} ещё не привязана ни к одному аккаунту.",
"error.param.provider_github": "GitHub",
"error.param.provider_google": "Google",
"error.param.provider_microsoft": "Microsoft",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "Преглед није подржан",
"share.modal.file-preview.error.not-supported.description": "Преглед за овај тип датотеке није подржан. Преузмите датотеку да бисте је видели.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Конфигурација",
"admin.config.category.general": "Опште",

View File

@@ -0,0 +1,411 @@
export default {
// Navbar
"navbar.upload": "Ladda upp",
"navbar.signin": "Logga in",
"navbar.home": "Startsida",
"navbar.signup": "Skapa konto",
"navbar.links.shares": "Mina delningar",
"navbar.links.reverse": "Omvända delningar",
"navbar.avatar.account": "Mitt konto",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Logga ut",
// END navbar
// /
"home.title": "En <h>egen</h> fildelningsplattform.",
"home.description": "Vill du verkligen lägga dina personliga filer hos en tredje part som WeTransfer?",
"home.bullet.a.name": "Lokalt installerad",
"home.bullet.a.description": "Hosta Pingvin Share på din egen maskin.",
"home.bullet.b.name": "Sekretess",
"home.bullet.b.description": "Dina filer är dina filer och bör aldrig hamna i händerna på tredje part.",
"home.bullet.c.name": "Ingen irriterande filstorleksbegränsning",
"home.bullet.c.description": "Ladda upp så stora filer som du vill. Endast din hårddisk kommer att vara din gräns.",
"home.button.start": "Kom igång",
"home.button.source": "Källkod",
// END /
// /auth/signin
"signin.title": "Välkommen tillbaka",
"signin.description": "Har du inget konto än?",
"signin.button.signup": "Skapa konto",
"signin.input.email-or-username": "E-post eller användarnamn",
"signin.input.email-or-username.placeholder": "Din e-postadress eller ditt användarnamn",
"signin.input.password": "Lösenord",
"signin.input.password.placeholder": "Lösenord",
"signin.button.submit": "Logga in",
"signIn.notify.totp-required.title": "Tvåfaktorsautentisering krävs",
"signIn.notify.totp-required.description": "Vänligen ange din tvåfaktorsautentiseringskod",
"signIn.oauth.or": "ELLER",
"signIn.oauth.github": "GitHub",
"signIn.oauth.google": "Google",
"signIn.oauth.microsoft": "Microsoft",
"signIn.oauth.discord": "Discord",
"signIn.oauth.oidc": "OpenID",
// END /auth/signin
// /auth/signup
"signup.title": "Skapa ett konto",
"signup.description": "Har du redan ett konto?",
"signup.button.signin": "Logga in",
"signup.input.username": "Användarnamn",
"signup.input.username.placeholder": "Ditt användarnamn",
"signup.input.email": "E-post",
"signup.input.email.placeholder": "Din e-post",
"signup.button.submit": "Kom igång",
// END /auth/signup
// /auth/totp
"totp.title": "TOTP-autentisering",
"totp.button.signIn": "Logga in",
// END /auth/totp
// /auth/reset-password
"resetPassword.title": "Glömt ditt lösenord?",
"resetPassword.description": "Ange din e-postadress för att återställa ditt lösenord.",
"resetPassword.notify.success": "Ett e-postmeddelande har skickats till dig med en länk för att återställa lösenordet.",
"resetPassword.button.back": "Tillbaka till inloggningssidan",
"resetPassword.text.resetPassword": "Återställ lösenord",
"resetPassword.text.enterNewPassword": "Ange ditt nya lösenord",
"resetPassword.input.password": "Nytt lösenord",
"resetPassword.notify.passwordReset": "Ditt lösenord har återställts.",
// /account
"account.title": "Mitt konto",
"account.card.info.title": "Kontoinformation",
"account.card.info.username": "Användarnamn",
"account.card.info.email": "E-post",
"account.notify.info.success": "Kontot uppdaterades",
"account.card.password.title": "Lösenord",
"account.card.password.old": "Gammalt lösenord",
"account.card.password.new": "Nytt lösenord",
"account.card.password.noPasswordSet": "Du har inget lösenord. Om du vill logga in med e-post och lösenord måste du ange ett lösenord.",
"account.notify.password.success": "Lösenordet har ändrats",
"account.card.oauth.title": "Inloggning via sociala nätverk",
"account.card.oauth.github": "GitHub",
"account.card.oauth.google": "Google",
"account.card.oauth.microsoft": "Microsoft",
"account.card.oauth.discord": "Discord",
"account.card.oauth.oidc": "OpenID",
"account.card.oauth.link": "Länka",
"account.card.oauth.unlink": "Avlänka",
"account.card.oauth.unlinked": "Avlänkad",
"account.modal.unlink.title": "Avlänka konto",
"account.modal.unlink.description": "Om du kopplar bort dina sociala konton kan det leda till att du förlorar ditt konto om du inte kommer ihåg ditt användarnamn och lösenord.",
"account.notify.oauth.unlinked.success": "Avlänkning utförd",
"account.card.security.title": "Säkerhet",
"account.card.security.totp.enable.description": "Ange ditt nuvarande lösenord för att aktivera TOTP",
"account.card.security.totp.disable.description": "Ange ditt lösenord för att inaktivera TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Aktivera TOTP",
"account.modal.totp.step1": "Steg 1: Lägg till din autentiserare",
"account.modal.totp.step2": "Steg 2: Bekräfta din kod",
"account.modal.totp.enterManually": "Ange manuellt",
"account.modal.totp.code": "Kod",
"account.modal.totp.clickToCopy": "Klicka för att kopiera",
"account.modal.totp.verify": "Verifiera",
"account.notify.totp.disable": "TOTP har inaktiverats",
"account.notify.totp.enable": "TOTP aktiverat",
"account.card.language.title": "Språk",
"account.card.language.description": "Projektet är översatt av gemenskapen. Vissa översättningar kan vara ofullständiga.",
"account.card.color.title": "Färgschema",
// ThemeSwitcher.tsx
"account.theme.dark": "Mörk",
"account.theme.light": "Ljus",
"account.theme.system": "System",
"account.button.delete": "Ta bort konto",
"account.modal.delete.title": "Ta bort konto",
"account.modal.delete.description": "Vill du verkligen ta bort ditt konto inklusive alla dina aktiva delningar?",
// END /account
// /account/shares
"account.shares.title": "Mina delningar",
"account.shares.title.empty": "Här var det tomt 👀",
"account.shares.description.empty": "Du har inga delningar.",
"account.shares.button.create": "Skapa en",
"account.shares.info.title": "Information om delning",
"account.shares.table.id": "ID",
"account.shares.table.name": "Namn",
"account.shares.table.description": "Beskrivning",
"account.shares.table.visitors": "Besökare",
"account.shares.table.expiresAt": "Förfaller den",
"account.shares.table.createdAt": "Skapad den",
"account.shares.table.size": "Storlek",
"account.shares.modal.share-informations": "Information om delning",
"account.shares.modal.share-link": "Delningslänk",
"account.shares.modal.delete.title": "Ta bort delning {share}",
"account.shares.modal.delete.description": "Vill du verkligen ta bort denna delning?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Omvända delningar",
"account.reverseShares.description": "En omvänd delning gör att du kan generera en unik URL som tillåter externa användare att skapa en delning.",
"account.reverseShares.title.empty": "Här var det tomt 👀",
"account.reverseShares.description.empty": "Du har inga omvända delningar.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Skapa omvänd delning",
"account.reverseShares.modal.expiration.label": "Förfaller",
"account.reverseShares.modal.expiration.minute-singular": "Minut",
"account.reverseShares.modal.expiration.minute-plural": "Minuter",
"account.reverseShares.modal.expiration.hour-singular": "Timme",
"account.reverseShares.modal.expiration.hour-plural": "Timmar",
"account.reverseShares.modal.expiration.day-singular": "Dag",
"account.reverseShares.modal.expiration.day-plural": "Dagar",
"account.reverseShares.modal.expiration.week-singular": "Vecka",
"account.reverseShares.modal.expiration.week-plural": "Veckor",
"account.reverseShares.modal.expiration.month-singular": "Månad",
"account.reverseShares.modal.expiration.month-plural": "Månader",
"account.reverseShares.modal.expiration.year-singular": "År",
"account.reverseShares.modal.expiration.year-plural": "År",
"account.reverseShares.modal.max-size.label": "Max storlek på delning",
"account.reverseShares.modal.send-email": "Skicka e-postavisering",
"account.reverseShares.modal.send-email.description": "Skicka ett e-postmeddelande när en delning skapas med denna länk för omvänd delning.",
"account.reverseShares.modal.max-use.label": "Maxanvändningar",
"account.reverseShares.modal.max-use.description": "Den maximala mängden gånger denna URL kan användas för att skapa en delning.",
"account.reverseShare.never-expires": "Denna omvända delning kommer aldrig att förfalla.",
"account.reverseShare.expires-on": "Denna sammanlagda delning löper ut på {expiration}.",
"account.reverseShares.table.no-shares": "Inga delningar har skapats ännu",
"account.reverseShares.table.count.singular": "delning",
"account.reverseShares.table.count.plural": "delningar",
"account.reverseShares.table.shares": "Delningar",
"account.reverseShares.table.remaining": "Återstående användningar",
"account.reverseShares.table.max-size": "Max storlek på delning",
"account.reverseShares.table.expires": "Förfaller den",
"account.reverseShares.modal.reverse-share-link": "Omvänd delningslänk",
"account.reverseShares.modal.delete.title": "Ta bort omvänd delning",
"account.reverseShares.modal.delete.description": "Vill du verkligen ta bort denna omvänd delning? Om du gör det, kommer de tillhörande delningarna också att raderas.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "Användarhantering",
"admin.button.config": "Konfiguration",
"admin.version": "Version",
// END /admin
// /admin/users
"admin.users.title": "Användarhantering",
"admin.users.table.username": "Användarnamn",
"admin.users.table.email": "E-post",
"admin.users.table.admin": "Administratör",
"admin.users.edit.update.title": "Uppdatera användare {username}",
"admin.users.edit.update.admin-privileges": "Administratörsbehörigheter",
"admin.users.edit.update.change-password.title": "Ändra lösenord",
"admin.users.edit.update.change-password.field": "Nytt lösenord",
"admin.users.edit.update.change-password.button": "Spara nytt lösenord",
"admin.users.edit.update.notify.password.success": "Lösenordet har ändrats",
"admin.users.edit.delete.title": "Ta bort användare {username}",
"admin.users.edit.delete.description": "Vill du verkligen ta bort denna användare och alla deras delningar?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Skapa användare",
"admin.users.modal.create.username": "Användarnamn",
"admin.users.modal.create.email": "E-post",
"admin.users.modal.create.password": "Lösenord",
"admin.users.modal.create.manual-password": "Sätt lösenord manuellt",
"admin.users.modal.create.manual-password.description": "Om den inte är markerad kommer användaren att få ett e-postmeddelande med en länk för att ange lösenordet.",
"admin.users.modal.create.admin": "Administratörsbehörigheter",
"admin.users.modal.create.admin.description": "Om detta markeras kommer användaren att kunna komma åt administratörspanelen.",
// END /admin/users
// /upload
"upload.title": "Ladda upp",
"upload.notify.generic-error": "Ett fel uppstod när din delning skulle slutföras.",
"upload.notify.count-failed": "{count} filer kunde inte laddas upp. Försöker igen.",
// Dropzone.tsx
"upload.dropzone.title": "Ladde upp filer",
"upload.dropzone.description": "Drag och släpp filer här för att starta din delning. Vi kan endast acceptera filer som är mindre än {maxSize} totalt.",
"upload.dropzone.notify.file-too-big": "Dina filer överskrider den maximala storleken på {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Namn",
"upload.filelist.size": "Storlek",
// showCreateUploadModal.tsx
"upload.modal.title": "Skapa delning",
"upload.modal.link.error.invalid": "Kan endast innehålla bokstäver, siffror, understreck och bindestreck",
"upload.modal.link.error.taken": "Denna länk används redan",
"upload.modal.not-signed-in": "Du är inte inloggad",
"upload.modal.not-signed-in-description": "Du kommer inte att kunna ta bort din delning manuellt och visa antalet besökare.",
"upload.modal.expires.never": "aldrig",
"upload.modal.expires.never-long": "Upphör aldrig",
"upload.modal.expires.error.too-long": "Förfallodatum överskrider maximalt utgångsdatum för {max}.",
"upload.modal.link.label": "Länk",
"upload.modal.expires.label": "Förfaller",
"upload.modal.expires.minute-singular": "Minut",
"upload.modal.expires.minute-plural": "Minuter",
"upload.modal.expires.hour-singular": "Timme",
"upload.modal.expires.hour-plural": "Timmar",
"upload.modal.expires.day-singular": "Dag",
"upload.modal.expires.day-plural": "Dagar",
"upload.modal.expires.week-singular": "Vecka",
"upload.modal.expires.week-plural": "Veckor",
"upload.modal.expires.month-singular": "Månad",
"upload.modal.expires.month-plural": "Månader",
"upload.modal.expires.year-singular": "År",
"upload.modal.expires.year-plural": "År",
"upload.modal.accordion.description.title": "Beskrivning",
"upload.modal.accordion.description.placeholder": "Anteckning till mottagare av denna delning",
"upload.modal.accordion.email.title": "E-postmottagare",
"upload.modal.accordion.email.placeholder": "Ange e-postmottagare",
"upload.modal.accordion.email.invalid-email": "Ogiltig e-postadress",
"upload.modal.accordion.security.title": "Säkerhetsalternativ",
"upload.modal.accordion.security.password.label": "Lösenordsskydd",
"upload.modal.accordion.security.password.placeholder": "Inget lösenord",
"upload.modal.accordion.security.max-views.label": "Max antal visningar",
"upload.modal.accordion.security.max-views.placeholder": "Ingen gräns",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "Denna delning kommer aldrig att upphöra.",
"upload.modal.completed.expires-on": "Denna delning upphör att gälla {expiration}.",
"upload.modal.completed.share-ready": "Delning redo",
// END /upload
// /share/[id]
"share.title": "Delning {shareId}",
"share.description": "Titta vad jag har delat med dig!",
"share.error.visitor-limit-exceeded.title": "Besökargränsen överskriden",
"share.error.visitor-limit-exceeded.description": "Gränsen för antalet besökare för denna delning har överskridits.",
"share.error.removed.title": "Delning borttagen",
"share.error.not-found.title": "Delningen hittades inte",
"share.error.not-found.description": "Delningen du letar efter existerar inte.",
"share.modal.password.title": "Lösenord krävs",
"share.modal.password.description": "För att komma åt denna delning, vänligen ange lösenordet för delningen.",
"share.modal.password": "Lösenord",
"share.modal.error.invalid-password": "Ogiltigt lösenord",
"share.button.download-all": "Ladda ner allt",
"share.notify.download-all-preparing": "Delningen förbereds. Försök igen om några minuter.",
"share.modal.file-link": "Fillänk",
"share.table.name": "Namn",
"share.table.size": "Storlek",
"share.modal.file-preview.error.not-supported.title": "Förhandsgranskning stöds ej",
"share.modal.file-preview.error.not-supported.description": "En förhandsvisning för filtypen stöds inte. Ladda ner filen för att se den.",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Redigera {shareId}",
"share.edit.append-upload": "Lägg till fil",
"share.edit.notify.generic-error": "Ett fel uppstod när din delning skulle slutföras.",
"share.edit.notify.save-success": "Delningen har uppdaterats",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "Konfiguration",
"admin.config.category.general": "Allmänt",
"admin.config.category.share": "Delning",
"admin.config.category.email": "E-post",
"admin.config.category.smtp": "SMTP",
"admin.config.category.oauth": "Inloggning via sociala nätverk",
"admin.config.general.app-name": "Appnamn",
"admin.config.general.app-name.description": "Namn på applikationen",
"admin.config.general.app-url": "Appens URL",
"admin.config.general.app-url.description": "På vilken URL Pingvin Share finns",
"admin.config.general.show-home-page": "Visa startsidan",
"admin.config.general.show-home-page.description": "Om du vill visa startsidan",
"admin.config.general.logo": "Logotyp",
"admin.config.general.logo.description": "Ändra din logotyp genom att ladda upp en ny bild. Bilden måste vara en PNG och bör ha formatet 1:1.",
"admin.config.general.logo.placeholder": "Välj bild",
"admin.config.email.enable-share-email-recipients": "Aktivera e-postmottagare för delning",
"admin.config.email.enable-share-email-recipients.description": "Om du vill tillåta e-post till delningsmottagare. Aktivera endast detta om du har aktiverat SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description": "Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description": "Meddelande som skickas till delningens mottagare. Tillgängliga variabler:\n {creator} - Användarnamnet för skaparen av delningen\n {shareUrl} - URL för delningen\n {desc} - Beskrivningen av delningen\n {expires} - Förfallodatumet för delningen\n Variablerna kommer att ersättas med det faktiska värdet.",
"admin.config.email.reverse-share-subject": "Omvänd delning ämne",
"admin.config.email.reverse-share-subject.description": "Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description": "Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description": "Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Återställ lösenord meddelande",
"admin.config.email.reset-password-message.description": "Meddelande som skickas när en användare begär en lösenordsåterställning. {url} kommer att ersättas med länken för lösenordsåterställningen.",
"admin.config.email.invite-subject": "Inbjudan ämne",
"admin.config.email.invite-subject.description": "Ämne i e-postmeddelandet som skickas när en administratör bjuder in en användare.",
"admin.config.email.invite-message": "Inbjudningsmeddelanden",
"admin.config.email.invite-message.description": "Meddelande som skickas när en administratör bjuder in en användare. {url} kommer att ersättas med inbjudningsadressen och {password} med lösenordet.",
"admin.config.share.allow-registration": "Tillåt registrering",
"admin.config.share.allow-registration.description": "Om registrering är tillåten",
"admin.config.share.allow-unauthenticated-shares": "Tillåt oautentiserade delningar",
"admin.config.share.allow-unauthenticated-shares.description": "Om oautentiserade användare kan skapa delningar",
"admin.config.share.max-expiration": "Max utgångsdatum",
"admin.config.share.max-expiration.description": "Max längd innan en delning förfaller i timmar. Sätt till 0 för att tillåta obegränsad förfallotid.",
"admin.config.share.max-size": "Max storlek",
"admin.config.share.max-size.description": "Maximal storlek för delning i bytes",
"admin.config.share.zip-compression-level": "Komprimeringsnivå för zip",
"admin.config.share.zip-compression-level.description": "Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
"admin.config.smtp.enabled": "Aktiverad",
"admin.config.smtp.enabled.description": "Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Adress",
"admin.config.smtp.host.description": "Adress för SMTP-servern",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port för SMTP-servern",
"admin.config.smtp.email": "E-post",
"admin.config.smtp.email.description": "E-postadress som e-postmeddelanden skickas från",
"admin.config.smtp.username": "Användarnamn",
"admin.config.smtp.username.description": "Användarnamn för SMTP-servern",
"admin.config.smtp.password": "Lösenord",
"admin.config.smtp.password.description": "Lösenord för SMTP-servern",
"admin.config.smtp.button.test": "Skicka testmeddelande",
"admin.config.oauth.allow-registration": "Tillåt registrering",
"admin.config.oauth.allow-registration.description": "Tillåt användare att registrera sig via social inloggning",
"admin.config.oauth.ignore-totp": "Ignorera TOTP",
"admin.config.oauth.ignore-totp.description": "Om du vill ignorera TOTP när användaren använder social inloggning",
"admin.config.oauth.github-enabled": "GitHub",
"admin.config.oauth.github-enabled.description": "Om GitHub-inloggning är aktiverad",
"admin.config.oauth.github-client-id": "GitHub Client ID",
"admin.config.oauth.github-client-id.description": "Client-ID för GitHub OAuth appen",
"admin.config.oauth.github-client-secret": "GitHub Client secret",
"admin.config.oauth.github-client-secret.description": "Client secret för GitHub OAuth appen",
"admin.config.oauth.google-enabled": "Google",
"admin.config.oauth.google-enabled.description": "Om Google-inloggning är aktiverad",
"admin.config.oauth.google-client-id": "Google Client ID",
"admin.config.oauth.google-client-id.description": "Client-ID för Google OAuth appen",
"admin.config.oauth.google-client-secret": "Google Client secret",
"admin.config.oauth.google-client-secret.description": "Client secret för Google OAuth appen",
"admin.config.oauth.microsoft-enabled": "Microsoft",
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
"admin.config.oauth.discord-enabled": "Discord",
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
"admin.config.oauth.discord-client-id": "Discord Client ID",
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
"admin.config.oauth.discord-client-secret": "Discord Client secret",
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
"admin.config.oauth.oidc-enabled": "OpenID",
"admin.config.oauth.oidc-enabled.description": "Whether OpenID login is enabled",
"admin.config.oauth.oidc-discovery-uri": "OpenID Discovery URI",
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID OAuth app",
"admin.config.oauth.oidc-client-id": "OpenID Client ID",
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID OAuth app",
"admin.config.oauth.oidc-client-secret": "OpenID Client secret",
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID OAuth app",
// 404
"404.description": "Hoppsan den här sidan finns inte.",
"404.button.home": "Ta mig tillbaka hem",
// error
"error.title": "Fel",
"error.description": "Hoppsan!",
"error.button.back": "Gå tillbaka",
"error.msg.default": "Någonting gick fel.",
"error.msg.access_denied": "Du avbröt autentiseringsprocessen, försök igen.",
"error.msg.expired_token": "Autentiseringsprocessen tog för lång tid, försök igen.",
"error.msg.no_user": "Användare som är länkad till detta {0} konto finns inte.",
"error.msg.no_email": "Kan inte hämta e-postadress från detta {0} konto.",
"error.msg.already_linked": "Detta {0} konto är redan länkat till ett annat konto.",
"error.msg.not_linked": "Detta {0} konto har ännu inte länkat till något konto.",
"error.param.provider_github": "GitHub",
"error.param.provider_google": "Google",
"error.param.provider_microsoft": "Microsoft",
"error.param.provider_discord": "Discord",
"error.param.provider_oidc": "OpenID",
// Common translations
"common.button.save": "Spara",
"common.button.create": "Skapa",
"common.button.submit": "Skicka",
"common.button.delete": "Ta bort",
"common.button.cancel": "Avbryt",
"common.button.confirm": "Bekräfta",
"common.button.disable": "Inaktivera",
"common.button.share": "Delning",
"common.button.generate": "Generera",
"common.button.done": "Klar",
"common.text.link": "Länk",
"common.text.or": "eller",
"common.button.go-back": "Gå tillbaka",
"common.notify.copied": "Din länk har kopierats till urklipp",
"common.success": "Slutförd",
"common.error": "Fel",
"common.error.unknown": "Ett okänt fel har uppstått",
"common.error.invalid-email": "Ogiltig e-postadress",
"common.error.too-short": "Måste minst vara {length} tecken långt",
"common.error.too-long": "Får högst innehålla {length} tecken",
"common.error.exact-length": "Måste vara exakt {length} tecken långt",
"common.error.invalid-number": "Måste vara ett tal",
"common.error.field-required": "Obligatoriskt fält"
};

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "ไม่รองรับการแสดงตัวอย่าง",
"share.modal.file-preview.error.not-supported.description": "ไม่รองรับการแสดงตัวอย่างสำหรับไฟล์ประเภทนี้ โปรดดาวน์โหลดไฟล์เพื่อดู",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "Edit {shareId}",
"share.edit.append-upload": "Append file",
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
"share.edit.notify.save-success": "Share updated successfully",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "การตั้งค่า",
"admin.config.category.general": "ทั่วไป",

View File

@@ -264,6 +264,12 @@ export default {
"share.modal.file-preview.error.not-supported.title": "该文件类型不支持预览",
"share.modal.file-preview.error.not-supported.description": "该文件类型不支持预览,请下载后打开查看",
// END /share/[id]
// /share/[id]/edit
"share.edit.title": "编辑 {shareId}",
"share.edit.append-upload": "追加文件",
"share.edit.notify.generic-error": "保存共享的过程中发生了错误",
"share.edit.notify.save-success": "共享已更新成功",
// END /share/[id]/edit
// /admin/config
"admin.config.title": "配置管理",
"admin.config.category.general": "通用",

View File

@@ -16,7 +16,7 @@ import { useModals } from "@mantine/modals";
import moment from "moment";
import Link from "next/link";
import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
import { TbEdit, TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
@@ -110,6 +110,11 @@ const MyShares = () => {
</td>
<td>
<Group position="right">
<Link href={`/share/${share.id}/edit`}>
<ActionIcon color="orange" variant="light" size={25}>
<TbEdit />
</ActionIcon>
</Link>
<ActionIcon
color="blue"
variant="light"

View File

@@ -0,0 +1,65 @@
import { LoadingOverlay } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react";
import showErrorModal from "../../../components/share/showErrorModal";
import shareService from "../../../services/share.service";
import { Share as ShareType } from "../../../types/share.type";
import useTranslate from "../../../hooks/useTranslate.hook";
import EditableUpload from "../../../components/upload/EditableUpload";
import Meta from "../../../components/Meta";
export function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: { shareId: context.params!.shareId },
};
}
const Share = ({ shareId }: { shareId: string }) => {
const t = useTranslate();
const modals = useModals();
const [isLoading, setIsLoading] = useState(true);
const [share, setShare] = useState<ShareType>();
useEffect(() => {
shareService
.getFromOwner(shareId)
.then((share) => {
setShare(share);
})
.catch((e) => {
const { error } = e.response.data;
if (e.response.status == 404) {
if (error == "share_removed") {
showErrorModal(
modals,
t("share.error.removed.title"),
e.response.data.message,
);
} else {
showErrorModal(
modals,
t("share.error.not-found.title"),
t("share.error.not-found.description"),
);
}
} else {
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
}
})
.finally(() => {
setIsLoading(false);
});
}, []);
if (isLoading) return <LoadingOverlay visible />;
return (
<>
<Meta title={t("share.edit.title", { shareId })} />
<EditableUpload shareId={shareId} files={share?.files || []} />
</>
);
};
export default Share;

View File

@@ -202,7 +202,9 @@ const Upload = ({
showCreateUploadModalCallback={showCreateUploadModalCallback}
isUploading={isUploading}
/>
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
{files.length > 0 && (
<FileList<FileUpload> files={files} setFiles={setFiles} />
)}
</>
);
};

View File

@@ -19,10 +19,18 @@ const completeShare = async (id: string) => {
return (await api.post(`shares/${id}/complete`)).data;
};
const revertComplete = async (id: string) => {
return (await api.delete(`shares/${id}/complete`)).data;
};
const get = async (id: string): Promise<Share> => {
return (await api.get(`shares/${id}`)).data;
};
const getFromOwner = async (id: string): Promise<Share> => {
return (await api.get(`shares/${id}/from-owner`)).data;
};
const getMetaData = async (id: string): Promise<ShareMetaData> => {
return (await api.get(`shares/${id}/metaData`)).data;
};
@@ -63,6 +71,10 @@ const downloadFile = async (shareId: string, fileId: string) => {
window.location.href = `${window.location.origin}/api/shares/${shareId}/files/${fileId}`;
};
const removeFile = async (shareId: string, fileId: string) => {
await api.delete(`shares/${shareId}/files/${fileId}`);
};
const uploadFile = async (
shareId: string,
readerEvent: ProgressEvent<FileReader>,
@@ -121,14 +133,17 @@ const removeReverseShare = async (id: string) => {
export default {
create,
completeShare,
revertComplete,
getShareToken,
get,
getFromOwner,
remove,
getMetaData,
doesFileSupportPreview,
getMyShares,
isShareIdAvailable,
downloadFile,
removeFile,
uploadFile,
setReverseShare,
createReverseShare,

View File

@@ -7,3 +7,5 @@ export type FileMetaData = {
name: string;
size: string;
};
export type FileListItem = FileUpload | (FileMetaData & { deleted?: boolean });

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share",
"version": "0.19.2",
"version": "0.20.3",
"scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",