diff --git a/classquiz/__init__.py b/classquiz/__init__.py index ec12387..45d9c15 100644 --- a/classquiz/__init__.py +++ b/classquiz/__init__.py @@ -29,6 +29,7 @@ from classquiz.routers import ( sitemap, remote, community, + avatar, ) from classquiz.socket_server import sio from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks @@ -100,5 +101,6 @@ app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", incl app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True) app.include_router(sitemap.router, tags=["sitemap"], prefix="/api/v1/sitemap", include_in_schema=True) app.include_router(community.router, tags=["community"], prefix="/api/v1/community", include_in_schema=True) +app.include_router(avatar.router, tags=["avatar"], prefix="/api/v1/avatar", include_in_schema=True) app.mount("/", ASGIApp(sio)) diff --git a/classquiz/routers/avatar.py b/classquiz/routers/avatar.py new file mode 100644 index 0000000..26967fe --- /dev/null +++ b/classquiz/routers/avatar.py @@ -0,0 +1,150 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +import gzip + +from fastapi import APIRouter, Response, HTTPException, Depends +import py_avataaars_no_png as av +from fastapi.responses import PlainTextResponse +from classquiz.config import redis + +from classquiz.auth import get_current_user +from classquiz.db.models import User + +router = APIRouter() + + +class AvatarItemsAsList: + skin_color = list(av.SkinColor) + hair_color = list(av.HairColor) + facial_hair_type = list(av.FacialHairType) + facial_hair_color = list(av.HairColor) + top_type = list(av.TopType) + hat_color = list(av.Color) + mouth_type = list(av.MouthType) + eyebrow_type = list(av.EyebrowType) + nose_type = list(av.NoseType) + accessories_type = list(av.AccessoriesType) + clothe_type = list(av.ClotheType) + clothe_color = list(av.Color) + clothe_graphic_type = list(av.ClotheGraphicType) + + +@router.get("/custom", response_class=PlainTextResponse) +async def get_customized_avatar( + resp: Response, + skin_color: int | None = 0, + hair_color: int | None = 0, + facial_hair_type: int | None = 0, + facial_hair_color: int | None = 0, + top_type: int | None = 0, + hat_color: int | None = 0, + mouth_type: int | None = 0, + eyebrow_type: int | None = 0, + nose_type: int | None = 0, + accessories_type: int | None = 0, + clothe_type: int | None = 0, + clothe_color: int | None = 0, + clothe_graphic_type: int | None = 0, +): + try: + skin_color = AvatarItemsAsList.skin_color[skin_color] + hair_color = AvatarItemsAsList.hat_color[hair_color] + facial_hair_type = AvatarItemsAsList.facial_hair_type[facial_hair_type] + facial_hair_color = AvatarItemsAsList.facial_hair_color[facial_hair_color] + top_type = AvatarItemsAsList.top_type[top_type] + hat_color = AvatarItemsAsList.hat_color[hat_color] + mouth_type = AvatarItemsAsList.mouth_type[mouth_type] + eyebrow_type = AvatarItemsAsList.eyebrow_type[eyebrow_type] + nose_type = AvatarItemsAsList.nose_type[nose_type] + accessories_type = AvatarItemsAsList.accessories_type[accessories_type] + clothe_type = AvatarItemsAsList.clothe_type[clothe_type] + clothe_color = AvatarItemsAsList.clothe_color[clothe_color] + clothe_graphic_type = AvatarItemsAsList.clothe_graphic_type[clothe_graphic_type] + except IndexError: + raise HTTPException(status_code=400, detail="One parameter-value doesn't exist.") + resp.headers.append("Content-Type", "image/svg+xml") + avatar = av.PyAvataaar( + style=av.AvatarStyle.TRANSPARENT, + skin_color=skin_color, + hair_color=hair_color, + facial_hair_type=facial_hair_type, + facial_hair_color=facial_hair_color, + top_type=top_type, + hat_color=hat_color, + mouth_type=mouth_type, + eyebrow_type=eyebrow_type, + nose_type=nose_type, + accessories_type=accessories_type, + clothe_type=clothe_type, + clothe_color=clothe_color, + clothe_graphic_type=clothe_graphic_type, + ).render_svg() + # print(f"skin_color: {len(AvatarItemsAsList.skin_color)},") + # print(f"hair_color: {len(AvatarItemsAsList.hair_color)},") + # print(f"facial_hair_type: {len(AvatarItemsAsList.facial_hair_type)},") + # print(f"facial_hair_color: {len(AvatarItemsAsList.facial_hair_color)},") + # print(f"top_type: {len(AvatarItemsAsList.top_type)},") + # print(f"hat_color: {len(AvatarItemsAsList.hat_color)},") + # print(f"mouth_type: {len(AvatarItemsAsList.mouth_type)},") + # print(f"eyebrow_type: {len(AvatarItemsAsList.eyebrow_type)},") + # print(f"nose_type: {len(AvatarItemsAsList.nose_type)},") + # print(f"accessories_type: {len(AvatarItemsAsList.accessories_type)},") + # print(f"clothe_type: {len(AvatarItemsAsList.clothe_type)},") + # print(f"clothe_color: {len(AvatarItemsAsList.clothe_color)},") + # print(f"clothe_graphic_type: {len(AvatarItemsAsList.clothe_graphic_type)},") + return avatar + + +@router.post("/save") +async def save_avatar( + skin_color: int | None = 0, + hair_color: int | None = 0, + facial_hair_type: int | None = 0, + facial_hair_color: int | None = 0, + top_type: int | None = 0, + hat_color: int | None = 0, + mouth_type: int | None = 0, + eyebrow_type: int | None = 0, + nose_type: int | None = 0, + accessories_type: int | None = 0, + clothe_type: int | None = 0, + clothe_color: int | None = 0, + clothe_graphic_type: int | None = 0, + user: User = Depends(get_current_user), +): + try: + skin_color = AvatarItemsAsList.skin_color[skin_color] + hair_color = AvatarItemsAsList.hat_color[hair_color] + facial_hair_type = AvatarItemsAsList.facial_hair_type[facial_hair_type] + facial_hair_color = AvatarItemsAsList.facial_hair_color[facial_hair_color] + top_type = AvatarItemsAsList.top_type[top_type] + hat_color = AvatarItemsAsList.hat_color[hat_color] + mouth_type = AvatarItemsAsList.mouth_type[mouth_type] + eyebrow_type = AvatarItemsAsList.eyebrow_type[eyebrow_type] + nose_type = AvatarItemsAsList.nose_type[nose_type] + accessories_type = AvatarItemsAsList.accessories_type[accessories_type] + clothe_type = AvatarItemsAsList.clothe_type[clothe_type] + clothe_color = AvatarItemsAsList.clothe_color[clothe_color] + clothe_graphic_type = AvatarItemsAsList.clothe_graphic_type[clothe_graphic_type] + except IndexError: + raise HTTPException(status_code=400, detail="One parameter-value doesn't exist.") + avatar = av.PyAvataaar( + style=av.AvatarStyle.TRANSPARENT, + skin_color=skin_color, + hair_color=hair_color, + facial_hair_type=facial_hair_type, + facial_hair_color=facial_hair_color, + top_type=top_type, + hat_color=hat_color, + mouth_type=mouth_type, + eyebrow_type=eyebrow_type, + nose_type=nose_type, + accessories_type=accessories_type, + clothe_type=clothe_type, + clothe_color=clothe_color, + clothe_graphic_type=clothe_graphic_type, + ).render_svg() + user.avatar = gzip.compress(str.encode(avatar)) + await redis.delete(user.email) + await user.update() diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3bdf7e0..015ca9d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1007,13 +1007,6 @@ packages: engines: { node: '>=6.0.0' } dev: true - /@neodrag/svelte/1.2.4: - resolution: - { - integrity: sha512-oGOxAgjtLbHyCJIWv35cvuHpgip9Kzn0ANrCidZb3rXV29w3AWhS7VtKAQfRinzFbmfK0Jeic7aSYdGmtKFKqw== - } - dev: true - /@nodelib/fs.scandir/2.1.5: resolution: { @@ -4929,13 +4922,6 @@ packages: sander: 0.5.1 dev: true - /sortablejs/1.15.0: - resolution: - { - integrity: sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w== - } - dev: true - /source-map-js/1.0.2: resolution: { diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 79628c0..44a64bd 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -236,5 +236,21 @@ }, "uploader": { "add_image": "Add image" + }, + "avatar_settings": { + "skin_color": "Skin color", + "top_type": "Top", + "hair_color": "Hair color", + "facial_hair_type": "Facial hair", + "facial_hair_color": "Facial hair color", + "mouth_type": "Mouth", + "eyebrow_type": "Eyebrow", + "accessories_type": "Glasses", + "hat_color": "Hat color", + "clothe_type": "Clothes", + "clothe_color": "Clothing color", + "clothe_graphic_type": "Graphic", + "thats_you": "That's You!", + "start_over": "Start over" } } diff --git a/frontend/src/routes/account/settings/+page.svelte b/frontend/src/routes/account/settings/+page.svelte index d11dba0..f8cce05 100644 --- a/frontend/src/routes/account/settings/+page.svelte +++ b/frontend/src/routes/account/settings/+page.svelte @@ -163,11 +163,16 @@ {:then user}
- Profile image of {user.username} +
+ Profile image of {user.username} +
+ Change avatar +
+
diff --git a/frontend/src/routes/account/settings/avatar/+page.svelte b/frontend/src/routes/account/settings/avatar/+page.svelte new file mode 100644 index 0000000..c263e58 --- /dev/null +++ b/frontend/src/routes/account/settings/avatar/+page.svelte @@ -0,0 +1,177 @@ + + + +
+
+
+ +
+
+
+
+ { + index = index - 1; + }} + disabled={index < 1}>Back +
+
+

+ {translation_map[data_keys[index]]} ({index + 1}/{data_keys.length}) +

+
+
+ Finish +
+
+
+ {#each Array.from(Array(item_count[data_keys[index]]).keys()) as key} + + {/each} +
+
+
+
+{#if finished} +
+

{$t('avatar_settings.thats_you')}

+ +
+ { + index = 0; + finished = false; + }}>{$t('avatar_settings.start_over')} + + {#if save_finished === undefined}{$t('words.save')} + {:else if save_finished === true} + + {:else if save_finished === false} + + {/if} + + Go back + { + finished = false; + }}>Close +
+
+{/if}