first release
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
22
Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Используем официальный образ Python как базовый
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Устанавливаем системные зависимости для работы с анимацией и SQLite
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libmagic1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Устанавливаем рабочую директорию
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем файлы приложения в контейнер
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
# Устанавливаем необходимые Python библиотеки
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Открываем порты для работы приложения
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Запускаем приложение
|
||||||
|
CMD ["hypercorn", "main:app", "--bind", "0.0.0.0:5000"]
|
||||||
BIN
assets/cross.png
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
assets/image.jpg
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
14
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: . # Путь к Dockerfile (в текущей директории)
|
||||||
|
ports:
|
||||||
|
- "80:5000" # Проброс портов
|
||||||
|
volumes:
|
||||||
|
- nas-share:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nas-share:
|
||||||
|
driver_opts:
|
||||||
|
type: cifs
|
||||||
|
o: "username=Shaman,password="
|
||||||
|
device: "//192.168.31.3/share/public/complete/workshop"
|
||||||
BIN
frontend/assets/steam-120.ico
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
frontend/assets/steam-32.ico
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -172,3 +172,76 @@ body {
|
||||||
top: 210px;
|
top: 210px;
|
||||||
left: 660px;
|
left: 660px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-container {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
position: static;
|
||||||
|
margin-top: 15px;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mode,
|
||||||
|
.tags,
|
||||||
|
.file-size,
|
||||||
|
.added-time {
|
||||||
|
position: static;
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-data {
|
||||||
|
margin-left: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-container {
|
||||||
|
position: static;
|
||||||
|
margin-top: 15px;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-container iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Workshop</title>
|
<title>workshop</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='assets/steam-120.ico') }}" type="image/x-icon">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="image-wrapper">
|
<div class="image-wrapper">
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,64 @@ body {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* кнопка "Связаться" */
|
||||||
|
.contact-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 50px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #417A9B;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-button:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.top-bar {
|
.top-bar {
|
||||||
background-color: #171D25;
|
background-color: #171D25;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 105px;
|
height: 105px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: space-between; /* Элементы слева и справа */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
padding-left: 20px; /* Отступ слева */
|
||||||
|
padding-right: 20px; /* Отступ справа */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-text {
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 15px; /* Отступ справа для текста */
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-text p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-text {
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 28px;
|
||||||
|
text-align: center;
|
||||||
|
flex-grow: 1; /* Элемент займет всю доступную ширину */
|
||||||
|
margin-left: -500px; /* Это выравнивает текст по правому краю */
|
||||||
|
margin-right: auto; /* Это выравнивает текст по левому краю */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -485,3 +531,136 @@ body {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
accent-color: #63AFCD;
|
accent-color: #63AFCD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 80px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 90%; /* Карточки занимают почти весь экран */
|
||||||
|
max-width: 300px; /* Ограничение по ширине */
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-rectangle {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-modes {
|
||||||
|
position: static;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
width: 90%;
|
||||||
|
height: auto;
|
||||||
|
padding: 15px;
|
||||||
|
max-width: 500px; /* Ограничиваем максимальную ширину для больших мобильных устройств */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
top: -30px;
|
||||||
|
right: -10px;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
margin-left: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-rectangle {
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-rectangle .text-left,
|
||||||
|
.modal-rectangle .text-center {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button,
|
||||||
|
.ok-button {
|
||||||
|
position: static;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok-button {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-stars {
|
||||||
|
width: 90%;
|
||||||
|
position: static;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button {
|
||||||
|
width: auto;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,26 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Workshop</title>
|
<title>workshop</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='assets/steam-120.ico') }}" type="image/x-icon">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='workshop.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='workshop.css') }}">
|
||||||
<script src="{{ url_for('static', filename='workshop.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='workshop.js') }}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<h1>Добро пожаловать в Workshop</h1>
|
<div class="left-text">
|
||||||
|
<p>Сделал: ©️𝙎𝙃∆𝙈∆𝙉©️</p>
|
||||||
|
<p>Неофициальный сайт<br>с картами для CS:GO<br>из мастерской Steam</p>
|
||||||
</div>
|
</div>
|
||||||
|
<h1 class="center-text">Добро пожаловать в Workshop</h1>
|
||||||
|
<a href="https://cloud.s.shsr.ru/apps/forms/s/xr8NyqdpsodwcNnRBSrMzJ4N" class="contact-button" target="_blank">Связаться</a> <!-- Добавлен target="_blank" -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="cards-container">
|
<div class="cards-container">
|
||||||
|
|
|
||||||
42
main.py
|
|
@ -5,12 +5,13 @@ import aiosqlite
|
||||||
from quart import Quart, render_template, request, send_from_directory, Response
|
from quart import Quart, render_template, request, send_from_directory, Response
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from babel.dates import format_datetime
|
from babel.dates import format_datetime
|
||||||
import locale
|
|
||||||
locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8')
|
|
||||||
|
|
||||||
app = Quart(__name__, template_folder='frontend', static_folder='frontend')
|
app = Quart(__name__, template_folder='frontend', static_folder='frontend')
|
||||||
|
|
||||||
DB_PATH = 'maps.db'
|
DB_PATH = 'maps.db'
|
||||||
|
DATA = "/data"
|
||||||
|
|
||||||
GAME_MODES = {
|
GAME_MODES = {
|
||||||
"Classic": "Классический",
|
"Classic": "Классический",
|
||||||
|
|
@ -45,32 +46,33 @@ async def get_maps(page=1, per_page=30):
|
||||||
return maps
|
return maps
|
||||||
|
|
||||||
def get_image_path(filepath):
|
def get_image_path(filepath):
|
||||||
image_path = os.path.join('J:/public/complete/workshop', filepath)
|
image_path = os.path.join(DATA, filepath)
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
return "http://127.0.0.1:5000/images/image.jpg"
|
return "/images/image.jpg"
|
||||||
return f"http://127.0.0.1:5000/images/{filepath.split('/')[0]}/{filepath.split('/')[1]}/{filepath.split('/')[1]}.jpg"
|
return f"/images/{filepath.split('/')[0]}/{filepath.split('/')[1]}/{filepath.split('/')[1]}.jpg"
|
||||||
|
|
||||||
def get_star_image(stars):
|
def get_star_image(stars):
|
||||||
if stars is None or stars == 0:
|
if stars is None or stars == 0:
|
||||||
return "http://127.0.0.1:5000/stars/0-star.png"
|
return "/stars/0-star.png"
|
||||||
return f"http://127.0.0.1:5000/stars/{stars}-star.png"
|
return f"/stars/{stars}-star.png"
|
||||||
|
|
||||||
@app.route('/images/<path:filename>')
|
@app.route('/images/<path:filename>')
|
||||||
async def serve_image(filename):
|
async def serve_image(filename):
|
||||||
image_path = os.path.join('J:/public/complete/workshop', filename)
|
image_path = os.path.join(DATA, filename)
|
||||||
if os.path.exists(image_path):
|
if os.path.exists(image_path):
|
||||||
return await send_from_directory('J:/public/complete/workshop', filename)
|
return await send_from_directory(DATA, filename)
|
||||||
else:
|
else:
|
||||||
default_image_path = os.path.join('J:/public/complete/workshop', 'image.jpg')
|
default_image_path = os.path.join(DATA, 'image.jpg')
|
||||||
if os.path.exists(default_image_path):
|
if os.path.exists(default_image_path):
|
||||||
return await send_from_directory('J:/public/complete/workshop', 'image.jpg')
|
return await send_from_directory(DATA, 'image.jpg')
|
||||||
return "Default image not found", 404
|
return "Default image not found", 404
|
||||||
|
|
||||||
@app.route('/stars/<filename>')
|
@app.route('/stars/<filename>')
|
||||||
async def serve_star_image(filename):
|
async def serve_star_image(filename):
|
||||||
star_path = os.path.join('J:/public/complete/workshop/stars', filename)
|
stars = os.path.join(DATA, 'stars')
|
||||||
|
star_path = os.path.join(stars, filename)
|
||||||
if os.path.exists(star_path):
|
if os.path.exists(star_path):
|
||||||
return await send_from_directory('J:/public/complete/workshop/stars', filename)
|
return await send_from_directory(stars, filename)
|
||||||
else:
|
else:
|
||||||
return "Star image not found", 404
|
return "Star image not found", 404
|
||||||
|
|
||||||
|
|
@ -91,10 +93,10 @@ async def download_bsp():
|
||||||
if not image_path:
|
if not image_path:
|
||||||
return "No image path provided", 400
|
return "No image path provided", 400
|
||||||
|
|
||||||
image_folder = os.path.dirname(image_path.replace("http://127.0.0.1:5000/images/", ""))
|
image_folder = os.path.dirname(image_path.replace("/images/", ""))
|
||||||
bsp_filename = None
|
bsp_filename = None
|
||||||
|
|
||||||
for file in os.listdir(os.path.join('J:/public/complete/workshop', image_folder)):
|
for file in os.listdir(os.path.join(DATA, image_folder)):
|
||||||
if file.endswith('.bsp'):
|
if file.endswith('.bsp'):
|
||||||
bsp_filename = file
|
bsp_filename = file
|
||||||
break
|
break
|
||||||
|
|
@ -102,7 +104,7 @@ async def download_bsp():
|
||||||
if not bsp_filename:
|
if not bsp_filename:
|
||||||
return "No .bsp file found in the same directory", 404
|
return "No .bsp file found in the same directory", 404
|
||||||
|
|
||||||
file_path = os.path.join('J:/public/complete/workshop', image_folder, bsp_filename)
|
file_path = os.path.join(DATA, image_folder, bsp_filename)
|
||||||
|
|
||||||
SPEED_LIMIT = 40 * 1024 * 1024 // 8
|
SPEED_LIMIT = 40 * 1024 * 1024 // 8
|
||||||
|
|
||||||
|
|
@ -159,7 +161,7 @@ async def main_page():
|
||||||
tags = 'Не найдено'
|
tags = 'Не найдено'
|
||||||
|
|
||||||
if file_path:
|
if file_path:
|
||||||
file_size = os.path.getsize(os.path.join('J:/public/complete/workshop', file_path))
|
file_size = os.path.getsize(os.path.join(DATA, file_path))
|
||||||
file_size_mb = file_size / (1024 * 1024)
|
file_size_mb = file_size / (1024 * 1024)
|
||||||
file_size_display = f"{file_size_mb:.2f} MB"
|
file_size_display = f"{file_size_mb:.2f} MB"
|
||||||
else:
|
else:
|
||||||
|
|
@ -282,4 +284,8 @@ async def get_maps_filtered(page=1, selected_game_modes=None, start_date=None, e
|
||||||
return maps
|
return maps
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='127.0.0.1', port=5000)
|
import hypercorn.asyncio
|
||||||
|
from hypercorn.config import Config
|
||||||
|
config = Config()
|
||||||
|
config.bind = ["0.0.0.0:5000"]
|
||||||
|
hypercorn.asyncio.run(app, config)
|
||||||
|
|
|
||||||
BIN
maps.db-shm
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
aiosqlite~=0.20.0
|
||||||
|
Hypercorn~=0.17.3
|
||||||
|
Quart~=0.19.6
|
||||||
|
babel~=2.16.0
|
||||||