Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -166,7 +166,7 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 166 |
<meta charset="UTF-8">
|
| 167 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 168 |
<title>Админ-панель</title>
|
| 169 |
-
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 170 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 171 |
<style>
|
| 172 |
:root {
|
|
@@ -177,114 +177,68 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 177 |
--text-dark: #333;
|
| 178 |
--text-on-accent: #003C43;
|
| 179 |
--danger: #E57373;
|
| 180 |
-
--warning: #
|
|
|
|
|
|
|
|
|
|
| 181 |
}
|
| 182 |
* { box-sizing: border-box; }
|
| 183 |
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; }
|
| 184 |
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 3px 15px rgba(0,0,0,0.08); }
|
| 185 |
-
h1 { font-weight: 600; color: var(--bg-medium);
|
|
|
|
|
|
|
| 186 |
|
| 187 |
.section { margin-bottom: 25px; }
|
| 188 |
|
| 189 |
-
.add-env-form {
|
| 190 |
-
display: flex;
|
| 191 |
-
flex-direction: column;
|
| 192 |
-
gap: 15px;
|
| 193 |
-
background: #f8f9fa;
|
| 194 |
-
padding: 15px;
|
| 195 |
-
border-radius: 10px;
|
| 196 |
-
border: 1px solid #e9ecef;
|
| 197 |
-
}
|
| 198 |
|
| 199 |
input[type="text"] {
|
| 200 |
-
width: 100%;
|
| 201 |
-
|
| 202 |
-
border: 1px solid #ddd;
|
| 203 |
-
border-radius: 8px;
|
| 204 |
-
font-size: 1rem;
|
| 205 |
-
font-family: inherit;
|
| 206 |
-
background: #fff;
|
| 207 |
-
-webkit-appearance: none;
|
| 208 |
}
|
| 209 |
|
| 210 |
-
.controls-row {
|
| 211 |
-
display: flex;
|
| 212 |
-
align-items: center;
|
| 213 |
-
justify-content: space-between;
|
| 214 |
-
gap: 15px;
|
| 215 |
-
flex-wrap: wrap;
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
.radio-group { display: flex; gap: 15px; }
|
| 219 |
.radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; }
|
| 220 |
|
| 221 |
.button {
|
| 222 |
-
padding:
|
| 223 |
-
|
| 224 |
-
border-radius: 8px;
|
| 225 |
-
background-color: var(--accent);
|
| 226 |
-
color: var(--text-on-accent);
|
| 227 |
-
font-weight: 600;
|
| 228 |
-
cursor: pointer;
|
| 229 |
-
text-decoration: none;
|
| 230 |
-
display: inline-flex;
|
| 231 |
-
align-items: center;
|
| 232 |
-
justify-content: center;
|
| 233 |
-
gap: 8px;
|
| 234 |
-
font-size: 1rem;
|
| 235 |
-
transition: opacity 0.2s;
|
| 236 |
}
|
| 237 |
-
.button:hover { opacity: 0.
|
| 238 |
.button:active { transform: scale(0.98); }
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
.env-list { list-style: none; padding: 0; margin: 0; }
|
| 241 |
.env-item {
|
| 242 |
-
background: #fff;
|
| 243 |
-
|
| 244 |
-
border-radius: 10px;
|
| 245 |
-
padding: 15px;
|
| 246 |
-
margin-bottom: 12px;
|
| 247 |
-
display: grid;
|
| 248 |
-
grid-template-columns: 1fr auto;
|
| 249 |
-
align-items: center;
|
| 250 |
-
gap: 15px;
|
| 251 |
-
box-shadow: 0 2px 5px rgba(0,0,0,0.02);
|
| 252 |
}
|
| 253 |
-
|
|
|
|
| 254 |
.env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
|
| 255 |
.env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
| 256 |
.env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; }
|
| 257 |
.env-keyword { font-style: italic; color: #666; font-size: 0.9rem;}
|
| 258 |
|
| 259 |
-
.env-link {
|
| 260 |
-
font-size: 0.9rem;
|
| 261 |
-
color: #007bff;
|
| 262 |
-
word-break: break-all;
|
| 263 |
-
text-decoration: none;
|
| 264 |
-
padding: 5px 0;
|
| 265 |
-
display: block;
|
| 266 |
-
}
|
| 267 |
|
| 268 |
-
.env-type-badge {
|
| 269 |
-
font-size: 0.75rem;
|
| 270 |
-
padding: 3px 8px;
|
| 271 |
-
border-radius: 20px;
|
| 272 |
-
font-weight: bold;
|
| 273 |
-
text-transform: uppercase;
|
| 274 |
-
white-space: nowrap;
|
| 275 |
-
}
|
| 276 |
.type-open { background-color: #d4edda; color: #155724; }
|
| 277 |
.type-closed { background-color: #f8d7da; color: #721c24; }
|
| 278 |
|
| 279 |
-
.env-actions { display: flex; gap: 8px; }
|
| 280 |
-
.stats-button { background-color: #5d4037; color: white; padding: 10px 15px; font-size: 0.9rem;}
|
| 281 |
-
.delete-button { background-color: var(--danger); color: white; padding: 10px 15px; font-size: 0.9rem;}
|
| 282 |
|
| 283 |
.message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; }
|
| 284 |
.message.success { background-color: #d4edda; color: #155724; }
|
| 285 |
.message.error { background-color: #f8d7da; color: #721c24; }
|
| 286 |
|
| 287 |
-
/* Modal */
|
| 288 |
.modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
|
| 289 |
.modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
|
| 290 |
.close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
|
|
@@ -292,47 +246,25 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 292 |
.stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
| 293 |
.stats-table th { background-color: var(--bg-medium); color: white; }
|
| 294 |
.stats-table tr:nth-child(even) { background-color: #f9f9f9; }
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
@media (max-width: 600px) {
|
| 298 |
body { padding: 10px; }
|
| 299 |
.container { padding: 15px; }
|
| 300 |
h1 { font-size: 1.3rem; margin-bottom: 20px; }
|
| 301 |
|
| 302 |
-
.controls-row {
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
}
|
| 306 |
-
.radio-group {
|
| 307 |
-
justify-content: space-between;
|
| 308 |
-
background: #fff;
|
| 309 |
-
padding: 10px;
|
| 310 |
-
border-radius: 8px;
|
| 311 |
-
border: 1px solid #ddd;
|
| 312 |
-
}
|
| 313 |
-
.button { width: 100%; padding: 14px; }
|
| 314 |
-
|
| 315 |
-
.env-item {
|
| 316 |
-
grid-template-columns: 1fr;
|
| 317 |
-
gap: 12px;
|
| 318 |
-
position: relative;
|
| 319 |
-
padding-bottom: 60px; /* Space for buttons */
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
.env-actions {
|
| 323 |
-
position: absolute;
|
| 324 |
-
bottom: 15px;
|
| 325 |
-
left: 15px;
|
| 326 |
-
right: 15px;
|
| 327 |
-
display: grid;
|
| 328 |
-
grid-template-columns: 3fr 1fr;
|
| 329 |
-
gap: 10px;
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
.env-actions button { width: 100%; }
|
| 333 |
-
.env-actions form { width: 100%; }
|
| 334 |
|
| 335 |
-
.modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; }
|
| 336 |
.stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; }
|
| 337 |
}
|
| 338 |
</style>
|
|
@@ -356,7 +288,7 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 356 |
<label><input type="radio" name="env_type" value="closed" checked> <i class="fas fa-lock"></i> Закрытая</label>
|
| 357 |
<label><input type="radio" name="env_type" value="open"> <i class="fas fa-globe"></i> Открытая</label>
|
| 358 |
</div>
|
| 359 |
-
<button type="submit" class="button"><i class="fas fa-plus-circle"></i> Создать</button>
|
| 360 |
</div>
|
| 361 |
</form>
|
| 362 |
</div>
|
|
@@ -366,9 +298,9 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 366 |
</div>
|
| 367 |
|
| 368 |
<div class="section">
|
| 369 |
-
{% if
|
| 370 |
<ul class="env-list">
|
| 371 |
-
{% for env in
|
| 372 |
<li class="env-item">
|
| 373 |
<div class="env-details">
|
| 374 |
<div class="env-header">
|
|
@@ -382,21 +314,58 @@ ADMHOSTO_TEMPLATE = '''
|
|
| 382 |
<a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
|
| 383 |
</div>
|
| 384 |
<div class="env-actions">
|
| 385 |
-
<button class="button
|
| 386 |
-
<form method="POST" action="{{ url_for('
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
</form>
|
| 389 |
</div>
|
| 390 |
</li>
|
| 391 |
{% endfor %}
|
| 392 |
</ul>
|
| 393 |
{% else %}
|
| 394 |
-
<div
|
| 395 |
{% endif %}
|
| 396 |
</div>
|
| 397 |
</div>
|
| 398 |
|
| 399 |
-
<!-- Modal -->
|
| 400 |
<div id="statsModal" class="modal">
|
| 401 |
<div class="modal-content">
|
| 402 |
<span class="close-modal" onclick="closeStats()">×</span>
|
|
@@ -909,20 +878,22 @@ select option {
|
|
| 909 |
<option value="shot on 35mm film, Kodak Portra 400">Пленка Kodak Portra</option>
|
| 910 |
</select>
|
| 911 |
</div>
|
| 912 |
-
|
| 913 |
-
<label
|
| 914 |
-
<
|
| 915 |
-
<
|
| 916 |
-
<
|
| 917 |
-
<
|
| 918 |
-
<
|
| 919 |
-
<
|
| 920 |
-
<
|
| 921 |
-
<
|
| 922 |
-
<
|
| 923 |
-
<
|
| 924 |
-
<
|
| 925 |
-
|
|
|
|
|
|
|
| 926 |
</div>
|
| 927 |
<div class="form-group full-width">
|
| 928 |
<label for="model_details">Одежда и Детали (Опишите ткань и фасон!)</label>
|
|
@@ -1231,19 +1202,23 @@ function toggleCreativeMode() {
|
|
| 1231 |
document.getElementById('object_background').disabled = isCreative;
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
-
function
|
| 1235 |
-
const
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
btn.
|
|
|
|
|
|
|
|
|
|
| 1243 |
});
|
| 1244 |
});
|
| 1245 |
}
|
| 1246 |
|
|
|
|
| 1247 |
async function processAndOpen() {
|
| 1248 |
const btn = document.querySelector('.action-btn');
|
| 1249 |
const originalText = btn.innerHTML;
|
|
@@ -1255,22 +1230,20 @@ async function processAndOpen() {
|
|
| 1255 |
const shotType = document.getElementById('shotType').value;
|
| 1256 |
const bodyType = document.getElementById('bodyType').value;
|
| 1257 |
const pose = document.getElementById('pose').value;
|
| 1258 |
-
const location = document.
|
| 1259 |
const light = document.getElementById('light').value;
|
| 1260 |
const camera = document.getElementById('camera').value;
|
| 1261 |
|
| 1262 |
if (isMyModel) {
|
| 1263 |
const details = document.getElementById('model_details').value || "the clothing from the reference image";
|
| 1264 |
fullPrompt = `${envKeyword}, VIRTUAL TRY-ON.
|
| 1265 |
-
INSTRUCTIONS
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
Task: Accurately dress the model from Image 1 in the clothing from Image 2. The final image must preserve the model's exact likeness and facial identity. The model should have a body type of "${bodyType}".
|
| 1269 |
-
Style: ${style}, hyper-detailed, luxury brand campaign, Vogue aesthetic, impeccable.
|
| 1270 |
Composition: ${shotType}.
|
| 1271 |
Final Scene: Place the model in the following environment: ${location}.
|
| 1272 |
-
Lighting: The scene
|
| 1273 |
-
Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD,
|
| 1274 |
} else {
|
| 1275 |
const age = document.getElementById('age').value;
|
| 1276 |
const nationality = document.getElementById('nationality').value;
|
|
@@ -1280,15 +1253,15 @@ Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD, tack
|
|
| 1280 |
const emotion = document.getElementById('emotion').value;
|
| 1281 |
const details = document.getElementById('model_details').value || "high-end fashion garments";
|
| 1282 |
|
| 1283 |
-
fullPrompt = `${envKeyword}, style:: ${style}, high fashion editorial, luxury brand campaign (like Dior, D&G), Vogue aesthetic, hyper-
|
| 1284 |
composition:: ${shotType}.
|
| 1285 |
subject:: An ultra-high-resolution, flawless photograph of a striking ${age} ${nationality} high fashion model.
|
| 1286 |
-
model_characteristics:: physique ${bodyType}, ${hairColor} ${hairstyle}
|
| 1287 |
clothing_focus:: The model is wearing ${details}, emphasizing haute couture craftsmanship.
|
| 1288 |
-
texture_&_material_fidelity:: Extreme macro precision on textiles. Render the fabric weave,
|
| 1289 |
-
human_realism_details:: Capture
|
| 1290 |
scene_environment:: ${location}, creating a sophisticated and aspirational atmosphere.
|
| 1291 |
-
technical:: Masterpiece professional photograph, ${light} meticulously crafted to sculpt the subject, shot on ${camera}, 8k UHD,
|
| 1292 |
}
|
| 1293 |
|
| 1294 |
} else if (currentMode === 'children') {
|
|
@@ -1306,7 +1279,7 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
|
|
| 1306 |
const emotion = document.getElementById('newborn_emotion').value;
|
| 1307 |
const pose = document.getElementById('newborn_pose').value;
|
| 1308 |
clothing_details = document.getElementById('newborn_details').value || "soft knitted fabric";
|
| 1309 |
-
subject = `
|
| 1310 |
pose_info = `The baby is ${pose}.`;
|
| 1311 |
} else {
|
| 1312 |
const age = document.getElementById('child_age').value;
|
|
@@ -1315,7 +1288,7 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
|
|
| 1315 |
const emotion = document.getElementById('child_emotion').value;
|
| 1316 |
const pose = document.getElementById('child_pose').value;
|
| 1317 |
clothing_details = document.getElementById('child_details').value || "detailed textured casual clothes";
|
| 1318 |
-
subject = `
|
| 1319 |
pose_info = `The child is ${pose}.`;
|
| 1320 |
}
|
| 1321 |
|
|
@@ -1323,8 +1296,8 @@ technical:: Masterpiece professional photograph, ${light} meticulously crafted t
|
|
| 1323 |
composition:: ${shotType}.
|
| 1324 |
subject:: ${subject} The photograph must look like a cover shot for a high-end children's fashion magazine.
|
| 1325 |
clothing_focus:: The child is wearing ${clothing_details}, presented as a luxury garment.
|
| 1326 |
-
texture_&_material_fidelity:: Macro-level detail on clothing textures. Focus on the weave of cotton, the softness of wool, the texture of denim. Show realistic wrinkles, creases from movement, and even subtle fabric pilling.
|
| 1327 |
-
human_realism_details:: Capture the pure, innocent beauty of the child.
|
| 1328 |
scene_activity:: ${pose_info} The location is ${location}, creating a whimsical and high-end narrative.
|
| 1329 |
technical:: Masterpiece photograph, ${light} creating a magical and soft atmosphere, shot on Fujifilm XT4, 56mm F1.2 lens, 8k, tack sharp focus, impeccable detail, perfect color grading, looks like a real captured moment of wonder from a luxury campaign.`;
|
| 1330 |
|
|
@@ -1344,11 +1317,11 @@ technical:: Masterpiece photograph, ${light} creating a magical and soft atmosph
|
|
| 1344 |
}
|
| 1345 |
|
| 1346 |
fullPrompt = `${envKeyword}, style:: Luxury product advertising, ${objectStyle}, sophisticated, sleek, ultra-photorealistic.
|
| 1347 |
-
subject:: A breathtaking, hyper-realistic photograph of the luxury product: ${objectName}. The image must evoke desire and exclusivity.
|
| 1348 |
-
material_focus:: Achieve
|
| 1349 |
scene_context:: Placed ${background}. Additional details: ${objectDetails}, arranged with artistic precision.
|
| 1350 |
composition:: ${objectComposition}, creating a powerful and elegant visual statement.
|
| 1351 |
-
technical:: Advertisement-grade photograph, ${objectLighting} designed to accentuate the product's luxury form, 8k UHD resolution, flawless focus, extreme macro detail, advanced ray-traced reflections, impeccably clean, exudes quality and high-end appeal, masterpiece.`;
|
| 1352 |
}
|
| 1353 |
|
| 1354 |
const cleanPrompt = fullPrompt.replace(/\\s+/g, ' ').replace(/\\n/g, ' ').trim();
|
|
@@ -1377,7 +1350,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 1377 |
switchMode('model');
|
| 1378 |
switchChildrenSubMode('newborn');
|
| 1379 |
autoAdjustDefaults();
|
| 1380 |
-
|
| 1381 |
});
|
| 1382 |
</script>
|
| 1383 |
|
|
@@ -1392,20 +1365,27 @@ def index():
|
|
| 1392 |
@app.route('/admhosto', methods=['GET'])
|
| 1393 |
def admhosto():
|
| 1394 |
data = load_data()
|
| 1395 |
-
|
|
|
|
|
|
|
| 1396 |
for env_id, env_data in data.items():
|
| 1397 |
-
|
| 1398 |
"id": env_id,
|
| 1399 |
"keyword": env_data.get("keyword", "N/A"),
|
| 1400 |
"type": env_data.get("type", "closed"),
|
| 1401 |
"hits": env_data.get("hits", 0),
|
| 1402 |
"created_at": env_data.get("created_at", ""),
|
| 1403 |
"link": url_for('serve_env', env_id=env_id, _external=True)
|
| 1404 |
-
}
|
| 1405 |
-
|
| 1406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1407 |
|
| 1408 |
-
return render_template_string(ADMHOSTO_TEMPLATE,
|
| 1409 |
|
| 1410 |
@app.route('/admhosto/create', methods=['POST'])
|
| 1411 |
def create_environment():
|
|
@@ -1428,7 +1408,8 @@ def create_environment():
|
|
| 1428 |
"device_token": None,
|
| 1429 |
"hits": 0,
|
| 1430 |
"logs": [],
|
| 1431 |
-
"created_at": datetime.utcnow().isoformat()
|
|
|
|
| 1432 |
}
|
| 1433 |
save_data(all_data)
|
| 1434 |
flash(f'Новая {env_type} среда с ID {new_id} создана.', 'success')
|
|
@@ -1438,9 +1419,48 @@ def create_environment():
|
|
| 1438 |
def delete_environment(env_id):
|
| 1439 |
all_data = load_data()
|
| 1440 |
if env_id in all_data:
|
| 1441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1442 |
save_data(all_data)
|
| 1443 |
-
flash(f'Среда {env_id} была удалена.', 'success')
|
| 1444 |
else:
|
| 1445 |
flash(f'Среда {env_id} не найдена.', 'error')
|
| 1446 |
return redirect(url_for('admhosto'))
|
|
@@ -1481,8 +1501,8 @@ def get_env_stats(env_id):
|
|
| 1481 |
def serve_env(env_id):
|
| 1482 |
data = load_data()
|
| 1483 |
env_data = data.get(env_id)
|
| 1484 |
-
if not env_data:
|
| 1485 |
-
return "Среда не
|
| 1486 |
|
| 1487 |
keyword = env_data.get("keyword", "")
|
| 1488 |
env_type = env_data.get("type", "closed")
|
|
@@ -1490,7 +1510,7 @@ def serve_env(env_id):
|
|
| 1490 |
current_log = {
|
| 1491 |
"time": datetime.utcnow().isoformat(),
|
| 1492 |
"ip": request.remote_addr,
|
| 1493 |
-
"ua": request.headers.get('User-Agent')[:
|
| 1494 |
}
|
| 1495 |
|
| 1496 |
env_data['hits'] = env_data.get('hits', 0) + 1
|
|
|
|
| 166 |
<meta charset="UTF-8">
|
| 167 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 168 |
<title>Админ-панель</title>
|
| 169 |
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 170 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 171 |
<style>
|
| 172 |
:root {
|
|
|
|
| 177 |
--text-dark: #333;
|
| 178 |
--text-on-accent: #003C43;
|
| 179 |
--danger: #E57373;
|
| 180 |
+
--warning: #ffb74d;
|
| 181 |
+
--info: #4fc3f7;
|
| 182 |
+
--success: #81c784;
|
| 183 |
+
--archive: #90a4ae;
|
| 184 |
}
|
| 185 |
* { box-sizing: border-box; }
|
| 186 |
body { font-family: 'Montserrat', sans-serif; background-color: var(--bg-light); color: var(--text-dark); margin: 0; padding: 15px; }
|
| 187 |
.container { max-width: 900px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 3px 15px rgba(0,0,0,0.08); }
|
| 188 |
+
h1, h2 { font-weight: 600; color: var(--bg-medium); text-align: center; }
|
| 189 |
+
h1 { margin-bottom: 25px; font-size: 1.5rem; }
|
| 190 |
+
h2 { font-size: 1.3rem; margin-top: 40px; border-bottom: 2px solid var(--accent); padding-bottom: 10px; margin-bottom: 20px; }
|
| 191 |
|
| 192 |
.section { margin-bottom: 25px; }
|
| 193 |
|
| 194 |
+
.add-env-form { display: flex; flex-direction: column; gap: 15px; background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #e9ecef; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
input[type="text"] {
|
| 197 |
+
width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem;
|
| 198 |
+
font-family: inherit; background: #fff; -webkit-appearance: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
+
.controls-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; flex-wrap: wrap; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
.radio-group { display: flex; gap: 15px; }
|
| 203 |
.radio-group label { cursor: pointer; display: flex; align-items: center; gap: 6px; font-weight: 500; font-size: 0.95rem; }
|
| 204 |
|
| 205 |
.button {
|
| 206 |
+
padding: 10px 15px; border: none; border-radius: 8px; color: white; font-weight: 600; cursor: pointer; text-decoration: none;
|
| 207 |
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-size: 0.9rem; transition: opacity 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
}
|
| 209 |
+
.button:hover { opacity: 0.85; }
|
| 210 |
.button:active { transform: scale(0.98); }
|
| 211 |
|
| 212 |
+
.button.primary { background-color: var(--accent); color: var(--text-on-accent); }
|
| 213 |
+
.button.danger { background-color: var(--danger); }
|
| 214 |
+
.button.warning { background-color: var(--warning); color: #333; }
|
| 215 |
+
.button.info { background-color: var(--info); }
|
| 216 |
+
.button.success { background-color: var(--success); }
|
| 217 |
+
|
| 218 |
.env-list { list-style: none; padding: 0; margin: 0; }
|
| 219 |
.env-item {
|
| 220 |
+
background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; padding: 15px; margin-bottom: 12px;
|
| 221 |
+
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.02);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
}
|
| 223 |
+
.env-item-archived { border-left: 4px solid var(--archive); }
|
| 224 |
+
|
| 225 |
.env-details { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
|
| 226 |
.env-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
| 227 |
.env-id { font-weight: 700; color: var(--bg-medium); font-size: 1.1rem; }
|
| 228 |
.env-keyword { font-style: italic; color: #666; font-size: 0.9rem;}
|
| 229 |
|
| 230 |
+
.env-link { font-size: 0.9rem; color: #007bff; word-break: break-all; text-decoration: none; padding: 5px 0; display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
+
.env-type-badge { font-size: 0.75rem; padding: 3px 8px; border-radius: 20px; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
.type-open { background-color: #d4edda; color: #155724; }
|
| 234 |
.type-closed { background-color: #f8d7da; color: #721c24; }
|
| 235 |
|
| 236 |
+
.env-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
|
|
|
|
|
| 237 |
|
| 238 |
.message { padding: 12px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-size: 0.95rem; }
|
| 239 |
.message.success { background-color: #d4edda; color: #155724; }
|
| 240 |
.message.error { background-color: #f8d7da; color: #721c24; }
|
| 241 |
|
|
|
|
| 242 |
.modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(2px); }
|
| 243 |
.modal-content { background-color: #fff; margin: 15% auto; padding: 25px; width: 90%; max-width: 600px; border-radius: 12px; position: relative; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
|
| 244 |
.close-modal { color: #888; position: absolute; right: 15px; top: 10px; font-size: 30px; font-weight: bold; cursor: pointer; padding: 5px; }
|
|
|
|
| 246 |
.stats-table th, .stats-table td { border: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
| 247 |
.stats-table th { background-color: var(--bg-medium); color: white; }
|
| 248 |
.stats-table tr:nth-child(even) { background-color: #f9f9f9; }
|
| 249 |
+
|
| 250 |
+
.empty-list-placeholder { text-align:center; padding: 20px; color: #888; }
|
| 251 |
+
.no-margin { margin-bottom: 0; }
|
| 252 |
|
| 253 |
+
@media (max-width: 768px) {
|
| 254 |
+
.env-item { grid-template-columns: 1fr; gap: 12px; }
|
| 255 |
+
.env-actions { justify-content: flex-start; }
|
| 256 |
+
.modal-content { margin: 10% auto; width: 95%; padding: 20px 15px; }
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
@media (max-width: 600px) {
|
| 260 |
body { padding: 10px; }
|
| 261 |
.container { padding: 15px; }
|
| 262 |
h1 { font-size: 1.3rem; margin-bottom: 20px; }
|
| 263 |
|
| 264 |
+
.controls-row { flex-direction: column; align-items: stretch; }
|
| 265 |
+
.radio-group { justify-content: space-between; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #ddd; }
|
| 266 |
+
.add-env-form .button { width: 100%; padding: 14px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
|
|
|
| 268 |
.stats-table th, .stats-table td { font-size: 0.75rem; padding: 6px 4px; }
|
| 269 |
}
|
| 270 |
</style>
|
|
|
|
| 288 |
<label><input type="radio" name="env_type" value="closed" checked> <i class="fas fa-lock"></i> Закрытая</label>
|
| 289 |
<label><input type="radio" name="env_type" value="open"> <i class="fas fa-globe"></i> Открытая</label>
|
| 290 |
</div>
|
| 291 |
+
<button type="submit" class="button primary"><i class="fas fa-plus-circle"></i> Создать</button>
|
| 292 |
</div>
|
| 293 |
</form>
|
| 294 |
</div>
|
|
|
|
| 298 |
</div>
|
| 299 |
|
| 300 |
<div class="section">
|
| 301 |
+
{% if active_environments %}
|
| 302 |
<ul class="env-list">
|
| 303 |
+
{% for env in active_environments %}
|
| 304 |
<li class="env-item">
|
| 305 |
<div class="env-details">
|
| 306 |
<div class="env-header">
|
|
|
|
| 314 |
<a href="{{ env.link }}" class="env-link" target="_blank">{{ env.link }}</a>
|
| 315 |
</div>
|
| 316 |
<div class="env-actions">
|
| 317 |
+
<button class="button info" onclick="openStats('{{ env.id }}')"><i class="fas fa-chart-bar"></i> Инфо</button>
|
| 318 |
+
<form method="POST" action="{{ url_for('toggle_type', env_id=env.id) }}" style="display:contents;">
|
| 319 |
+
<button type="submit" class="button warning">
|
| 320 |
+
<i class="fas fa-{{ 'lock-open' if env.type == 'closed' else 'lock' }}"></i> {{ 'Открыть' if env.type == 'closed' else 'Закрыть' }}
|
| 321 |
+
</button>
|
| 322 |
+
</form>
|
| 323 |
+
{% if env.type == 'closed' %}
|
| 324 |
+
<form method="POST" action="{{ url_for('clear_user', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Отвязать пользователя от среды {{ env.id }}? Первый, кто зайдет по ссылке, станет владельцем.');">
|
| 325 |
+
<button type="submit" class="button success"><i class="fas fa-user-slash"></i> Сброс</button>
|
| 326 |
+
</form>
|
| 327 |
+
{% endif %}
|
| 328 |
+
<form method="POST" action="{{ url_for('delete_environment', env_id=env.id) }}" style="display:contents;" onsubmit="return confirm('Переместить среду {{ env.id }} в архив?');">
|
| 329 |
+
<button type="submit" class="button danger"><i class="fas fa-archive"></i></button>
|
| 330 |
+
</form>
|
| 331 |
+
</div>
|
| 332 |
+
</li>
|
| 333 |
+
{% endfor %}
|
| 334 |
+
</ul>
|
| 335 |
+
{% else %}
|
| 336 |
+
<div class="empty-list-placeholder">Список активных сред пуст</div>
|
| 337 |
+
{% endif %}
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<div class="section no-margin">
|
| 341 |
+
<h2><i class="fas fa-archive"></i> Архив</h2>
|
| 342 |
+
{% if archived_environments %}
|
| 343 |
+
<ul class="env-list">
|
| 344 |
+
{% for env in archived_environments %}
|
| 345 |
+
<li class="env-item env-item-archived">
|
| 346 |
+
<div class="env-details">
|
| 347 |
+
<div class="env-header">
|
| 348 |
+
<span class="env-id">{{ env.id }}</span>
|
| 349 |
+
<span class="env-type-badge type-{{ env.type }}">
|
| 350 |
+
{{ 'ЗАКРЫТАЯ' if env.type == 'closed' else 'ОТКРЫТАЯ' }}
|
| 351 |
+
</span>
|
| 352 |
+
</div>
|
| 353 |
+
<span class="env-keyword">{{ env.keyword }}</span>
|
| 354 |
+
</div>
|
| 355 |
+
<div class="env-actions">
|
| 356 |
+
<form method="POST" action="{{ url_for('restore_environment', env_id=env.id) }}" style="display:contents;">
|
| 357 |
+
<button type="submit" class="button success"><i class="fas fa-undo"></i> Восстановить</button>
|
| 358 |
</form>
|
| 359 |
</div>
|
| 360 |
</li>
|
| 361 |
{% endfor %}
|
| 362 |
</ul>
|
| 363 |
{% else %}
|
| 364 |
+
<div class="empty-list-placeholder">Архив пуст</div>
|
| 365 |
{% endif %}
|
| 366 |
</div>
|
| 367 |
</div>
|
| 368 |
|
|
|
|
| 369 |
<div id="statsModal" class="modal">
|
| 370 |
<div class="modal-content">
|
| 371 |
<span class="close-modal" onclick="closeStats()">×</span>
|
|
|
|
| 878 |
<option value="shot on 35mm film, Kodak Portra 400">Пленка Kodak Portra</option>
|
| 879 |
</select>
|
| 880 |
</div>
|
| 881 |
+
<div class="form-group full-width">
|
| 882 |
+
<label>Локация</label>
|
| 883 |
+
<div id="locationSelector" class="style-grid">
|
| 884 |
+
<button type="button" class="style-btn active" data-value="in a clean white seamless studio background">Студия (белый фон)</button>
|
| 885 |
+
<button type="button" class="style-btn" data-value="in a creative editorial studio with mirrors and geometric shapes">Студия "Эдиториал"</button>
|
| 886 |
+
<button type="button" class="style-btn" data-value="on a rainy night street in Tokyo with neon lights">Ночной Токио (неон)</button>
|
| 887 |
+
<button type="button" class="style-btn" data-value="in a minimalist modern interior with concrete walls">Лофт (бетон)</button>
|
| 888 |
+
<button type="button" class="style-btn" data-value="on a rooftop overlooking the city skyline at sunset">Крыша (закат)</button>
|
| 889 |
+
<button type="button" class="style-btn" data-value="in a luxurious vintage room with classic furniture">Роскошный интерьер</button>
|
| 890 |
+
<button type="button" class="style-btn" data-value="in a dense, magical forest with sunbeams filtering through">Сказочный лес</button>
|
| 891 |
+
<button type="button" class="style-btn" data-value="on a beautiful sandy beach during the golden hour">Пляж (золотой час)</button>
|
| 892 |
+
<button type="button" class="style-btn" data-value="in a vibrant, bustling urban market">Городской рынок</button>
|
| 893 |
+
<button type="button" class="style-btn" data-value="inside a futuristic, sci-fi corridor with glowing lines">НФ коридор</button>
|
| 894 |
+
<button type="button" class="style-btn" data-value="in an old, atmospheric library with tall bookshelves">Старая библиотека</button>
|
| 895 |
+
<button type="button" class="style-btn" data-value="at a vibrant, colorful carnival at night">Ночной карнавал</button>
|
| 896 |
+
</div>
|
| 897 |
</div>
|
| 898 |
<div class="form-group full-width">
|
| 899 |
<label for="model_details">Одежда и Детали (Опишите ткань и фасон!)</label>
|
|
|
|
| 1202 |
document.getElementById('object_background').disabled = isCreative;
|
| 1203 |
}
|
| 1204 |
|
| 1205 |
+
function setupClickableSelectors() {
|
| 1206 |
+
const selectors = ['styleSelector', 'locationSelector'];
|
| 1207 |
+
selectors.forEach(selectorId => {
|
| 1208 |
+
const container = document.getElementById(selectorId);
|
| 1209 |
+
if (!container) return;
|
| 1210 |
+
const buttons = container.querySelectorAll('.style-btn');
|
| 1211 |
+
|
| 1212 |
+
buttons.forEach(btn => {
|
| 1213 |
+
btn.addEventListener('click', () => {
|
| 1214 |
+
buttons.forEach(innerBtn => innerBtn.classList.remove('active'));
|
| 1215 |
+
btn.classList.add('active');
|
| 1216 |
+
});
|
| 1217 |
});
|
| 1218 |
});
|
| 1219 |
}
|
| 1220 |
|
| 1221 |
+
|
| 1222 |
async function processAndOpen() {
|
| 1223 |
const btn = document.querySelector('.action-btn');
|
| 1224 |
const originalText = btn.innerHTML;
|
|
|
|
| 1230 |
const shotType = document.getElementById('shotType').value;
|
| 1231 |
const bodyType = document.getElementById('bodyType').value;
|
| 1232 |
const pose = document.getElementById('pose').value;
|
| 1233 |
+
const location = document.querySelector('#locationSelector .style-btn.active').dataset.value;
|
| 1234 |
const light = document.getElementById('light').value;
|
| 1235 |
const camera = document.getElementById('camera').value;
|
| 1236 |
|
| 1237 |
if (isMyModel) {
|
| 1238 |
const details = document.getElementById('model_details').value || "the clothing from the reference image";
|
| 1239 |
fullPrompt = `${envKeyword}, VIRTUAL TRY-ON.
|
| 1240 |
+
INSTRUCTIONS: This is a virtual try-on task. Use the model's photo for identity, face, and pose. Use the garment photo for the clothing.
|
| 1241 |
+
Task: Accurately transfer the clothing onto the model. The final image must preserve the model's exact facial identity, body shape of "${bodyType}", and pose.
|
| 1242 |
+
Style: ${style}, hyper-detailed, luxury brand campaign, Vogue aesthetic, ultra-photorealistic.
|
|
|
|
|
|
|
| 1243 |
Composition: ${shotType}.
|
| 1244 |
Final Scene: Place the model in the following environment: ${location}.
|
| 1245 |
+
Lighting: The scene must have ${light}.
|
| 1246 |
+
Technical: Masterpiece professional photograph, shot on ${camera}, 8k UHD, razor-sharp focus, extreme detail, perfect color grading, no digital artifacts.`;
|
| 1247 |
} else {
|
| 1248 |
const age = document.getElementById('age').value;
|
| 1249 |
const nationality = document.getElementById('nationality').value;
|
|
|
|
| 1253 |
const emotion = document.getElementById('emotion').value;
|
| 1254 |
const details = document.getElementById('model_details').value || "high-end fashion garments";
|
| 1255 |
|
| 1256 |
+
fullPrompt = `${envKeyword}, style:: ${style}, high fashion editorial, luxury brand campaign (like Dior, D&G), Vogue aesthetic, hyper-realistic photography, award-winning.
|
| 1257 |
composition:: ${shotType}.
|
| 1258 |
subject:: An ultra-high-resolution, flawless photograph of a striking ${age} ${nationality} high fashion model.
|
| 1259 |
+
model_characteristics:: physique ${bodyType}, ${hairColor} ${hairstyle} with individual strands visible and natural flyaways, expression ${emotion} (powerful yet serene), pose ${pose}.
|
| 1260 |
clothing_focus:: The model is wearing ${details}, emphasizing haute couture craftsmanship.
|
| 1261 |
+
texture_&_material_fidelity:: Extreme macro precision on textiles. Render the fabric weave, individual threads, visible stitching, material weight, realistic creases and folds, tactile surface imperfections like denim twill or wool fibers, and how light interacts with the fabric.
|
| 1262 |
+
human_realism_details:: Capture hyper-realistic skin texture, showing pores, vellus hair, and subtle imperfections. Avoid any plastic or airbrushed look. Expertly sculpted light highlights the bone structure. Eyes must have realistic reflections and depth.
|
| 1263 |
scene_environment:: ${location}, creating a sophisticated and aspirational atmosphere.
|
| 1264 |
+
technical:: Masterpiece professional photograph, ${light} meticulously crafted to sculpt the subject with soft shadows and catchlights in eyes, shot on ${camera}, 8k UHD, razor-sharp focus, breathtaking detail, uncompressed, color graded to perfection.`;
|
| 1265 |
}
|
| 1266 |
|
| 1267 |
} else if (currentMode === 'children') {
|
|
|
|
| 1279 |
const emotion = document.getElementById('newborn_emotion').value;
|
| 1280 |
const pose = document.getElementById('newborn_pose').value;
|
| 1281 |
clothing_details = document.getElementById('newborn_details').value || "soft knitted fabric";
|
| 1282 |
+
subject = `An ultra-photorealistic portrait of a ${age} ${ethnicity} baby. Face expression is ${emotion}.`;
|
| 1283 |
pose_info = `The baby is ${pose}.`;
|
| 1284 |
} else {
|
| 1285 |
const age = document.getElementById('child_age').value;
|
|
|
|
| 1288 |
const emotion = document.getElementById('child_emotion').value;
|
| 1289 |
const pose = document.getElementById('child_pose').value;
|
| 1290 |
clothing_details = document.getElementById('child_details').value || "detailed textured casual clothes";
|
| 1291 |
+
subject = `An ultra-photorealistic portrait of a ${age} ${ethnicity} ${gender}. Face expression is ${emotion}.`;
|
| 1292 |
pose_info = `The child is ${pose}.`;
|
| 1293 |
}
|
| 1294 |
|
|
|
|
| 1296 |
composition:: ${shotType}.
|
| 1297 |
subject:: ${subject} The photograph must look like a cover shot for a high-end children's fashion magazine.
|
| 1298 |
clothing_focus:: The child is wearing ${clothing_details}, presented as a luxury garment.
|
| 1299 |
+
texture_&_material_fidelity:: Macro-level detail on clothing textures. Focus on the weave of cotton, the softness of wool fibers, the texture of denim. Show realistic wrinkles, creases from movement, and even subtle fabric pilling. Absolute texture fidelity is crucial.
|
| 1300 |
+
human_realism_details:: Capture the pure, innocent beauty of the child. Hyper-realistic, dewy skin with a natural glow and visible texture, not airbrushed. The light should have a painterly, almost magical quality, highlighting their features beautifully. Eyes must be expressive, with realistic reflections and depth. Individual hair strands should be visible.
|
| 1301 |
scene_activity:: ${pose_info} The location is ${location}, creating a whimsical and high-end narrative.
|
| 1302 |
technical:: Masterpiece photograph, ${light} creating a magical and soft atmosphere, shot on Fujifilm XT4, 56mm F1.2 lens, 8k, tack sharp focus, impeccable detail, perfect color grading, looks like a real captured moment of wonder from a luxury campaign.`;
|
| 1303 |
|
|
|
|
| 1317 |
}
|
| 1318 |
|
| 1319 |
fullPrompt = `${envKeyword}, style:: Luxury product advertising, ${objectStyle}, sophisticated, sleek, ultra-photorealistic.
|
| 1320 |
+
subject:: A breathtaking, hyper-realistic photograph of the luxury product: ${objectName}. The image must evoke desire and exclusivity, looking like a real, professionally shot advertisement.
|
| 1321 |
+
material_focus:: Achieve 1000% physical accuracy with an emphasis on perfection. Render pristine, flawless surfaces with micro-scratches and realistic imperfections. Showcase the intricate details of the material grain, polished metal sheen with fingerprints, or crystal-clear refractions. Even microscopic dust particles should look clean and perfect.
|
| 1322 |
scene_context:: Placed ${background}. Additional details: ${objectDetails}, arranged with artistic precision.
|
| 1323 |
composition:: ${objectComposition}, creating a powerful and elegant visual statement.
|
| 1324 |
+
technical:: Advertisement-grade photograph, ${objectLighting} designed to accentuate the product's luxury form and textures, 8k UHD resolution, flawless focus, extreme macro detail, advanced ray-traced reflections and caustics, impeccably clean, exudes quality and high-end appeal, masterpiece.`;
|
| 1325 |
}
|
| 1326 |
|
| 1327 |
const cleanPrompt = fullPrompt.replace(/\\s+/g, ' ').replace(/\\n/g, ' ').trim();
|
|
|
|
| 1350 |
switchMode('model');
|
| 1351 |
switchChildrenSubMode('newborn');
|
| 1352 |
autoAdjustDefaults();
|
| 1353 |
+
setupClickableSelectors();
|
| 1354 |
});
|
| 1355 |
</script>
|
| 1356 |
|
|
|
|
| 1365 |
@app.route('/admhosto', methods=['GET'])
|
| 1366 |
def admhosto():
|
| 1367 |
data = load_data()
|
| 1368 |
+
active_environments = []
|
| 1369 |
+
archived_environments = []
|
| 1370 |
+
|
| 1371 |
for env_id, env_data in data.items():
|
| 1372 |
+
env_item = {
|
| 1373 |
"id": env_id,
|
| 1374 |
"keyword": env_data.get("keyword", "N/A"),
|
| 1375 |
"type": env_data.get("type", "closed"),
|
| 1376 |
"hits": env_data.get("hits", 0),
|
| 1377 |
"created_at": env_data.get("created_at", ""),
|
| 1378 |
"link": url_for('serve_env', env_id=env_id, _external=True)
|
| 1379 |
+
}
|
| 1380 |
+
if env_data.get("archived"):
|
| 1381 |
+
archived_environments.append(env_item)
|
| 1382 |
+
else:
|
| 1383 |
+
active_environments.append(env_item)
|
| 1384 |
+
|
| 1385 |
+
active_environments.sort(key=lambda x: x['created_at'], reverse=True)
|
| 1386 |
+
archived_environments.sort(key=lambda x: x['created_at'], reverse=True)
|
| 1387 |
|
| 1388 |
+
return render_template_string(ADMHOSTO_TEMPLATE, active_environments=active_environments, archived_environments=archived_environments)
|
| 1389 |
|
| 1390 |
@app.route('/admhosto/create', methods=['POST'])
|
| 1391 |
def create_environment():
|
|
|
|
| 1408 |
"device_token": None,
|
| 1409 |
"hits": 0,
|
| 1410 |
"logs": [],
|
| 1411 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 1412 |
+
"archived": False
|
| 1413 |
}
|
| 1414 |
save_data(all_data)
|
| 1415 |
flash(f'Новая {env_type} среда с ID {new_id} создана.', 'success')
|
|
|
|
| 1419 |
def delete_environment(env_id):
|
| 1420 |
all_data = load_data()
|
| 1421 |
if env_id in all_data:
|
| 1422 |
+
all_data[env_id]['archived'] = True
|
| 1423 |
+
save_data(all_data)
|
| 1424 |
+
flash(f'Среда {env_id} перемещена в архив.', 'success')
|
| 1425 |
+
else:
|
| 1426 |
+
flash(f'Среда {env_id} не найдена.', 'error')
|
| 1427 |
+
return redirect(url_for('admhosto'))
|
| 1428 |
+
|
| 1429 |
+
@app.route('/admhosto/restore/<env_id>', methods=['POST'])
|
| 1430 |
+
def restore_environment(env_id):
|
| 1431 |
+
all_data = load_data()
|
| 1432 |
+
if env_id in all_data:
|
| 1433 |
+
all_data[env_id]['archived'] = False
|
| 1434 |
+
save_data(all_data)
|
| 1435 |
+
flash(f'Среда {env_id} восстановлена из архива.', 'success')
|
| 1436 |
+
else:
|
| 1437 |
+
flash(f'Среда {env_id} не найдена.', 'error')
|
| 1438 |
+
return redirect(url_for('admhosto'))
|
| 1439 |
+
|
| 1440 |
+
@app.route('/admhosto/clear_user/<env_id>', methods=['POST'])
|
| 1441 |
+
def clear_user(env_id):
|
| 1442 |
+
all_data = load_data()
|
| 1443 |
+
if env_id in all_data and all_data[env_id].get('type') == 'closed':
|
| 1444 |
+
all_data[env_id]['device_token'] = None
|
| 1445 |
+
save_data(all_data)
|
| 1446 |
+
flash(f'Пользователь отвязан от среды {env_id}.', 'success')
|
| 1447 |
+
else:
|
| 1448 |
+
flash(f'Ошибка: Среда не найдена или не является закрытой.', 'error')
|
| 1449 |
+
return redirect(url_for('admhosto'))
|
| 1450 |
+
|
| 1451 |
+
@app.route('/admhosto/toggle_type/<env_id>', methods=['POST'])
|
| 1452 |
+
def toggle_type(env_id):
|
| 1453 |
+
all_data = load_data()
|
| 1454 |
+
if env_id in all_data:
|
| 1455 |
+
current_type = all_data[env_id].get('type', 'closed')
|
| 1456 |
+
if current_type == 'closed':
|
| 1457 |
+
all_data[env_id]['type'] = 'open'
|
| 1458 |
+
flash(f'Среда {env_id} теперь открыта.', 'success')
|
| 1459 |
+
else:
|
| 1460 |
+
all_data[env_id]['type'] = 'closed'
|
| 1461 |
+
all_data[env_id]['device_token'] = None
|
| 1462 |
+
flash(f'Среда {env_id} теперь закрыта. Пользователь сброшен.', 'success')
|
| 1463 |
save_data(all_data)
|
|
|
|
| 1464 |
else:
|
| 1465 |
flash(f'Среда {env_id} не найдена.', 'error')
|
| 1466 |
return redirect(url_for('admhosto'))
|
|
|
|
| 1501 |
def serve_env(env_id):
|
| 1502 |
data = load_data()
|
| 1503 |
env_data = data.get(env_id)
|
| 1504 |
+
if not env_data or env_data.get("archived"):
|
| 1505 |
+
return "Среда не найдена или заархивирована.", 404
|
| 1506 |
|
| 1507 |
keyword = env_data.get("keyword", "")
|
| 1508 |
env_type = env_data.get("type", "closed")
|
|
|
|
| 1510 |
current_log = {
|
| 1511 |
"time": datetime.utcnow().isoformat(),
|
| 1512 |
"ip": request.remote_addr,
|
| 1513 |
+
"ua": request.headers.get('User-Agent')[:150]
|
| 1514 |
}
|
| 1515 |
|
| 1516 |
env_data['hits'] = env_data.get('hits', 0) + 1
|