first commit
8
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
91
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="false" level="WARNING" enabled_by_default="false">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="71">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="multidict" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="yarl" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="numpy" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="async-timeout" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="aiosqlite" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="typing_extensions" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="python-dotenv" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="aiohttp" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="discord.py" />
|
||||||
|
<item index="9" class="java.lang.String" itemvalue="aiosignal" />
|
||||||
|
<item index="10" class="java.lang.String" itemvalue="attrs" />
|
||||||
|
<item index="11" class="java.lang.String" itemvalue="frozenlist" />
|
||||||
|
<item index="12" class="java.lang.String" itemvalue="idna" />
|
||||||
|
<item index="13" class="java.lang.String" itemvalue="discord" />
|
||||||
|
<item index="14" class="java.lang.String" itemvalue="config" />
|
||||||
|
<item index="15" class="java.lang.String" itemvalue="httpx" />
|
||||||
|
<item index="16" class="java.lang.String" itemvalue="greenlet" />
|
||||||
|
<item index="17" class="java.lang.String" itemvalue="kivy-deps.glew" />
|
||||||
|
<item index="18" class="java.lang.String" itemvalue="listview" />
|
||||||
|
<item index="19" class="java.lang.String" itemvalue="h11" />
|
||||||
|
<item index="20" class="java.lang.String" itemvalue="urlextract" />
|
||||||
|
<item index="21" class="java.lang.String" itemvalue="setuptools" />
|
||||||
|
<item index="22" class="java.lang.String" itemvalue="MarkupSafe" />
|
||||||
|
<item index="23" class="java.lang.String" itemvalue="kivy-deps.sdl2" />
|
||||||
|
<item index="24" class="java.lang.String" itemvalue="disnake.py" />
|
||||||
|
<item index="25" class="java.lang.String" itemvalue="filelock" />
|
||||||
|
<item index="26" class="java.lang.String" itemvalue="Pygments" />
|
||||||
|
<item index="27" class="java.lang.String" itemvalue="certifi" />
|
||||||
|
<item index="28" class="java.lang.String" itemvalue="anyio" />
|
||||||
|
<item index="29" class="java.lang.String" itemvalue="pyimgur" />
|
||||||
|
<item index="30" class="java.lang.String" itemvalue="docutils" />
|
||||||
|
<item index="31" class="java.lang.String" itemvalue="passlib" />
|
||||||
|
<item index="32" class="java.lang.String" itemvalue="pywin32" />
|
||||||
|
<item index="33" class="java.lang.String" itemvalue="pydantic" />
|
||||||
|
<item index="34" class="java.lang.String" itemvalue="Kivy-Garden" />
|
||||||
|
<item index="35" class="java.lang.String" itemvalue="Werkzeug" />
|
||||||
|
<item index="36" class="java.lang.String" itemvalue="asgiref" />
|
||||||
|
<item index="37" class="java.lang.String" itemvalue="click" />
|
||||||
|
<item index="38" class="java.lang.String" itemvalue="Flask-SQLAlchemy" />
|
||||||
|
<item index="39" class="java.lang.String" itemvalue="psutil" />
|
||||||
|
<item index="40" class="java.lang.String" itemvalue="openai" />
|
||||||
|
<item index="41" class="java.lang.String" itemvalue="kivy-deps.angle" />
|
||||||
|
<item index="42" class="java.lang.String" itemvalue="pydantic_core" />
|
||||||
|
<item index="43" class="java.lang.String" itemvalue="platformdirs" />
|
||||||
|
<item index="44" class="java.lang.String" itemvalue="charset-normalizer" />
|
||||||
|
<item index="45" class="java.lang.String" itemvalue="pypiwin32" />
|
||||||
|
<item index="46" class="java.lang.String" itemvalue="httpcore" />
|
||||||
|
<item index="47" class="java.lang.String" itemvalue="distro" />
|
||||||
|
<item index="48" class="java.lang.String" itemvalue="decorator" />
|
||||||
|
<item index="49" class="java.lang.String" itemvalue="SQLAlchemy" />
|
||||||
|
<item index="50" class="java.lang.String" itemvalue="requests" />
|
||||||
|
<item index="51" class="java.lang.String" itemvalue="Jinja2" />
|
||||||
|
<item index="52" class="java.lang.String" itemvalue="sniffio" />
|
||||||
|
<item index="53" class="java.lang.String" itemvalue="Flask-Login" />
|
||||||
|
<item index="54" class="java.lang.String" itemvalue="urllib3" />
|
||||||
|
<item index="55" class="java.lang.String" itemvalue="itsdangerous" />
|
||||||
|
<item index="56" class="java.lang.String" itemvalue="uritools" />
|
||||||
|
<item index="57" class="java.lang.String" itemvalue="Flask" />
|
||||||
|
<item index="58" class="java.lang.String" itemvalue="blinker" />
|
||||||
|
<item index="59" class="java.lang.String" itemvalue="annotated-types" />
|
||||||
|
<item index="60" class="java.lang.String" itemvalue="Kivy" />
|
||||||
|
<item index="61" class="java.lang.String" itemvalue="chardet" />
|
||||||
|
<item index="62" class="java.lang.String" itemvalue="tqdm" />
|
||||||
|
<item index="63" class="java.lang.String" itemvalue="imgurpython" />
|
||||||
|
<item index="64" class="java.lang.String" itemvalue="colorama" />
|
||||||
|
<item index="65" class="java.lang.String" itemvalue="pillow" />
|
||||||
|
<item index="66" class="java.lang.String" itemvalue="self" />
|
||||||
|
<item index="67" class="java.lang.String" itemvalue="pytz" />
|
||||||
|
<item index="68" class="java.lang.String" itemvalue="disnake" />
|
||||||
|
<item index="69" class="java.lang.String" itemvalue="typing-extensions" />
|
||||||
|
<item index="70" class="java.lang.String" itemvalue="aisqlite" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N806" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.12 (Marmok-bot)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/workshop.iml" filepath="$PROJECT_DIR$/.idea/workshop.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/workshop.iml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
1629734
Navigation.json
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
assets/cross.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/image.jpg
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/search-icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/stars/0-star.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/stars/0-star_large.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/stars/1-star.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/stars/1-star_large.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/stars/2-star.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/stars/2-star_large.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/stars/3-star.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/stars/3-star_large.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/stars/4-star.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/stars/4-star_large.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
assets/stars/5-star.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/stars/5-star_large.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
174
frontend/main.css
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
body {
|
||||||
|
background-color: #1B2838;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
background-color: #233B53;
|
||||||
|
width: 950px;
|
||||||
|
min-height: 555px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
width: 635px;
|
||||||
|
height: 360px;
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-container {
|
||||||
|
width: 635px;
|
||||||
|
height: auto;
|
||||||
|
background-color: #19222C;
|
||||||
|
border: 3px solid #35465E;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 10px 15px 10px 15px;
|
||||||
|
margin-left: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
color: #A1B0C7;
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
width: 138px;
|
||||||
|
height: 35px;
|
||||||
|
background: linear-gradient(to bottom, #A4D007, #536904);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 19px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
background: linear-gradient(to bottom, #8DC50E, #475F2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mode {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 660px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #A1B0C7;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-data {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
position: absolute;
|
||||||
|
top: 100px;
|
||||||
|
left: 660px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #A1B0C7;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
position: absolute;
|
||||||
|
top: 150px;
|
||||||
|
left: 660px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #A1B0C7;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size .dynamic-data {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #A1B0C7;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.added-time {
|
||||||
|
position: absolute;
|
||||||
|
top: 170px;
|
||||||
|
left: 660px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #A1B0C7;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.added-time .dynamic-data {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #A1B0C7;
|
||||||
|
margin-left: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
width: 265px;
|
||||||
|
height: 150px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 210px;
|
||||||
|
left: 660px;
|
||||||
|
}
|
||||||
64
frontend/main.html
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Workshop</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="image-wrapper">
|
||||||
|
<div class="image-container">
|
||||||
|
<img src="{{ image_url }}" alt="Map Image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-container">
|
||||||
|
<div class="card-title">{{ map_title }}</div>
|
||||||
|
<div class="description">
|
||||||
|
{{ description }}
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('download_bsp', image_path=image_url) }}">
|
||||||
|
<button class="download-btn">🡇 Скачать</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-mode">
|
||||||
|
<span>Режим игры:</span>
|
||||||
|
<span class="dynamic-data">{{ game_mode | default('Не указан') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tags">
|
||||||
|
<span>Метки:</span>
|
||||||
|
<span class="dynamic-data">{{ tags | default('Не указаны') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-size">
|
||||||
|
<span>Размер файла:</span>
|
||||||
|
<span class="dynamic-data">{{ file_size | default('Не указан') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="added-time">
|
||||||
|
<span>Добавлен:</span>
|
||||||
|
<span class="dynamic-data">{{ added_time | default('Не указано') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if youtube_link %}
|
||||||
|
<div class="youtube-container">
|
||||||
|
<iframe
|
||||||
|
width="265"
|
||||||
|
height="150"
|
||||||
|
src="{{ youtube_link | replace('watch?v=', 'embed/') }}"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
8
frontend/main.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const cards = document.querySelectorAll('.card');
|
||||||
|
cards.forEach(card => {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
window.location.href = '/main';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
487
frontend/workshop.css
Normal file
|
|
@ -0,0 +1,487 @@
|
||||||
|
body {
|
||||||
|
background-color: #1B2A3C;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
background-color: #171D25;
|
||||||
|
width: 100%;
|
||||||
|
height: 105px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 105px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 200px);
|
||||||
|
grid-gap: 105px 20px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-rectangle {
|
||||||
|
width: 295px;
|
||||||
|
height: 370px;
|
||||||
|
background: linear-gradient(to left, #0E141C, #111A23, #15202C);
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 40px;
|
||||||
|
border-radius: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-products-title {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-modes-title {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-modes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mode {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mode input[type="checkbox"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mode:hover {
|
||||||
|
background-color: #1B2A3C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-mode:hover::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 295px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #1B2A3C;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
width: 81px;
|
||||||
|
height: 14px;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
margin-top: 5px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card img:not(.stars) {
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-popup {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
background-color: #61ABD7;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
left: 220px;
|
||||||
|
top: 0;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-popup::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: -20px;
|
||||||
|
border-width: 10px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent #61ABD7 transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-popup strong {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-popup p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .description-popup {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
position: relative;
|
||||||
|
bottom: 25px;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagebtn {
|
||||||
|
width: 40px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #2B475E;
|
||||||
|
color: #63AFCD;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagebtn:hover {
|
||||||
|
background-color: #66C0F4;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagelink {
|
||||||
|
background-color: transparent;
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination_space {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagebtn:hover, .pagelink:hover {
|
||||||
|
background-color: #2A5B70;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button {
|
||||||
|
width: 148px;
|
||||||
|
height: 30px;
|
||||||
|
background: linear-gradient(to top, #384A65, #57749E);
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: normal;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-button:active {
|
||||||
|
background: linear-gradient(to top, #2C3E55, #3D5B80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1001;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 530px;
|
||||||
|
height: 315px;
|
||||||
|
background: linear-gradient(to left, #333840, #333840);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
right: -15px;
|
||||||
|
color: white;
|
||||||
|
font-size: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #f1f1f1;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
margin-left: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-rectangle {
|
||||||
|
position: absolute;
|
||||||
|
top: 105px;
|
||||||
|
left: 25px;
|
||||||
|
width: 480px;
|
||||||
|
height: 42px;
|
||||||
|
background-color: #292B2F;
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 1002;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-rectangle .text-left {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-rectangle .text-center {
|
||||||
|
left: 240px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input {
|
||||||
|
background-color: #33363E;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 30px;
|
||||||
|
width: 205px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1003;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input::-webkit-calendar-picker-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-input:focus {
|
||||||
|
outline: none;
|
||||||
|
background-color: #4c4f58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -270px;
|
||||||
|
right: 25px;
|
||||||
|
width: 80px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #32363F;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-button:hover {
|
||||||
|
background-color: #4A4F5A;
|
||||||
|
box-shadow: 0px 6px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -270px;
|
||||||
|
right: 130px;
|
||||||
|
width: 50px;
|
||||||
|
height: 30px;
|
||||||
|
background-color: #6EA720;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok-button:hover {
|
||||||
|
background-color: #A6FC30;
|
||||||
|
box-shadow: 0px 6px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-top: 420px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 250px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: #2C3E55;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding-left: 10px;
|
||||||
|
outline: none;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
background-color: #3E4A61;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button img {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-stars {
|
||||||
|
margin-top: 420px;
|
||||||
|
margin-right: 260px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-stars {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 420px;
|
||||||
|
margin-right: -660px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #2C3E55;
|
||||||
|
width: 300px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-title {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars-filter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars-filter label {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars-filter input[type="checkbox"] {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #63AFCD;
|
||||||
|
}
|
||||||
138
frontend/workshop.html
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Workshop</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='workshop.css') }}">
|
||||||
|
<script src="{{ url_for('static', filename='workshop.js') }}" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="top-bar">
|
||||||
|
<h1>Добро пожаловать в Workshop</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-container">
|
||||||
|
<div class="cards-container">
|
||||||
|
{% for map in maps_data %}
|
||||||
|
<div class="card">
|
||||||
|
<a href="/main?image_url={{ get_image_path(map[0]) }}&map_title={{ map[1] }}&{{ filters }}">
|
||||||
|
<img src="{{ get_image_path(map[0]) }}" alt="Map Image" width="200" height="110">
|
||||||
|
</a>
|
||||||
|
<img src="{{ get_star_image(map[2]) }}" alt="{{ map[2] }} stars" class="stars" width="81" height="14">
|
||||||
|
<div class="card-title">{{ map[1] }}</div>
|
||||||
|
<div class="description-popup">
|
||||||
|
<strong>{{ map[1] }}</strong>
|
||||||
|
<p>{{ map[3] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-rectangle">
|
||||||
|
<div class="sort-button-container">
|
||||||
|
<button class="sort-button">Сортировать по дате</button>
|
||||||
|
</div>
|
||||||
|
<div class="game-modes">
|
||||||
|
<div class="show-products-title">
|
||||||
|
Показать продукты, попадающие в каждую из выбранных категорий:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-modes-title">РЕЖИМ ИГРЫ</div>
|
||||||
|
|
||||||
|
{% for mode, label in {
|
||||||
|
"Classic": "Классический",
|
||||||
|
"Deathmatch": "Бой насмерть",
|
||||||
|
"Demolition": "Уничтожение объекта",
|
||||||
|
"Armsrace": "Гонка вооружений",
|
||||||
|
"Custom": "Пользовательский",
|
||||||
|
"Training": "Обучение",
|
||||||
|
"Co-op Strike": "Совместный налёт",
|
||||||
|
"Wingman": "Напарники",
|
||||||
|
"Flying Scoutsman": "Перелётные снайперы"
|
||||||
|
}.items() %}
|
||||||
|
<label class="game-mode">
|
||||||
|
<input type="checkbox" class="game-mode-checkbox" value="{{ mode }}" {% if mode in filters %}checked{% endif %}>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="search-container">
|
||||||
|
<form method="get" action="/">
|
||||||
|
<div class="search-input-container">
|
||||||
|
<input type="text" id="search" class="search-input" name="search_title" placeholder="Поиск по названию" value="{{ request.args.get('search_title', '') }}">
|
||||||
|
<button class="search-button" type="submit">
|
||||||
|
<img src="/images/search-icon.png" alt="Поиск">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sortModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-btn">×</span>
|
||||||
|
<h2>Сортировать по дате</h2>
|
||||||
|
<div class="modal-rectangle">
|
||||||
|
<span class="text-left">С</span>
|
||||||
|
<div class="first-rectangle">
|
||||||
|
<input type="date" class="date-input" />
|
||||||
|
</div>
|
||||||
|
<span class="text-center">ПО</span>
|
||||||
|
<div class="second-rectangle">
|
||||||
|
<input type="date" class="date-input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ok-button">ОК</button>
|
||||||
|
<button class="cancel-button">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-stars">
|
||||||
|
<div class="filter-title">
|
||||||
|
<label>Фильтр по количеству звезд:</label>
|
||||||
|
</div>
|
||||||
|
<div class="stars-filter">
|
||||||
|
<label><input type="checkbox" name="stars" value="1" {% if 'stars' in request.args and '1' in request.args.getlist('stars') %}checked{% endif %}> 1</label>
|
||||||
|
<label><input type="checkbox" name="stars" value="2" {% if 'stars' in request.args and '2' in request.args.getlist('stars') %}checked{% endif %}> 2</label>
|
||||||
|
<label><input type="checkbox" name="stars" value="3" {% if 'stars' in request.args and '3' in request.args.getlist('stars') %}checked{% endif %}> 3</label>
|
||||||
|
<label><input type="checkbox" name="stars" value="4" {% if 'stars' in request.args and '4' in request.args.getlist('stars') %}checked{% endif %}> 4</label>
|
||||||
|
<label><input type="checkbox" name="stars" value="5" {% if 'stars' in request.args and '5' in request.args.getlist('stars') %}checked{% endif %}> 5</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
{% if page > 1 %}
|
||||||
|
<a class="pagebtn" href="/?page={{ page - 1 }}&{{ filters }}"><</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page > 3 %}
|
||||||
|
<a class="pagelink" href="/?page=1&{{ filters }}">1</a>
|
||||||
|
<span class="pagination_space">...</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
{% if p >= page - 1 and p <= page + 2 %}
|
||||||
|
{% if p == page %}
|
||||||
|
<span class="pagelink" style="color: #417A9B;">{{ p }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a class="pagelink" href="/?page={{ p }}&{{ filters }}">{{ p }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if total_pages > 3 and page < total_pages - 2 %}
|
||||||
|
<span class="pagination_space">...</span>
|
||||||
|
<a class="pagelink" href="/?page={{ total_pages }}&{{ filters }}">{{ total_pages }}</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if page < total_pages %}
|
||||||
|
<a class="pagebtn" href="/?page={{ page + 1 }}&{{ filters }}">></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
144
frontend/workshop.js
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const sortButton = document.querySelector('.sort-button');
|
||||||
|
const modal = document.getElementById('sortModal');
|
||||||
|
const closeButton = document.querySelector('.close-btn');
|
||||||
|
const cancelButton = document.querySelector('.cancel-button');
|
||||||
|
const okButton = document.querySelector('.ok-button');
|
||||||
|
const startDateInput = document.querySelector('.first-rectangle .date-input');
|
||||||
|
const endDateInput = document.querySelector('.second-rectangle .date-input');
|
||||||
|
const checkboxes = document.querySelectorAll('.game-mode-checkbox');
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Устанавливаем состояние чекбоксов при загрузке страницы
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = params.getAll('game_modes').includes(checkbox.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Открыть модальное окно при нажатии на кнопку
|
||||||
|
sortButton.addEventListener('click', function () {
|
||||||
|
modal.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Закрыть модальное окно при нажатии на крестик
|
||||||
|
closeButton.addEventListener('click', function () {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Закрыть модальное окно при нажатии на кнопку "Отмена"
|
||||||
|
cancelButton.addEventListener('click', function () {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Применить фильтры по дате и режимам игры при нажатии на кнопку "OK"
|
||||||
|
okButton.addEventListener('click', function () {
|
||||||
|
const startDate = startDateInput.value;
|
||||||
|
const endDate = endDateInput.value;
|
||||||
|
|
||||||
|
// Фильтры по датам
|
||||||
|
if (startDate) params.set('start_date', startDate);
|
||||||
|
else params.delete('start_date');
|
||||||
|
|
||||||
|
if (endDate) params.set('end_date', endDate);
|
||||||
|
else params.delete('end_date');
|
||||||
|
|
||||||
|
// Фильтры по режимам игры
|
||||||
|
const selectedGameModes = Array.from(checkboxes)
|
||||||
|
.filter(checkbox => checkbox.checked)
|
||||||
|
.map(checkbox => checkbox.value);
|
||||||
|
|
||||||
|
// Обновляем параметры для выбранных режимов
|
||||||
|
params.delete('game_modes'); // Удаляем старые значения
|
||||||
|
selectedGameModes.forEach(mode => params.append('game_modes', mode));
|
||||||
|
|
||||||
|
params.set('page', 1); // Сбрасываем на первую страницу
|
||||||
|
modal.style.display = 'none'; // Закрываем модальное окно
|
||||||
|
window.location.search = params.toString(); // Перезагружаем страницу с новыми параметрами
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновление параметров при изменении чекбоксов
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
const selectedGameModes = Array.from(checkboxes)
|
||||||
|
.filter(checkbox => checkbox.checked)
|
||||||
|
.map(checkbox => checkbox.value);
|
||||||
|
|
||||||
|
// Обновляем параметры URL
|
||||||
|
params.delete('game_modes'); // Удаляем старые значения
|
||||||
|
selectedGameModes.forEach(mode => params.append('game_modes', mode));
|
||||||
|
|
||||||
|
params.set('page', 1); // Сбрасываем на первую страницу
|
||||||
|
window.location.search = params.toString(); // Перезагружаем страницу с новыми параметрами
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const searchInput = document.querySelector('.search-input');
|
||||||
|
|
||||||
|
// Функция для обработки поиска
|
||||||
|
function performSearch() {
|
||||||
|
const query = searchInput.value.toLowerCase();
|
||||||
|
const cards = document.querySelectorAll('.card');
|
||||||
|
|
||||||
|
cards.forEach(card => {
|
||||||
|
const title = card.querySelector('.card-title').textContent.toLowerCase();
|
||||||
|
if (title.includes(query)) {
|
||||||
|
card.style.display = 'block'; // Показываем карточку, если название соответствует запросу
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none'; // Скрываем карточку, если название не соответствует запросу
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск по нажатию клавиши Enter
|
||||||
|
searchInput.addEventListener('keydown', function (event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Поиск по клику на лупу (если такая кнопка добавлена)
|
||||||
|
const searchButton = document.querySelector('.search-button');
|
||||||
|
if (searchButton) {
|
||||||
|
searchButton.addEventListener('click', performSearch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const starsCheckboxes = document.querySelectorAll('input[name="stars"]');
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Устанавливаем состояние чекбоксов при загрузке страницы
|
||||||
|
const selectedStars = params.get('stars');
|
||||||
|
if (selectedStars) {
|
||||||
|
starsCheckboxes.forEach(checkbox => {
|
||||||
|
if (checkbox.value === selectedStars) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновление параметров при изменении чекбоксов для звезд
|
||||||
|
starsCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', function () {
|
||||||
|
// Снимаем отметки с других чекбоксов
|
||||||
|
starsCheckboxes.forEach(otherCheckbox => {
|
||||||
|
if (otherCheckbox !== checkbox) {
|
||||||
|
otherCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedStar = checkbox.checked ? checkbox.value : null;
|
||||||
|
|
||||||
|
// Обновляем параметры URL
|
||||||
|
params.delete('stars');
|
||||||
|
if (selectedStar) {
|
||||||
|
params.set('stars', selectedStar); // Устанавливаем выбранную звезду
|
||||||
|
}
|
||||||
|
|
||||||
|
params.set('page', 1); // Сбрасываем на первую страницу
|
||||||
|
window.location.search = params.toString(); // Перезагружаем страницу с новыми параметрами
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
285
main.py
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
import aiosqlite
|
||||||
|
from quart import Quart, render_template, request, send_from_directory, Response
|
||||||
|
from datetime import 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')
|
||||||
|
|
||||||
|
DB_PATH = 'maps.db'
|
||||||
|
|
||||||
|
GAME_MODES = {
|
||||||
|
"Classic": "Классический",
|
||||||
|
"Deathmatch": "Бой насмерть",
|
||||||
|
"Demolition": "Уничтожение объекта",
|
||||||
|
"Armsrace": "Гонка вооружений",
|
||||||
|
"Custom": "Пользовательский",
|
||||||
|
"Training": "Обучение",
|
||||||
|
"Co-op Strike": "Совместный налёт",
|
||||||
|
"Wingman": "Напарники",
|
||||||
|
"Flying Scoutsman": "Перелётные снайперы"
|
||||||
|
}
|
||||||
|
|
||||||
|
last_download_times = {}
|
||||||
|
DOWNLOAD_COOLDOWN = 10
|
||||||
|
|
||||||
|
async def get_maps(page=1, per_page=30):
|
||||||
|
async with aiosqlite.connect(DB_PATH) as conn:
|
||||||
|
cursor = await conn.cursor()
|
||||||
|
|
||||||
|
offset = (page - 1) * per_page
|
||||||
|
|
||||||
|
await cursor.execute('''
|
||||||
|
SELECT FilePath, Title, COALESCE(Stars, 0) as Stars, Description
|
||||||
|
FROM maps
|
||||||
|
ORDER BY DateTime DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
''', (per_page, offset))
|
||||||
|
|
||||||
|
maps = await cursor.fetchall()
|
||||||
|
|
||||||
|
return maps
|
||||||
|
|
||||||
|
def get_image_path(filepath):
|
||||||
|
image_path = os.path.join('J:/public/complete/workshop', filepath)
|
||||||
|
if not os.path.exists(image_path):
|
||||||
|
return "http://127.0.0.1:5000/images/image.jpg"
|
||||||
|
return f"http://127.0.0.1:5000/images/{filepath.split('/')[0]}/{filepath.split('/')[1]}/{filepath.split('/')[1]}.jpg"
|
||||||
|
|
||||||
|
def get_star_image(stars):
|
||||||
|
if stars is None or stars == 0:
|
||||||
|
return "http://127.0.0.1:5000/stars/0-star.png"
|
||||||
|
return f"http://127.0.0.1:5000/stars/{stars}-star.png"
|
||||||
|
|
||||||
|
@app.route('/images/<path:filename>')
|
||||||
|
async def serve_image(filename):
|
||||||
|
image_path = os.path.join('J:/public/complete/workshop', filename)
|
||||||
|
if os.path.exists(image_path):
|
||||||
|
return await send_from_directory('J:/public/complete/workshop', filename)
|
||||||
|
else:
|
||||||
|
default_image_path = os.path.join('J:/public/complete/workshop', 'image.jpg')
|
||||||
|
if os.path.exists(default_image_path):
|
||||||
|
return await send_from_directory('J:/public/complete/workshop', 'image.jpg')
|
||||||
|
return "Default image not found", 404
|
||||||
|
|
||||||
|
@app.route('/stars/<filename>')
|
||||||
|
async def serve_star_image(filename):
|
||||||
|
star_path = os.path.join('J:/public/complete/workshop/stars', filename)
|
||||||
|
if os.path.exists(star_path):
|
||||||
|
return await send_from_directory('J:/public/complete/workshop/stars', filename)
|
||||||
|
else:
|
||||||
|
return "Star image not found", 404
|
||||||
|
|
||||||
|
@app.route('/download_bsp')
|
||||||
|
async def download_bsp():
|
||||||
|
user_ip = request.remote_addr
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
last_time = last_download_times.get(user_ip, 0)
|
||||||
|
|
||||||
|
if current_time - last_time < DOWNLOAD_COOLDOWN:
|
||||||
|
wait_time = DOWNLOAD_COOLDOWN - (current_time - last_time)
|
||||||
|
return f"Please wait {int(wait_time)} seconds before downloading again.", 429
|
||||||
|
|
||||||
|
last_download_times[user_ip] = current_time
|
||||||
|
|
||||||
|
image_path = request.args.get('image_path')
|
||||||
|
if not image_path:
|
||||||
|
return "No image path provided", 400
|
||||||
|
|
||||||
|
image_folder = os.path.dirname(image_path.replace("http://127.0.0.1:5000/images/", ""))
|
||||||
|
bsp_filename = None
|
||||||
|
|
||||||
|
for file in os.listdir(os.path.join('J:/public/complete/workshop', image_folder)):
|
||||||
|
if file.endswith('.bsp'):
|
||||||
|
bsp_filename = file
|
||||||
|
break
|
||||||
|
|
||||||
|
if not bsp_filename:
|
||||||
|
return "No .bsp file found in the same directory", 404
|
||||||
|
|
||||||
|
file_path = os.path.join('J:/public/complete/workshop', image_folder, bsp_filename)
|
||||||
|
|
||||||
|
SPEED_LIMIT = 40 * 1024 * 1024 // 8
|
||||||
|
|
||||||
|
async def file_stream():
|
||||||
|
with open(file_path, 'rb') as f:
|
||||||
|
while chunk := f.read(SPEED_LIMIT):
|
||||||
|
yield chunk
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Disposition": f"attachment; filename={bsp_filename}"
|
||||||
|
}
|
||||||
|
return Response(file_stream(), headers=headers, content_type='application/octet-stream')
|
||||||
|
|
||||||
|
@app.route('/main')
|
||||||
|
async def main_page():
|
||||||
|
image_url = request.args.get('image_url', 'default_image.jpg')
|
||||||
|
map_title = request.args.get('map_title', 'Default Map Title')
|
||||||
|
|
||||||
|
async with aiosqlite.connect(DB_PATH) as conn:
|
||||||
|
cursor = await conn.cursor()
|
||||||
|
await cursor.execute('''
|
||||||
|
SELECT GameMode, Tags, FilePath, DateTime, YoutubeLink, Description
|
||||||
|
FROM maps
|
||||||
|
WHERE Title = ?
|
||||||
|
''', (map_title,))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
|
||||||
|
game_mode = row[0] if row else None
|
||||||
|
tags = row[1] if row else None
|
||||||
|
file_path = row[2] if row else None
|
||||||
|
date_time = row[3] if row else None
|
||||||
|
youtube_link = row[4] if row else None
|
||||||
|
description = row[5] if row else "Нет описания"
|
||||||
|
|
||||||
|
if date_time:
|
||||||
|
dt = datetime.fromisoformat(date_time)
|
||||||
|
added_time = format_datetime(dt, format='d MMM y г., HH:mm', locale='ru_RU')
|
||||||
|
else:
|
||||||
|
added_time = 'Не найдено'
|
||||||
|
|
||||||
|
if game_mode:
|
||||||
|
game_modes = game_mode.split(', ')
|
||||||
|
game_modes = [GAME_MODES.get(mode, mode) for mode in game_modes]
|
||||||
|
game_mode = ', '.join(game_modes)
|
||||||
|
else:
|
||||||
|
game_mode = 'Не найден'
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
tags_list = tags.split(', ')
|
||||||
|
tags_list = [GAME_MODES.get(tag, tag) for tag in tags_list]
|
||||||
|
tags = ', '.join(tags_list)
|
||||||
|
else:
|
||||||
|
tags = 'Не найдено'
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
file_size = os.path.getsize(os.path.join('J:/public/complete/workshop', file_path))
|
||||||
|
file_size_mb = file_size / (1024 * 1024)
|
||||||
|
file_size_display = f"{file_size_mb:.2f} MB"
|
||||||
|
else:
|
||||||
|
file_size_display = 'Не найден'
|
||||||
|
|
||||||
|
return await render_template(
|
||||||
|
'main.html',
|
||||||
|
image_url=image_url,
|
||||||
|
map_title=map_title,
|
||||||
|
game_mode=game_mode,
|
||||||
|
tags=tags,
|
||||||
|
file_size=file_size_display,
|
||||||
|
added_time=added_time,
|
||||||
|
youtube_link=youtube_link,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def index():
|
||||||
|
page = int(request.args.get('page', 1))
|
||||||
|
selected_game_modes = request.args.getlist('game_modes')
|
||||||
|
start_date = request.args.get('start_date')
|
||||||
|
end_date = request.args.get('end_date')
|
||||||
|
search_title = request.args.get('search_title')
|
||||||
|
selected_stars = request.args.get('stars')
|
||||||
|
|
||||||
|
maps_data = await get_maps_filtered(page, selected_game_modes, start_date, end_date, search_title, selected_stars)
|
||||||
|
|
||||||
|
async with aiosqlite.connect(DB_PATH) as conn:
|
||||||
|
cursor = await conn.cursor()
|
||||||
|
|
||||||
|
query = 'SELECT COUNT(*) FROM maps WHERE 1=1'
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if selected_game_modes:
|
||||||
|
placeholders = ', '.join('?' for _ in selected_game_modes)
|
||||||
|
query += f' AND GameMode IN ({placeholders})'
|
||||||
|
params.extend(selected_game_modes)
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
query += ' AND DateTime >= ?'
|
||||||
|
params.append(start_date)
|
||||||
|
if end_date:
|
||||||
|
query += ' AND DateTime <= ?'
|
||||||
|
params.append(end_date)
|
||||||
|
|
||||||
|
if search_title:
|
||||||
|
query += ' AND Title LIKE ?'
|
||||||
|
params.append(f'%{search_title}%')
|
||||||
|
|
||||||
|
if selected_stars:
|
||||||
|
query += ' AND Stars = ?'
|
||||||
|
params.append(selected_stars)
|
||||||
|
|
||||||
|
await cursor.execute(query, params)
|
||||||
|
total_maps = await cursor.fetchone()
|
||||||
|
|
||||||
|
total_maps = total_maps[0]
|
||||||
|
per_page = 30
|
||||||
|
total_pages = (total_maps + per_page - 1) // per_page
|
||||||
|
|
||||||
|
filters = '&'.join(
|
||||||
|
[f'game_modes={mode}' for mode in selected_game_modes] +
|
||||||
|
([f'start_date={start_date}'] if start_date else []) +
|
||||||
|
([f'end_date={end_date}'] if end_date else []) +
|
||||||
|
([f'search_title={search_title}'] if search_title else []) +
|
||||||
|
([f'stars={selected_stars}'] if selected_stars else [])
|
||||||
|
)
|
||||||
|
|
||||||
|
return await render_template(
|
||||||
|
'workshop.html',
|
||||||
|
maps_data=maps_data,
|
||||||
|
page=page,
|
||||||
|
total_pages=total_pages,
|
||||||
|
get_image_path=get_image_path,
|
||||||
|
get_star_image=get_star_image,
|
||||||
|
selected_game_modes=selected_game_modes,
|
||||||
|
filters=filters,
|
||||||
|
selected_stars=selected_stars
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_maps_filtered(page=1, selected_game_modes=None, start_date=None, end_date=None, search_title=None, selected_stars=None):
|
||||||
|
async with aiosqlite.connect(DB_PATH) as conn:
|
||||||
|
cursor = await conn.cursor()
|
||||||
|
|
||||||
|
offset = (page - 1) * 30
|
||||||
|
query = '''
|
||||||
|
SELECT FilePath, Title, COALESCE(Stars, 0) as Stars, Description
|
||||||
|
FROM maps
|
||||||
|
WHERE 1=1
|
||||||
|
'''
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if selected_stars:
|
||||||
|
query += ' AND Stars = ?'
|
||||||
|
params.append(selected_stars)
|
||||||
|
|
||||||
|
if search_title:
|
||||||
|
query += ' AND Title LIKE ?'
|
||||||
|
params.append(f'%{search_title}%')
|
||||||
|
|
||||||
|
if selected_game_modes:
|
||||||
|
query += ' AND GameMode IN ({})'.format(','.join('?' for _ in selected_game_modes))
|
||||||
|
params.extend(selected_game_modes)
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
query += ' AND DateTime >= ?'
|
||||||
|
params.append(start_date)
|
||||||
|
if end_date:
|
||||||
|
query += ' AND DateTime <= ?'
|
||||||
|
params.append(end_date)
|
||||||
|
|
||||||
|
query += ' ORDER BY DateTime DESC LIMIT ? OFFSET ?'
|
||||||
|
params.extend([30, offset])
|
||||||
|
|
||||||
|
await cursor.execute(query, params)
|
||||||
|
maps = await cursor.fetchall()
|
||||||
|
|
||||||
|
return maps
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host='127.0.0.1', port=5000)
|
||||||