Merge pull request 'update malouze cooking' (#6) from main into malouze-cooking

Reviewed-on: https://codeberg.org/adrien-lsh/ft_transcendence/pulls/6
This commit is contained in:
kbz_8
2024-02-14 17:53:57 +00:00
91 changed files with 2665 additions and 1360 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
db.sqlite3 db.sqlite3
**/migrations/** **/migrations/**
/profiles/static/avatars/* /profiles/static/avatars/*
!/profiles/static/avatars/default.env !/profiles/static/avatars/default.avif
*.mo

View File

@ -1,4 +1,24 @@
# BACKEND # FT_TRANSCENDENCE
# Modules
| Module | Nb point | state|
| ------ | -------- | ---- |
| Multiplayer | 2 | 🚧
| Remote | 2 | 🚧
| Bootstrap | 1 | 🏁 |
| Django | 2 | 🏁 |
| Bdd | 1 | 🚧 |
| Accounts | 2 | 🏁 |
| WebGL | 2 | 🚧 |
| Other game | 2 | 🚧 |
| Chat | 2 | 🏁 |
| Translation | 1 | 🚧 |
| Other browser | 1 | 🏁 |
| Smartphone support | 1 | 🚧 |
| --- | --- | ---
| Ready | 8 | |
| Total | 19 | |
## Installation ## Installation
@ -37,6 +57,10 @@ python manage.py makemigrations chat
python manage.py makemigrations tournament python manage.py makemigrations tournament
python manage.py migrate python manage.py migrate
``` ```
- Compile translations
```
python manage.py compilemessages
```
- Start the developpement server - Start the developpement server
``` ```
python manage.py runserver 0.0.0.0:8000 python manage.py runserver 0.0.0.0:8000

View File

@ -0,0 +1,22 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-01 13:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: views/login.py:22
msgid "Invalid username or password."
msgstr "Nom d'utilisateur ou mot de passe incorect."

View File

@ -4,6 +4,7 @@ from rest_framework import permissions, status
from django.http import HttpRequest from django.http import HttpRequest
from django.contrib.auth import login from django.contrib.auth import login
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from django.utils.translation import gettext as _
from ..serializers.login import LoginSerializer from ..serializers.login import LoginSerializer
@ -18,6 +19,6 @@ class LoginView(APIView):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
user = serializer.get_user(data) user = serializer.get_user(data)
if user is None: if user is None:
return Response({'login': ['Invalid username or password.']}, status.HTTP_400_BAD_REQUEST) return Response({'login': [_('Invalid username or password.')]}, status.HTTP_401_UNAUTHORIZED)
login(request, user) login(request, user)
return Response({'id': user.pk}, status=status.HTTP_200_OK) return Response({'id': user.pk}, status=status.HTTP_200_OK)

View File

@ -1,6 +1,9 @@
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from games.models import GameModel
from profiles.models import FriendModel, AskFriendModel
import time import time
import json import json
@ -31,19 +34,7 @@ class ChatNoticeConsumer(WebsocketConsumer):
self.accept() self.accept()
message_time: int = int(time.time() * 1000) self.sync()
targets = list(self.channel_layer.users_channels.keys())
for target in targets:
channel = self.channel_layer.users_channels.get(target)
if (channel == None or target == user.pk):
continue
async_to_sync(self.channel_layer.send)(channel, {
'type':"online_users",
'author_id':user.pk,
'targets': targets,
'time':message_time,
'status': 200,
})
def disconnect(self, code): def disconnect(self, code):
@ -51,22 +42,15 @@ class ChatNoticeConsumer(WebsocketConsumer):
if (user.is_anonymous or not user.is_authenticated): if (user.is_anonymous or not user.is_authenticated):
return return
self.channel_layer.users_channels.pop(user.pk) del self.channel_layer.users_channels[user.pk]
del self.channel_layer.invite[user.pk]
message_time: int = int(time.time() * 1000) for inviter_id, inviteds_id in self.channel_layer.invite.items():
if (user.pk in inviteds_id):
self.channel_layer.invite[inviter_id].remove(user.pk)
self.sync()
targets = list(self.channel_layer.users_channels.keys())
for target in targets:
channel = self.channel_layer.users_channels.get(target)
if (channel == None or target == user.pk):
continue
async_to_sync(self.channel_layer.send)(channel, {
'type':"online_users",
'author_id':user.pk,
'targets': targets,
'time':message_time,
'status': 200,
})
def receive(self, text_data=None, bytes_data=None): def receive(self, text_data=None, bytes_data=None):
@ -89,16 +73,18 @@ class ChatNoticeConsumer(WebsocketConsumer):
if (self.channel_layer == None): if (self.channel_layer == None):
return return
message_time: int = text_data_json.get('time')
if (message_time == None):
message_time: int = int(time.time() * 1000) message_time: int = int(time.time() * 1000)
status = 200;
#print("receive" + str(user.pk))
result = None
try: try:
status = getattr(self, "pre_" + type_notice)(user, targets) status, result = getattr(self, "pre_" + type_notice)(user, targets)
except AttributeError: except AttributeError as error:
print(f"La fonction pre_{type_notice} n'existe pas.") status = 200
if (status < 300):
if targets == "all": if targets == "all":
targets = list(self.channel_layer.users_channels.keys()) targets = list(self.channel_layer.users_channels.keys())
@ -106,33 +92,93 @@ class ChatNoticeConsumer(WebsocketConsumer):
channel = self.channel_layer.users_channels.get(target) channel = self.channel_layer.users_channels.get(target)
if (channel == None or target == user.pk): if (channel == None or target == user.pk):
if (channel == None): if (channel == None):
status = 404 status = 444 # target not connected
continue continue
async_to_sync(self.channel_layer.send)(channel, { async_to_sync(self.channel_layer.send)(channel, {
'type':type_notice, 'type':type_notice,
'author_id':user.pk, 'author_id':user.pk,
'content':content, 'content':content,
'result':result,
'targets': targets, 'targets': targets,
'time':message_time, 'time':message_time,
'status': 200, 'status': 200,
}) })
async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), {
'type':type_notice, 'type':type_notice,
'author_id':user.pk, 'author_id':user.pk,
'content':"notice return", 'result':result,
'targets': targets, 'targets': targets,
'time':message_time, 'time':message_time,
'status':status, 'status':status,
}) })
def sync(self, user = None, level = None):
sendToUser = True
if (user == None):
user = self.scope["user"]
sendToUser = False
if (level == None):
level = 0
message_time: int = int(time.time() * 1000)
if (sendToUser):
targets = [user.pk]
else:
targets = list(self.channel_layer.users_channels.keys())
for target in targets:
channel = self.channel_layer.users_channels.get(target)
if (channel == None or (not sendToUser and target == user.pk)):
continue
async_to_sync(self.channel_layer.send)(channel, {
'type':"online_users",
'author_id':user.pk,
'targets': targets,
'time':message_time,
'status': 200,
})
if (level >= 1):
async_to_sync(self.channel_layer.send)(channel, {
'type':"invite",
'author_id':user.pk,
'targets': targets,
'time':message_time,
'status': 200,
})
def pre_invite(self, user, targets): def pre_invite(self, user, targets):
status = 200
if (user.is_anonymous or not user.is_authenticated):
return 400, None
status = 200
for target in targets: for target in targets:
if (target in self.channel_layer.invite[user.pk]): if (target in self.channel_layer.invite[user.pk]):
status = 409 status = 409
return status continue
channel = self.channel_layer.users_channels.get(target)
if (channel == None):
status = 404
continue
# Add the invited in "self.channel_layer.invite"
if (user.pk != target):
self.channel_layer.invite[user.pk].append(target)
return status, None
def get_invites(self, user):
invites = []
for inviter_id, inviteds_id in self.channel_layer.invite.items():
if (user.pk in inviteds_id and user.pk != inviter_id):
invites.append(inviter_id)
return invites;
def invite(self, event): def invite(self, event):
@ -140,38 +186,194 @@ class ChatNoticeConsumer(WebsocketConsumer):
if (user.is_anonymous or not user.is_authenticated): if (user.is_anonymous or not user.is_authenticated):
return return
if (user.pk in self.channel_layer.invite[event["author_id"]]): invites = self.get_invites(user)
return
if (user.pk != event["author_id"]):
self.channel_layer.invite[event["author_id"]].append(user.pk)
self.send(text_data=json.dumps({ self.send(text_data=json.dumps({
'type':event['type'], 'type':event['type'],
'author_id':event['author_id'], 'author_id':event['author_id'],
'content':event['content'], 'invites': invites,
'targets': event['targets'], 'targets': event['targets'],
'time': event['time'], 'time': event['time'],
'status':event['status'], 'status':event['status'],
})) }))
def pre_online_users(self, user, targets): def pre_accept_invite(self, user, targets):
pass
def online_users(self, event): if (user.is_anonymous or not user.is_authenticated):
return 400, None
if (user.pk not in self.channel_layer.invite[targets[0]]):
return 400, None
self.channel_layer.invite[targets[0]].remove(user.pk)
if (targets[0] in self.channel_layer.invite[user.pk]):
self.channel_layer.invite[user.pk].remove(targets[0])
id_game = GameModel().create([user.pk, targets[0]]);
return 200, id_game
def accept_invite(self, event):
user = self.scope["user"] user = self.scope["user"]
#if (user.is_anonymous or not user.is_authenticated): if (user.is_anonymous or not user.is_authenticated):
#return return
#print("online_users" + str(user.pk)) invites = self.get_invites(user)
event['content'] = self.channel_layer.users_channels
self.send(text_data=json.dumps({ self.send(text_data=json.dumps({
'type':event['type'], 'type':event['type'],
'author_id':event['author_id'], 'author_id':event['author_id'],
'content':event['content'], 'id_game': event['result'],
'invites': invites,
'time': event['time'],
'status':event['status'],
}))
def pre_refuse_invite(self, user, targets):
if (user.is_anonymous or not user.is_authenticated):
return 400, None
if (user.pk not in self.channel_layer.invite[targets[0]]):
return 400, None
self.channel_layer.invite[targets[0]].remove(user.pk)
if (targets[0] in self.channel_layer.invite[user.pk]):
self.channel_layer.invite[user.pk].remove(targets[0])
return 200, None
def refuse_invite(self, event):
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
invites = self.get_invites(user)
self.send(text_data=json.dumps({
'type':event['type'],
'author_id':event['author_id'],
'invites': invites,
'time': event['time'],
'status':event['status'],
}))
def pre_ask_friend(self, user, targets):
if (user.is_anonymous or not user.is_authenticated):
return 400, None
if (AskFriendModel.objects.filter(asker=user.pk, asked=targets[0])):
return 409, None
if (FriendModel().isFriend(user.pk, targets[0])):
return 409, None
if (targets[0] != None):
AskFriendModel(asker=user.pk, asked=targets[0]).save()
return 200, None
def send_ask_friend(self, event):
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
asked = AskFriendModel().getAsked(user.pk)
asker = AskFriendModel().getAsker(user.pk)
online_friends = self.get_online_friend(user)
self.send(text_data=json.dumps({
'type':event['type'],
'author_id':event['author_id'],
'targets':event['targets'],
'asker': asker,
'asked': asked,
'online': online_friends,
'time': event['time'],
'status':event['status'],
}))
def ask_friend(self, event):
self.send_ask_friend(event)
def delete_ask(self, asker, user_asked):
if (user_asked.is_anonymous or not user_asked.is_authenticated):
return 400, None
asked = user_asked.pk
if (not AskFriendModel.objects.filter(asker=asker, asked=asked)):
return 404, None
if (FriendModel().isFriend(asker, asked)):
return 409, None
if (not AskFriendModel().deleteAsk(asker, asked)):
return 400, None
return 200, None
def pre_accept_friend(self, user, targets):
status, result = self.delete_ask(targets[0], user)
if (status == 200):
FriendModel(user_id1=user.pk, user_id2=targets[0]).save()
return status, result
def accept_friend(self, event):
self.send_ask_friend(event)
def pre_refuse_friend(self, user, targets):
return self.delete_ask(targets[0], user)
def refuse_friend(self, event):
self.send_ask_friend(event)
def pre_remove_friend(self, user, targets):
if (user.is_anonymous or not user.is_authenticated):
return 400, None
if (not FriendModel().isFriend(user.pk, targets[0])):
return 409, None
if (not FriendModel().deleteFriend(user.pk, targets[0])):
return 400, None
return 200, None
def remove_friend(self, event):
self.send_ask_friend(event)
def get_online_friend(self, user):
online_friends = {}
for friend in FriendModel().getFriends(user.pk):
if (friend in self.channel_layer.users_channels):
online_friends[friend] = "green"
else:
online_friends[friend] = "red"
return online_friends
def online_users(self, event):
user = self.scope["user"]
if (user.is_anonymous or not user.is_authenticated):
return
online_friends = self.get_online_friend(user)
self.send(text_data=json.dumps({
'type':event['type'],
'author_id':event['author_id'],
'online':online_friends,
'time': event['time'], 'time': event['time'],
'status':event['status'], 'status':event['status'],
})) }))

View File

@ -1,12 +1,3 @@
*{
color: #cccccc;
font-size: 35px;
background-color: #1a1a1a;
}
body {
}
#app #avatar { #app #avatar {
max-height: 10em; max-height: 10em;
max-width: 10em; max-width: 10em;
@ -34,3 +25,7 @@ body {
opacity: 0; opacity: 0;
transition: opacity 0.25s; transition: opacity 0.25s;
} }
#languageSelector > .dropdown-item.active {
background-color: transparent;
}

View File

@ -1,17 +0,0 @@
#app #main .account
{
color: #1a1a1a;
}
#app #main
{
width: 60%;
display: flex;
flex-direction: column;
color: #1a1a1a;
}
#app #main .profile
{
color: #1a1a1a;
}

View File

@ -1,14 +1,25 @@
#app * {
font-size: 30px;
}
#app #username #app #username
{ {
font-size: 0.8em; font-size: 0.8em;
} }
#app #block { #app #block, #app #friend {
cursor: pointer; cursor: pointer;
font-size: 0.7em; font-size: 0.7em;
text-decoration: underline; text-decoration: underline;
} }
#app { #app {
margin-top: 20px; margin-top: 1em;
}
#app #yes, #app #no {
display:inline;
cursor: pointer;
font-size: 0.7em;
text-decoration: underline;
} }

View File

@ -1,3 +1,7 @@
#app * {
font-size: 40px;
}
#app img #app img
{ {
@ -78,7 +82,6 @@
border: none; border: none;
outline: none; outline: none;
border-bottom: 0.15em solid green; border-bottom: 0.15em solid green;
caret-color: green;
color: green; color: green;
font-size: 0.8em; font-size: 0.8em;
} }
@ -106,9 +109,8 @@
word-wrap: break-word; word-wrap: break-word;
} }
#app #invite { #app #invite, #app #yes, #app #no {
position: relative; position: relative;
background-color: green;
border: none; border: none;
color: white; color: white;
text-align: center; text-align: center;
@ -118,3 +120,15 @@
width: 4em; width: 4em;
cursor: pointer; cursor: pointer;
} }
#app #yes, #app #no {
position: relative;
border: none;
color: white;
text-align: center;
text-decoration: none;
font-size: 0.8em;
height: 2em;
width: 2em;
cursor: pointer;
}

View File

@ -0,0 +1,11 @@
#app * {
font-size: 30px;
}
#app #main
{
width: 60%;
display: flex;
flex-direction: column;
}

View File

@ -1,45 +1,74 @@
import { reloadView } from '../index.js'
export default class LanguageManager { export default class LanguageManager {
constructor() { constructor() {
this.availableLanguages = ['en', 'fr']; this.availableLanguages = ['en', 'fr', 'tp', 'cr'];
this.dict = null;
this.currentLang = 'en' this.currentLang = 'en'
this.chosenLang = localStorage.getItem('preferedLanguage') || this.currentLang; this.chosenLang = localStorage.getItem('preferedLanguage') || this.currentLang;
if (this.chosenLang !== this.currentLang && this.availableLanguages.includes(this.chosenLang)) { if (this.chosenLang !== this.currentLang && this.availableLanguages.includes(this.chosenLang)) {
this.translatePage(); this.loading = this.translatePage();
this.currentLang = this.chosenLang;
} else {
this.loading = this.loadDict(this.chosenLang);
} }
document.getElementById('languageDisplay').innerHTML =
document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML;
} }
async translatePage() { async translatePage() {
if (this.currentLang === this.chosenLang) if (this.currentLang === this.chosenLang)
return; return;
let dictUrl = `${location.origin}/static/js/lang/${this.chosenLang}.json`; await this.loadDict(this.chosenLang);
let translation = await fetch(dictUrl).then(response => { if (!this.dict)
if (response.status !== 200)
return null;
return response.json();
});
if (!translation) {
console.log(`No translation found for language ${this.chosenLang}`);
return 1; return 1;
}
document.querySelectorAll('[data-i18n]').forEach(el => { document.querySelectorAll('[data-i18n]').forEach(el => {
let key = el.getAttribute('data-i18n'); let key = el.getAttribute('data-i18n');
el.innerHTML = translation[key]; el.innerHTML = this.dict[key];
}) })
await reloadView();
this.currentLang = this.chosenLang;
return 0; return 0;
} }
async changeLanguage(lang) { async changeLanguage(lang) {
if (lang === this.currentLang || !this.availableLanguages.includes(lang)) if (lang === this.currentLang || !this.availableLanguages.includes(lang))
return; return 1;
this.chosenLang = lang; this.chosenLang = lang;
if (await this.translatePage() !== 0) if (await this.translatePage() !== 0)
return; return 1;
this.currentLang = this.chosenLang;
localStorage.setItem('preferedLanguage', lang); localStorage.setItem('preferedLanguage', lang);
document.getElementById('languageDisplay').innerHTML =
document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML;
return 0;
}
async loadDict(lang) {
let dictUrl = `${location.origin}/static/js/lang/${lang}.json`;
let response = await fetch(dictUrl);
if (response.status !== 200) {
console.log(`No translation found for language ${lang}`);
return;
}
this.dict = await response.json();
}
async waitLoading() {
await this.loading;
}
get(key, defaultTxt) {
if (!this.dict)
return defaultTxt;
return this.dict[key] || defaultTxt;
} }
} }

View File

@ -9,7 +9,7 @@ class MyProfile extends Profile
*/ */
constructor (client) constructor (client)
{ {
super(client, "me") super(client, "../me")
} }
/** /**
@ -19,7 +19,7 @@ class MyProfile extends Profile
*/ */
async change_avatar(form_data) async change_avatar(form_data)
{ {
let response = await this.client._patch_file(`/api/profiles/me`, form_data); let response = await this.client._patch_file(`/api/profiles/settings`, form_data);
let response_data = await response.json() let response_data = await response.json()
return response_data; return response_data;

View File

@ -16,19 +16,16 @@ class Account
/** /**
* @param {String} username * @param {String} username
* @param {String} password * @param {String} password
* @returns {?Promise<Object>} * @returns {Response}
*/ */
async create(username, password) async create(username, password)
{ {
let response = await this.client._post("/api/accounts/register", {username: username, password: password}); let response = await this.client._post("/api/accounts/register", {username: username, password: password});
let response_data = await response.json()
if (response_data == "user created") if (response.status === 201)
{
await this.client._update_logged(true); await this.client._update_logged(true);
return null;
} return response;
return response_data
} }
/** /**

View File

@ -1,3 +1,4 @@
import { navigateTo } from "../../index.js";
import {create_popup} from "../../utils/noticeUtils.js"; import {create_popup} from "../../utils/noticeUtils.js";
class Notice { class Notice {
@ -5,11 +6,10 @@ class Notice {
this.client = client; this.client = client;
this.data = {}; this.data = {};
// users online, invited by, asked friend by, // users online, invited by ..., asked by ..., asker to ...
let data_variable = ["online", "invited", "asked"]; let data_variable = ["online", "invited", "asked", "asker"];
for (let i in data_variable) { for (let i in data_variable)
this.data[data_variable[i]] = []; this.data[data_variable[i]] = [];
}
this.connect(); this.connect();
@ -20,15 +20,20 @@ class Notice {
this.chatSocket = new WebSocket(url); this.chatSocket = new WebSocket(url);
this.chatSocket.onmessage = (event) =>{ this.chatSocket.onmessage = (event) =>{
let data = JSON.parse(event.data); let send = JSON.parse(event.data);
//console.log("notice: ", data); //console.log("notice: ", send);
if (data.type == "invite")
this.receiveInvite(data); try {
else if (data.type == "online_users" || data.type == "disconnect") this["receive_" + send.type](send);
this.receiveOnlineUser(data); }
catch (error) {
console.log("receive_" + send.type + ": Function not found");
}
} }
this.chatSocket.onopen = (event) => { this.chatSocket.onopen = (event) => {
this.getOnlineUser(); this.getOnlineUser();
this.ask_friend();
} }
} }
@ -37,64 +42,126 @@ class Notice {
return ; return ;
this.chatSocket.close(); this.chatSocket.close();
}
async reconnect() {
this.disconnect();
this.connect();
}
async accept_invite(invitedBy) {
this.sendRequest({
type: "accept_invite",
targets: [invitedBy],
});
} }
async sendInvite(id_inviter, id_inviteds) { async receive_accept_invite(send) {
if (this.chatSocket == undefined) this.data["invited"] = send.invites;
return; let id_game = send["id_game"];
navigateTo("/game/" + id_game);
this.chatSocket.send(JSON.stringify({ }
async refuse_invite(invitedBy) {
this.sendRequest({
type: "refuse_invite",
targets: [invitedBy],
});
}
async receive_refuse_invite(send) {
this.data["invited"] = send.invites;
if (send.author_id == this.client.me.id) {
if (this.rewrite_invite !== undefined)
this.rewrite_invite();
}
else {
let sender = await this.client.profiles.getProfileId(send.author_id);
create_popup(sender.username + " refuse your invitation");
}
}
async send_invite(id_inviteds) {
this.sendRequest({
type: "invite", type: "invite",
targets: id_inviteds, targets: id_inviteds,
})); time: new Date().getTime(),
});
} }
async receiveInvite(data) { async receive_invite(send) {
if (data.content === "notice return") { if (this.client.me == undefined)
if (data.status == 200) { return ;
for (let target in data.targets)
this.data["invited"].push(target); let content = send.invites;
if (send.author_id == this.client.me.id) {
if (send.status == 200) {
for (let target in send.targets)
return create_popup("Invitation send"); return create_popup("Invitation send");
} }
else if (data.status == 404) else if (send.status == 444)
return create_popup("User not connected"); return create_popup("User not connected");
else if (data.status == 409) else if (send.status == 409)
return create_popup("Already invited"); return create_popup("Already invited");
} }
else { else {
let sender = await this.client.profiles.getProfile(data.author_id);
this.inviter.push(data.author_id); // Regarder qu'il est bien invité par l'auteur
// Et qu'il n'est pas déjà invité
if (!content.includes(send.author_id) ||
this.data["invited"].includes(send.author_id))
return;
this.data["invited"] = content;
let sender = await this.client.profiles.getProfileId(send.author_id);
create_popup("Invitation received by " + sender.username); create_popup("Invitation received by " + sender.username);
// Géré la reception de l'invitation if (this.rewrite_invite !== undefined)
this.rewrite_invite();
} }
} }
async getOnlineUser() { async getOnlineUser() {
if (this.chatSocket == undefined)
return;
this.online_users = {}; this.online_users = {};
this.chatSocket.send(JSON.stringify({ this.sendRequest({
type: "online_users", type: "online_users",
targets: "all", targets: [],
})); time: new Date().getTime(),
});
} }
async receiveOnlineUser(data) { async receive_online_users(send) {
if (data.content !== undefined) { let content = send.online;
if (content !== undefined) {
if (this.online_users.length > 0) { if (this.data["online"].length > 0) {
// get all disconnect user // get all disconnect user
let disconnects = this.online_users.filter(id => !Object.keys(data.content).includes(id)); //let disconnects = this.data["online"].filter(id => !Object.keys(content).includes(id));
let disconnects = [];
for (const [key, value] of Object.entries(this.data["online"])) {
if (content[key] == "red" && value == "green")
disconnects.push(key);
}
// delete invite // delete invite
this.data["invited"] = this.data["invited"].filter(id => !disconnects.includes(id)); this.data["invited"] = this.data["invited"].filter(id => !disconnects.includes(id));
@ -102,12 +169,139 @@ class Notice {
//console.log(this.data["invited"]); //console.log(this.data["invited"]);
} }
this.data["online"] = Object.keys(data.content); this.data["online"] = content;
if (this.rewrite_usernames !== undefined) if (this.rewrite_usernames !== undefined)
this.rewrite_usernames(); this.rewrite_usernames();
} }
} }
async ask_friend(user_id=undefined) {
this.sendRequest({
type: "ask_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_ask_friend(send) {
let my_id = (this.client.me && this.client.me.id) || send.author_id;
if (send.author_id == my_id) {
if (send.status == 400)
create_popup("Friend ask error");
else if (send.status == 409)
create_popup("Already asked friend");
}
//if (!send.asked.includes(send.author_id) ||
//this.data["asked"].includes(send.author_id))
//return;
//if (!send.asker.includes(send.author_id) ||
//this.data["asker"].includes(send.author_id))
//return;
this.data["asked"] = send.asked;
this.data["asker"] = send.asker;
if (send.author_id != my_id) {
let sender = await this.client.profiles.getProfileId(send.author_id);
if (this.data["asker"].includes(send.author_id))
create_popup(sender.username + " ask you as friend");
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
}
}
async remove_friend(user_id) {
this.sendRequest({
type: "remove_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_remove_friend(send) {
if (send.author_id == this.client.me.id) {
if (send.status == 400)
create_popup("Error remove Friend");
else if (send.status == 409)
create_popup("Not friend, wtf");
}
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
this.receive_online_users(send);
}
async accept_friend(user_id) {
this.sendRequest({
type: "accept_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_accept_friend(send) {
this.data["asked"] = send.asked;
this.data["asker"] = send.asker;
let sender = await this.client.profiles.getProfileId(send.author_id);
if (send.author_id == this.client.me.id) {
if (send.status == 400)
create_popup("Error accept Friend");
else if (send.status == 404)
create_popup("Not found request Friend");
else if (send.status == 409)
create_popup("Already Friend, wtf");
}
else {
create_popup(sender.username + " accept your friend request");
}
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
this.receive_online_users(send);
}
async refuse_friend(user_id) {
this.sendRequest({
type: "refuse_friend",
targets: [user_id],
time: new Date().getTime(),
});
}
async receive_refuse_friend(send) {
this.data["asked"] = send.asked;
this.data["asker"] = send.asker;
let sender = await this.client.profiles.getProfileId(send.author_id);
if (send.author_id == this.client.me.id) {
if (send.status == 400)
create_popup("Error refuse Friend");
else if (send.status == 404)
create_popup("Not found request Friend");
else if (send.status == 409)
create_popup("Already Friend, WTF");
}
if (this.rewrite_profile !== undefined)
await this.rewrite_profile();
}
async sendRequest(content) {
if (this.chatSocket == undefined)
return;
this.chatSocket.send(JSON.stringify(content));
}
} }
export {Notice} export {Notice}

View File

@ -53,7 +53,7 @@ class Client
this.tournaments = new Tourmanents(this); this.tournaments = new Tourmanents(this);
/** /**
* @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthentificated() * @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthenticated()
*/ */
this._logged = undefined; this._logged = undefined;
@ -73,13 +73,14 @@ class Client
this.notice = new Notice(this); this.notice = new Notice(this);
this.lang = new LanguageManager; this.lang = new LanguageManager;
} }
/** /**
* The only right way to determine is the user is logged * The only right way to determine is the user is logged
* @returns {Promise<Boolean>} * @returns {Promise<Boolean>}
*/ */
async isAuthentificate() async isAuthenticated()
{ {
if (this._logged == undefined) if (this._logged == undefined)
this._logged = await this._test_logged(); this._logged = await this._test_logged();
@ -114,6 +115,7 @@ class Client
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-CSRFToken": getCookie("csrftoken"), "X-CSRFToken": getCookie("csrftoken"),
'Accept-Language': this.lang.currentLang,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
@ -190,6 +192,7 @@ class Client
{ {
this.me = new MyProfile(this); this.me = new MyProfile(this);
await this.me.init(); await this.me.init();
this.notice.reconnect();
document.getElementById('navbarLoggedOut').classList.add('d-none'); document.getElementById('navbarLoggedOut').classList.add('d-none');
document.getElementById('navbarLoggedIn').classList.remove('d-none'); document.getElementById('navbarLoggedIn').classList.remove('d-none');
document.getElementById('navbarDropdownButton').innerHTML = this.me.username; document.getElementById('navbarDropdownButton').innerHTML = this.me.username;
@ -198,6 +201,7 @@ class Client
else else
{ {
this.me = undefined; this.me = undefined;
this.notice.reconnect();
document.getElementById('navbarLoggedOut').classList.remove('d-none'); document.getElementById('navbarLoggedOut').classList.remove('d-none');
document.getElementById('navbarLoggedIn').classList.add('d-none'); document.getElementById('navbarLoggedIn').classList.add('d-none');
document.getElementById('navbarDropdownButton').innerHTML = 'Me'; document.getElementById('navbarDropdownButton').innerHTML = 'Me';
@ -232,7 +236,7 @@ class Client
} }
/** /**
* Determine if the user is logged. NEVER USE IT, USE isAuthentificated() * Determine if the user is logged. NEVER USE IT, USE isAuthenticated()
* @returns {Promise<Boolean>} * @returns {Promise<Boolean>}
*/ */
async _test_logged() async _test_logged()

View File

@ -4,6 +4,7 @@ import { GameConfig } from "./GameConfig.js"
import { Player } from "./Player.js"; import { Player } from "./Player.js";
import { Time } from "./Time.js"; import { Time } from "./Time.js";
import { Wall } from "./Wall.js"; import { Wall } from "./Wall.js";
import { Client } from "../client.js";
class Game class Game
{ {
@ -21,7 +22,7 @@ class Game
/** /**
* *
* @returns {Number} * @returns {Promise<Number>}
*/ */
async init() async init()
{ {
@ -32,23 +33,52 @@ class Game
let response_data = await response.json(); let response_data = await response.json();
/**
* @type {[Number]}
*/
this.players_id = response_data.players_id; this.players_id = response_data.players_id;
/**
* @type {String}
*/
this.state = response_data.state; this.state = response_data.state;
/**
* @type {Boolean}
*/
this.started = response_data.started; this.started = response_data.started;
/**
* @type {Boolean}
*/
this.finished = response_data.finished; this.finished = response_data.finished;
/**
* @type {Number}
*/
this.winner_id = this.finished ? response_data.winner_id : undefined; this.winner_id = this.finished ? response_data.winner_id : undefined;
if (this.finished === true) if (this.finished === true)
return 0; return 0;
/**
* @type {GameConfig}
*/
this.config = new GameConfig(this.client); this.config = new GameConfig(this.client);
let ret = await this.config.init(); let ret = await this.config.init();
if (ret !== 0) if (ret !== 0)
return ret; return ret;
/**
* @type {Time}
*/
this.time = new Time(); this.time = new Time();
this.last_pos = null
/**
* @type {Boolean}
*/
this._inited = false; this._inited = false;
return 0; return 0;
@ -72,14 +102,14 @@ class Game
* *
* @param {CanvasRenderingContext2D} ctx * @param {CanvasRenderingContext2D} ctx
*/ */
draw(ctx) render(ctx)
{ {
if(ctx instanceof CanvasRenderingContext2D) if(ctx instanceof CanvasRenderingContext2D)
{ {
ctx.clearRect(0, 0, this.config.size_x, this.config.size_y); ctx.clearRect(0, 0, this.config.size_x, this.config.size_y);
} }
this.draw_sides(ctx); this.draw_sides(ctx);
this.ball.draw(ctx); this.ball.render(ctx);
} }
_send(data) _send(data)
@ -93,50 +123,25 @@ class Game
} }
} }
/**
* @param {Number} position
* @param {Number} time
*/
_send_paddle_position(position, time) _send_paddle_position(position, time)
{ {
if (this.last_pos !== null && this.last_pos.time >= time) this._send({"detail": "update_my_paddle_pos", ...{"time": time, "position": position}});
return;
this.last_pos = {"time": time, "position": position};
this._send({"detail": "update_my_paddle_pos", ...this.last_pos});
}
_receive_player_join(player_data)
{
console.log(player_data)
let index = this.players.indexOf((player) => player.id === player_data.user_id);
this.players[index].is_connected = true;
}
_receive_player_leave(player_data)
{
let index = this.players.indexOf((player) => player.id === player_data.user_id);
this.players[index].is_connected = false;
}
_receive_update_ball(data)
{
this.ball.position_x = data.position_x
this.ball.position_y = data.position_y
this.ball.position_x = data.position_x
this.ball.position_x = data.position_x
} }
_receive_update_paddle(data) _receive_update_paddle(data)
{ {
let player = this.players.find((player) => player.id === data.user_id); let player = this.players.find((player) => player.id === data.user_id);
if (player === null) player.from_json(data);
{
this._receive_player_join(data);
return;
} }
player.is_connected = data.is_connected;
player.update_pos(data.position.position, data.position.time); _receive_ball(data)
{
this.ball.from_json(data);
} }
_receive(data) _receive(data)
@ -144,26 +149,30 @@ class Game
if (data.detail === "update_paddle") if (data.detail === "update_paddle")
this._receive_update_paddle(data); this._receive_update_paddle(data);
else if (data.detail === "update_ball") else if (data.detail === "update_ball")
this._receive_update_ball(data); this._receive_ball(data)
else if (data.detail === "init_game") else if (data.detail === "init_game")
this._init_game(data) this._init_game(data);
else if (data.detail === "player_join")
this._receive_player_join(data)
else if (data.detail === "player_leave")
this._receive_player_leave(data)
} }
_init_game(data) _init_game(data)
{ {
const ball_data = data.ball; /**
this.ball = new Ball(this, ball_data.position_x, ball_data.position_y, ball_data.velocity_x, ball_data.velocity_y); * @type {Ball}
*/
this.ball = (new Ball(this)).from_json(data.ball)
/**
* @type {[Wall]}
*/
this.walls = []; this.walls = [];
const walls_data = data.walls; const walls_data = data.walls;
walls_data.forEach((wall_data) => { walls_data.forEach((wall_data) => {
this.walls.push(new Wall().from_json(wall_data)); this.walls.push(new Wall().from_json(wall_data));
}); });
/**
* @type {[Player]}
*/
this.players = [] this.players = []
const players_data = data.players; const players_data = data.players;
players_data.forEach((player_data) => { players_data.forEach((player_data) => {

View File

@ -24,14 +24,17 @@ class GameConfig
* @type {Number} * @type {Number}
*/ */
this.size_x = response_data.MAP_SIZE_X; this.size_x = response_data.MAP_SIZE_X;
/** /**
* @type {Number} * @type {Number}
*/ */
this.size_y = response_data.MAP_SIZE_Y; this.size_y = response_data.MAP_SIZE_Y;
/** /**
* @type {Number} * @type {Number}
*/ */
this.center_x = this.size_x / 2; this.center_x = this.size_x / 2;
/** /**
* @type {Number} * @type {Number}
*/ */
@ -63,10 +66,12 @@ class GameConfig
* @type {Number} * @type {Number}
*/ */
this.ball_size = response_data.BALL_SIZE; this.ball_size = response_data.BALL_SIZE;
/** /**
* @type {Number} * @type {Number}
*/ */
this.ball_spawn_x = this.center_x; this.ball_spawn_x = this.center_x;
/** /**
* @type {Number} * @type {Number}
*/ */

View File

@ -9,8 +9,32 @@ class Segment
*/ */
constructor(start, stop) constructor(start, stop)
{ {
/**
* @type {Point}
*/
this.start = start === undefined ? new Point() : start; this.start = start === undefined ? new Point() : start;
/**
* @type {Point}
*/
this.stop = stop === undefined ? new Point() : stop; this.stop = stop === undefined ? new Point() : stop;
}
angle()
{
let x = this.start.x - this.stop.x,
y = this.start.y - this.stop.y;
return Math.atan2(y, x);
}
len()
{
let x = this.start.x - this.stop.x,
y = this.start.y - this.stop.y;
return (x ** 2 + y ** 2) ** (1 / 2);
} }
/** /**

View File

@ -17,7 +17,9 @@ class Time
deltaTime() deltaTime()
{ {
return (this._current_frame - this._last_frame) !== NaN ? this._current_frame - this._last_frame : 0; if (this._last_frame === undefined)
return 0;
return (this._current_frame - this._last_frame);
} }
deltaTimeSecond() deltaTimeSecond()

View File

@ -23,7 +23,7 @@ class MatchMaking
*/ */
async start(receive_func, disconnect_func, mode) async start(receive_func, disconnect_func, mode)
{ {
if (!await this.client.isAuthentificate()) if (!await this.client.isAuthenticated())
return null; return null;
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${mode}`; let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${mode}`;

View File

@ -5,7 +5,7 @@ class Profile
/** /**
* @param {Client} client * @param {Client} client
*/ */
constructor (client, username, id = undefined, avatar_url = undefined) constructor (client, username=undefined, id=undefined, avatar_url=undefined)
{ {
/** /**
* @type {Client} client * @type {Client} client
@ -31,6 +31,7 @@ class Profile
* @type {Boolean} * @type {Boolean}
*/ */
this.isBlocked = false; this.isBlocked = false;
this.isFriend = false;
} }
/** /**
@ -39,7 +40,11 @@ class Profile
*/ */
async init() async init()
{ {
let response = await this.client._get(`/api/profiles/${this.username}`); let response;
if (this.username !== undefined)
response = await this.client._get(`/api/profiles/user/${this.username}`);
else
response = await this.client._get(`/api/profiles/id/${this.id}`);
if (response.status !== 200) if (response.status !== 200)
return response.status; return response.status;
@ -47,25 +52,49 @@ class Profile
let response_data = await response.json(); let response_data = await response.json();
this.id = response_data.user_id; this.id = response_data.user_id;
this.username = response_data.username; this.username = response_data.username;
this.avatar_url = response_data.avatar_url; this.avatar_url = response_data.avatar;
if (this.client.me == undefined) await this.getBlock();
return; await this.getFriend();
}
async getBlock() {
let block_response = await this.client._get("/api/profiles/block"); let block_response = await this.client._get("/api/profiles/block");
if (block_response.status != 200) if (block_response.status != 200)
return return
let block_data = await block_response.json(); let block_data = await block_response.json();
let block_list = JSON.parse(block_data); let block_list = JSON.parse(block_data["blockeds"]);
let client_id = block_data["user_id"];
block_list.forEach(block => { block_list.forEach(block => {
let blocker = block.fields.blocker; let blocker = block.fields.blocker;
let blocked = block.fields.blocked; let blocked = block.fields.blocked;
if (blocker == this.client.me.user_id && blocked == user_id) if (blocker == client_id && blocked == this.id)
return this.isBlocked = true; return this.isBlocked = true;
}); });
} }
async getFriend() {
let friend_response = await this.client._get("/api/profiles/friend");
this.isFriend = false;
if (friend_response.status != 200)
return this.isFriend;
let friend_data = await friend_response.json();
let friends_list = friend_data["friends"];
let client_id = friend_data["user_id"];
friends_list.forEach(friend => {
if (friend == this.id) {
this.isFriend = true;
return this.isFriend;
}
});
return this.isFriend;
}
} }
export {Profile} export {Profile}

View File

@ -24,7 +24,7 @@ class Profiles
let profiles = [] let profiles = []
response_data.forEach((profile) => { response_data.forEach((profile) => {
profiles.push(new Profile(this.client, profile.username, profile.user_id, profile.avatar_url)) profiles.push(new Profile(this.client, profile.username, profile.user_id, profile.avatar))
}); });
return profiles; return profiles;
} }
@ -32,7 +32,7 @@ class Profiles
/** /**
* *
* @param {String} username * @param {String} username
* @returns {?Profile} * @returns {?Promise<Profile>}
*/ */
async getProfile(username) async getProfile(username)
{ {
@ -42,6 +42,14 @@ class Profiles
return profile; return profile;
} }
async getProfileId(id)
{
let profile = new Profile(this.client, undefined, id);
if (await profile.init())
return null;
return profile;
}
/** /**
* Block a user * Block a user
* @param {Number} user_id * @param {Number} user_id
@ -51,7 +59,7 @@ class Profiles
// blocker & blocked // blocker & blocked
let response = await this.client._post("/api/profiles/block", { let response = await this.client._post("/api/profiles/block", {
users_id:[this.client.me.user_id, user_id], users_id:[this.client.me.id, user_id],
}); });
let data = await response.json(); let data = await response.json();
@ -68,7 +76,7 @@ class Profiles
// blocker & blocked // blocker & blocked
let response = await this.client._delete("/api/profiles/block", { let response = await this.client._delete("/api/profiles/block", {
users_id:[this.client.me.user_id, user_id], users_id:[this.client.me.id, user_id],
}); });
let data = await response.json(); let data = await response.json();

View File

@ -120,7 +120,7 @@ class Tourmanent
*/ */
async join(receive_func, disconnect_func) async join(receive_func, disconnect_func)
{ {
if (!await this.client.isAuthentificate()) if (!await this.client.isAuthenticated())
return null; return null;
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/tournaments/${this.id}`; let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/tournaments/${this.id}`;

View File

@ -1,10 +1,8 @@
import { Client } from "./api/client.js"; import { Client } from "./api/client.js";
import LoginView from "./views/accounts/LoginView.js";
import Dashboard from "./views/Dashboard.js"; import Dashboard from "./views/Dashboard.js";
import Search from "./views/Search.js"; import Search from "./views/Search.js";
import HomeView from "./views/HomeView.js"; import HomeView from "./views/HomeView.js";
import RegisterView from "./views/accounts/RegisterView.js";
import LogoutView from "./views/accounts/LogoutView.js"; import LogoutView from "./views/accounts/LogoutView.js";
import GameOfflineView from "./views/GameOfflineView.js"; import GameOfflineView from "./views/GameOfflineView.js";
//import GameView from "./views/GameView.js"; //import GameView from "./views/GameView.js";
@ -13,17 +11,19 @@ import GameView from "./views/GameView3D.js";
import PageNotFoundView from './views/PageNotFoundView.js' import PageNotFoundView from './views/PageNotFoundView.js'
import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js"; import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
import MeView from "./views/MeView.js"; import SettingsView from "./views/SettingsView.js";
import ProfilePageView from "./views/ProfilePageView.js"; import ProfilePageView from "./views/ProfilePageView.js";
import MatchMakingView from "./views/MatchMakingView.js"; import MatchMakingView from "./views/MatchMakingView.js";
import TournamentPageView from "./views/tournament/TournamentPageView.js"; import TournamentPageView from "./views/tournament/TournamentPageView.js";
import TournamentsView from "./views/tournament/TournamentsListView.js"; import TournamentsView from "./views/tournament/TournamentsListView.js";
import TournamentCreateView from "./views/tournament/TournamentCreateView.js"; import TournamentCreateView from "./views/tournament/TournamentCreateView.js";
import AuthenticationView from "./views/accounts/AuthenticationView.js";
let client = new Client(location.protocol + "//" + location.host) let client = new Client(location.origin);
let lang = client.lang;
let lastView = undefined let lastView = undefined;
let lastPageUrlBeforeLogin = undefined let lastPageUrlBeforeLogin = undefined;
const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$"); const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");
@ -37,11 +37,12 @@ const getParams = match => {
}; };
const navigateTo = async (uri) => { const navigateTo = async (uri) => {
if (await router(uri) !== 0)
return;
history.pushState(null, null, uri); history.pushState(null, null, uri);
if (await router(uri) !== 0)
return;
let link = document.querySelector('a[href=\'' + location.pathname + '\']'); let link = document.querySelector('a[href=\'' + location.pathname + '\']');
if (link) { if (link) {
document.querySelector('[data-link].active')?.classList.remove('active'); document.querySelector('[data-link].active')?.classList.remove('active');
@ -49,9 +50,14 @@ const navigateTo = async (uri) => {
} }
}; };
const reloadView = async _ => {
await lastView?.leavePage();
await renderView(lastView);
}
async function renderView(view) async function renderView(view)
{ {
let content = await view.getHtml(); let content = await view?.getHtml();
if (content == null) if (content == null)
return 1; return 1;
@ -74,12 +80,12 @@ const router = async(uri) => {
{ path: "/tournaments/create", view: TournamentCreateView }, { path: "/tournaments/create", view: TournamentCreateView },
{ path: "/tournaments/:id", view: TournamentPageView }, { path: "/tournaments/:id", view: TournamentPageView },
{ path: "/tournaments/", view: TournamentsView }, { path: "/tournaments/", view: TournamentsView },
{ path: "/login", view: LoginView }, { path: "/login", view: AuthenticationView },
{ path: "/register", view: AuthenticationView },
{ path: "/logout", view: LogoutView }, { path: "/logout", view: LogoutView },
{ path: "/register", view: RegisterView },
{ path: "/search", view: Search }, { path: "/search", view: Search },
{ path: "/home", view: HomeView }, { path: "/home", view: HomeView },
{ path: "/me", view: MeView }, { path: "/settings", view: SettingsView },
{ path: "/matchmaking", view: MatchMakingView }, { path: "/matchmaking", view: MatchMakingView },
{ path: "/games/offline", view: GameOfflineView }, { path: "/games/offline", view: GameOfflineView },
{ path: "/games/:id", view: GameView }, { path: "/games/:id", view: GameView },
@ -110,12 +116,13 @@ const router = async(uri) => {
const view = new match.route.view(getParams(match), lastPageUrlBeforeLogin); const view = new match.route.view(getParams(match), lastPageUrlBeforeLogin);
if (!(view instanceof AuthenticationView) && ! (view instanceof LogoutView))
lastPageUrlBeforeLogin = uri;
if (view instanceof AbstractRedirectView && await view.redirect()) if (view instanceof AbstractRedirectView && await view.redirect())
return 1; return 1;
lastView = view; lastView = view;
if (uri !== '/login' && uri !== '/register' && uri !== '/logout')
lastPageUrlBeforeLogin = uri;
if (await renderView(view)) if (await renderView(view))
return 1; return 1;
@ -138,13 +145,22 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
//Languages //Languages
await lang.waitLoading();
Array.from(document.getElementById('languageSelector').children).forEach(el => { Array.from(document.getElementById('languageSelector').children).forEach(el => {
el.onclick = _ => client.lang.changeLanguage(el.value); el.onclick = async _ => {
if (await lang.changeLanguage(el.value))
return;
console.log(lang);
document.querySelector('#languageSelector > .active')?.classList.remove('active');
el.classList.add('active');
};
}); });
document.querySelector(`#languageSelector > [value=${lang.chosenLang}]`)
?.classList.add('active');
await client.isAuthentificate(); await client.isAuthenticated();
router(location.pathname); router(location.pathname);
document.querySelector('a[href=\'' + location.pathname + '\']')?.classList.add('active'); document.querySelector('a[href=\'' + location.pathname + '\']')?.classList.add('active');
}); });
export { client, navigateTo } export { client, lang, navigateTo, reloadView }

View File

@ -0,0 +1,39 @@
{
"navbarSearch": "cherchbeh",
"navbarHome": "Quoicoubouse",
"navbarLogin": "Quoicouconnec",
"navbarRegister": "Quoicougistré",
"navbarProfile": "Mon crampté Profile",
"navbarSettings": "Roue Crampté",
"navbarLogout": "Déconnexion crampté",
"homeWindowTitle": "Quoicoubouse",
"homeTitle": "Quoicoubouse",
"homeOnline": "Jouer en crampté",
"homeOffline": "Jouer hors crampté",
"homeSettings": "Roue Crampté",
"homeLogout": "Déconnexion crampté",
"loginWindowTitle": "Quoicouconnec",
"loginFormTitle": "Quoicouconnec",
"loginFormUsername": "Nom d'crampté",
"loginFormPassword": "Mot de crampté",
"loginFormButton": "Quoicouconnec",
"loginNoAccount": "Pas de compte encore crampté?",
"loginRegister": "Quoicougistré",
"errorEmptyField": "Ce champ ne peut pas être vide crampté.",
"logoutWindowTitle": "Déconnexion crampté",
"registerWindowTitle": "Quoicougistré",
"registerFormTitle": "Quoicougistré",
"registerFormUsername": "Nom d'crampté",
"registerFormPassword": "Mot de crampté",
"registerFormButton": "Quoicougistré",
"registerAlreadyAccount": "Déjà un compte crampté?",
"registerLogin": "Quoicouconnec",
"404WindowTitle": "Pas crampté",
"SearchWindowTitle": "cherchbeh",
"profileAddFriend": "Demander le cramptéman",
"profileRemoveFriend": "Supprimer le cramptéman",
"profileDenyRequest": "Refuser le cramptéman",
"profileAcceptRequest": "Accepter le cramptéman",
"profileUnblock": "Quoicoudebloquer",
"profileBlock": "Quoicoubloquer"
}

View File

@ -5,5 +5,35 @@
"navbarRegister": "Register", "navbarRegister": "Register",
"navbarProfile": "My Profile", "navbarProfile": "My Profile",
"navbarSettings": "Settings", "navbarSettings": "Settings",
"navbarLogout": "Logout" "navbarLogout": "Logout",
"homeWindowTitle": "Home",
"homeTitle": "Home",
"homeOnline": "Play online",
"homeOffline": "Play offline",
"homeSettings": "Settings",
"homeLogout": "Logout",
"loginWindowTitle": "Login",
"loginFormTitle": "Login",
"loginFormUsername": "Username",
"loginFormPassword": "Password",
"loginFormButton": "Login",
"loginNoAccount": "No account yet?",
"loginRegister": "Register",
"errorEmptyField": "This field may not be blank.",
"logoutWindowTitle": "Logout",
"registerWindowTitle": "Register",
"registerFormTitle": "Register",
"registerFormUsername": "Username",
"registerFormPassword": "Password",
"registerFormButton": "Register",
"registerAlreadyAccount": "Already have an account?",
"registerLogin": "Login",
"404WindowTitle": "Not Found",
"SearchWindowTitle": "Search",
"profileAddFriend": "Ask Friend",
"profileRemoveFriend": "Remove Friend",
"profileDenyRequest": "Decline Friend",
"profileAcceptRequest": "Accept Friend",
"profileUnblock": "Unblock",
"profileBlock": "Block"
} }

View File

@ -1,9 +1,39 @@
{ {
"navbarSearch": "Recherche", "navbarSearch": "Recherche",
"navbarHome": "Maison", "navbarHome": "Maison",
"navbarLogin": "Se connecter", "navbarLogin": "Connexion",
"navbarRegister": "S'inscrire", "navbarRegister": "S'inscrire",
"navbarProfile": "Mon Profil", "navbarProfile": "Mon Profil",
"navbarSettings": "Paramètres", "navbarSettings": "Paramètres",
"navbarLogout": "Se déconnecter" "navbarLogout": "Déconnexion",
"homeWindowTitle": "Maison",
"homeTitle": "Maison",
"homeOnline": "Jouer en ligne",
"homeOffline": "Jouer hors ligne",
"homeSettings": "Paramètres",
"homeLogout": "Déconnexion",
"loginWindowTitle": "Connexion",
"loginFormTitle": "Connexion",
"loginFormUsername": "Nom d'utilisateur",
"loginFormPassword": "Mot de passe",
"loginFormButton": "Connexion",
"loginNoAccount": "Pas de compte?",
"loginRegister": "S'inscrire",
"errorEmptyField": "Ce champ ne peut être vide.",
"logoutWindowTitle": "Déconnexion",
"registerWindowTitle": "S'inscrire",
"registerFormTitle": "S'inscrire",
"registerFormUsername": "Nom d'utilisateur",
"registerFormPassword": "Mot de passe",
"registerFormButton": "S'inscrire",
"registerAlreadyAccount": "Déjà un compte?",
"registerLogin": "Connexion",
"404WindowTitle": "Pas trouvé",
"SearchWindowTitle": "Recherche",
"profileAddFriend": "Demander en ami",
"profileRemoveFriend": "Retirer l'ami",
"profileDenyRequest": "Refuser l'ami",
"profileAcceptRequest": "Accepter l'ami",
"profileUnblock": "Débloquer",
"profileBlock": "Bloquer"
} }

View File

@ -0,0 +1,40 @@
{
"navbarSearch": "Lukin",
"navbarHome": "Tomo",
"navbarLogin": "Open",
"navbarRegister": "Sitelen",
"navbarProfile": "Sitelen mi",
"navbarSettings": "Nasin",
"navbarLogout": "Tawa ala",
"homeWindowTitle": "Tomo",
"homeTitle": "Tomo",
"homeOnline": "Mute tawa",
"homeOffline": "Mute lon",
"homeSettings": "Nasin",
"homeLogout": "Tawa ala",
"loginWindowTitle": "Open",
"loginFormTitle": "Open",
"loginFormUsername": "nimi pi jan Open",
"loginFormPassword": "nimi nasa",
"loginFormButton": "Open",
"loginNoAccount": "sina wile ala wile jo e nimi pi jan Open?",
"loginRegister": "Sitelen",
"errorEmptyField": "nimi ni li wile sitelen.",
"logoutWindowTitle": "Tawa ala",
"registerWindowTitle": "Sitelen",
"registerFormTitle": "Sitelen",
"registerFormUsername": "nimi pi jan sin",
"registerFormPassword": "nimi nasa",
"registerFormButton": "Sitelen",
"registerAlreadyAccount": "sina jo ala jo e nimi pi jan sin?",
"registerLogin": "Open",
"404WindowTitle": "Ala o lukin e ni",
"SearchWindowTitle": "Lukin",
"profileAddFriend": "kama jo e jan",
"profileRemoveFriend": "tawa ala e jan",
"profileDenyRequest": "ante e ijo ni",
"profileAcceptRequest": "kama jo e ijo ni",
"profileUnblock": "Tawa ala e nimi pi jan ni",
"profileBlock": "Tawa e nimi pi jan ni"
}

View File

@ -19,6 +19,10 @@ export default class extends AbstractView {
document.getElementById('stopGameButton').onclick = this.stopGame.bind(this); document.getElementById('stopGameButton').onclick = this.stopGame.bind(this);
} }
async leavePage() {
this.game?.cleanup();
}
startGame() { startGame() {
if (this.game == null) { if (this.game == null) {
document.getElementById('startGameButton').innerHTML = 'Reset Game'; document.getElementById('startGameButton').innerHTML = 'Reset Game';
@ -26,6 +30,7 @@ export default class extends AbstractView {
} }
else { else {
document.getElementById('app').removeChild(this.game.canvas); document.getElementById('app').removeChild(this.game.canvas);
document.getElementById('app').removeChild(this.game.scoresDisplay);
this.game.cleanup(); this.game.cleanup();
this.game = new Game; this.game = new Game;
} }

View File

@ -13,7 +13,7 @@ export default class extends AbstractView
this.my_player = undefined; this.my_player = undefined;
} }
keyStretchHandler(event) keyReleaseHandler(event)
{ {
const idx = this.keys_pressed.indexOf(event.key); const idx = this.keys_pressed.indexOf(event.key);
if (idx != -1) if (idx != -1)
@ -26,7 +26,7 @@ export default class extends AbstractView
this.keys_pressed.push(event.key); this.keys_pressed.push(event.key);
} }
draw() render_game()
{ {
const canva = document.getElementById('canva'); const canva = document.getElementById('canva');
@ -40,36 +40,39 @@ export default class extends AbstractView
ctx.beginPath(); ctx.beginPath();
this.game.draw(ctx); this.game.render(ctx);
ctx.strokeStyle = "#000000"; ctx.strokeStyle = "#000000";
ctx.lineWidth = 10; ctx.lineWidth = 1;
ctx.stroke(); ctx.stroke();
} }
render_game() render()
{ {
let loop_id = setInterval(() => { let loop_id = setInterval(() => {
if (this.game === undefined) if (this.game === undefined)
clearInterval(loop_id); clearInterval(loop_id);
if (this.my_player) if (this.my_player)
this.my_player.update_paddle(this.keys_pressed); this.my_player.update_paddle(this.keys_pressed);
this.draw(); this.render_game();
this.game?.time.new_frame(); this.game?.time.new_frame();
//clearInterval(loop_id);
// 1 sec fps // 1 sec fps
}, 1000 / 60); }, 1000 / 60);
} }
register_key() register_key()
{ {
document.addEventListener('keydown', this.keyPressHandler.bind(this)); this.keyPressHandler = this.keyPressHandler.bind(this);
document.addEventListener('keyup', this.keyStretchHandler.bind(this)); this.keyReleaseHandler = this.keyReleaseHandler.bind(this);
document.addEventListener('keydown', this.keyPressHandler);
document.addEventListener('keyup', this.keyReleaseHandler);
} }
unregister_key() unregister_key()
{ {
document.removeEventListener('keydown', this.keyPressHandler); document.removeEventListener('keydown', this.keyPressHandler);
document.removeEventListener('keyup', this.keyStretchHandler); document.removeEventListener('keyup', this.keyReleaseHandler);
} }
async join_game() async join_game()
@ -99,7 +102,7 @@ export default class extends AbstractView
this.register_key() this.register_key()
this.render_game(); this.render();
} }
async update_game_state() async update_game_state()

View File

@ -1,18 +1,19 @@
import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js"; import { lang } from "../index.js";
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentificateView { export default class extends AbstractAuthenticatedView {
constructor(params) { constructor(params) {
super(params, "Home"); super(params, 'homeWindowTitle');
this.redirect_url = "/login" this.redirect_url = "/login"
} }
async getHtml() { async getHtml() {
return /* HTML */ ` return /* HTML */ `
<h1>HOME</h1> <h1>${lang.get('homeTitle', 'Home')}</h1>
<a href="/matchmaking" class="nav__link" data-link>Play a game</a> <a href="/matchmaking" data-link>${lang.get('homeOnline', 'Play online')}</a>
<a href="/games/offline" class="nav__link" data-link>Play offline</a> <a href="/games/offline" data-link>${lang.get('homeOffline', 'Play offline')}</a>
<a href="/me" class="nav__link" data-link>Me</a> <a href="/settings" data-link>${lang.get('homeSettings', 'Settings')}</a>
<a href="/logout" class="nav__link" data-link>Logout</a> <a href="/logout" data-link>${lang.get('homeLogout', 'Logout')}</a>
`; `;
} }
} }

View File

@ -1,8 +1,8 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from "../index.js";
import { clear, fill_errors } from "../utils/formUtils.js"; import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js"; import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentifiedView { export default class extends AbstractAuthenticatedView {
constructor(params) constructor(params)
{ {
super(params, "Matchmaking"); super(params, "Matchmaking");

View File

@ -1,13 +1,15 @@
import AbstractView from "./abstracts/AbstractView.js"; import AbstractView from "./abstracts/AbstractView.js";
import { lang } from '../index.js'
export default class extends AbstractView { export default class extends AbstractView {
constructor(params) { constructor(params) {
super(params, "Dashboard"); super(params, '404WindowTitle');
} }
async getHtml() { async getHtml() {
return ` return `
<h1>404 Bozo</h1> <h1>404 Bozo</h1>
<img src="https://media.giphy.com/media/pm0BKtuBFpdM4/giphy.gif">
<p>Git gud</p> <p>Git gud</p>
`; `;
} }

View File

@ -1,25 +1,26 @@
import AbstractView from "./abstracts/AbstractView.js"; import AbstractView from "./abstracts/AbstractView.js";
import { client } from "../index.js" import { client, lang } from "../index.js"
export default class extends AbstractView { export default class extends AbstractView {
constructor(params) { constructor(params) {
super(params, params.username); super(params, decodeURI(params.username));
this.username = params.username; this.username = decodeURI(params.username);
} }
async postInit() async postInit()
{ {
this.profile = await client.profiles.getProfile(this.username); this.profile = await client.profiles.getProfile(this.username);
if (this.profile === null) if (this.profile === null)
return 404; return 404;
this.userId = this.profile.id; this.user_id = this.profile.id;
this.info = document.getElementById("info"); this.info = document.getElementById("info");
// Username // Username
let username = document.createElement("a"); let username = document.createElement("a");
username.id = "username"; username.id = "username";
username.appendChild(document.createTextNode(this.profile.username)); username.appendChild(document.createTextNode(this.username));
this.info.appendChild(username); this.info.appendChild(username);
this.info.appendChild(document.createElement("br")); this.info.appendChild(document.createElement("br"));
@ -31,30 +32,97 @@ export default class extends AbstractView {
this.info.appendChild(avatar); this.info.appendChild(avatar);
await this.blockButton(); await this.blockButton();
await this.friendButton();
client.notice.rewrite_profile = async () => {
let result = await this.profile.getFriend();
await this.profile.getBlock()
await this.friendButton();
}
} }
async blockButton() { async blockButton() {
// Block option // Block option
if (await client.isAuthentificate() === false) if (await client.isAuthenticated() === false)
return; return;
if (client.me.id != this.userId) { if (client.me.id != this.user_id) {
let block = document.getElementById("block") || document.createElement("a"); let block = document.getElementById("block");
if (block == undefined) {
block = document.createElement("p");
this.info.appendChild(block);
}
block.id = "block"; block.id = "block";
block.innerText = "";
block.onclick = async () => { block.onclick = async () => {
if (!this.profile.isBlocked) if (!this.profile.isBlocked)
await client.profiles.block(this.userId); await client.profiles.block(this.user_id);
else else
await client.profiles.deblock(this.userId); await client.profiles.deblock(this.user_id);
this.profile = await client.profiles.getProfile(this.username); this.profile = await client.profiles.getProfile(this.username);
this.blockButton(); this.blockButton();
}; };
if (this.profile.isBlocked) if (this.profile.isBlocked)
block.appendChild(document.createTextNode("Deblock")); block.textContent = lang.get('profileUnblock', 'Unblock');
else else
block.appendChild(document.createTextNode("Block")); block.textContent = lang.get('profileBlock', 'Block');
this.info.appendChild(block); }
}
async friendButton() {
if (await client.isAuthenticated() === false)
return;
if (client.me.id != this.user_id) {
let yes = document.getElementById("yes") || document.createElement("p");
let no = document.getElementById("no") || document.createElement("p");
let friend = document.getElementById("friend") || document.createElement("p");
if (client.notice.data["asker"].includes(this.user_id)) {
if (friend)
friend.remove();
yes.id = "yes";
yes.textContent = lang.get('profileAcceptRequest', 'Accept Friend');
yes.onclick = async () => {
client.notice.accept_friend(this.user_id);
}
no.id = "no";
no.textContent = lang.get('profileDenyRequest', 'Decline Friend');
no.onclick = async () => {
client.notice.refuse_friend(this.user_id);
}
this.info.appendChild(yes);
this.info.appendChild(document.createTextNode(" "));
this.info.appendChild(no);
}
else {
if (yes && no)
yes.remove(); no.remove();
friend.id = "friend"
friend.onclick = async () => {
if (this.profile.isFriend)
await client.notice.remove_friend(this.user_id);
else
await client.notice.ask_friend(this.user_id);
await client.profiles.getProfile(this.username);
this.friendButton();
};
if (this.profile.isFriend)
friend.textContent = lang.get('profileRemoveFriend', 'Remove Friend');
else {
friend.textContent = lang.get('profileAddFriend', 'Ask Friend');
}
this.info.appendChild(friend);
}
} }
} }

View File

@ -1,27 +1,27 @@
import AbstractView from "./abstracts/AbstractView.js"; import AbstractView from "./abstracts/AbstractView.js";
import {client} from "../index.js"; import { client, lang } from "../index.js";
import {Message} from "../api/chat/message.js" import {Message} from "../api/chat/message.js"
export default class extends AbstractView { export default class extends AbstractView {
constructor(params) { constructor(params) {
super(params, "Search"); super(params, 'SearchWindowTitle');
} }
async wait_get_online_users() { async wait_get_online_users() {
return new Promise((resolve) => { return new Promise((resolve) => {
const checkInterval = setInterval(() => { const checkInterval = setInterval(() => {
//console.log(client.notice.data["online"].length); console.log(client.notice.data["online"]);
if (client.notice.data["online"].length > 0) { if (Object.keys(client.notice.data["online"]).length > 0) {
clearInterval(checkInterval); clearInterval(checkInterval);
resolve(); resolve();
} }
}, 100); }, 1);
}); });
} }
async postInit() { async postInit() {
let logged = await client.isAuthentificate(); let logged = await client.isAuthenticated();
let profiles = await client.profiles.all(); let profiles = await client.profiles.all();
//console.log(client.notice.data); //console.log(client.notice.data);
@ -29,10 +29,12 @@ export default class extends AbstractView {
return console.log("Error"); return console.log("Error");
//await client.notice.getOnlineUser(); //await client.notice.getOnlineUser();
await this.wait_get_online_users(); //await this.wait_get_online_users();
client.notice.rewrite_usernames = this.rewrite_usernames; client.notice.rewrite_usernames = this.rewrite_usernames;
client.notice.rewrite_invite = this.display_invite;
let search = document.getElementById("input_user"); let search = document.getElementById("input_user");
if (search != undefined)
search.oninput = () => this.display_users(logged, profiles); search.oninput = () => this.display_users(logged, profiles);
let chat_input = document.getElementById("input_chat"); let chat_input = document.getElementById("input_chat");
@ -66,7 +68,12 @@ export default class extends AbstractView {
username.setAttribute('data-link', ''); username.setAttribute('data-link', '');
username.id = `username${user.id}` username.id = `username${user.id}`
username.href = `/profiles/${user.username}`; username.href = `/profiles/${user.username}`;
username.style.color = client.notice.data["online"].includes(user.id.toString()) ? "green" : "red"; if (logged && user.id == client.me.id)
username.style.color = "green";
else {
let online = client.notice.data["online"][user.id];
username.style.color = online !== undefined ? online : "gray";
}
username.appendChild(document.createTextNode(user.username)); username.appendChild(document.createTextNode(user.username));
new_user.appendChild(username); new_user.appendChild(username);
@ -135,8 +142,14 @@ export default class extends AbstractView {
profiles.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach((user) => { profiles.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach((user) => {
let username = document.getElementById(`username${user.id}`); let username = document.getElementById(`username${user.id}`);
if (username !== null) if (username !== null) {
username.style.color = client.notice.data["online"].includes(user.id.toString()) ? "green" : "red"; if (user.id == client.me.id)
username.style.color = "green";
else {
let online = client.notice.data["online"][user.id];
username.style.color = online !== undefined ? online : "gray";
}
}
}); });
} }
@ -175,13 +188,15 @@ export default class extends AbstractView {
chat_input.maxLength=255; chat_input.maxLength=255;
chat.appendChild(chat_input); chat.appendChild(chat_input);
let members_id = client.channels.channel.members_id;
chat_input.onkeydown = async () => { chat_input.onkeydown = async () => {
if (event.keyCode == 13 && client.channels.channel != undefined) { if (event.keyCode == 13 && client.channels.channel != undefined) {
//let chat_input = document.getElementById("input_chat"); //let chat_input = document.getElementById("input_chat");
let chat_text = chat_input.value; let chat_text = chat_input.value;
let receivers_id = []; let receivers_id = [];
client.channels.channel.members_id.forEach((member_id) => { members_id.forEach((member_id) => {
if (member_id != client.me.id) if (member_id != client.me.id)
receivers_id.push(profiles.filter(user => user.id == member_id)[0].id); receivers_id.push(profiles.filter(user => user.id == member_id)[0].id);
}); });
@ -196,7 +211,7 @@ export default class extends AbstractView {
// Scroll to the bottom of messages // Scroll to the bottom of messages
messages.scrollTop = messages.scrollHeight; messages.scrollTop = messages.scrollHeight;
this.display_invite(chat); this.display_invite();
} }
@ -242,33 +257,86 @@ export default class extends AbstractView {
} }
async display_members(chat, profiles) { async display_members(chat, profiles) {
let members_id = client.channels.channel.members_id;
let members = document.createElement("h2"); let members = document.createElement("h2");
members.id = "members"; members.id = "members";
let usernames = ""; let usernames = "";
client.channels.channel.members_id.forEach((member_id) => { members_id.forEach((member_id) => {
if (member_id != client.me.id) { if (member_id != client.me.id) {
if (usernames.length > 0) if (usernames.length > 0)
usernames += ", "; usernames += ", ";
usernames += (profiles.filter(user => user.id == member_id)[0].username); usernames += (profiles.filter(user => user.id == member_id)[0].username);
} }
}); });
members.appendChild(document.createTextNode(usernames)); members.textContent = usernames;
chat.appendChild(members); chat.appendChild(members);
return members return members
} }
async display_invite(chat, profiles) { async display_invite() {
let chat = document.getElementById("chat");
if (chat == undefined)
return ;
let members_id = client.channels.channel.members_id;
let others = members_id.filter(id => id !== client.me.id);
let invite = document.getElementById("invite") || document.createElement("button");
let yes = document.getElementById("yes") || document.createElement("button");
let no = document.getElementById("no") || document.createElement("button");
let invitedBy = undefined;
for (let x in others) {
if (client.notice.data["invited"].includes(others[x])) {
invitedBy = others[x];
}
}
if (invitedBy == undefined) {
if (yes && no) {
yes.remove();
no.remove();
}
// Button to send invite to play // Button to send invite to play
let invite = document.getElementById("invite") || document.createElement("button");
invite.id = "invite"; invite.id = "invite";
invite.style.background = "orange";
invite.innerText = "invite"; invite.innerText = "invite";
invite.title = "Invite to play a game"
invite.onclick = async () => { invite.onclick = async () => {
await client.notice.sendInvite(client.me.id, await client.notice.send_invite(others);
client.channels.channel.members_id.filter(id => id !== client.me.id));
}; };
chat.appendChild(invite); chat.appendChild(invite);
}
else {
if (invite)
invite.remove()
yes.id = "yes";
yes.style.background = "green";
yes.title = "Accept to play a game"
yes.onclick = async () => {
await client.notice.accept_invite(invitedBy);
};
no.id = "no";
no.style.background = "red";
no.title = "Refuse to play a game"
no.onclick = async () => {
await client.notice.refuse_invite(invitedBy);
};
chat.appendChild(yes);
chat.appendChild(no);
}
} }

View File

@ -1,12 +1,13 @@
import { client, navigateTo } from "../index.js"; import { client, navigateTo } from "../index.js";
import { clear, fill_errors } from "../utils/formUtils.js"; import { clear, fill_errors } from "../utils/formUtils.js";
import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js"; import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentificateView export default class extends AbstractAuthenticatedView
{ {
constructor(params) constructor(params)
{ {
super(params, "Me"); super(params, "Settings");
this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB
} }
async postInit() async postInit()
@ -24,7 +25,7 @@ export default class extends AbstractAuthentificateView
document.getElementById("avatar").remove(); document.getElementById("avatar").remove();
let avatar = document.createElement("img"); let avatar = document.createElement("img");
avatar.id = "avatar"; avatar.id = "avatar";
avatar.src = profile.avatar_url; avatar.src = profile.avatar_url + '?t=' +new Date().getTime();
document.getElementsByClassName("avatar")[0].appendChild(avatar); document.getElementsByClassName("avatar")[0].appendChild(avatar);
} }
} }
@ -35,7 +36,7 @@ export default class extends AbstractAuthentificateView
let response_data = await client.account.delete(current_password); let response_data = await client.account.delete(current_password);
console.log(await client.isAuthentificate()) console.log(await client.isAuthenticated())
if (response_data === null || response_data === "user deleted") if (response_data === null || response_data === "user deleted")
{ {
navigateTo("/login"); navigateTo("/login");
@ -78,18 +79,24 @@ export default class extends AbstractAuthentificateView
if (avatar.files[0] !== undefined) if (avatar.files[0] !== undefined)
{ {
if (avatar.files[0].size > this.PROFILE_PICTURE_MAX_SIZE) {
document.getElementById("save-profile").classList.add('text-danger');
document.getElementById("save-profile").innerHTML = "Image too large :/";
return;
}
let form_data = new FormData(); let form_data = new FormData();
form_data.append("file", avatar.files[0]); form_data.append("avatar", avatar.files[0]);
await client.me.change_avatar(form_data); await client.me.change_avatar(form_data);
this.display_avatar(); this.display_avatar();
} }
document.getElementById("save-profile").classList.remove('text-danger');
document.getElementById("save-profile").innerHTML = "Saved"; document.getElementById("save-profile").innerHTML = "Saved";
} }
async getHtml() async getHtml()
{ {
return /* HTML */ ` return /* HTML */ `
<link rel="stylesheet" href="/static/css/me.css"> <link rel="stylesheet" href="/static/css/settings.css">
<h1>ME</h1> <h1>ME</h1>
<div id="main"> <div id="main">
<div class="avatar"> <div class="avatar">

View File

@ -2,13 +2,13 @@ import { client, navigateTo } from "../../index.js";
import AbstractRedirectView from "./AbstractRedirectView.js"; import AbstractRedirectView from "./AbstractRedirectView.js";
export default class extends AbstractRedirectView{ export default class extends AbstractRedirectView{
constructor(params, title) { constructor(params, titleKey, uri = "/login") {
super(params, title, "/login"); super(params, titleKey, uri);
} }
async redirect() async redirect()
{ {
if (await client.isAuthentificate() === false) if (await client.isAuthenticated() === false)
{ {
navigateTo(this.redirect_url); navigateTo(this.redirect_url);
return 1; return 1;

View File

@ -2,13 +2,13 @@ import { client, navigateTo } from "../../index.js";
import AbstractRedirectView from "./AbstractRedirectView.js"; import AbstractRedirectView from "./AbstractRedirectView.js";
export default class extends AbstractRedirectView{ export default class extends AbstractRedirectView{
constructor(params, title, url) { constructor(params, titleKey, uri = "/home") {
super(params, title, url); super(params, titleKey, uri);
} }
async redirect() async redirect()
{ {
if (await client.isAuthentificate() === false) if (await client.isAuthenticated() === false)
return 0; return 0;
navigateTo(this.redirect_url); navigateTo(this.redirect_url);
return 1; return 1;

View File

@ -2,14 +2,14 @@ import { navigateTo } from "../../index.js";
import AbstractView from "./AbstractView.js"; import AbstractView from "./AbstractView.js";
export default class extends AbstractView{ export default class extends AbstractView{
constructor(params, title, url) constructor(params, titleKey, uri)
{ {
super(params, title); super(params, titleKey);
this.redirect_url = url; this.redirect_url = uri;
} }
async redirect() async redirect()
{ {
navigateTo(url); navigateTo(this.redirect_url);
} }
} }

View File

@ -1,7 +1,9 @@
import {lang} from '../../index.js'
export default class { export default class {
constructor(params, title) { constructor(params, titleKey) {
this.params = params; this.params = params;
this.title = title; this.titleKey = titleKey;
} }
async postInit() { async postInit() {
@ -11,7 +13,7 @@ export default class {
} }
setTitle() { setTitle() {
document.title = this.title; document.title = lang.get(this.titleKey, 'Bozo Pong');
} }
async getHtml() { async getHtml() {

View File

@ -0,0 +1,177 @@
import { client, lang, navigateTo } from "../../index.js";
import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthenticatedView from "../abstracts/AbstractNonAuthenticatedView.js";
export default class extends AbstractNonAuthenticatedView
{
constructor(params, lastUrlBeforeLogin = '/home')
{
super(params, 'loginWindowTitle', lastUrlBeforeLogin);
this.redirect_url = lastUrlBeforeLogin;
this.current_mode = undefined
}
async leavePage()
{
this.current_mode = undefined;
}
/**
* @returns {Promise}
*/
async postInit()
{
let element = document.getElementById("toggle-register-login");
element.onclick = this.toggle_register_login.bind(this);
let new_mode = location.pathname.slice(1);
this.update_mode(new_mode);
document.getElementById("button").onclick = this.authentication.bind(this);
let username_input = document.getElementById('username-input'),
password_input = document.getElementById('password-input');
[username_input, password_input].forEach(input => {
input.addEventListener('keydown', async ev => {
if (ev.key === 'Enter')
await this.authentication.bind(this)()
});
});
username_input.focus();
}
/**
* Check if field is normal
* @param username {String}
* @param password {String}
* @returns {Boolean}
*/
basic_verif(username, password)
{
if (username === '')
document.getElementById('username').innerHTML = lang.get('errorEmptyField', 'This field may not be blank.');
if (password === '')
document.getElementById('password').innerHTML = lang.get('errorEmptyField', 'This field may not be blank.');
if (username === '' || password === '')
return false;
return true
}
/**
* @returns { undefined }
*/
toggle_register_login(event)
{
event.preventDefault();
let new_mode = this.current_mode === "register" ? "login" : "register";
this.update_mode(new_mode);
}
/**
* @param {String} new_mode
*/
update_mode(new_mode)
{
if (new_mode === this.current_mode)
return;
this.current_mode = new_mode;
let title = document.getElementById("title"),
username_label = document.getElementById("username-label"),
password_label = document.getElementById("password-label"),
toggle_register_login = document.getElementById("toggle-register-login"),
toggle_register_login_label = document.getElementById("toggle-register-login-label"),
button = document.getElementById("button")
;
let title_text = this.current_mode === "register" ? "registerFormTitle" : "loginFormTitle";
title.innerText = lang.get(title_text, "ERROR LANG");
let username_label_text = this.current_mode === "register" ? "registerFormUsername" : "loginFormUsername";
username_label.innerText = lang.get(username_label_text, "ERROR LANG");
let password_label_text = this.current_mode === "register" ? "registerFormPassword" : "loginFormPassword";
password_label.innerText = lang.get(password_label_text, "ERROR LANG");
let toggle_register_login_label_text = this.current_mode === "register" ? "registerAlreadyAccount" : "loginNoAccount";
toggle_register_login_label.innerText = lang.get(toggle_register_login_label_text, "ERROR LANG");;
let toggle_register_login_text = this.current_mode === "register" ? "registerLogin" : "loginRegister";
toggle_register_login.innerText = lang.get(toggle_register_login_text, "ERROR LANG");
let button_text = this.current_mode === "register" ? "registerFormButton" : "loginFormButton";
button.innerText = lang.get(button_text, "ERROR LANG");
this.titleKey = this.current_mode === 'register' ? 'registerWindowTitle' : 'loginWindowTitle';
this.setTitle();
}
/**
* @returns {Promise}
*/
async authentication()
{
let username = document.getElementById("username-input").value,
password = document.getElementById("password-input").value;
if (!this.basic_verif())
return;
let response;
if (this.current_mode === "register")
response = await client.account.create(username, password)
else
response = await client.login(username, password);
if (response.status === 200 || response.status === 201)
{
navigateTo(this.redirect_url);
return;
}
let response_data = await response.json()
console.log(response_data);
clear("innerHTML", ["username", "password", 'login']);
fill_errors(response_data, "innerHTML");
}
async getHtml()
{
return /* HTML */ `
<div class='container-fluid'>
<form class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<h4 class='text-center fw-semibold mb-4' id="title">Loading...</h4>
<div class='form-floating mb-2'>
<input type='text' class='form-control' id='username-input' placeholder='Username'>
<label for='usernameInput' id='username-label'>Loading...</label>
<span class='text-danger' id='username'></span>
</div>
<div class='form-floating'>
<input type='password' class='form-control' id='password-input' placeholder='Password'>
<label for='password-input' id='password-label'>Loading...</label>
<span class='text-danger' id='password'></span>
</div>
<div class='d-flex'>
<button type='button' class='btn btn-primary mt-3 mb-2' id='button'>Loading...</button>
<span class='text-danger my-auto mx-2' id='login'></span>
<div class='ms-auto mt-auto flex-row d-flex gap-2' id="toggle">
<p id='toggle-register-login-label'>Loading...</p>
<a id="toggle-register-login" href='#'>Loading...</a>
</div>
</div>
</form>
</div>
`;
}
}

View File

@ -1,79 +0,0 @@
import { client, navigateTo } from "../../index.js";
import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js";
async function login(redirectTo = '/home')
{
clear('innerHTML', ['username', 'password', 'login']);
let username = document.getElementById('usernameInput').value;
let password = document.getElementById('passwordInput').value;
if (username === '') {
document.getElementById('username').innerHTML = 'This field may not be blank.';
}
if (password === '') {
document.getElementById('password').innerHTML = 'This field may not be blank.';
}
if (username === '' || password === '')
return;
let response = await client.login(username, password);
if (response.status == 200) {
await client.notice.disconnect();
await client.notice.connect();
navigateTo(redirectTo);
} else {
let error = await response.json();
fill_errors(error, "innerHTML");
}
}
export default class extends AbstractNonAuthentifiedView {
constructor(params, lastUrlBeforeLogin = '/home') {
super(params, "Login", lastUrlBeforeLogin);
this.redirectTo = lastUrlBeforeLogin;
}
async postInit()
{
let usernameField = document.getElementById('usernameInput');
usernameField.addEventListener('keydown', ev => {
if (ev.key === 'Enter')
login(this.redirectTo);
});
usernameField.focus();
let passwordField = document.getElementById('passwordInput');
passwordField.addEventListener('keydown', ev => {
if (ev.key === 'Enter')
login(this.redirectTo);
});
document.getElementById('loginButton').onclick = _ => login(this.redirectTo);
}
async getHtml() {
return `
<div class='container-fluid'>
<form class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<h4 class='text-center fw-semibold mb-4'>Login</h4>
<div class='form-floating mb-2'>
<input type='text' class='form-control' id='usernameInput' placeholder='Username'>
<label for='usernameInput'>Username</label>
<span class='text-danger' id='username'></span>
</div>
<div class='form-floating'>
<input type='password' class='form-control' id='passwordInput' placeholder='Password'>
<label for='passwordInput'>Password</label>
<span class='text-danger' id='password'></span>
</div>
<div class='d-flex'>
<button type='button' class='btn btn-primary mt-3 mb-2' id='loginButton'>Login</button>
<span class='text-danger my-auto mx-2' id='login'></span>
<p class='ms-auto mt-auto'>No account yet? <a href='/register' data-link>Register</a></p>
</div>
</form>
</div>
`;
}
}

View File

@ -1,10 +1,10 @@
import { client, navigateTo } from "../../index.js"; import { client, navigateTo } from "../../index.js";
import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentifiedView export default class extends AbstractAuthenticatedView
{ {
constructor(params, lastPageUrl = '/login') { constructor(params, lastPageUrl = '/login') {
super(params, "Logout"); super(params, 'logoutWindowTitle', lastPageUrl);
this.lastPageUrl = lastPageUrl; this.lastPageUrl = lastPageUrl;
} }

View File

@ -1,77 +0,0 @@
import { client, navigateTo } from "../../index.js";
import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js";
async function register(redirectTo = '/home')
{
let username = document.getElementById("usernameInput").value;
let password = document.getElementById("passwordInput").value;
if (username === '' || password === '') {
clear("innerHTML", ["username", "password"]);
if (username === '')
document.getElementById('username').innerHTML = 'This field may not be blank.';
if (password === '')
document.getElementById('password').innerHTML = 'This field may not be blank.';
return;
}
let response_data = await client.account.create(username, password);
if (response_data == null)
{
navigateTo(redirectTo);
return;
}
clear("innerHTML", ["username", "password", 'register']);
fill_errors(response_data, "innerHTML");
}
export default class extends AbstractNonAuthentifiedView {
constructor(params, lastUrlBeforeLogin = '/home') {
super(params, "Register", lastUrlBeforeLogin);
this.redirectTo = lastUrlBeforeLogin;
}
async postInit()
{
let usernameField = document.getElementById('usernameInput');
usernameField.addEventListener('keydown', ev => {
if (ev.key === 'Enter')
register(this.redirectTo);
});
usernameField.focus();
let passwordField = document.getElementById('passwordInput');
passwordField.addEventListener('keydown', ev => {
if (ev.key === 'Enter')
register(this.redirectTo);
});
document.getElementById("registerButton").onclick = _ => register(this.redirectTo);
}
async getHtml() {
return `
<div class='container-fluid'>
<form class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
<h4 class='text-center fw-semibold mb-4'>Register</h4>
<div class='form-floating mb-2'>
<input type='text' class='form-control' id='usernameInput' placeholder='Username'>
<label for='usernameInput'>Username</label>
<span class='text-danger' id='username'></span>
</div>
<div class='form-floating'>
<input type='password' class='form-control' id='passwordInput' placeholder='Password'>
<label for='passwordInput'>Password</label>
<span class='text-danger' id='password'></span>
</div>
<div class='d-flex'>
<button type='button' class='btn btn-primary mt-3 mb-2' id='registerButton'>Register</button>
<span class='text-danger my-auto mx-2' id='register'></span>
<p class='ms-auto mt-auto'>Already have an account? <a href='/login' data-link>Login</a></p>
</div>
</form>
</div>
`;
}
}

View File

@ -1,8 +1,8 @@
import {client, navigateTo} from "../../index.js"; import {client, navigateTo} from "../../index.js";
import { clear, fill_errors } from "../../utils/formUtils.js"; import { clear, fill_errors } from "../../utils/formUtils.js";
import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentifiedView export default class extends AbstractAuthenticatedView
{ {
constructor(params) constructor(params)
{ {

View File

@ -1,7 +1,7 @@
import {client, navigateTo} from "../../index.js"; import {client, navigateTo} from "../../index.js";
import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentifiedView export default class extends AbstractAuthenticatedView
{ {
constructor(params) constructor(params)
{ {

View File

@ -1,7 +1,7 @@
import {client} from "../../index.js"; import {client} from "../../index.js";
import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js";
export default class extends AbstractAuthentifiedView export default class extends AbstractAuthenticatedView
{ {
constructor(params) constructor(params)
{ {

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bozo Pong</title> <title>Bozo Pong</title>
<link rel="stylesheet" href="{% static 'css/bootstrap/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'css/bootstrap/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/index.css' %}">
</head> </head>
<body data-bs-theme="dark"> <body data-bs-theme="dark">
<nav class="navbar navbar-expand-lg bg-body-tertiary rounded m-2"> <nav class="navbar navbar-expand-lg bg-body-tertiary rounded m-2">
@ -17,12 +18,14 @@
</div> </div>
<div class="navbar-nav justify-content-end d-flex flex-row gap-2"> <div class="navbar-nav justify-content-end d-flex flex-row gap-2">
<div class='nav-item dropdown-center me-2'> <div class='nav-item dropdown-center me-2'>
<a class='nav-link dropdown-toggle' role='button' data-bs-toggle='dropdown'> <a id='languageDisplay' class='nav-link dropdown-toggle' role='button' data-bs-toggle='dropdown'>
Lang. Lang.
</a> </a>
<div id='languageSelector' class='dropdown-menu dropdown-menu-end position-absolute text-center px-2' style='min-width: 65px'> <div id='languageSelector' class='dropdown-menu dropdown-menu-end position-absolute text-center px-2' style='min-width: 65px'>
<button value='en' type='button' class="dropdow-item nav-link text-center py-1">&#127468;&#127463; EN</a> <button value='en' type='button' class="dropdown-item nav-link text-center py-1">🇬🇧 EN</button>
<button value='fr' type='button' class="dropdow-item nav-link text-center py-1">&#127467;&#127479; FR</a> <button value='fr' type='button' class="dropdown-item nav-link text-center py-1">🇫🇷 FR</button>
<button value='tp' type='button' class="dropdown-item nav-link text-center py-1">🗣️👍🖼️❤️ toki pona</button>
<button value='cr' type='button' class="dropdown-item nav-link text-center py-1" style="background-image: url(https://image.noelshack.com/fichiers/2023/06/1/1675686620-full.png); background-size: cover; background-position: center; background-repeat: no-repeat; background-color: transparent;">Crampté</button>
</div> </div>
</div> </div>
<div id='navbarLoggedOut' class='d-flex flex-row gap-2'> <div id='navbarLoggedOut' class='d-flex flex-row gap-2'>
@ -35,7 +38,7 @@
</a> </a>
<div class='dropdown-menu dropdown-menu-end position-absolute text-end px-2' style='min-width: 100px'> <div class='dropdown-menu dropdown-menu-end position-absolute text-end px-2' style='min-width: 100px'>
<a data-i18n='navbarProfile' id='myProfileLink' href='' class="dropdow-item nav-link pt-1" data-link>My Profile</a> <a data-i18n='navbarProfile' id='myProfileLink' href='' class="dropdow-item nav-link pt-1" data-link>My Profile</a>
<a data-i18n='navbarSettings' href="/me" class="dropdow-item nav-link pt-0" data-link>Settings</a> <a data-i18n='navbarSettings' href="/settings" class="dropdow-item nav-link pt-0" data-link>Settings</a>
<hr class='dropdown-separator my-auto mx-1'></hr> <hr class='dropdown-separator my-auto mx-1'></hr>
<a data-i18n='navbarLogout' href="/logout" class="dropdow-item nav-link pb-1" data-link>Logout</a> <a data-i18n='navbarLogout' href="/logout" class="dropdow-item nav-link pb-1" data-link>Logout</a>
</div> </div>

View File

@ -1,3 +1,24 @@
from django.test import TestCase import json
from os import listdir
from django.test import Client, TestCase
from django.contrib.staticfiles import finders
# Create your tests here. # Create your tests here.
class DictionnariesTest(TestCase):
def setUp(self) -> None:
self.client = Client();
self.directory = finders.find('js/lang/');
def test_lang(self):
keys = None
json_files = listdir(self.directory);
for file in json_files:
with open(f'{self.directory}/{file}') as f:
data: dict = json.load(f);
if (keys is None):
keys = set(data.keys());
else:
self.assertEqual(set(data.keys()), keys);

View File

@ -12,7 +12,7 @@ MAP_CENTER_Y = MAP_SIZE_Y / 2
WALL_RATIO = 1 WALL_RATIO = 1
BALL_SPEED_INC = 1 BALL_SPEED_INC = 1
BALL_SPEED_START = 1 BALL_SPEED_START = 170
BALL_SIZE = 4 BALL_SIZE = 4
BALL_SPAWN_POS_X = MAP_SIZE_X / 2 BALL_SPAWN_POS_X = MAP_SIZE_X / 2
BALL_SPAWN_POS_Y = MAP_SIZE_Y / 2 BALL_SPAWN_POS_Y = MAP_SIZE_Y / 2

View File

@ -1,23 +1,36 @@
from __future__ import annotations
from .. import config from .. import config
from .Point import Point
import math
class Ball: class Ball:
def __init__(self) -> None: def __init__(self) -> None:
self.postion_x: float = config.BALL_SPAWN_POS_X self.size: float
self.postion_y: float = config.BALL_SPAWN_POS_Y self.position: Point
self.velocity_x: float = config.BALL_SPEED_START self.angle: float
self.velocity_y: float = config.BALL_SPEED_START self.speed: float
self.size: float = config.BALL_SIZE
def to_dict(self): self.reset()
def reset(self) -> None:
self.size = config.BALL_SIZE
self.position = Point(config.BALL_SPAWN_POS_X + self.size / 2, config.BALL_SPAWN_POS_Y + self.size / 2)
self.angle = math.pi * 1
self.speed = config.BALL_SPEED_START
def to_dict(self) -> dict:
data: dict = { data: dict = {
"size": self.size, "size": self.size,
"position_x": self.postion_x, "speed": self.speed,
"position_y": self.postion_y, "position": self.position.to_dict(),
"velocity_x": self.velocity_x, "angle": self.angle,
"velocity_y": self.velocity_y,
} }
return data return data
def __str__(self) -> str:
return f"Ball(size: {self.size}, speed: {self.speed}, angle: {self.angle}, position: {self.position})"

View File

@ -44,8 +44,8 @@ class Game(AbstractRoom):
angle: float = (i * 2 * math.pi / nb_sides) + (math.pi * 3 / nb_sides) angle: float = (i * 2 * math.pi / nb_sides) + (math.pi * 3 / nb_sides)
x: float = config.MAP_CENTER_X + radius * math.cos(angle) x: float = round(config.MAP_CENTER_X + radius * math.cos(angle))
y: float = config.MAP_CENTER_Y + radius * math.sin(angle) y: float = round(config.MAP_CENTER_Y + radius * math.sin(angle))
polygon.append(Point(x, y)) polygon.append(Point(x, y))
@ -107,19 +107,17 @@ class Game(AbstractRoom):
def _player_join(self, user_id: int, socket: WebsocketConsumer): def _player_join(self, user_id: int, socket: WebsocketConsumer):
# check if player is already connected
player = self.get_player_by_user_id(user_id) player = self.get_player_by_user_id(user_id)
if (player is None): if (player is None):
return None return None
# check if player is already connected
if (player.is_connected()): if (player.is_connected()):
player.disconnect(1001) player.disconnect(1001)
player.socket = socket player.socket = socket
if (self.everbody_is_here()): if (self.everbody_is_here()):
print("chie moi dessu")
print("start")
self.start() self.start()
self._update_player(player) self._update_player(player)

View File

@ -1,12 +1,29 @@
from __future__ import annotations
from math import dist
class Point: class Point:
def __init__(self, x: float, y: float) -> None: def __init__(self, x: float, y: float) -> None:
self.x = x self.x = x
self.y = y self.y = y
def to_dict(self): def __str__(self) -> str:
return f"Point(x: {self.x}, y: {self.y})"
def __repr__(self) -> str:
return f"Point(x: {self.x}, y: {self.x})"
def __eq__(self, __value: object) -> bool:
return (self.x == __value.x and self.y == __value.y)
def distance(self, point: Point):
return dist((point.x, point.y), (self.x, self.y))
def copy(self):
return Point(self.x, self.y)
def to_dict(self) -> dict:
data: dict[str: float] = { data: dict[str: float] = {
"x": self.x, "x": self.x,

View File

@ -1,5 +1,8 @@
from .Point import Point from .Point import Point
from .Vector import Vector
import math
class Segment: class Segment:
@ -7,7 +10,25 @@ class Segment:
self.start: Point = start self.start: Point = start
self.stop: Point = stop self.stop: Point = stop
def to_dict(self): def angle(self):
return math.atan2((self.start.x - self.start.y), (self.stop.x - self.stop.y))
def length(self):
return self.start.distance(self.stop)
def is_on(self, C: Point):
return (self.start.distance(C) + self.stop.distance(C) == self.length())
def __repr__(self) -> str:
return f"Segment(start: {self.start}, stop: {self.stop})"
def __str__(self) -> str:
return f"Segment(start: {self.start}, stop: {self.stop})"
def copy(self):
return Segment(self.start.copy(), self.stop.copy())
def to_dict(self) -> dict:
data: dict[str: dict] = { data: dict[str: dict] = {
"start": self.start.to_dict(), "start": self.start.to_dict(),

35
games/objects/Vector.py Normal file
View File

@ -0,0 +1,35 @@
from __future__ import annotations
import math
from .Point import Point
class Vector:
def __init__(self, x: float, y: float) -> None:
self.norm: float = math.dist((0, 0), (x, y))
self.x: float = x
self.y: float = y
def __truediv__(self, denominator: float):
return Vector(self.x / denominator, self.y / denominator)
def angle(self, vector: Vector):
scalar_product: float = self.scalar(vector)
if (scalar_product is None):
return None
cos: float = scalar_product / (vector.norm * self.norm)
angle: float = math.acos(cos)
return angle
def scalar(self, vector: Vector):
return self.x * vector.x + vector.y * self.y
def __str__(self) -> str:
return f"Vector(x: {self.x}, y: {self.y}, norme: {self.norm})"
def __eq__(self, __value: Vector) -> bool:
return (self.x == __value.x and
self.x == __value.x and
self.norm == __value.norm)

View File

@ -9,73 +9,259 @@ if TYPE_CHECKING:
from .objects.Ball import Ball from .objects.Ball import Ball
from .objects.Point import Point from .objects.Point import Point
from .objects.Vector import Point
from .objects.Segment import Segment from .objects.Segment import Segment
from .objects.Vector import Vector
from . import config
from . import config from . import config
import math import math
import asyncio
from asgiref.sync import SyncToAsync
from time import sleep from time import sleep
#see the video to understand the algorithme VERTICALLY = 1
#https://www.youtube.com/watch?v=KOYoMYWUTEo NORMAL = 2
def determine_director_coefficient(segment: Segment):
return ((segment.start.y - segment.stop.y) / (segment.start.x - segment.stop.x))
def determine_ordinate_at_origin(point: Point, director_cofficient: float): def get_sign(num: float) -> int:
return point.y - point.x * director_cofficient if (num == 0):
return 0
if (num > 0):
return 1
if (num < 0):
return -1
def determine_intersection(director_coefficient1: float, ordinate_at_origin1: float, director_coefficient2: float, ordinate_at_origin2: float): def get_derive(segment: Segment) -> float:
if (director_coefficient1 == director_coefficient2):
if (segment.start.x == segment.stop.x):
return None return None
return (ordinate_at_origin1 + ordinate_at_origin2) / (director_coefficient1 + director_coefficient2)
def determine_intersections(ball: Ball, segments: list[Segment]): return (segment.stop.y - segment.start.y) / (segment.stop.x - segment.start.x)
intersections: list[Point] = [] def get_intercept(derive: float, point: Point) -> float:
if (derive is None):
return None
return point.y - (point.x * derive)
def get_constant(segment: Segment) -> float:
return segment.start.x
def identify(segment: Segment) -> str:
if (segment.start.x == segment.stop.x):
return VERTICALLY
return NORMAL
def get_interception(segment1: Segment, segment2: Segment):
if (identify(segment1) == VERTICALLY and identify(segment2) == VERTICALLY):
return None
# because of in matematics world y = 10 is above y = 5 and on a display it is inverted I invert the coordonate
inverted_segment1 = Segment(Point(segment1.start.x, config.MAP_SIZE_Y - segment1.start.y), Point(segment1.stop.x, config.MAP_SIZE_Y - segment1.stop.y))
inverted_segment2 = Segment(Point(segment2.start.x, config.MAP_SIZE_Y - segment2.start.y), Point(segment2.stop.x, config.MAP_SIZE_Y - segment2.stop.y))
if (identify(segment1) == NORMAL and identify(segment2) == NORMAL):
# representation m * x + p
m1 = get_derive(inverted_segment1)
m2 = get_derive(inverted_segment2)
p1 = get_intercept(m1, inverted_segment1.start)
p2 = get_intercept(m2, inverted_segment2.start)
# m1 * x + p1 = m2 * x + p2
# m1 * x = m2 * x + p2 -p1
# m1 * x - m2 * x = p1 - p2
# x * (m1 - m2) = p1 - p2
# x = (p1 - p2) / (m1 - m2)
if (m1 == m2):
return None
# reinvert
x: float = (p1 - p2) / (m1 - m2) * (-1)
y: float = config.MAP_SIZE_Y - (m1 * x + p1)
else:
if (identify(inverted_segment1) == VERTICALLY):
constant: float = get_constant(inverted_segment1)
m: float = get_derive(inverted_segment2)
p: float = get_intercept(m, inverted_segment2.start)
else:
constant: float = get_constant(inverted_segment2)
m: float = get_derive(inverted_segment1)
p: float = get_intercept(m, inverted_segment1.start)
x: float = constant
y: float = config.MAP_SIZE_Y - (m * x + p)
impact: Point = Point(x, y)
return impact
def get_impact_point(segments: list[Segment], ball: Ball) -> dict:
cos: float = round(math.cos(ball.angle), 6)
sin: float = round(math.sin(ball.angle), 6)
point: Point = Point(ball.position.x + cos, ball.position.y - sin)
ball_segment = Segment(ball.position, point)
closest: dict = None
for segment in segments: for segment in segments:
# form m * x + p impact: Point = get_interception(segment, ball_segment)
m: float = determine_director_coefficient(segment)
p: float = determine_ordinate_at_origin(segment.start, m)
x: float = determine_intersection(m, p, ball.velocity_y, 0) if (impact is None):
if (x is None):
continue continue
y: float = m * x + p diff_x: float = ball.position.x - impact.x
if (get_sign(diff_x) == get_sign(cos) and cos != 0):
continue
intersections.append(Point(x, y)) diff_y: float = (ball.position.y - impact.y)
if (get_sign(diff_y) != get_sign(sin) and sin != 0):
continue
return intersections impact_with_padding: Point = impact.copy()
def determine_distance_between_ball_and_wall(ball: Ball, segments: list[Segment]): impact_with_padding.x += (ball.size / 2) * get_sign(cos) * (-1)
impact_with_padding.y += (ball.size / 2) * get_sign(sin)
intersections: list[Point] = determine_intersections(ball, segments) if (closest is None or impact_with_padding.distance(ball.position) < closest.get("distance")):
closest = {
"impact_with_padding": impact_with_padding,
"impact": impact,
"segment": segment,
"distance": impact_with_padding.distance(ball.position),
}
distances = list(map(math.dist, intersections)) return closest
return min(distances) def wall_collision(ball_angle: float, wall_angle: float) -> float:
def render(ball: Ball, game: Game): ball_cos: float = math.cos(ball_angle)
ball_sin: float = math.sin(ball_angle)
segments: list[Segment] = [player.rail for player in game.players] incident_angle: float = ball_angle - wall_angle
reflection_angle: float = wall_angle - incident_angle
new_cos: float = math.cos(reflection_angle)
new_sin: float = math.sin(reflection_angle)
new_angle: float = math.atan2(new_sin, new_cos)
return new_angle
def paddle_collision(ball: Ball, impact: Point, player: Player):
diff_x: float = player.rail.stop.x - player.rail.start.x
diff_y: float = player.rail.stop.y - player.rail.start.y
paddle_center_x: float = player.rail.start.x + diff_x * player.position.position
paddle_center_y: float = player.rail.start.y + diff_y * player.position.position
paddle_center: Point = Point(paddle_center_x, paddle_center_y)
rail_length: float = player.rail.length()
paddle_length: float = rail_length * config.PADDLE_RATIO;
start_x: float = paddle_center.x - (diff_x * (paddle_length / 2 / rail_length))
start_y: float = paddle_center.y - (diff_y * (paddle_length / 2 / rail_length))
stop_x: float = paddle_center.x + (diff_x * (paddle_length / 2 / rail_length))
stop_y: float = paddle_center.y + (diff_y * (paddle_length / 2 / rail_length))
start: Point = Point(start_x, start_y)
stop: Point = Point(stop_x, stop_y)
paddle: Segment = Segment(start, stop)
if (not paddle.is_on(impact)):
print(not paddle.is_on(impact))
return None
return ball.angle + math.pi
def collision(game: Game, impact_data: dict) -> bool:
segment: Segment = impact_data.get("segment")
player_hitted = None
for player in game.players:
if (not player.is_connected()):
continue
if (player.rail is segment):
player_hitted = player
break
surface_angle: float = math.atan2(segment.start.y - segment.stop.y, segment.start.x - segment.stop.y)
angle: float
if (player_hitted is None):
angle = wall_collision(game.ball.angle, surface_angle)
else:
angle = paddle_collision(game.ball, impact_data.get("impact"), player_hitted)
if (angle is None):
return False
game.ball.angle = angle
return True
async def update_ball(game: Game, impact_data: dict) -> None:
distance: float = impact_data.get("distance")
time_before_impact: float = distance / game.ball.speed
await asyncio.sleep(time_before_impact)
hit: bool = collision(game, impact_data)
if (hit == False):
await asyncio.sleep(0.1)
print("Goal")
game.ball.reset()
else:
game.ball.position = impact_data.get("impact_with_padding")
await SyncToAsync(game.broadcast)("update_ball", game.ball.to_dict())
async def render(game: Game):
while True:
segments: list[Segment] = [player.rail for player in game.players] + [wall.rail for wall in game.walls]
impact_data: dict = get_impact_point(segments, game.ball)
await update_ball(game, impact_data)
print(determine_distance_between_ball_and_wall(ball))
def routine(game: Game): def routine(game: Game):
asyncio.run(render(game))
while True: while True:
for player in game._updated_players: for player in game._updated_players:
game.broadcast("update_paddle", player.to_dict(), [player]) game.broadcast("update_paddle", player.to_dict(), [player])
game._updated_players.clear() game._updated_players.clear()
if (game.started):
game.ball.postion_x = game.ball.postion_x + game.ball.velocity_x
game.ball.postion_y = game.ball.postion_y + game.ball.velocity_y
game.broadcast("update_ball", game.ball.to_dict())
sleep(1 / config.SERVER_TPS) sleep(1 / config.SERVER_TPS)

View File

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from .models import ProfileModel, BlockModel from .models import ProfileModel, BlockModel, FriendModel, AskFriendModel
# Register your models here. # Register your models here.
admin.site.register(ProfileModel) admin.site.register(ProfileModel)
admin.site.register(BlockModel) admin.site.register(BlockModel)
admin.site.register(FriendModel)
admin.site.register(AskFriendModel)

View File

@ -14,7 +14,7 @@ def upload_to(instance, filename: str):
# Create your models here. # Create your models here.
class ProfileModel(models.Model): class ProfileModel(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
avatar_url = models.ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif") #blank=True, null=True) avatar = models.ImageField(upload_to=upload_to, default="./profiles/static/avatars/default.avif")
def get_game(self) -> int: def get_game(self) -> int:
for game in game_manager._game_list: for game in game_manager._game_list:
@ -35,3 +35,67 @@ class BlockModel(models.Model):
def __str__(self): def __str__(self):
return "blocker_id: " + str(self.blocker) + ", blocked_id: " + str(self.blocked) return "blocker_id: " + str(self.blocker) + ", blocked_id: " + str(self.blocked)
class AskFriendModel(models.Model):
asker = IntegerField(primary_key=False)
asked = IntegerField(primary_key=False)
def getAsked(self, asker):
askeds = []
for ask in AskFriendModel.objects.filter(asker=asker):
askeds.append(ask.asked)
return askeds
def getAsker(self, asked):
askers = []
for ask in AskFriendModel.objects.filter(asked=asked):
askers.append(ask.asker)
return askers
def deleteAsk(self, asker, asked):
deleted = AskFriendModel.objects.filter(asker=asker, asked=asked)
if (deleted.count() == 0 or not deleted):
return False
deleted.delete()
return True
class FriendModel(models.Model):
user_id1 = IntegerField(primary_key=False)
user_id2 = IntegerField(primary_key=False)
def getFriends(self, user_id):
friends = []
for friend in FriendModel.objects.filter(user_id1=user_id):
friends.append(friend.user_id2)
for friend in FriendModel.objects.filter(user_id2=user_id):
friends.append(friend.user_id1)
return friends
def isFriend(self, user_id1, user_id2):
return user_id2 in self.getFriends(user_id1)
def deleteFriend(self, user_id1, user_id2):
first = FriendModel.objects.filter(user_id1=user_id1, user_id2=user_id2)
if (first.count() == 1):
first.delete()
return True
second = FriendModel.objects.filter(user_id1=user_id2, user_id2=user_id1)
if (second.count() == 1):
second.delete()
return True
return False

View File

@ -1,11 +1,20 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ProfileModel from .models import ProfileModel
from django.conf import settings
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
username = serializers.ReadOnlyField(source='user.username') username = serializers.ReadOnlyField(source='user.username')
avatar_url = serializers.ImageField(required=False) avatar = serializers.ImageField(required=False)
class Meta: class Meta:
model = ProfileModel model = ProfileModel
fields = ["username", "avatar_url", "user_id"] fields = ["username", "avatar", "user_id"]
def validate_avatar(self, value):
'''
Check that the image is not too large
'''
if value.size > settings.PROFILE_PICTURE_MAX_SIZE:
raise serializers.ValidationError('Image is too large.');
return value;

View File

@ -7,7 +7,7 @@ class ProfileTest(TestCase):
def setUp(self): def setUp(self):
self.user: User = User.objects.create(username='bozo', password='password') self.user: User = User.objects.create(username='bozo', password='password')
self.user.save() self.user.save()
self.expected_response = {'avatar_url': '/static/avatars/default.avif', 'user_id': 1, 'username': 'bozo'} self.expected_response = {'avatar': '/static/avatars/default.avif', 'user_id': 1, 'username': 'bozo'}
self.url = "/api/profiles/" self.url = "/api/profiles/"

View File

@ -1,15 +1,16 @@
from django.urls import path from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import viewsets from . import viewsets
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("me", viewsets.MyProfileViewSet.as_view({'patch': 'partial_update', 'get': 'retrieve'}), name="my_profile_page"), path("settings", viewsets.MyProfileViewSet.as_view({'patch': 'partial_update'}), name="my_profile_page"),
path("me", viewsets.MyProfileViewSet.as_view({'get': 'retrieve'}), name="my_profile_page"),
path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"), path("", viewsets.ProfileViewSet.as_view({'get': 'list'}), name="profiles_list"),
path("block", views.BlocksView.as_view(), name="block_page"), path("block", views.BlocksView.as_view(), name="block_page"),
path("block/<int:pk>", views.BlockView.as_view(), name="block_page"), path("block/<int:pk>", views.BlockView.as_view(), name="block_page"),
path("<str:username>", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"), path("friend", views.FriendsView.as_view(), name="friend_page"),
path("user/<str:username>", viewsets.ProfileViewSet.as_view({'get': 'retrieve'}), name="profile_page"),
path("id/<int:pk>", viewsets.ProfileViewSet.as_view({'get': 'retrieve_id'}), name="profile_page"),
] + static("/static/avatars/", document_root="./avatars") ]

View File

@ -4,7 +4,7 @@ from rest_framework import authentication, permissions, status
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from django.core import serializers from django.core import serializers
from .models import BlockModel from .models import BlockModel, FriendModel
class BlockView(APIView): class BlockView(APIView):
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
@ -25,7 +25,7 @@ class BlocksView(APIView):
def get(self, request): def get(self, request):
blocks = BlockModel.objects.filter(blocker=request.user.pk) blocks = BlockModel.objects.filter(blocker=request.user.pk)
if (blocks): if (blocks):
return Response(serializers.serialize("json", BlockModel.objects.filter(blocker=request.user.pk)), status=status.HTTP_200_OK) return Response({"blockeds": serializers.serialize("json", BlockModel.objects.filter(blocker=request.user.pk)), "user_id": request.user.pk}, status=status.HTTP_200_OK)
return Response({}, status=status.HTTP_204_NO_CONTENT) return Response({}, status=status.HTTP_204_NO_CONTENT)
def post(self, request): def post(self, request):
@ -33,7 +33,10 @@ class BlocksView(APIView):
users_id = request.data.get("users_id", None) users_id = request.data.get("users_id", None)
if (users_id == None): if (users_id == None):
return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST) return Response({"Error send None"}, status=status.HTTP_400_BAD_REQUEST)
if (users_id[0] == None or users_id[1] == None):
return Response({"Error send blocker/ed None"}, status=status.HTTP_400_BAD_REQUEST)
if (BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])): if (BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])):
return Response({"Already Exist"}, status=status.HTTP_409_CONFLICT) return Response({"Already Exist"}, status=status.HTTP_409_CONFLICT)
@ -52,9 +55,11 @@ class BlocksView(APIView):
if (users_id == None): if (users_id == None):
return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST) return Response({"Error"}, status=status.HTTP_400_BAD_REQUEST)
if (users_id[0] == None or users_id[1] == None):
return Response({"Error send blocker/ed None"}, status=status.HTTP_400_BAD_REQUEST)
block = BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1]) block = BlockModel.objects.filter(blocker=users_id[0], blocked=users_id[1])
print(list(block))
if (block.count() > 1): if (block.count() > 1):
return Response("Not normal >:[", status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response("Not normal >:[", status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@ -63,3 +68,13 @@ class BlocksView(APIView):
block.delete() block.delete()
return Response("Deleted", status=status.HTTP_200_OK) return Response("Deleted", status=status.HTTP_200_OK)
class FriendsView(APIView):
permission_classes = (permissions.IsAuthenticated,)
authentication_classes = (SessionAuthentication,)
def get(self, request):
friends = FriendModel().getFriends(request.user.pk)
if (friends):
return Response({"friends": friends, "user_id": request.user.pk}, status=status.HTTP_200_OK)
return Response({}, status=status.HTTP_204_NO_CONTENT)

View File

@ -23,14 +23,23 @@ class ProfileViewSet(viewsets.ModelViewSet):
if (not user): if (not user):
return Response({"detail": "Profile not found."}, status=status.HTTP_404_NOT_FOUND) return Response({"detail": "Profile not found."}, status=status.HTTP_404_NOT_FOUND)
instance = self.queryset().get(pk=user[0].pk) instance = self.queryset().get(pk=user[0].pk)
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:] instance.avatar.name = instance.avatar.name[instance.avatar.name.find("static") - 1:]
return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK)
def retrieve_id(self, request: HttpRequest, pk=None):
user = User.objects.filter(pk=pk)
if (not user):
return Response({"detail": "Profile not found."}, status=status.HTTP_404_NOT_FOUND)
instance = self.queryset().get(pk=user[0].pk)
instance.avatar.name = instance.avatar.name[instance.avatar.name.find("static") - 1:]
return Response(self.serializer_class(instance).data, return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
def list(self, request: HttpRequest): def list(self, request: HttpRequest):
serializer = ProfileSerializer(self.queryset(), many=True) serializer = ProfileSerializer(self.queryset(), many=True)
for profile in serializer.data: for profile in serializer.data:
profile["avatar_url"] = profile["avatar_url"][profile["avatar_url"].find("static") - 1:] profile["avatar"] = profile["avatar"][profile["avatar"].find("static") - 1:]
return Response(serializer.data) return Response(serializer.data)
class MyProfileViewSet(viewsets.ModelViewSet): class MyProfileViewSet(viewsets.ModelViewSet):
@ -45,16 +54,18 @@ class MyProfileViewSet(viewsets.ModelViewSet):
return obj return obj
def perform_update(self, serializer, pk=None): def perform_update(self, serializer, pk=None):
profile: ProfileModel = self.get_object() serializer.is_valid(raise_exception=True);
avatar = self.request.data.get("file", None) profile: ProfileModel = self.get_object();
avatar = serializer.validated_data.get('avatar');
if (avatar is not None): if (avatar is not None):
if (profile.avatar_url.name != "./profiles/static/avatars/default.avif"): if (profile.avatar.name != "./profiles/static/avatars/default.avif"):
profile.avatar_url.storage.delete(profile.avatar_url.name) profile.avatar.storage.delete(profile.avatar.name)
profile.avatar_url = avatar profile.avatar = avatar
profile.save() profile.save()
def retrieve(self, request: HttpRequest, pk=None): def retrieve(self, request: HttpRequest, pk=None):
instance: ProfileModel = self.get_object() instance: ProfileModel = self.get_object()
instance.avatar_url.name = instance.avatar_url.name[instance.avatar_url.name.find("static") - 1:] instance.avatar.name = instance.avatar.name[instance.avatar.name.find("static") - 1:]
return Response(self.serializer_class(instance).data, return Response(self.serializer_class(instance).data,
status=status.HTTP_200_OK) status=status.HTTP_200_OK)

1
run.sh
View File

@ -8,4 +8,5 @@ python manage.py makemigrations profiles
python manage.py makemigrations chat python manage.py makemigrations chat
python manage.py makemigrations tournament python manage.py makemigrations tournament
python manage.py migrate python manage.py migrate
python manage.py compilemessages
python manage.py runserver 0.0.0.0:8000 python manage.py runserver 0.0.0.0:8000

View File

@ -15,7 +15,7 @@ class TournamentSerializer(serializers.ModelSerializer):
fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id"] fields = ["name", "nb_players", "nb_players_by_game", "level", "started", "finished", "levels", "id"]
def get_levels(self, instance: TournamentModel): def get_levels(self, instance: TournamentModel):
levels: [[int]] = [] levels: list[list[int]] = []
for i in range(instance.level): for i in range(instance.level):
games_id: [int] = instance.get_games_id_by_level(i) games_id: [int] = instance.get_games_id_by_level(i)
if (games_id == []): if (games_id == []):

View File

@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
import os import os
from pathlib import Path from pathlib import Path
from django.utils.translation import gettext_lazy as _
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -79,6 +80,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
] ]
ROOT_URLCONF = 'transcendence.urls' ROOT_URLCONF = 'transcendence.urls'
@ -137,6 +139,11 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en', _('English')),
('fr', _('French')),
]
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'
USE_I18N = True USE_I18N = True
@ -153,3 +160,7 @@ STATIC_URL = 'static/'
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Profile picture upload limit
PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024 # 2MB