Added function to customize the user avatar

This commit is contained in:
Mawoka
2023-01-29 11:40:43 +01:00
parent 32c8b95c51
commit bbd4ce66ad
6 changed files with 355 additions and 19 deletions
+2
View File
@@ -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))
+150
View File
@@ -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()
-14
View File
@@ -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:
{
+16
View File
@@ -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"
}
}
@@ -163,11 +163,16 @@
<Spinner />
{:then user}
<div class="w-full grid grid-cols-6">
<img
class="rounded-md md:w-80"
src="/api/v1/users/avatar"
alt="Profile image of {user.username}"
/>
<div>
<img
class="rounded-md md:w-80"
src="/api/v1/users/avatar"
alt="Profile image of {user.username}"
/>
<div class="m-2 flex justify-center">
<BrownButton href="/account/settings/avatar">Change avatar</BrownButton>
</div>
</div>
<div class="grid grid-rows-2 col-start-2 col-end-7">
<div class="grid grid-cols-2">
<div>
@@ -0,0 +1,177 @@
<!--
- 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/.
-->
<script lang="ts">
import BrownButton from '$lib/components/buttons/brown.svelte';
import { fade, fly } from 'svelte/transition';
import { bounceOut } from 'svelte/easing';
import Spinner from '$lib/Spinner.svelte';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
const item_count = {
skin_color: 7,
top_type: 35,
hair_color: 10,
facial_hair_type: 6,
facial_hair_color: 10,
mouth_type: 12,
eyebrow_type: 13,
accessories_type: 7,
hat_color: 15,
clothe_type: 9,
clothe_color: 15,
clothe_graphic_type: 11
};
const translation_map = {
skin_color: $t('avatar_settings.skin_color'),
top_type: $t('avatar_settings.top_type'),
hair_color: $t('avatar_settings.hair_color'),
facial_hair_type: $t('avatar_settings.facial_hair_type'),
facial_hair_color: $t('avatar_settings.facial_hair_color'),
mouth_type: $t('avatar_settings.mouth_type'),
eyebrow_type: $t('avatar_settings.eyebrow_type'),
accessories_type: $t('avatar_settings.accessories_type'),
hat_color: $t('avatar_settings.hat_color'),
clothe_type: $t('avatar_settings.clothe_type'),
clothe_color: $t('avatar_settings.clothe_color'),
clothe_graphic_type: $t('avatar_settings.clothe_graphic_type')
};
let data = {
skin_color: 0,
top_type: 0,
hair_color: 0,
facial_hair_type: 0,
facial_hair_color: 0,
mouth_type: 0,
eyebrow_type: 0,
accessories_type: 0,
hat_color: 0,
clothe_type: 0,
clothe_color: 0,
clothe_graphic_type: 0
};
const data_keys = Object.keys(data);
let index = 0;
// let index = 10;
let save_finished: undefined | boolean = undefined;
let finished = false;
let image_url;
$: console.log('index', index);
const get_image_url = (input_data) => {
return `/api/v1/avatar/custom?${new URLSearchParams(input_data).toString()}`;
};
$: image_url = get_image_url(data);
const save_avatar = async () => {
save_finished = false;
const res = await fetch(`/api/v1/avatar/save?${new URLSearchParams(data).toString()}`, {
method: 'POST'
});
if (res.ok) {
save_finished = true;
}
};
</script>
<div class="h-full">
<div class="grid grid-cols-6 overflow-hidden h-full">
<div class="border-r-4 border-black h-full">
<img src={image_url} />
</div>
<div class="col-start-2 col-end-7 overflow-scroll">
<div class="flex pl-2">
<div class="mr-auto">
<BrownButton
on:click={() => {
index = index - 1;
}}
disabled={index < 1}>Back</BrownButton
>
</div>
<div class="mx-auto">
<h2 class="text-2xl">
{translation_map[data_keys[index]]} ({index + 1}/{data_keys.length})
</h2>
</div>
<div class="ml-auto">
<BrownButton disabled={index < 11}>Finish</BrownButton>
</div>
</div>
<div class="grid grid-cols-4">
{#each Array.from(Array(item_count[data_keys[index]]).keys()) as key}
<button
class="hover:opacity-80 transition-all"
on:click={() => {
data[data_keys[index]] = key;
if (index < 11) {
index++;
} else {
finished = true;
}
}}
>
<img
src={get_image_url({ ...data, [data_keys[index]]: key })}
in:fade={{ duration: 100 }}
/>
</button>
{/each}
</div>
</div>
</div>
</div>
{#if finished}
<div
class="fixed top-0 left-0 w-full h-full z-30 bg-black flex justify-center flex-col bg-opacity-90"
out:fade={{ duration: 200 }}
in:fade={{ duration: 300 }}
>
<h1 class="m-auto text-4xl" in:fade={{ delay: 3500 }}>{$t('avatar_settings.thats_you')}</h1>
<img
class="m-auto w-1/2 h-1/2 z-20"
src={get_image_url(data)}
in:fly={{ delay: 500, duration: 4000, y: -500, easing: bounceOut }}
/>
<div class="m-auto grid grid-cols-2 gap-4" in:fade={{ delay: 3500 }}>
<BrownButton
on:click={() => {
index = 0;
finished = false;
}}>{$t('avatar_settings.start_over')}</BrownButton
>
<BrownButton on:click={save_avatar} flex={true} disabled={save_finished === true}>
{#if save_finished === undefined}{$t('words.save')}
{:else if save_finished === true}
<svg
class="h-6 w-6"
aria-hidden="true"
fill="none"
stroke="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{:else if save_finished === false}
<Spinner my_20={false} />
{/if}
</BrownButton>
<BrownButton href="/account/settings">Go back</BrownButton>
<BrownButton
on:click={() => {
finished = false;
}}>Close</BrownButton
>
</div>
</div>
{/if}