first commit

This commit is contained in:
shaman_lesnoy 2024-12-29 01:39:52 +03:00
commit 9e4041cd0b
33 changed files with 1631162 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View 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

View 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>

View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
assets/cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/search-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/stars/0-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
assets/stars/1-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
assets/stars/2-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
assets/stars/3-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
assets/stars/4-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
assets/stars/5-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

174
frontend/main.css Normal file
View 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
View 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
View 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
View 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
View 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">&times;</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 }}">&#60;</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 }}">&#62;</a>
{% endif %}
</div>
</div>
</body>
</html>

144
frontend/workshop.js Normal file
View 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
View 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)

BIN
maps.db Normal file

Binary file not shown.

BIN
maps.db-shm Normal file

Binary file not shown.

0
maps.db-wal Normal file
View file