Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2bca72cd4 | |||
| cc4b7c9efc | |||
| aae651aa8b | |||
| ec560f3447 | |||
| 6df0f24ef6 | |||
| d8b97ebe17 | |||
| 98d30c85b2 | |||
| ec36271886 | |||
| 029c8a6650 | |||
| 0a6e9a25ed | |||
| e764d565c1 | |||
| b0fc705d26 | |||
| cb1fc01ad6 | |||
| 5299f3d1af | |||
| 27704b97f8 | |||
| 2eaae81f28 | |||
| 6f5d27f6a2 | |||
| 938d4cf3b5 | |||
| 167896aedd |
@@ -0,0 +1,10 @@
|
||||
POSTGRES_PASSWORD=coucou
|
||||
JWT_SECRET=superlongsecretkeyatleast32characterspleasenevercommitthis
|
||||
POSTGRES_DB=database
|
||||
POSTGRES_HOST=database
|
||||
POSTGRES_USER=user
|
||||
GITHUB_CLIENT_ID=Ov23li6ovg3fzec5IO5D
|
||||
GITHUB_CLIENT_SECRET=0345e959e8f0e9f784061c5c90ee227ddb2ef9ab
|
||||
GITHUB_CALLBACK_URL=http://localhost:8080/api/auth/github/callback
|
||||
|
||||
pogpog
|
||||
@@ -1,16 +1,24 @@
|
||||
all : up
|
||||
all :
|
||||
@$(call random_shmol_cat, "hELLO", "nice human corrector", $(CLS), )
|
||||
@docker compose -f ./docker-compose.yml up -d
|
||||
|
||||
up :
|
||||
no_cache :
|
||||
@docker compose -f ./docker-compose.yml build --no-cache
|
||||
@docker compose -f ./docker-compose.yml up -d
|
||||
|
||||
clean :
|
||||
@$(call print_cat, $(CLEAR), $(C_225), $(C_320), $(C_450), $(call pad_word, 10, "Objects"), $(call pad_word, 12, "Exterminated"));
|
||||
@docker compose -f ./docker-compose.yml down -t 1
|
||||
|
||||
fclean :
|
||||
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "All⠀clean"), $(call pad_word, 12, "Miaster"));
|
||||
@docker compose -f ./docker-compose.yml down -v -t 1
|
||||
@docker system prune -af --volumes
|
||||
|
||||
re : fclean up
|
||||
re : fclean no_cache
|
||||
@$(call print_cat, $(CLEAR), $(C_120), $(C_300), $(C_210), $(call pad_word, 10, "Re-Doing"), $(call pad_word, 12, "Miaster"));
|
||||
|
||||
.PHONY : all no_cache clean fclean re
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
volumes:
|
||||
pgdata:
|
||||
data:
|
||||
|
||||
networks:
|
||||
transcendence:
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql
|
||||
- data:/var/lib/postgresql/data/pg15/
|
||||
env_file:
|
||||
- ../.env
|
||||
networks:
|
||||
@@ -24,6 +24,8 @@ services:
|
||||
build: ./srcs/backend
|
||||
expose:
|
||||
- "3001"
|
||||
# ports:
|
||||
# - "3001:3001"
|
||||
depends_on:
|
||||
- database
|
||||
volumes:
|
||||
@@ -38,7 +40,7 @@ services:
|
||||
container_name: frontend
|
||||
build: ./srcs/frontend/
|
||||
ports:
|
||||
- "8443:443"
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
|
||||
@@ -127,7 +127,7 @@ async function createTables()
|
||||
status VARCHAR(20) DEFAULT 'waiting',
|
||||
max_players INT DEFAULT 8,
|
||||
current_round INT DEFAULT 0,
|
||||
max_rounds INT DEFAULT 5,
|
||||
max_rounds INT DEFAULT 3,
|
||||
round_duration INT DEFAULT 90,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
started_at TIMESTAMP,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
RUN mkdir -p /etc/backend/.ssl
|
||||
RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout /etc/backend/.ssl/key.pem \
|
||||
-out /etc/backend/.ssl/cert.pem \
|
||||
-subj "/CN=localhost" \
|
||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import express from 'express';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import cors from 'cors';
|
||||
import {Server} from 'socket.io';
|
||||
import authRouter from './routes/auth.js';
|
||||
@@ -14,11 +13,7 @@ import setupSocketIO from './services/socket.js';
|
||||
import avatarService from './services/avatar.js';
|
||||
|
||||
const app = express();
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('/etc/backend/.ssl/key.pem'),
|
||||
cert: fs.readFileSync('/etc/backend/.ssl/cert.pem')
|
||||
};
|
||||
const server = https.createServer(httpsOptions, app);
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server,
|
||||
{
|
||||
cors:
|
||||
|
||||
@@ -26,17 +26,6 @@ router.post('/login', async(req, res) =>
|
||||
res.status(result.status).json(result.data);
|
||||
});
|
||||
|
||||
router.post('/logout', async(req, res) =>
|
||||
{
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
if (!token)
|
||||
return (res.status(401).json({error: 'Missing token'}));
|
||||
|
||||
const result = await authService.logout(token);
|
||||
res.status(result.status).json(result.data);
|
||||
});
|
||||
|
||||
router.get('/github', (req, res) => {
|
||||
const githubAuthUrl = `https://github.com/login/oauth/authorize?` +
|
||||
`client_id=${process.env.GITHUB_CLIENT_ID}&` +
|
||||
|
||||
@@ -25,7 +25,7 @@ router.post('/upload', authenticateToken, upload.single('avatar'), async(req, re
|
||||
res.status(result.status).json(result.data);
|
||||
});
|
||||
|
||||
router.delete('/delete', authenticateToken, async(req, res) =>
|
||||
router.delete('/', authenticateToken, async(req, res) =>
|
||||
{
|
||||
const result = await avatarService.deleteAvatar(req.user.userId);
|
||||
res.status(result.status).json(result.data);
|
||||
|
||||
@@ -2,30 +2,6 @@ import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import {query} from '../db.js';
|
||||
|
||||
async function logout(token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!token)
|
||||
return ({status: 400, data: {error: 'Missing token'}});
|
||||
try
|
||||
{
|
||||
jwt.verify(token, process.env.JWT_SECRET);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ({status: 401, data: {error: 'Invalid token'}});
|
||||
}
|
||||
|
||||
return ({status: 200, data: {message: 'Logged out'}});
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.error(err);
|
||||
return ({status: 500, data: {error: 'Server error'}});
|
||||
}
|
||||
}
|
||||
|
||||
async function login(username, password)
|
||||
{
|
||||
try
|
||||
@@ -84,4 +60,4 @@ async function register(username, password)
|
||||
}
|
||||
};
|
||||
|
||||
export default {register, login, logout};
|
||||
export default {register, login};
|
||||
|
||||
@@ -69,9 +69,6 @@ async function deleteAvatar(userId) {
|
||||
if (currentAvatar === null)
|
||||
return ({status: 404, data: {error: 'User not found'}});
|
||||
|
||||
if (currentAvatar === DEFAULT_AVATAR)
|
||||
return ({status: 400, data: {error: 'Cannot delete default avatar'}});
|
||||
|
||||
// Reset the avatar to the default one
|
||||
await setAvatar(DEFAULT_AVATAR, userId);
|
||||
|
||||
|
||||
@@ -30,63 +30,6 @@ async function broadcastRoomsList(io) {
|
||||
}
|
||||
}
|
||||
|
||||
function startRoomTimer(io, roomId, seconds)
|
||||
{
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
if (gameState.timerInterval)
|
||||
clearInterval(gameState.timerInterval);
|
||||
|
||||
gameState.timerSeconds = seconds;
|
||||
|
||||
gameState.timerInterval = setInterval(() => {
|
||||
gameState.timerSeconds--;
|
||||
|
||||
if (gameState.timerSeconds < 0)
|
||||
gameState.timerSeconds = 0;
|
||||
|
||||
if (gameState.timerSeconds <= 0)
|
||||
{
|
||||
io.to(roomId).emit('game-timer-sync', {
|
||||
remaining: 0
|
||||
});
|
||||
clearInterval(gameState.timerInterval);
|
||||
gameState.timerInterval = null;
|
||||
io.to(roomId).emit('game-timer-ended', { message: 'Temps écoulé !' });
|
||||
|
||||
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
|
||||
const nextDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
gameState.drawer = nextDrawer;
|
||||
|
||||
|
||||
gameState.currentWord = '';
|
||||
gameState.revealedLetters = [];
|
||||
gameState.revealedWord = [];
|
||||
gameState.guessedLetters = [];
|
||||
gameState.wrongGuesses = 0;
|
||||
|
||||
io.to(roomId).emit('game-new-round', {
|
||||
drawer: nextDrawer
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
io.to(roomId).emit('game-timer-sync', {
|
||||
remaining: gameState.timerSeconds
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopRoomTimer(roomId)
|
||||
{
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState || !gameState.timerInterval) return;
|
||||
clearInterval(gameState.timerInterval);
|
||||
gameState.timerInterval = null;
|
||||
}
|
||||
|
||||
// Check if a playing game has only 1 player left and auto-stop it
|
||||
async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||
if (!dbRoomId) return;
|
||||
@@ -100,7 +43,6 @@ async function checkAndStopSinglePlayerGame(io, roomId, dbRoomId) {
|
||||
const players = await gameRoomService.getRoomPlayers(dbRoomId);
|
||||
if (players.length <= 1) {
|
||||
console.log(`Room ${dbRoomId} has only ${players.length} player(s) left, ending game`);
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
// Update room status to 'ended'
|
||||
await gameRoomService.updateRoomStatus(dbRoomId, 'waiting');
|
||||
@@ -250,9 +192,7 @@ function setupSocketIO(io)
|
||||
revealedLetters: gameState.revealedLetters,
|
||||
revealedWord: gameState.revealedWord || [],
|
||||
guessedLetters: gameState.guessedLetters,
|
||||
players: gameState.players,
|
||||
scores: gameState.scores || {},
|
||||
timer: gameState.timerSeconds || 0
|
||||
players: gameState.players
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -262,15 +202,6 @@ function setupSocketIO(io)
|
||||
if (socket.gameRoomId) {
|
||||
const roomId = socket.gameRoomId;
|
||||
const dbRoomId = socket.gameRoomDbId;
|
||||
const userId = socket.user.userId;
|
||||
|
||||
if (dbRoomId && userId) {
|
||||
try {
|
||||
await gameRoomService.leaveRoom(dbRoomId, userId);
|
||||
} catch (err) {
|
||||
console.error('Error removing player from room on socket leave:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
socket.to(roomId).emit('game-player-left', {
|
||||
username: socket.user.username,
|
||||
@@ -337,8 +268,7 @@ function setupSocketIO(io)
|
||||
revealedWord: gameState.revealedWord || [],
|
||||
guessedLetters: gameState.guessedLetters,
|
||||
players: gameState.players,
|
||||
scores: gameState.scores || {},
|
||||
timer: gameState.timerSeconds || 0
|
||||
scores: gameState.scores || {}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -460,7 +390,6 @@ function setupSocketIO(io)
|
||||
const gameState = gameRooms.get(roomId);
|
||||
if (!gameState) return;
|
||||
|
||||
startRoomTimer(io, roomId, 60);
|
||||
gameState.currentWord = data.word.toLowerCase();
|
||||
gameState.revealedLetters = new Array(data.word.length).fill(false);
|
||||
gameState.revealedWord = new Array(data.word.length).fill('_');
|
||||
@@ -623,8 +552,6 @@ function setupSocketIO(io)
|
||||
// Update round start scores for next round
|
||||
gameState.roundStartScores = { ...gameState.scores };
|
||||
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
io.to(roomId).emit('game-word-found', {
|
||||
word: gameState.currentWord,
|
||||
winner: username,
|
||||
@@ -686,7 +613,6 @@ function setupSocketIO(io)
|
||||
// If the drawer left and there are still enough players, choose a new drawer
|
||||
if (wasDrawer && gameState.players.length >= 1)
|
||||
{
|
||||
stopRoomTimer(roomId);
|
||||
// Pick the next player as the new drawer
|
||||
gameState.currentPlayerIndex = gameState.currentPlayerIndex % gameState.players.length;
|
||||
const newDrawer = gameState.players[gameState.currentPlayerIndex];
|
||||
@@ -706,7 +632,6 @@ function setupSocketIO(io)
|
||||
reason: 'drawer_left',
|
||||
message: `${username} (dessinateur) a quitté, ${newDrawer} devient le nouveau dessinateur`
|
||||
});
|
||||
startRoomTimer(io, roomId, 60);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,7 +652,6 @@ function setupSocketIO(io)
|
||||
socket.on('game-end', async () => {
|
||||
const roomId = socket.gameRoomId;
|
||||
if (!roomId) return;
|
||||
stopRoomTimer(roomId);
|
||||
|
||||
// Update room status to 'waiting' in database
|
||||
const dbRoomId = socket.gameRoomDbId;
|
||||
@@ -806,16 +730,6 @@ function setupSocketIO(io)
|
||||
_tetrisRelayToOpponent(socket, 'tetris:lines-cleared', data);
|
||||
});
|
||||
|
||||
// Relay pur : shield-activated → adversaire uniquement
|
||||
socket.on('tetris:shield-activated', () => {
|
||||
_tetrisRelayToOpponent(socket, 'tetris:shield-activated', {});
|
||||
});
|
||||
|
||||
// Relay pur : shield-deactivated → adversaire uniquement
|
||||
socket.on('tetris:shield-deactivated', () => {
|
||||
_tetrisRelayToOpponent(socket, 'tetris:shield-deactivated', {});
|
||||
});
|
||||
|
||||
// start-duel → relayé aux DEUX joueurs de la room (inclut l'émetteur)
|
||||
socket.on('tetris:start-duel', () => {
|
||||
const code = socket.tetrisRoomCode;
|
||||
@@ -943,14 +857,6 @@ function setupSocketIO(io)
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dbRoomId && socket.user.userId) {
|
||||
try {
|
||||
await gameRoomService.leaveRoom(dbRoomId, socket.user.userId);
|
||||
} catch (err) {
|
||||
console.error('Error removing disconnected player from room:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Regular player disconnect
|
||||
socket.to(roomId).emit('game-player-left', {
|
||||
username: socket.user.username,
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
FROM nginx:alpine
|
||||
RUN apk add --no-cache openssl && \
|
||||
mkdir -p /etc/nginx/ssl && \
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout /etc/nginx/ssl/key.pem \
|
||||
-out /etc/nginx/ssl/cert.pem \
|
||||
-subj "/CN=localhost" \
|
||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
|
||||
COPY src /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 443
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,9 +1,5 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
error_page 497 =301 https://$host:8443$request_uri;
|
||||
listen 80;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
@@ -15,33 +11,27 @@ server {
|
||||
|
||||
# Backend API
|
||||
location /api/ {
|
||||
proxy_pass https://backend:3001;
|
||||
proxy_ssl_verify off;
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
|
||||
# Socket.IO WebSocket proxying
|
||||
location /socket.io/ {
|
||||
proxy_pass https://backend:3001;
|
||||
proxy_ssl_verify off;
|
||||
proxy_pass http://backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /avatar/ {
|
||||
proxy_pass https://backend:3001/avatar/;
|
||||
proxy_pass http://backend:3001/avatar/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_ssl_verify off;
|
||||
proxy_hide_header Content-Type;
|
||||
add_header Cache-Control "public, max-age=3600";
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 994 B After Width: | Height: | Size: 994 B |
|
Before Width: | Height: | Size: 1018 B After Width: | Height: | Size: 1018 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 955 B |
|
Before Width: | Height: | Size: 1022 B After Width: | Height: | Size: 1022 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 887 B After Width: | Height: | Size: 887 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1000 B After Width: | Height: | Size: 1000 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 437 KiB After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 517 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |