diff --git a/violet-server/server/docker-compose.yaml b/violet-server/server/docker-compose.yaml index dafa970b1..51f4911e2 100644 --- a/violet-server/server/docker-compose.yaml +++ b/violet-server/server/docker-compose.yaml @@ -13,7 +13,7 @@ services: local-redis: platform: linux/x86_64 - image: redis + image: redis:latest restart: always container_name: violet-dev-redis command: sh -cx "redis-server --daemonize yes && redis-cli config set notify-keyspace-events KEA && sleep infinity" diff --git a/violet-server/server/package-lock.json b/violet-server/server/package-lock.json index ceea74ae5..9ad9684d6 100644 --- a/violet-server/server/package-lock.json +++ b/violet-server/server/package-lock.json @@ -12,7 +12,7 @@ "@aws-sdk/client-s3": "^3.651.1", "@aws-sdk/s3-presigned-post": "^3.651.1", "@golevelup/ts-jest": "^0.5.5", - "@nestjs/common": "^11.0.16", + "@nestjs/common": "^10.4.18", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.4.2", "@nestjs/jwt": "^10.0.0", @@ -2760,11 +2760,12 @@ } }, "node_modules/@nestjs/common": { - "version": "11.0.16", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.16.tgz", - "integrity": "sha512-agvuQ8su4aZ+PVxAmY89odG1eR97HEQvxPmTMdDqyvDWzNerl7WQhUEd+j4/UyNWcF1or1UVcrtPj52x+eUSsA==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.18.tgz", + "integrity": "sha512-9SrTth6YJJ9CjVnekw9WP8kaiwh+tSgR0KIrPYV/aWgF9D0175uDJUglzbiCfUbkPyHN8jcKXUXd3EVPDY6BNA==", "license": "MIT", "dependencies": { + "file-type": "20.4.1", "iterare": "1.2.1", "tslib": "2.8.1", "uid": "2.0.2" @@ -2776,7 +2777,6 @@ "peerDependencies": { "class-transformer": "*", "class-validator": "*", - "file-type": "^20.4.1", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, @@ -3990,7 +3990,6 @@ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", @@ -4008,8 +4007,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tootallnate/once": { "version": "1.1.2", @@ -7301,8 +7299,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/figures": { "version": "3.2.0", @@ -7353,7 +7350,6 @@ "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", "license": "MIT", - "peer": true, "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", @@ -10723,7 +10719,6 @@ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -12018,7 +12013,6 @@ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", "license": "MIT", - "peer": true, "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" @@ -12481,7 +12475,6 @@ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", "license": "MIT", - "peer": true, "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -12960,7 +12953,6 @@ "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -15563,10 +15555,11 @@ } }, "@nestjs/common": { - "version": "11.0.16", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.16.tgz", - "integrity": "sha512-agvuQ8su4aZ+PVxAmY89odG1eR97HEQvxPmTMdDqyvDWzNerl7WQhUEd+j4/UyNWcF1or1UVcrtPj52x+eUSsA==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.18.tgz", + "integrity": "sha512-9SrTth6YJJ9CjVnekw9WP8kaiwh+tSgR0KIrPYV/aWgF9D0175uDJUglzbiCfUbkPyHN8jcKXUXd3EVPDY6BNA==", "requires": { + "file-type": "20.4.1", "iterare": "1.2.1", "tslib": "2.8.1", "uid": "2.0.2" @@ -16473,7 +16466,6 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", - "peer": true, "requires": { "debug": "^4.4.0", "fflate": "^0.8.2", @@ -16483,8 +16475,7 @@ "@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "peer": true + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, "@tootallnate/once": { "version": "1.1.2", @@ -18961,8 +18952,7 @@ "fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "peer": true + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, "figures": { "version": "3.2.0", @@ -19002,7 +18992,6 @@ "version": "20.4.1", "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", - "peer": true, "requires": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", @@ -21574,8 +21563,7 @@ "peek-readable": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", - "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", - "peer": true + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==" }, "picocolors": { "version": "1.1.1", @@ -22518,7 +22506,6 @@ "version": "10.2.2", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", - "peer": true, "requires": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" @@ -22855,7 +22842,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", - "peer": true, "requires": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -23108,8 +23094,7 @@ "uint8array-extras": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", - "peer": true + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==" }, "undici-types": { "version": "5.26.5", @@ -23221,7 +23206,7 @@ "@aws-sdk/s3-presigned-post": "^3.651.1", "@golevelup/ts-jest": "^0.5.5", "@nestjs/cli": "^10.4.9", - "@nestjs/common": "^11.0.16", + "@nestjs/common": "^10.4.18", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.4.2", "@nestjs/jwt": "^10.0.0", @@ -25345,10 +25330,11 @@ } }, "@nestjs/common": { - "version": "11.0.16", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.16.tgz", - "integrity": "sha512-agvuQ8su4aZ+PVxAmY89odG1eR97HEQvxPmTMdDqyvDWzNerl7WQhUEd+j4/UyNWcF1or1UVcrtPj52x+eUSsA==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.18.tgz", + "integrity": "sha512-9SrTth6YJJ9CjVnekw9WP8kaiwh+tSgR0KIrPYV/aWgF9D0175uDJUglzbiCfUbkPyHN8jcKXUXd3EVPDY6BNA==", "requires": { + "file-type": "20.4.1", "iterare": "1.2.1", "tslib": "2.8.1", "uid": "2.0.2" @@ -26255,7 +26241,6 @@ "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", - "peer": true, "requires": { "debug": "^4.4.0", "fflate": "^0.8.2", @@ -26265,8 +26250,7 @@ "@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "peer": true + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, "@tootallnate/once": { "version": "1.1.2", @@ -28743,8 +28727,7 @@ "fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "peer": true + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, "figures": { "version": "3.2.0", @@ -28784,7 +28767,6 @@ "version": "20.4.1", "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", - "peer": true, "requires": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", @@ -31356,8 +31338,7 @@ "peek-readable": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", - "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", - "peer": true + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==" }, "picocolors": { "version": "1.1.1", @@ -32300,7 +32281,6 @@ "version": "10.2.2", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", - "peer": true, "requires": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" @@ -32637,7 +32617,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", - "peer": true, "requires": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -32890,8 +32869,7 @@ "uint8array-extras": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", - "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", - "peer": true + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==" }, "undici-types": { "version": "5.26.5", diff --git a/violet-server/server/package.json b/violet-server/server/package.json index 00a0f8025..9c1d513d7 100644 --- a/violet-server/server/package.json +++ b/violet-server/server/package.json @@ -23,7 +23,7 @@ "@aws-sdk/client-s3": "^3.651.1", "@aws-sdk/s3-presigned-post": "^3.651.1", "@golevelup/ts-jest": "^0.5.5", - "@nestjs/common": "^11.0.16", + "@nestjs/common": "^10.4.18", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.4.2", "@nestjs/jwt": "^10.0.0", diff --git a/violet-server/server/src/comment/dtos/comment-get.dto.ts b/violet-server/server/src/comment/dtos/comment-get.dto.ts index b751b8d64..1824f2f6b 100644 --- a/violet-server/server/src/comment/dtos/comment-get.dto.ts +++ b/violet-server/server/src/comment/dtos/comment-get.dto.ts @@ -3,7 +3,7 @@ import { Type } from 'class-transformer'; import { Comment } from 'src/comment/entity/comment.entity'; import { IsArray, - IsNumber, + IsInt, IsOptional, IsString, Matches, @@ -21,7 +21,7 @@ export class CommentGetDto { } export class CommentGetResponseDtoElement { - @IsNumber() + @IsInt() @ApiProperty({ description: 'Comment Id', required: true, diff --git a/violet-server/server/src/comment/dtos/comment-post.dto.ts b/violet-server/server/src/comment/dtos/comment-post.dto.ts index a7a88036f..c1c44dabf 100644 --- a/violet-server/server/src/comment/dtos/comment-post.dto.ts +++ b/violet-server/server/src/comment/dtos/comment-post.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { - IsNumber, + IsInt, IsOptional, IsString, Matches, @@ -28,7 +28,7 @@ export class CommentPostDto { @MaxLength(500) body: string; - @IsNumber() + @IsInt() @IsOptional() @ApiProperty({ description: 'Parent Comment', diff --git a/violet-server/server/src/stats/dtos/stats.dto.ts b/violet-server/server/src/stats/dtos/stats.dto.ts index bcfbb2c0f..a9c004f71 100644 --- a/violet-server/server/src/stats/dtos/stats.dto.ts +++ b/violet-server/server/src/stats/dtos/stats.dto.ts @@ -1,15 +1,15 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsNumber, IsArray } from 'class-validator'; +import { IsInt, IsArray } from 'class-validator'; export class StatsResponseDto { - @IsNumber() + @IsInt() @ApiProperty({ description: '총 사용자 수', required: true, }) totalUsers: number; - @IsNumber() + @IsInt() @ApiProperty({ description: '총 댓글 수', required: true, diff --git a/violet-server/server/src/view/dtos/view-get.dto.ts b/violet-server/server/src/view/dtos/view-get.dto.ts index 9b6d18389..922ebd86b 100644 --- a/violet-server/server/src/view/dtos/view-get.dto.ts +++ b/violet-server/server/src/view/dtos/view-get.dto.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, - IsNumber, + IsInt, IsOptional, IsString, Matches, @@ -18,7 +18,7 @@ export const RANK_REQUEST_TYPE = { }; export class ViewGetRequestDto { - @IsNumber() + @IsInt() @Type(() => Number) @Min(0) @ApiProperty({ @@ -27,7 +27,7 @@ export class ViewGetRequestDto { }) offset: number; - @IsNumber() + @IsInt() @Type(() => Number) @Min(0) @Max(1000) @@ -45,14 +45,14 @@ export class ViewGetRequestDto { } export class ViewGetResponseDtoElement { - @IsNumber() + @IsInt() @ApiProperty({ description: 'Article Id', required: true, }) articleId: number; - @IsNumber() + @IsInt() @ApiProperty({ description: 'Count', required: true, diff --git a/violet-server/server/src/view/dtos/view-post.dto.ts b/violet-server/server/src/view/dtos/view-post.dto.ts index be8a046e1..606af758a 100644 --- a/violet-server/server/src/view/dtos/view-post.dto.ts +++ b/violet-server/server/src/view/dtos/view-post.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { - IsNumber, + IsInt, IsOptional, IsString, Matches, @@ -17,7 +17,7 @@ export const RANK_REQUEST_TYPE = { }; export class ViewPostRequestDto { - @IsNumber() + @IsInt() @Type(() => Number) @ApiProperty({ description: 'ArticleId', @@ -25,7 +25,7 @@ export class ViewPostRequestDto { }) articleId: number; - @IsNumber() + @IsInt() @Type(() => Number) @Min(0) @Max(1000) diff --git a/violet/lib/api/api.enums.swagger.dart b/violet/lib/api/api.enums.swagger.dart new file mode 100644 index 000000000..9ee7d429f --- /dev/null +++ b/violet/lib/api/api.enums.swagger.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:collection/collection.dart'; + +enum UserRole { + @JsonValue(null) + swaggerGeneratedUnknown(null), + + @JsonValue('admin') + admin('admin'), + @JsonValue('user') + user('user'); + + final String? value; + + const UserRole(this.value); +} diff --git a/violet/lib/api/api.swagger.chopper.dart b/violet/lib/api/api.swagger.chopper.dart index 6e4727258..c2a543632 100644 --- a/violet/lib/api/api.swagger.chopper.dart +++ b/violet/lib/api/api.swagger.chopper.dart @@ -1,3 +1,4 @@ +// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND part of 'api.swagger.dart'; @@ -41,14 +42,14 @@ final class _$Api extends Api { @override Future> _apiV2CommentGet( - {required CommentGetDto? body}) { + {required String? where}) { final Uri $url = Uri.parse('/api/v2/comment'); - final $body = body; + final Map $params = {'where': where}; final Request $request = Request( 'GET', $url, client.baseUrl, - body: $body, + parameters: $params, ); return client.send($request); } @@ -66,6 +67,17 @@ final class _$Api extends Api { return client.send($request); } + @override + Future> _apiV2CommentIdHiddenPatch({required num? id}) { + final Uri $url = Uri.parse('/api/v2/comment/${id}/hidden'); + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + ); + return client.send($request); + } + @override Future> _apiV2UserGet() { final Uri $url = Uri.parse('/api/v2/user'); @@ -90,6 +102,17 @@ final class _$Api extends Api { return client.send($request); } + @override + Future>> _apiV2UserListGet() { + final Uri $url = Uri.parse('/api/v2/user/list'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send, User>($request); + } + @override Future> _apiV2UserDiscordGet() { final Uri $url = Uri.parse('/api/v2/user/discord'); @@ -221,4 +244,37 @@ final class _$Api extends Api { ); return client.send($request); } + + @override + Future> _apiV2StatsGet() { + final Uri $url = Uri.parse('/api/v2/stats'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> _apiV2BookmarkBackupPost() { + final Uri $url = Uri.parse('/api/v2/bookmark/backup'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> _apiV2BookmarkGet() { + final Uri $url = Uri.parse('/api/v2/bookmark'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } } diff --git a/violet/lib/api/api.swagger.dart b/violet/lib/api/api.swagger.dart index 5320ff566..526586c66 100644 --- a/violet/lib/api/api.swagger.dart +++ b/violet/lib/api/api.swagger.dart @@ -1,6 +1,7 @@ // ignore_for_file: type=lint import 'package:json_annotation/json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart' as json; import 'package:collection/collection.dart'; import 'dart:convert'; @@ -11,6 +12,8 @@ import 'dart:async'; import 'package:http/http.dart' as http; import 'package:http/http.dart' show MultipartFile; import 'package:chopper/chopper.dart' as chopper; +import 'api.enums.swagger.dart' as enums; +export 'api.enums.swagger.dart'; part 'api.swagger.chopper.dart'; part 'api.swagger.g.dart'; @@ -64,18 +67,20 @@ abstract class Api extends ChopperService { Future _apiV2HmacGet(); ///Get Comment + ///@param where Where to get Future> apiV2CommentGet( - {required CommentGetDto? body}) { + {required String? where}) { generatedMapping.putIfAbsent( CommentGetResponseDto, () => CommentGetResponseDto.fromJsonFactory); - return _apiV2CommentGet(body: body); + return _apiV2CommentGet(where: where); } ///Get Comment + ///@param where Where to get @Get(path: '/api/v2/comment') Future> _apiV2CommentGet( - {@Body() required CommentGetDto? body}); + {@Query('where') required String? where}); ///Post Comment Future apiV2CommentPost({required CommentPostDto? body}) { @@ -90,6 +95,21 @@ abstract class Api extends ChopperService { Future _apiV2CommentPost( {@Body() required CommentPostDto? body}); + ///Toggle comment hidden status + ///@param id + Future apiV2CommentIdHiddenPatch({required num? id}) { + return _apiV2CommentIdHiddenPatch(id: id); + } + + ///Toggle comment hidden status + ///@param id + @Patch( + path: '/api/v2/comment/{id}/hidden', + optionalBody: true, + ) + Future _apiV2CommentIdHiddenPatch( + {@Path('id') required num? id}); + ///Get current user information Future> apiV2UserGet() { generatedMapping.putIfAbsent(User, () => User.fromJsonFactory); @@ -114,6 +134,17 @@ abstract class Api extends ChopperService { Future _apiV2UserPost( {@Body() required UserRegisterDTO? body}); + ///Get all users + Future>> apiV2UserListGet() { + generatedMapping.putIfAbsent(User, () => User.fromJsonFactory); + + return _apiV2UserListGet(); + } + + ///Get all users + @Get(path: '/api/v2/user/list') + Future>> _apiV2UserListGet(); + ///Get userAppIds registered by discord id Future> apiV2UserDiscordGet() { @@ -262,48 +293,39 @@ abstract class Api extends ChopperService { @Query('viewSeconds') required num? viewSeconds, @Query('userAppId') required String? userAppId, }); -} - -@JsonSerializable(explicitToJson: true) -class CommentGetDto { - const CommentGetDto({ - required this.where, - }); - factory CommentGetDto.fromJson(Map json) => - _$CommentGetDtoFromJson(json); + ///통계 데이터 조회 + Future> apiV2StatsGet() { + generatedMapping.putIfAbsent( + StatsResponseDto, () => StatsResponseDto.fromJsonFactory); - static const toJsonFactory = _$CommentGetDtoToJson; - Map toJson() => _$CommentGetDtoToJson(this); + return _apiV2StatsGet(); + } - @JsonKey(name: 'where') - final String where; - static const fromJsonFactory = _$CommentGetDtoFromJson; + ///통계 데이터 조회 + @Get(path: '/api/v2/stats') + Future> _apiV2StatsGet(); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other is CommentGetDto && - (identical(other.where, where) || - const DeepCollectionEquality().equals(other.where, where))); + ///Create Bookmark Backup + Future apiV2BookmarkBackupPost() { + return _apiV2BookmarkBackupPost(); } - @override - String toString() => jsonEncode(this); - - @override - int get hashCode => - const DeepCollectionEquality().hash(where) ^ runtimeType.hashCode; -} + ///Create Bookmark Backup + @Post( + path: '/api/v2/bookmark/backup', + optionalBody: true, + ) + Future _apiV2BookmarkBackupPost(); -extension $CommentGetDtoExtension on CommentGetDto { - CommentGetDto copyWith({String? where}) { - return CommentGetDto(where: where ?? this.where); + ///Get User Bookmarks + Future apiV2BookmarkGet() { + return _apiV2BookmarkGet(); } - CommentGetDto copyWithWrapped({Wrapped? where}) { - return CommentGetDto(where: (where != null ? where.value : this.where)); - } + ///Get User Bookmarks + @Get(path: '/api/v2/bookmark') + Future _apiV2BookmarkGet(); } @JsonSerializable(explicitToJson: true) @@ -513,6 +535,7 @@ class User { required this.createdAt, required this.updatedAt, required this.userAppId, + required this.role, required this.discordId, required this.avatar, required this.nickname, @@ -531,6 +554,15 @@ class User { final DateTime updatedAt; @JsonKey(name: 'userAppId') final String userAppId; + @JsonKey( + name: 'role', + toJson: userRoleToJson, + fromJson: userRoleRoleFromJson, + ) + final enums.UserRole role; + static enums.UserRole userRoleRoleFromJson(Object? value) => + userRoleFromJson(value, enums.UserRole.user); + @JsonKey(name: 'discordId') final String discordId; @JsonKey(name: 'avatar') @@ -554,6 +586,8 @@ class User { (identical(other.userAppId, userAppId) || const DeepCollectionEquality() .equals(other.userAppId, userAppId)) && + (identical(other.role, role) || + const DeepCollectionEquality().equals(other.role, role)) && (identical(other.discordId, discordId) || const DeepCollectionEquality() .equals(other.discordId, discordId)) && @@ -573,6 +607,7 @@ class User { const DeepCollectionEquality().hash(createdAt) ^ const DeepCollectionEquality().hash(updatedAt) ^ const DeepCollectionEquality().hash(userAppId) ^ + const DeepCollectionEquality().hash(role) ^ const DeepCollectionEquality().hash(discordId) ^ const DeepCollectionEquality().hash(avatar) ^ const DeepCollectionEquality().hash(nickname) ^ @@ -585,6 +620,7 @@ extension $UserExtension on User { DateTime? createdAt, DateTime? updatedAt, String? userAppId, + enums.UserRole? role, String? discordId, String? avatar, String? nickname}) { @@ -593,6 +629,7 @@ extension $UserExtension on User { createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, userAppId: userAppId ?? this.userAppId, + role: role ?? this.role, discordId: discordId ?? this.discordId, avatar: avatar ?? this.avatar, nickname: nickname ?? this.nickname); @@ -603,6 +640,7 @@ extension $UserExtension on User { Wrapped? createdAt, Wrapped? updatedAt, Wrapped? userAppId, + Wrapped? role, Wrapped? discordId, Wrapped? avatar, Wrapped? nickname}) { @@ -611,6 +649,7 @@ extension $UserExtension on User { createdAt: (createdAt != null ? createdAt.value : this.createdAt), updatedAt: (updatedAt != null ? updatedAt.value : this.updatedAt), userAppId: (userAppId != null ? userAppId.value : this.userAppId), + role: (role != null ? role.value : this.role), discordId: (discordId != null ? discordId.value : this.discordId), avatar: (avatar != null ? avatar.value : this.avatar), nickname: (nickname != null ? nickname.value : this.nickname)); @@ -885,6 +924,151 @@ extension $ViewGetResponseDtoExtension on ViewGetResponseDto { } } +@JsonSerializable(explicitToJson: true) +class StatsResponseDto { + const StatsResponseDto({ + required this.totalUsers, + required this.totalComments, + required this.userGrowth, + required this.commentGrowth, + }); + + factory StatsResponseDto.fromJson(Map json) => + _$StatsResponseDtoFromJson(json); + + static const toJsonFactory = _$StatsResponseDtoToJson; + Map toJson() => _$StatsResponseDtoToJson(this); + + @JsonKey(name: 'totalUsers') + final double totalUsers; + @JsonKey(name: 'totalComments') + final double totalComments; + @JsonKey(name: 'userGrowth') + final Object userGrowth; + @JsonKey(name: 'commentGrowth') + final Object commentGrowth; + static const fromJsonFactory = _$StatsResponseDtoFromJson; + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is StatsResponseDto && + (identical(other.totalUsers, totalUsers) || + const DeepCollectionEquality() + .equals(other.totalUsers, totalUsers)) && + (identical(other.totalComments, totalComments) || + const DeepCollectionEquality() + .equals(other.totalComments, totalComments)) && + (identical(other.userGrowth, userGrowth) || + const DeepCollectionEquality() + .equals(other.userGrowth, userGrowth)) && + (identical(other.commentGrowth, commentGrowth) || + const DeepCollectionEquality() + .equals(other.commentGrowth, commentGrowth))); + } + + @override + String toString() => jsonEncode(this); + + @override + int get hashCode => + const DeepCollectionEquality().hash(totalUsers) ^ + const DeepCollectionEquality().hash(totalComments) ^ + const DeepCollectionEquality().hash(userGrowth) ^ + const DeepCollectionEquality().hash(commentGrowth) ^ + runtimeType.hashCode; +} + +extension $StatsResponseDtoExtension on StatsResponseDto { + StatsResponseDto copyWith( + {double? totalUsers, + double? totalComments, + Object? userGrowth, + Object? commentGrowth}) { + return StatsResponseDto( + totalUsers: totalUsers ?? this.totalUsers, + totalComments: totalComments ?? this.totalComments, + userGrowth: userGrowth ?? this.userGrowth, + commentGrowth: commentGrowth ?? this.commentGrowth); + } + + StatsResponseDto copyWithWrapped( + {Wrapped? totalUsers, + Wrapped? totalComments, + Wrapped? userGrowth, + Wrapped? commentGrowth}) { + return StatsResponseDto( + totalUsers: (totalUsers != null ? totalUsers.value : this.totalUsers), + totalComments: + (totalComments != null ? totalComments.value : this.totalComments), + userGrowth: (userGrowth != null ? userGrowth.value : this.userGrowth), + commentGrowth: + (commentGrowth != null ? commentGrowth.value : this.commentGrowth)); + } +} + +String? userRoleNullableToJson(enums.UserRole? userRole) { + return userRole?.value; +} + +String? userRoleToJson(enums.UserRole userRole) { + return userRole.value; +} + +enums.UserRole userRoleFromJson( + Object? userRole, [ + enums.UserRole? defaultValue, +]) { + return enums.UserRole.values.firstWhereOrNull((e) => e.value == userRole) ?? + defaultValue ?? + enums.UserRole.swaggerGeneratedUnknown; +} + +enums.UserRole? userRoleNullableFromJson( + Object? userRole, [ + enums.UserRole? defaultValue, +]) { + if (userRole == null) { + return null; + } + return enums.UserRole.values.firstWhereOrNull((e) => e.value == userRole) ?? + defaultValue; +} + +String userRoleExplodedListToJson(List? userRole) { + return userRole?.map((e) => e.value!).join(',') ?? ''; +} + +List userRoleListToJson(List? userRole) { + if (userRole == null) { + return []; + } + + return userRole.map((e) => e.value!).toList(); +} + +List userRoleListFromJson( + List? userRole, [ + List? defaultValue, +]) { + if (userRole == null) { + return defaultValue ?? []; + } + + return userRole.map((e) => userRoleFromJson(e.toString())).toList(); +} + +List? userRoleNullableListFromJson( + List? userRole, [ + List? defaultValue, +]) { + if (userRole == null) { + return defaultValue; + } + + return userRole.map((e) => userRoleFromJson(e.toString())).toList(); +} + typedef $JsonFactory = T Function(Map json); class $CustomJsonDecoder { diff --git a/violet/lib/api/api.swagger.g.dart b/violet/lib/api/api.swagger.g.dart index ba62bd522..64f502d12 100644 --- a/violet/lib/api/api.swagger.g.dart +++ b/violet/lib/api/api.swagger.g.dart @@ -6,16 +6,6 @@ part of 'api.swagger.dart'; // JsonSerializableGenerator // ************************************************************************** -CommentGetDto _$CommentGetDtoFromJson(Map json) => - CommentGetDto( - where: json['where'] as String, - ); - -Map _$CommentGetDtoToJson(CommentGetDto instance) => - { - 'where': instance.where, - }; - CommentGetResponseDtoElement _$CommentGetResponseDtoElementFromJson( Map json) => CommentGetResponseDtoElement( @@ -71,6 +61,7 @@ User _$UserFromJson(Map json) => User( createdAt: DateTime.parse(json['createdAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String), userAppId: json['userAppId'] as String, + role: User.userRoleRoleFromJson(json['role']), discordId: json['discordId'] as String, avatar: json['avatar'] as String, nickname: json['nickname'] as String, @@ -81,6 +72,7 @@ Map _$UserToJson(User instance) => { 'createdAt': instance.createdAt.toIso8601String(), 'updatedAt': instance.updatedAt.toIso8601String(), 'userAppId': instance.userAppId, + 'role': userRoleToJson(instance.role), 'discordId': instance.discordId, 'avatar': instance.avatar, 'nickname': instance.nickname, @@ -154,3 +146,19 @@ Map _$ViewGetResponseDtoToJson(ViewGetResponseDto instance) => { 'elements': instance.elements.map((e) => e.toJson()).toList(), }; + +StatsResponseDto _$StatsResponseDtoFromJson(Map json) => + StatsResponseDto( + totalUsers: (json['totalUsers'] as num).toDouble(), + totalComments: (json['totalComments'] as num).toDouble(), + userGrowth: json['userGrowth'] as Object, + commentGrowth: json['commentGrowth'] as Object, + ); + +Map _$StatsResponseDtoToJson(StatsResponseDto instance) => + { + 'totalUsers': instance.totalUsers, + 'totalComments': instance.totalComments, + 'userGrowth': instance.userGrowth, + 'commentGrowth': instance.commentGrowth, + }; diff --git a/violet/lib/pages/article_info/article_info_page.dart b/violet/lib/pages/article_info/article_info_page.dart index ad2f347c0..35fcc939a 100644 --- a/violet/lib/pages/article_info/article_info_page.dart +++ b/violet/lib/pages/article_info/article_info_page.dart @@ -41,6 +41,7 @@ import 'package:violet/pages/viewer/viewer_page.dart'; import 'package:violet/pages/viewer/viewer_page_provider.dart'; import 'package:violet/script/script_manager.dart'; import 'package:violet/server/violet.dart'; +import 'package:violet/server/violet_v2.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/style/palette.dart'; import 'package:violet/variables.dart'; @@ -265,7 +266,7 @@ class ArticleInfoPage extends StatelessWidget { readButtonEvent(BuildContext context, ArticleInfo data, [int? page]) async { if (Settings.useVioletServer) { Future.delayed(const Duration(milliseconds: 100)).then((value) async { - await VioletServer.view(data.queryResult.id()); + await VioletServerV2.view(data.queryResult.id()); }); } await (await User.getInstance()).insertUserLog(data.queryResult.id(), 0); diff --git a/violet/lib/pages/bookmark/crop_bookmark.dart b/violet/lib/pages/bookmark/crop_bookmark.dart index c1273d3c2..a5c53b38e 100644 --- a/violet/lib/pages/bookmark/crop_bookmark.dart +++ b/violet/lib/pages/bookmark/crop_bookmark.dart @@ -344,7 +344,6 @@ class _CropBookmarkPageState extends State { CropBookmarkPage( bookmarks: bookmarks .sortedBy((e) => DateTime.parse(e.datetime())) - .reversed .toList()), opaque: false, ); diff --git a/violet/lib/pages/common/utils.dart b/violet/lib/pages/common/utils.dart index 0ca0db60a..482e0b9ea 100644 --- a/violet/lib/pages/common/utils.dart +++ b/violet/lib/pages/common/utils.dart @@ -16,6 +16,7 @@ import 'package:violet/pages/article_info/article_info_page.dart'; import 'package:violet/pages/viewer/viewer_page.dart'; import 'package:violet/pages/viewer/viewer_page_provider.dart'; import 'package:violet/server/violet.dart'; +import 'package:violet/server/violet_v2.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/widgets/article_item/image_provider_manager.dart'; @@ -124,7 +125,7 @@ Future getImageProvider(QueryResult queryResult) async { Future showViewer(BuildContext context, int articleId, int page) async { if (Settings.useVioletServer) { Future.delayed(const Duration(milliseconds: 100)).then((value) async { - await VioletServer.view(articleId); + await VioletServerV2.view(articleId); }); } diff --git a/violet/lib/pages/hot/hot_page.dart b/violet/lib/pages/hot/hot_page.dart index e42681875..dbcf0e080 100644 --- a/violet/lib/pages/hot/hot_page.dart +++ b/violet/lib/pages/hot/hot_page.dart @@ -13,6 +13,7 @@ import 'package:violet/locale/locale.dart'; import 'package:violet/model/article_list_item.dart'; import 'package:violet/pages/segment/double_tap_to_top.dart'; import 'package:violet/server/violet.dart'; +import 'package:violet/server/violet_v2.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/widgets/article_item/article_list_item_widget.dart'; import 'package:violet/widgets/search_bar.dart'; @@ -182,19 +183,22 @@ class _HotPageState extends ThemeSwitchableState Future _request([bool reload = false]) { _memoizer = AsyncMemoizer(); return _memoizer!.runOnce(() async { - final value = await VioletServer.top(0, 600, i2t()); + final response = await VioletServerV2.instance + .apiV2ViewGet(offset: 0, count: 600, type: i2t()); + final value = response.body; - if (value is int) { - return (value, null); + if (value == null) { + return (response.statusCode, null); } - if (value == null || value.length == 0) { + if (value.elements.isEmpty) { return const (900, null); } var queryRaw = '${translate2query('${Settings.includeTags} ${Settings.serializedExcludeTags}')} AND '; - queryRaw += '(${value.map((e) => 'Id=${e.$1}').join(' OR ')})'; + queryRaw += + '(${value.elements.map((e) => 'Id=${e.articleId}').join(' OR ')})'; final query = await QueryManager.query(queryRaw); if (query.results!.isEmpty) { @@ -207,13 +211,13 @@ class _HotPageState extends ThemeSwitchableState } final result = <(QueryResult, int)>[]; - value.forEach((element) { - if (qr[element.$1.toString()] == null) { + for (var element in value.elements) { + if (qr[element.articleId.toString()] == null) { // TODO: Handle qurey not found - return; + continue; } - result.add((qr[element.$1.toString()]!, element.$2)); - }); + result.add((qr[element.articleId.toString()]!, element.count as int)); + } if (reload) setState(() {}); diff --git a/violet/lib/pages/viewer/overlay/viewer_tab_panel.dart b/violet/lib/pages/viewer/overlay/viewer_tab_panel.dart index 081f80a39..cafeea38b 100644 --- a/violet/lib/pages/viewer/overlay/viewer_tab_panel.dart +++ b/violet/lib/pages/viewer/overlay/viewer_tab_panel.dart @@ -17,6 +17,7 @@ import 'package:violet/pages/common/utils.dart'; import 'package:violet/pages/viewer/viewer_page.dart'; import 'package:violet/pages/viewer/viewer_page_provider.dart'; import 'package:violet/server/violet.dart'; +import 'package:violet/server/violet_v2.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/variables.dart'; import 'package:violet/widgets/article_item/article_list_item_widget.dart'; @@ -418,7 +419,7 @@ class __ArtistsArticleTabListState extends State<_ArtistsArticleTabList> Future _showViewer(QueryResult e) async { if (Settings.useVioletServer) { Future.delayed(const Duration(milliseconds: 100)).then((value) async { - await VioletServer.view(e.id()); + await VioletServerV2.view(e.id()); }); } diff --git a/violet/lib/server/violet_v2.dart b/violet/lib/server/violet_v2.dart index d4559b45b..1bea6dccb 100644 --- a/violet/lib/server/violet_v2.dart +++ b/violet/lib/server/violet_v2.dart @@ -4,12 +4,13 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:violet/api/api.swagger.dart'; import 'package:violet/server/wsalt.dart'; class VioletServerV2 { - static const protocol = 'https'; - static const host = 'koromo.xyz'; + static const protocol = 'http'; + static const host = 'localhost:3000'; static const api = '$protocol://$host'; static late final Api instance; @@ -20,6 +21,24 @@ class VioletServerV2 { interceptors: [HmacInterceptor()], ); } + + static String? _userId; + static Future _getUserAppId() async { + if (_userId == null) { + final prefs = await SharedPreferences.getInstance(); + _userId = prefs.getString('fa_userid'); + } + return _userId!; + } + + static Future view(int articleid) async { + final userId = await _getUserAppId(); + await VioletServerV2.instance.apiV2ViewPost( + articleId: articleid, + viewSeconds: 0, + userAppId: userId, + ); + } } class HmacInterceptor implements Interceptor { diff --git a/violet/lib/widgets/article_item/article_list_item_widget.dart b/violet/lib/widgets/article_item/article_list_item_widget.dart index 065d130bd..ff15caf22 100644 --- a/violet/lib/widgets/article_item/article_list_item_widget.dart +++ b/violet/lib/widgets/article_item/article_list_item_widget.dart @@ -27,6 +27,7 @@ import 'package:violet/pages/viewer/viewer_page.dart'; import 'package:violet/pages/viewer/viewer_page_provider.dart'; import 'package:violet/script/script_manager.dart'; import 'package:violet/server/violet.dart'; +import 'package:violet/server/violet_v2.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/style/palette.dart'; import 'package:violet/util/call_once.dart'; @@ -34,6 +35,7 @@ import 'package:violet/widgets/article_item/article_list_item_widget_controller. import 'package:violet/widgets/article_item/image_provider_manager.dart'; import 'package:violet/widgets/article_item/thumbnail.dart'; import 'package:violet/widgets/article_item/thumbnail_view_page.dart'; +import 'package:violet/widgets/article_item/thumbnail_view_page2.dart'; import 'package:violet/widgets/toast.dart'; class ArticleListItemWidget extends StatefulWidget { @@ -250,7 +252,7 @@ class _ArticleListItemWidgetState extends State _viewArticle() async { if (Settings.useVioletServer) { Future.delayed(const Duration(milliseconds: 100)).then((value) async { - await VioletServer.view(data.queryResult.id()); + await VioletServerV2.view(data.queryResult.id()); }); } await (await User.getInstance()).insertUserLog(data.queryResult.id(), 0); @@ -386,7 +388,7 @@ class _ArticleListItemWidgetState extends State Animation secondaryAnimation, Widget wi) { return FadeTransition(opacity: animation, child: wi); }, - pageBuilder: (_, __, ___) => ThumbnailViewPage( + pageBuilder: (_, __, ___) => ThumbnailViewPage2( thumbnail: c.thumbnail.value, headers: c.headers, heroKey: data.thumbnailTag, diff --git a/violet/lib/widgets/article_item/thumbnail_view_page2.dart b/violet/lib/widgets/article_item/thumbnail_view_page2.dart new file mode 100644 index 000000000..38f148ba2 --- /dev/null +++ b/violet/lib/widgets/article_item/thumbnail_view_page2.dart @@ -0,0 +1,189 @@ +// This source code is a part of Project Violet. +// Copyright (C) 2020-2024. violet-team. Licensed under the Apache-2.0 License. + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flare_flutter/flare_actor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/gestures.dart'; +import 'package:violet/settings/settings.dart'; +import 'dart:ui'; + +class ThumbnailViewPage2 extends StatefulWidget { + final String? thumbnail; + final String heroKey; + final Map? headers; + final bool showUltra; + final Color? glowColor; + final double glowRadius; + final double shadowBlurRadius; + final double shadowSpreadRadius; + final Offset shadowOffset; + + const ThumbnailViewPage2({ + super.key, + required this.thumbnail, + required this.headers, + required this.heroKey, + required this.showUltra, + this.glowColor, + this.glowRadius = 20.0, + this.shadowBlurRadius = 10.0, + this.shadowSpreadRadius = 2.0, + this.shadowOffset = const Offset(0, 3), + }); + + @override + State createState() => _ThumbnailViewPage2State(); +} + +class _ThumbnailViewPage2State extends State { + double scale = 1.0; + double latest = 1.0; + double opacity = 1.0; + Offset position = Offset.zero; + bool isDragging = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (details) { + Navigator.pop(context); + }, + child: Container( + color: Colors.transparent, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Center( + child: GestureDetector( + onScaleStart: (detail) { + tapCount = 2; + }, + onScaleUpdate: (detail) { + setState(() { + scale = latest * detail.scale; + if (isDragging) { + position += detail.focalPointDelta; + } + }); + + if (scale < 0.6) Navigator.pop(context); + }, + onScaleEnd: (detail) { + latest = scale; + tapCount = 0; + isDragging = false; + }, + onVerticalDragStart: (detail) { + dragStart = detail.localPosition.dy; + isDragging = true; + }, + onVerticalDragUpdate: (detail) { + if (zooming || tapCount == 2) { + setState(() { + scale += (detail.delta.dy) / 100; + }); + latest = scale; + if (scale < 0.6) Navigator.pop(context); + } else if (tapCount != 2 || + (detail.localPosition.dy - dragStart).abs() > 70) { + Navigator.pop(context); + } + }, + onTapDown: (detail) { + tapCount++; + DateTime now = DateTime.now(); + if (currentBackPressTime == null || + now.difference(currentBackPressTime!) > + const Duration(milliseconds: 300)) { + currentBackPressTime = now; + return; + } + zooming = true; + }, + onTapUp: (detail) { + tapCount--; + zooming = false; + }, + onTapCancel: () { + tapCount = 0; + }, + child: Container( + padding: const EdgeInsets.all(0), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(1)), + boxShadow: [ + BoxShadow( + color: (widget.glowColor ?? Settings.majorColor) + .withOpacity(0.3), + spreadRadius: widget.shadowSpreadRadius, + blurRadius: widget.shadowBlurRadius, + offset: widget.shadowOffset, + ), + ], + ), + child: Listener( + onPointerSignal: (pointerSignal) { + if (pointerSignal is PointerScrollEvent) { + setState(() { + scale = (scale + (pointerSignal.scrollDelta.dy * 0.001)) + .clamp(0.5, 3.0); + latest = scale; + }); + } + }, + child: Transform.scale( + scale: scale, + child: Transform.translate( + offset: position, + child: Hero( + tag: widget.heroKey, + child: CachedNetworkImage( + imageUrl: widget.thumbnail ?? '', + fit: BoxFit.contain, + httpHeaders: widget.headers, + placeholder: (b, c) { + if (!Settings.simpleItemWidgetLoadingIcon) { + return const FlareActor( + 'assets/flare/Loading2.flr', + alignment: Alignment.center, + fit: BoxFit.fitHeight, + animation: 'Alarm', + ); + } else { + return Center( + child: SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator( + color: Settings.majorColor.withAlpha(150), + ), + ), + ); + } + }, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + } + + int tapCount = 0; + double dragStart = 0; + bool zooming = false; + DateTime? currentBackPressTime; +} diff --git a/violet/swaggers/api.yaml b/violet/swaggers/api.yaml index 91def83b1..a470ebaf7 100644 --- a/violet/swaggers/api.yaml +++ b/violet/swaggers/api.yaml @@ -22,13 +22,13 @@ paths: get: operationId: CommentController_getComment summary: Get Comment - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CommentGetDto' + parameters: + - name: where + required: true + in: query + description: Where to get + schema: + type: string responses: '201': description: Comment Elements @@ -53,6 +53,21 @@ paths: description: '' tags: - comment + /api/v2/comment/{id}/hidden: + patch: + operationId: CommentController_toggleCommentHidden + summary: Toggle comment hidden status + parameters: + - name: id + required: true + in: path + schema: + type: number + responses: + '200': + description: '' + tags: + - comment /api/v2/user: get: operationId: UserController_getCurrentUser @@ -82,6 +97,22 @@ paths: description: '' tags: - user + /api/v2/user/list: + get: + operationId: UserController_getAllUsers + summary: Get all users + parameters: [] + responses: + '201': + description: List of users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + tags: + - user /api/v2/user/discord: get: operationId: UserController_listDiscordUserAppIds @@ -246,6 +277,40 @@ paths: description: '' tags: - view + /api/v2/stats: + get: + operationId: StatsController_getStats + summary: 통계 데이터 조회 + parameters: [] + responses: + '200': + description: 통계 데이터 + content: + application/json: + schema: + $ref: '#/components/schemas/StatsResponseDto' + tags: + - stats + /api/v2/bookmark/backup: + post: + operationId: BookmarkController_createBookmarkBackup + summary: Create Bookmark Backup + parameters: [] + responses: + '201': + description: '' + tags: + - bookmark + /api/v2/bookmark: + get: + operationId: BookmarkController_getUserBookmarks + summary: Get User Bookmarks + parameters: [] + responses: + '200': + description: '' + tags: + - bookmark info: title: Violet Server API Docs description: Violet Server API description @@ -255,14 +320,6 @@ tags: [] servers: [] components: schemas: - CommentGetDto: - type: object - properties: - where: - type: string - description: Where to get - required: - - where CommentGetResponseDtoElement: type: object properties: @@ -329,6 +386,13 @@ components: userAppId: type: string description: User Id + role: + type: string + description: User Role + enum: + - admin + - user + default: user discordId: type: string description: Discord Id @@ -343,6 +407,7 @@ components: - createdAt - updatedAt - userAppId + - role - discordId - avatar - nickname @@ -401,3 +466,23 @@ components: $ref: '#/components/schemas/ViewGetResponseDtoElement' required: - elements + StatsResponseDto: + type: object + properties: + totalUsers: + type: number + description: 총 사용자 수 + totalComments: + type: number + description: 총 댓글 수 + userGrowth: + type: object + description: 사용자 성장 데이터 + commentGrowth: + type: object + description: 댓글 성장 데이터 + required: + - totalUsers + - totalComments + - userGrowth + - commentGrowth