add web
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Not Found{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.sidebar,
|
||||
#footer,
|
||||
#websitewidemessage,
|
||||
.subnav-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}</style>
|
||||
<div style="min-height: 100vh;width: 100%;" class="d-flex align-items-center justify-content-center">
|
||||
<div class="p-3" style="background-color: rgb(31,31,31);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);">
|
||||
<h1>403 Forbidden</h1>
|
||||
<p>You do not have the required permission to access this page, please contact an administrator if you think this is a mistake.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Not Found{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.sidebar,
|
||||
#footer,
|
||||
#websitewidemessage,
|
||||
.subnav-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}</style>
|
||||
<div style="min-height: 100vh;width: 100%;" class="d-flex align-items-center justify-content-center">
|
||||
<div class="p-3" style="background-color: rgb(31,31,31);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);">
|
||||
<h1>404 Not Found</h1>
|
||||
<p>This page does not exist or has been removed, we apologise for any inconvenience caused.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Method Not Allowed{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.sidebar,
|
||||
#footer,
|
||||
#websitewidemessage,
|
||||
.subnav-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}</style>
|
||||
<div style="min-height: 100vh;width: 100%;" class="d-flex align-items-center justify-content-center">
|
||||
<div class="p-3" style="background-color: rgb(31,31,31);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);">
|
||||
<h1>405 Method Not Allowed</h1>
|
||||
<p>The method is not allowed for the requested URL.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Not Found{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.sidebar,
|
||||
#footer,
|
||||
#websitewidemessage,
|
||||
.subnav-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}</style>
|
||||
<div style="min-height: 100vh;width: 100%;" class="d-flex align-items-center justify-content-center">
|
||||
<div class="p-3" style="background-color: rgb(31,31,31);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);">
|
||||
<h1 class="mb-1">500 Internal Server Error</h1>
|
||||
<p class="mb-2">Uh oh, something went wrong on our end, we apologise for any inconvenience caused.</p>
|
||||
{% if error %}
|
||||
<div class="form-floating">
|
||||
<textarea class="form-control" style="resize: none;min-height: 250px;" readonly>
|
||||
ExecType: {{error.type}}
|
||||
ExecValue: {{error.value}}
|
||||
===== TRACEBACK =====
|
||||
{{error.traceback}}
|
||||
======== END ========
|
||||
</textarea>
|
||||
<label for="floatingTextarea2">Error Info</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-secondary" style="font-size:12px" class="mt-2">Route: {{page}} | Please report this error in our Discord Server</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,184 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
|
||||
<head>
|
||||
<!--[if IE 6]>
|
||||
trans rights are human rights!
|
||||
kaitlyn was here ;)
|
||||
view the source for this at https://github.com/ariezmeoww/vortexi-src
|
||||
<![endif]-->
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
|
||||
|
||||
* {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-optical-sizing: auto;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin: 0 .05em 0 .1em;
|
||||
vertical-align: -0.1em;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-top: -50px !important;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="/static/img/vortex.ico" type="image/x-icon" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/twemoji@latest/dist/twemoji.min.js" crossorigin="anonymous"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
||||
<link href="/static/css/global.css" rel="stylesheet" />
|
||||
<link href="/static/css/header.css?random={{ BuildVersionDontGetThisMixedUpCuh }}" rel="stylesheet" />
|
||||
{% if currentuser: %}
|
||||
<script src="/static/js/presence.js"></script>{% endif %}
|
||||
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
|
||||
<title>{% block title %}{% endblock %} - Vortexi</title>
|
||||
<meta name="robots" content="noindex">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body {% if website_wide_message %}class="has-alert" {% endif %}>
|
||||
<div class="width-100">
|
||||
{% if currentuser: %}
|
||||
<button class="toggle-sidebar btn btn-link text-white d-lg-none position-fixed top-0 start-0 m-2 z-3">
|
||||
<i class="bi bi-list fs-4"></i>
|
||||
</button>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-nav">
|
||||
<a href="/home" class="nav-link"><i class="bi bi-house-fill"></i> Home</a>
|
||||
<a href="/users/{{currentuser.id}}/profile" class="nav-link"><i class="bi bi-person-fill"></i> Profile</a>
|
||||
<a href="/games" class="nav-link"><i class="bi bi-controller"></i> Games</a>
|
||||
<a href="/catalog" class="nav-link"><i class="bi bi-shop"></i> Catalog</a>
|
||||
<a href="/develop" class="nav-link"><i class="bi bi-code-square"></i> Develop</a>
|
||||
<a href="/users/{{currentuser.id}}/friends" class="nav-link">
|
||||
<i class="bi bi-people-fill"></i> Friends
|
||||
{% if currentuser.friend_requests > 0: %}
|
||||
<span class="badge text-bg-danger ms-1">{{currentuser.friend_requests}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/groups" class="nav-link"><i class="bi bi-collection"></i> Groups</a>
|
||||
<a href="/users" class="nav-link"><i class="bi bi-person-fill"></i> Users</a>
|
||||
<a href="/avatar" class="nav-link"><i class="bi bi-person-badge"></i> Avatar</a>
|
||||
<a href="/messages" class="nav-link">
|
||||
<i class="bi bi-envelope-fill"></i> Messages
|
||||
{% if currentuser.unread_messages > 0: %}
|
||||
<span class="badge text-bg-danger ms-1">{{currentuser.unread_messages}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/trade" class="nav-link">
|
||||
<i class="bi bi-arrow-left-right"></i> Trades
|
||||
{% if currentuser.inbound_trades > 0: %}
|
||||
<span class="badge text-bg-danger ms-1">{{currentuser.inbound_trades}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/download" class="nav-link"><i class="bi bi-download"></i> Downloads</a>
|
||||
<a href="https://discord.gg/REPLACEME" class="nav-link"><i class="bi bi-discord"></i> Discord</a>
|
||||
{% if currentuser.is_admin: %}
|
||||
<a href="/admin/" class="nav-link">
|
||||
<i class="bi bi-shield-fill"></i> Admin Panel
|
||||
{% if currentuser.pending_asset_count > 0: %}
|
||||
<span class="badge text-bg-danger ms-1">{{currentuser.pending_asset_count}}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<nav class="navbar">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="toggle-sidebar btn btn-link text-white me-2" type="button">
|
||||
<i class="bi bi-list fs-4"></i>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/home">
|
||||
<h1 class="title-logo">Vortexi</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div
|
||||
class=" d-inline-flex align-items-center bg-dark text-white rounded-pill px-3 py-1 border shadow-sm gap-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="rbx-icon-nav-robux me-1 text-white"></span>
|
||||
<span class="fw-semibold" id="robux-amount">{{currentuser.robux}}</span>
|
||||
</div>
|
||||
|
||||
<div class=" d-flex align-items-center">
|
||||
<span class="rbx-icon-nav-tix me-1 text-white"></span>
|
||||
<span class="fw-semibold" id="tix-amount">{{currentuser.tix}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="nav-item dropdown d-inline-flex align-items-center rounded-pill px-3 py-1 bg-dark border shadow-sm">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center p-0" href="#" role="button"
|
||||
data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<img class="rounded-circle me-2" width="32" height="32"
|
||||
src="/Thumbs/Head.ashx?x=48&y=48&userId={{currentuser.id}}" alt="{{currentuser.username}}" />
|
||||
<b><span class="d-none d-md-inline">{{currentuser.username}}</span></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="/settings">Settings</a></li>
|
||||
<li><a class="dropdown-item" href="/membership">Membership</a></li>
|
||||
<li><a class="dropdown-item" href="/giftcard-redeem">Redeem Codes</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<form action="/logout" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<button class="dropdown-item text-danger fw-semibold" type="submit">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="w-100 border-bottom subnav-bar ps-3 pe-3">
|
||||
<a href="/users/{{currentuser.id}}/profile">Profile</a>
|
||||
<a href="/users/{{currentuser.id}}/friends">Friends{% if currentuser.friend_requests > 0: %}<span
|
||||
class="badge text-bg-danger ms-1">{{currentuser.friend_requests}}</span>{%endif%}</a>
|
||||
<a href="/users">Users</a>
|
||||
<a href="/avatar">Avatar</a>
|
||||
<a href="/messages">Messages{% if currentuser.unread_messages > 0: %}<span
|
||||
class="badge text-bg-danger ms-1">{{currentuser.unread_messages}}</span>{%endif%}</a>
|
||||
<a href="/groups">Groups</a>
|
||||
<a href="/trade">Trades{% if currentuser.inbound_trades > 0: %}<span
|
||||
class="badge text-bg-danger ms-1">{{currentuser.inbound_trades}}</span>{%endif%}</a>
|
||||
<a href="/download">Downloads</a>
|
||||
<a href="https://discord.gg/REPLACEME">Discord</a>
|
||||
{% if currentuser.is_admin: %}
|
||||
<a href="/admin/">Admin Panel{% if currentuser.pending_asset_count > 0: %}<span
|
||||
class="badge text-bg-danger ms-1">{{currentuser.pending_asset_count}}</span>{%endif%}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if website_wide_message: %}
|
||||
<div id="websitewidemessage" class="alert text-white" role="alert">
|
||||
<p>{{ website_wide_message | safe }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script src="/static/js/global.js" crossorigin="anonymous"></script>
|
||||
<script src="/static/js/twemoji.js" crossorigin="anonymous"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}About us{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container" style="min-height: 100vh;margin-top: 100px;">
|
||||
<h1>About Vortexi</h1>
|
||||
</div>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Asset Copier</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<p class="m-0" style="font-size:small;">Do not abuse this tool, allowed assets: ( TShirt, Hat, Shirt, Pants, Head, Face, Gear, Accessories )</p>
|
||||
<p class="m-0 text-danger" style="font-size:small;">WARNING: When cloning accessories please be careful cloning 2016+ assets as some accessories attachments are too new for our client to handle</p>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" class="d-flex">
|
||||
<input type="text" name="asseturl" class="form-control" placeholder="Asset URL">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary ms-3" value="Copy Asset">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,119 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Asset Moderation</h1>
|
||||
<p class="m-0" style="font-size:small;">Do not approve any controversial, NSFW, racist, anti semitism, furry content and aggressive swearing</p>
|
||||
<p class="m-0" style="font-size:small;">For images every second any transparent part of the image will change colors this will help find any hidden content</p>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="linebreak"></div>
|
||||
{% for pendingAsset in PendingAssets: %}
|
||||
<div class="border p-2 mb-1">
|
||||
{% if 'Asset' in pendingAsset %}
|
||||
<h5 class="m-0">{{pendingAsset.Asset.name}}</h5>
|
||||
<div class="d-flex mb-2" style="font-size: small;">
|
||||
<p class="m-0 text-secondary">AssetId: <span class="text-white">{{pendingAsset.Asset.id}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">Created: <span class="text-white">{{pendingAsset.Asset.created_at}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">CreatorId: <span class="text-white">{{pendingAsset.Asset.creator_id}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">Creator Type: <span class="text-white">{% if pendingAsset.Asset.creator_type == 1%}Group{%else%}User{%endif%}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">AssetType: <span class="text-white">{{pendingAsset.Asset.asset_type.name}}</span></p>
|
||||
</div>
|
||||
{%endif%}
|
||||
{% if 'Group' in pendingAsset %}
|
||||
<h5 class="m-0">{{pendingAsset.Group.name}}</h5>
|
||||
<div class="d-flex mb-2" style="font-size: small;">
|
||||
<p class="m-0 text-secondary">GroupId: <span class="text-white">{{pendingAsset.Group.id}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">Created: <span class="text-white">{{pendingAsset.Icon.created_at}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">CreatorId: <span class="text-white">{{pendingAsset.Creator.id}}</span></p>
|
||||
</div>
|
||||
{%endif%}
|
||||
{% if pendingAsset.ParentAsset: %}
|
||||
<div class="d-flex border p-1" style="font-size: small;">
|
||||
<p class="m-0 text-secondary">Parent AssetId: <span class="text-white">{{pendingAsset.ParentAsset.id}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">Parent Name: <span class="text-white">{{pendingAsset.ParentAsset.name}}</span></p>
|
||||
<p class="m-0 text-secondary ms-2">Parent AssetType: <span class="text-white">{{pendingAsset.ParentAsset.asset_type.name}}</span></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if 'Asset' in pendingAsset %}
|
||||
{% if pendingAsset.Asset.asset_type.value == 1 and not pendingAsset.AssetThumbnail: %}
|
||||
<div class="d-flex justify-content-center">
|
||||
<img width="512" height="512" class="mt-2 content-image" src="https://sussybakaamongus.s3.eu-north-1.amazonaws.com/{{pendingAsset.AssetVersion.content_hash}}">
|
||||
</div>
|
||||
{%endif%}
|
||||
{% if pendingAsset.Asset.asset_type.value == 9: %}
|
||||
{% if pendingAsset.PlaceIcon: %}
|
||||
<div class="d-flex justify-content-center">
|
||||
<img width="512" height="512" class="mt-2 content-image" src="https://sussybakaamongus.s3.eu-north-1.amazonaws.com/{{pendingAsset.AssetVersion.contenthash}}">
|
||||
</div>
|
||||
{%endif%}
|
||||
{%endif%}
|
||||
{% if pendingAsset.Asset.asset_type.value == 3 %}
|
||||
<audio controls class="ms-auto me-auto">
|
||||
<source src="https://sussybakaamongus.s3.eu-north-1.amazonaws.com/{{pendingAsset.AssetVersion.content_hash}}" type="audio/mpeg">
|
||||
</audio>
|
||||
{%endif%}
|
||||
{%endif%}
|
||||
{% if pendingAsset.AssetThumbnail: %}
|
||||
<div class="d-flex justify-content-center">
|
||||
<img class="mt-2 content-image" src="https://sussybakaamongus.s3.eu-north-1.amazonaws.com/{{pendingAsset.AssetVersion.content_hash}}" style="max-width: 512px;height: auto;max-height: 1024px;">
|
||||
</div>
|
||||
{%endif%}
|
||||
{% if 'Group' in pendingAsset %}
|
||||
{% if pendingAsset.Icon: %}
|
||||
<div class="d-flex justify-content-center">
|
||||
<img width="512" height="512" class="mt-2 content-image" src="https://sussybakaamongus.s3.eu-north-1.amazonaws.com/{{pendingAsset.Icon.content_hash}}">
|
||||
</div>
|
||||
{%endif%}
|
||||
{%endif%}
|
||||
{% if 'Asset' in pendingAsset %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="post" action="/admin/pending-assets/{%if pendingAsset.AssetThumbnail%}{{pendingAsset.AssetVersion.id}}{%else%}{{pendingAsset.Asset.id}}{%endif%}/approve{%if pendingAsset.PlaceIcon%}-icon{%endif%}{%if pendingAsset.AssetThumbnail%}-thumbnail{%endif%}"><button class="btn btn-success w-100 btn-sm mt-2">Approve Content</button></form>
|
||||
</div>
|
||||
<div class="col">
|
||||
<form method="post" action="/admin/pending-assets/{%if pendingAsset.AssetThumbnail%}{{pendingAsset.AssetVersion.id}}{%else%}{{pendingAsset.Asset.id}}{%endif%}/decline{%if pendingAsset.PlaceIcon%}-icon{%endif%}{%if pendingAsset.AssetThumbnail%}-thumbnail{%endif%}"><button type="submit" class="btn btn-danger w-100 btn-sm mt-2">Deny Content</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{%else%}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="post" action="/admin/pending-assets/{{pendingAsset.Icon.content_hash}}/approve-group-icon"><button class="btn btn-success w-100 btn-sm mt-2">Approve Content</button></form>
|
||||
</div>
|
||||
<div class="col">
|
||||
<form method="post" action="/admin/pending-assets/{{pendingAsset.Icon.content_hash}}/deny-group-icon"><button type="submit" class="btn btn-danger w-100 btn-sm mt-2">Deny Content</button></form>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/* Every second get every image with the classname "content-image" and change the background color to a random gradient */
|
||||
setInterval(() => {
|
||||
let images = document.getElementsByClassName("content-image");
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
let image = images[i];
|
||||
let randomColor = Math.floor(Math.random()*16777215).toString(16);
|
||||
let randomColor2 = Math.floor(Math.random()*16777215).toString(16);
|
||||
let randomAngle = Math.floor(Math.random()*360);
|
||||
image.style.background = `linear-gradient(${randomAngle}deg, #${randomColor}, #${randomColor2})`;
|
||||
}
|
||||
}, 500);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Bundle Copier</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<p class="m-0" style="font-size:small;">Do not abuse this tool, some extra modifications may be needed when cloning a bundle as it is not always perfect once cloned. Also it might take awhile for it copy all assets, and we recommend to not use it when anyone is in game.</p>
|
||||
<p class="m-0 text-danger" style="font-size:small;">WARNING: PLEASE make sure the bundle you are copying has not already been uploaded</p>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" class="d-flex">
|
||||
<input type="text" name="bundle-id" class="form-control" placeholder="Bundle Id">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary ms-3" value="Copy Bundle">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Create Asset</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-floating mt-2">
|
||||
<input type="text" class="form-control" id="asset-name-input" placeholder="Name" name="asset-name" required>
|
||||
<label for="asset-name-input">Asset Name</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<select name="asset-type" class="form-control" required>
|
||||
<option value="8">Hat</option>
|
||||
<option value="17">Head</option>
|
||||
<option value="18">Face</option>
|
||||
<option value="19">Gear</option>
|
||||
<option value="41">Hair Accessory</option>
|
||||
<option value="42">Face Accessory</option>
|
||||
<option value="43">Neck Accessory</option>
|
||||
<option value="44">Shoulder Accessory</option>
|
||||
<option value="45">Front Accessory</option>
|
||||
<option value="46">Back Accessory</option>
|
||||
<option value="47">Waist Accessory</option>
|
||||
<option value="62">Video</option>
|
||||
</select>
|
||||
<label for="floatingNumber">Asset Type</label>
|
||||
</div>
|
||||
|
||||
<input type="file" name="file" id="file" class="form-control mt-2" required/>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-light mt-2 w-100 btn-sm" type="submit">Create Asset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Create Giftcard</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<div class="form-floating">
|
||||
<select id="floatingSelect" class="form-control" name="giftcard-type">
|
||||
<option value="0">Outrageous Builders Club</option>
|
||||
<option value="1">Turbo Builders Club</option>
|
||||
<option value="2">Robux</option>
|
||||
<option value="3">Tickets</option>
|
||||
<option value="4">Item</option>
|
||||
</select>
|
||||
<label for="floatingSelect">Giftcard Type</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<input type="number" class="form-control" id="floatingNumber" placeholder="Value" name="copies" required>
|
||||
<label for="floatingNumber">Copies</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<input type="text" class="form-control" id="floatingInput" placeholder="Value" name="value" required>
|
||||
<label for="floatingInput">Value</label>
|
||||
</div>
|
||||
<p class="m-0" style="font-size: 13px;">OBC GC Value = How many months</p>
|
||||
<p class="m-0" style="font-size: 13px;">TBC GC Value = How many weeks</p>
|
||||
<p class="m-0" style="font-size: 13px;">Robux and Tickets GC Value = How much currency</p>
|
||||
<p class="m-0" style="font-size: 13px;">Item GC Value = Asset ID</p>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-primary mt-2 w-100 btn-sm" type="submit">Create Giftcard</button>
|
||||
</form>
|
||||
{% if codes: %}
|
||||
<div class="linebreak"></div>
|
||||
<h2>Giftcard Codes</h2>
|
||||
<div class="border rounded p-2">
|
||||
{% for code in codes: %}
|
||||
<span class="m-0">{{code}}</span>
|
||||
<br>
|
||||
{%endfor%}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Create User</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="floatingInput" placeholder="Username" name="username" required>
|
||||
<label for="floatingInput">Username</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" name="password" required>
|
||||
<label for="floatingPassword">Password</label>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-primary mt-2 w-100 btn-sm" type="submit">Create User</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a><a href="/admin/fflags/create" class="btn border-success btn-sm mb-2 ms-2">Create New Group</a>
|
||||
<h1>FFlags Groups</h1>
|
||||
<div class="linebreak"></div>
|
||||
<div class="row">
|
||||
{% for fflaggroup in groups %}
|
||||
<div class="col-12">
|
||||
<a href="/admin/fflags/{{fflaggroup.group_id}}">
|
||||
<div class="rounded border p-2 mb-2">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="m-0">{{fflaggroup.name}}</h2>
|
||||
<p class="m-0 text-secondary">{{fflaggroup.description}}</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Created: <span class="text-white">{{fflaggroup.created_at}}</span></p>
|
||||
<p class="text-secondary m-0">Last Updated: <span class="text-white">{{fflaggroup.updated_at}}</span></p>
|
||||
<p class="text-secondary m-0">Enabled: <span class="text-white">{{ fflaggroup.enabled }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,84 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="overflow-x:scroll;">
|
||||
<a href="/admin/fflags" class="btn border-primary btn-sm mb-2">Back to FFlag Groups</a><a href="/admin/fflags/{{group.id}}/delete" class="btn border-danger btn-sm mb-2 ms-2">Delete Group</a>
|
||||
<h1 class="m-0">{{group.name}}</h1>
|
||||
<p class="m-0 d-flex">{{group.description}}</p>
|
||||
<div class="p-2 border mt-2 mb-2 rounded">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Created: <span class="text-white">{{group.created_at}}</span></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Last Updated: <span class="text-white">{{group.updated_at}}</span></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Enabled: <span class="text-white">{{ group.enabled }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<h4 class="m-0">Import from file</h4>
|
||||
<p class="m-0">Note: This will remove all current flags and replace them with the file</p>
|
||||
<form method="post" action="/admin/fflags/{{group.group_id}}/import" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Import</button>
|
||||
</form>
|
||||
<div class="linebreak"></div>
|
||||
<h4>Flags ({{flagcount}})</h4>
|
||||
<form method="get" action="/admin/fflags/{{group.group_id}}">
|
||||
<div class="mb-3 d-flex">
|
||||
<input class="form-control rounded-end-0" type="text" id="search" name="search" placeholder="Search" value="{% if search %}{{search}}{%endif%}">
|
||||
<button type="submit" class="btn btn-primary btn-sm rounded-start-0">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-striped-columns">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col-3">Name</th>
|
||||
<th scope="col-2">Type</th>
|
||||
<th scope="col-7">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for fflag in FFlagsLookupResults %}
|
||||
<tr>
|
||||
<th scope="row">{{fflag.name}}</th>
|
||||
<td>{% if fflag.flag_type == 1: %}boolean{% endif %}{% if fflag.flag_type == 2: %}number{% endif %}{% if fflag.flag_type == 3: %}string{% endif %}</td>
|
||||
<td class="text-wrap w-100">{{ b64decode(fflag.flag_value).decode('utf-8') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="d-flex w-100 mt-2" style="font-size: 14px;">
|
||||
<a class="text-decoration-none ms-auto {% if not FFlagsLookupResults.has_prev: %}text-secondary{%endif%}" {% if FFlagsLookupResults.has_prev: %}href="/admin/fflags/{{group.group_id}}?page={{FFlagsLookupResults.prev_num}}{% if search %}?search={{search}}{%endif%}"{%endif%}>Previous</a>
|
||||
<p class="m-0 ms-2 me-2 text-white">Page {{FFlagsLookupResults.page}} of {{FFlagsLookupResults.pages}}</p>
|
||||
<a class="text-decoration-none me-auto {% if not FFlagsLookupResults.has_next: %}text-secondary{%endif%}" {% if FFlagsLookupResults.has_next: %}href="/admin/fflags/{{group.group_id}}?page={{FFlagsLookupResults.next_num}}{% if search %}?search={{search}}{%endif%}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin/gameservers" class="btn border-primary btn-sm mb-2">Back to Game Server Manager</a>
|
||||
<h1>Create Server</h1>
|
||||
<div class="linebreak"></div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" action="/admin/gameservers/create">
|
||||
<div class="mb-2">
|
||||
<label for="serverNameInput" class="form-label m-0">Server name</label>
|
||||
<input type="text" class="form-control" id="serverNameInput" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="serverIPinput" class="form-label m-0">Server IP</label>
|
||||
<input type="text" class="form-control" id="serverIPinput" name="serverip" placeholder="1.1.1.1" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="serverPortInput" class="form-label m-0">Server Port</label>
|
||||
<input type="number" class="form-control" id="serverPortInput" name="serverport" placeholder="80" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="serverAccessKeyInput" class="form-label m-0">Access Key</label>
|
||||
<input type="password" class="form-control" id="serverAccessKeyInput" name="accesskey" placeholder="Access Key" required>
|
||||
</div>
|
||||
<div class="mb-2 row">
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isThumbnailer" type="checkbox" id="isThumbnailerCheckbox" checked>
|
||||
<label class="form-check-label" for="isThumbnailerCheckbox">
|
||||
<p>Allow Thumbnail Rendering</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isGameHoster" type="checkbox" id="isGameHosterCheckbox" checked>
|
||||
<label class="form-check-label" for="isGameHosterCheckbox">
|
||||
<p>Allow Game server hosting</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 fw-bold">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h1>Are you sure?</h1>
|
||||
<p class="m-0 d-flex text-secondary">You are deleting {{gameserver.serverName}} ( {{gameserver.serverId}} )</p>
|
||||
<form method="post" class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="hidden" name="serverId" value="{{gameserver.serverId}}">
|
||||
<input type="submit" class="btn btn-danger" value="Delete">
|
||||
<a class="btn border-primary" href="/admin/gameservers/{{gameserver.serverId}}">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a><a href="/admin/gameservers/create" class="btn border-success btn-sm mb-2 ms-2">Create server</a>
|
||||
<h1>Game Server Manager</h1>
|
||||
<p class="m-0 d-flex text-secondary">Please be careful when adding and editing gameservers!</p>
|
||||
<div class="linebreak"></div>
|
||||
<div class="row">
|
||||
{% for gameserver in gameservers %}
|
||||
<div class="col-12">
|
||||
<a href="/admin/gameservers/{{gameserver.serverId}}">
|
||||
<div class="gameserver-box rounded border p-2 mb-2 {% if not gameserver.isRCCOnline: %}redborder {% else %} {% if gameserver.heartbeatResponseTime > 0.6 %}yellowborder{% else %}greenborder{% endif %}{% endif %}">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h2 class="m-0">{{gameserver.serverName}}</h2>
|
||||
<p class="m-0 text-secondary">{{gameserver.serverId}}</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Last heartbeat: <span class="text-white">{{gameserver.lastHeartbeat}}</span></p>
|
||||
<p class="text-secondary m-0">RCC Memory Usage: <span class="text-white">{{gameserver.RCCmemoryUsage}} MB</span></p>
|
||||
<p class="text-secondary m-0">Response Time: <span class="text-white">{{ round(gameserver.heartbeatResponseTime,3) }} secs</span></p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Thumbnail Queue Size: <span class="text-white">{{gameserver.thumbnailQueueSize}}</span></p>
|
||||
<p class="text-secondary m-0">isThumbnailer: <span class="text-white">{{gameserver.allowThumbnailGen}}</span></p>
|
||||
<p class="text-secondary m-0">isGameHoster: <span class="text-white">{{ gameserver.allowGameServerHost }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h1>Are you sure?</h1>
|
||||
<p class="m-0 d-flex text-secondary">Are you sure you want to refresh the AccessKey for {{gameserver.serverName}} ( {{gameserver.serverId}} )</p>
|
||||
<form method="post" class="mt-2">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="hidden" name="serverId" value="{{gameserver.serverId}}">
|
||||
<button type="submit" class="btn btn-light"><i class="bi bi-arrow-clockwise"></i> Refresh Key</button>
|
||||
<a class="btn border-primary" href="/admin/gameservers/{{gameserver.serverId}}">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,84 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(175, 175, 175) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="/admin/gameservers" class="btn border-primary btn-sm mb-2">Back to Game Server Manager</a>
|
||||
<a href="/admin/gameservers/{{gameserver.serverId}}/delete" class="btn border-danger btn-sm mb-2 ms-2">Delete Server</a>
|
||||
<a href="/admin/gameservers/{{gameserver.serverId}}/refresh-accesskey" class="btn border-success btn-sm mb-2 ms-2">Refresh AccessKey</a>
|
||||
</div>
|
||||
<h1 class="m-0">{{gameserver.serverName}}</h1>
|
||||
<p class="m-0 d-flex">{{gameserver.serverId}}</p>
|
||||
<div class="p-2 border mt-2 mb-2 rounded {% if not gameserver.isRCCOnline: %}redborder {% else %} {% if gameserver.heartbeatResponseTime > 0.6 %}yellowborder{% else %}greenborder{% endif %}{% endif %}">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<p class="text-secondary m-0">Last heartbeat: <span class="text-white">{{gameserver.lastHeartbeat}}</span></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="text-secondary m-0">RCC Memory Usage: <span class="text-white">{{gameserver.RCCmemoryUsage}} MB</span></p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="text-secondary m-0">Response Time: <span class="text-white">{{ round(gameserver.heartbeatResponseTime,3) }} secs</span></p>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<p class="text-secondary m-0">Thumbnail Queue Size: <span class="text-white">{{gameserver.thumbnailQueueSize}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" action="/admin/gameservers/{{gameserver.serverId}}/edit">
|
||||
<div class="mb-2">
|
||||
<label for="serverNameInput" class="form-label m-0">Server name</label>
|
||||
<input type="text" class="form-control" id="serverNameInput" name="name" placeholder="Name" value="{{gameserver.serverName}}" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="serverIPinput" class="form-label m-0">Server IP</label>
|
||||
<input type="text" class="form-control" id="serverIPinput" name="serverip" placeholder="1.1.1.1" value="{{gameserver.serverIP}}" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="serverPortInput" class="form-label m-0">Server Port</label>
|
||||
<input type="number" class="form-control" id="serverPortInput" name="serverport" placeholder="80" value="{{gameserver.serverPort}}" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="serverAccessKeyInput" class="form-label m-0">Access Key</label>
|
||||
<input type="password" class="form-control" id="serverAccessKeyInput" name="accesskey" placeholder="Access Key" value="{{gameserver.accessKey}}" required>
|
||||
</div>
|
||||
<div class="mb-2 row">
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isThumbnailer" type="checkbox" id="isThumbnailerCheckbox" {% if gameserver.allowThumbnailGen: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isThumbnailerCheckbox">
|
||||
<p>Allow Thumbnail Rendering</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isGameHoster" type="checkbox" id="isGameHosterCheckbox" {% if gameserver.allowGameServerHost: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isGameHosterCheckbox">
|
||||
<p>Allow Game server hosting</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 fw-bold">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Kuromi was here :D-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<div class="d-md-flex align-items-center mb-1">
|
||||
<div class="d-flex align-items-center mb-2 mb-md-0">
|
||||
<img src="/Thumbs/Head.ashx?x=100&y=100&userId={{user.id}}" class="rounded-2" width="90px" height="90px">
|
||||
<div class="ms-2">
|
||||
<h3 class="m-0"><span id="random-greeting"></span>, {{user.username}}!</h3>
|
||||
<p class="m-0" style="font-size: 12px; color: rgb(200,200,200);">Server Time: {{stats.SystemTime}} UTC</b></p>
|
||||
<p class="m-0" style="font-size: 12px; color: rgb(200,200,200);">kuromi was here! :3</b></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center ms-auto p-2 me-2" style="background-color: rgb(27,27,27);box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);">
|
||||
<p class="col-6 col-md-3 m-0">Online: <b class="text-white">{{stats.UsersOnline}}</b></p>
|
||||
<p class="col-6 col-md-3 m-0">In-Game: <b class="text-white">{{stats.UsersPlaying}}</b></p>
|
||||
<p class="col-6 col-md-3 m-0">Active Servers: <b class="text-white">{{stats.PlaceServersCount}}</b></p>
|
||||
<p class="col-6 col-md-3 m-0">Users today: <b class="text-white">{{stats.UsersSignedUpToday}}</b></p>
|
||||
<p class="col-6 col-md-3 m-0">Assets Pending: <b class="text-white">{{stats.PendingAssets}}</b></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-2">
|
||||
{% for permission in permissions %}
|
||||
{% if "Hidden" not in permission: %}
|
||||
<div class="col-md-4 mb-1 p-1">
|
||||
<a href="{{permission.route}}">
|
||||
<div class="admin-box p-2">
|
||||
<h2 class="mb-1 text-truncate"><i class="bi {{permission.icon}} me-2"></i>{{permission.Name}}</h2>
|
||||
<p class="m-0 mb-2 text-truncate">{{permission.Description}}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const greetings = ["Hello", "Bonjour", "Hola", "Ciao", "Hallo", "Olá", "Hej", "Salam", "Namaste", "Konnichiwa"];
|
||||
const randomGreeting = greetings[Math.floor(Math.random() * greetings.length)];
|
||||
document.getElementById("random-greeting").textContent = randomGreeting;
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin/manage-assets/{{asset.id}}" class="btn border-primary btn-sm mb-2">Back to Asset Manager</a>
|
||||
<h1 class="text-secondary">Insert <span class="text-white">{{asset.name}}</span> into Item Release Pool</h1>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if asset %}
|
||||
<form method="post">
|
||||
<label for="assetnameinput" class="form-label m-0">Asset Name</label>
|
||||
<input type="text" name="assetname" id="assetnameinput" class="form-control" placeholder="Name" value="{{asset.name}}">
|
||||
<label for="assetdescriptioninput" class="form-label m-0">Asset Description</label>
|
||||
<textarea type="text" name="assetdescription" id="assetdescriptioninput" class="form-control" placeholder="Description">{{asset.description}}</textarea>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="assetpricerobuxinput" class="form-label m-0">Robux Price</label>
|
||||
<input type="number" name="assetpricerobux" id="assetpricerobuxinput" class="form-control" placeholder="Robux Price" value="{{asset.price_robux}}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="assetpricetixinput" class="form-label m-0">Tickets Price</label>
|
||||
<input type="number" name="assetpricetix" id="assetpricetixinput" class="form-control" placeholder="Tix Price" value="{{asset.price_tix}}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-secondary" style="font-size: small;">note: if you want an item to be free both robux and tix prices have to be set to 0</p>
|
||||
<div class="linebreak"></div>
|
||||
<div class="mt-2">
|
||||
<h5 class="text-center">Offsale After</h5>
|
||||
<div class="row">
|
||||
<div class="col-2 ms-auto">
|
||||
<div class="form-floating">
|
||||
<input type="number" name="offsale-at-hours" id="offsale-at-hours" class="form-control" placeholder="Hours">
|
||||
<label for="offsale-at-hours" class="form-label m-0">Hours</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="form-floating">
|
||||
<input type="number" name="offsale-at-minutes" id="offsale-at-minutes" class="form-control" placeholder="Minutes">
|
||||
<label for="offsale-at-minutes" class="form-label m-0">Minutes</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 me-auto">
|
||||
<div class="form-floating">
|
||||
<input type="number" name="offsale-at-seconds" id="offsale-at-seconds" class="form-control" placeholder="Seconds">
|
||||
<label for="offsale-at-seconds" class="form-label m-0">Seconds</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary m-0 text-center" style="font-size: 13px;">Amount of time item before the item goes offsale after it releases | Leave empty if you do not want an offsale time</p>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<label for="assetserialamount" class="form-label m-0">Serial Amount</label>
|
||||
<input type="number" name="assetserialamount" id="assetserialamount" class="form-control" placeholder="Serial Amount" value="{{asset.serial_count}}">
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isLimited" type="checkbox" id="isLimitedCheckbox" {% if asset.is_limited: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isLimitedCheckbox">
|
||||
<p>Limited</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isLimitedUnique" type="checkbox" id="isLimitedUniqueCheckbox" {% if asset.is_limited_unique: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isLimitedUniqueCheckbox">
|
||||
<p>Limited Unique</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">Created: <span class="text-white">{{asset.created_at}}</span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">Updated: <span class="text-white">{{asset.updated_at}}</span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">Original Roblox AssetId: <span class="text-white">{{asset.roblox_asset_id}}</span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">AssetType: <span class="text-white">{{asset.asset_type.name}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary mt-1 w-100 btn-sm fw-bold" value="Insert Item">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(200,200,200) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Item Release Pool</h1>
|
||||
<p class="m-0 text-secondary" style="font-size:small;">Next Drop Time: <span class="text-white">{{NextItemDropDatetime}}</span></p>
|
||||
<div class="linebreak"></div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div>
|
||||
{% for Item in PlannedItems: %}
|
||||
<div class="bg-dark mt-1 mb-1 p-2">
|
||||
<h5 class="m-0 mb-1 text-secondary">Name: <span class="text-white">{{Item.name}}</span></h5>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Description: <span class="text-white">{{Item.description}}</span></p>
|
||||
<div class="linebreak"></div>
|
||||
<div class="d-flex">
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">AssetId: <span class="text-white">{{Item.id}}</span></p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">Robux Price: <span class="text-white">{{Item.robux_price}}</span></p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">Tickets Price: <span class="text-white">{{Item.tickets_price}}</span></p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">IsLimited: <span class="text-white">{{Item.is_limited}}</span></p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">IsLimitedUnique: <span class="text-white">{{Item.is_limited_unique}}</span></p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">Serial Count: <span class="text-white">{{Item.serial_count}}</span></p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 14px;">Offsale After: <span class="text-white">{{Item.offsale_after}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor%}
|
||||
</div>
|
||||
{% if len(PlannedItems) == 0 %}
|
||||
<p class="m-5 text-secondary text-center">No planned items</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.bg-dark {
|
||||
background-color: rgb(30,30,30) !important;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.75);
|
||||
}
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Lottery</h1>
|
||||
<p class="m-0" style="font-size:small;">Give away limiteds under accounts which is deleted or inactive for more than 31 days. There must be at least 10 users online to give away any limiteds</p>
|
||||
<div class="linebreak"></div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<button type="submit" class="btn btn-primary btn-sm mb-2 w-100">Give Away Selected User Limiteds</button>
|
||||
|
||||
<div style="display: flex;flex-wrap: wrap;gap: 2px;justify-content: flex-start;">
|
||||
{% for User in EligibleUsers: %}
|
||||
<div class="bg-dark p-2 d-flex mb-1 me-1" style="width: calc( 50% - 6px);">
|
||||
<img class="rounded-2 border overflow-hidden me-2" width="48" height="48" src="/Thumbs/Head.ashx?x=48&y=48&userId={{User.id}}">
|
||||
<div>
|
||||
<h5 class="m-0">{{User.username}} {% if User.accountstatus != 1: %}<span class="badge text-bg-danger me-1">Banned</span>{% endif %}{% if User.lastonline < InactiveTime %}<span class="badge text-bg-warning">Inactive</span>{% endif %}</h5>
|
||||
<p class="m-0">Total Limiteds: <span class="text-white">{{ GetTotalLimiteds(User) }}</span></p>
|
||||
</div>
|
||||
<input class="form-check-input h-100 ms-auto" style="aspect-ratio: 1/1;width: auto;" type="radio" value="{{User.id}}" id="user-checkbox-{{User.id}}" name="selected-user">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
</form>
|
||||
{% if len(EligibleUsers) == 0: %}
|
||||
<p class="m-0 text-secondary mt-5 mb-5 text-center w-100">
|
||||
No eligible users found
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,142 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Asset Manager</h1>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" class="d-flex" action="/admin/manage-assets">
|
||||
<input type="text" name="assetid" class="form-control" placeholder="AssetId" {% if asset: %}value="{{asset.id}}"{%endif%}>
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary ms-3" value="Search">
|
||||
</form>
|
||||
{% if asset %}
|
||||
<div class="linebreak"></div>
|
||||
<form method="post">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="/catalog/{{asset.id}}/">< View asset on catalog</a>
|
||||
{% if IsItemEligibleForItemReleasePool and CanManageItemReleasePool: %}
|
||||
<a href="/admin/manage-assets/{{asset.id}}/insert-item-pool" class="ms-auto text-primary">Insert Item into Item Release Pool</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<label for="assetnameinput" class="form-label m-0">Asset Name</label>
|
||||
<input type="text" name="assetname" id="assetnameinput" class="form-control" placeholder="Name" value="{{asset.name}}">
|
||||
<label for="assetdescriptioninput" class="form-label m-0">Asset Description</label>
|
||||
<textarea type="text" name="assetdescription" id="assetdescriptioninput" class="form-control" placeholder="Description">{{asset.description}}</textarea>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label for="assetpricerobuxinput" class="form-label m-0">Robux Price</label>
|
||||
<input type="number" name="assetpricerobux" id="assetpricerobuxinput" class="form-control" placeholder="Robux Price" value="{{asset.price_robux}}">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="assetpricetixinput" class="form-label m-0">Tickets Price</label>
|
||||
<input type="number" name="assetpricetix" id="assetpricetixinput" class="form-control" placeholder="Tix Price" value="{{asset.price_tix}}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-secondary" style="font-size: small;">note: if you want an item to be free both robux and tix prices have to be set to 0</p>
|
||||
<div class="linebreak"></div>
|
||||
<div class="mt-2">
|
||||
<div class="row">
|
||||
<div class="col-6 ms-auto">
|
||||
<div class="form-floating">
|
||||
<input type="datetime-local" name="offsale-at" id="offsale-at" class="form-control" placeholder="Offsale At">
|
||||
<label for="offsale-at" class="form-label m-0">Offsale At</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 me-auto">
|
||||
<div class="form-floating">
|
||||
<select name="offsale-timezone" class="form-control">
|
||||
<option value="0">UTC</option>
|
||||
<option value="1">UTC+1</option>
|
||||
<option value="2">UTC+2</option>
|
||||
<option value="3">UTC+3</option>
|
||||
<option value="4">UTC+4</option>
|
||||
<option value="5">UTC+5</option>
|
||||
<option value="6">UTC+6</option>
|
||||
<option value="7">UTC+7</option>
|
||||
<option value="8">UTC+8</option>
|
||||
<option value="9">UTC+9</option>
|
||||
<option value="10">UTC+10</option>
|
||||
<option value="11">UTC+11</option>
|
||||
<option value="12">UTC+12</option>
|
||||
<option value="-1">UTC-1</option>
|
||||
<option value="-2">UTC-2</option>
|
||||
<option value="-3">UTC-3</option>
|
||||
<option value="-4">UTC-4</option>
|
||||
<option value="-5">UTC-5</option>
|
||||
<option value="-6">UTC-6</option>
|
||||
<option value="-7">UTC-7</option>
|
||||
<option value="-8">UTC-8</option>
|
||||
<option value="-9">UTC-9</option>
|
||||
<option value="-10">UTC-10</option>
|
||||
<option value="-11">UTC-11</option>
|
||||
<option value="-12">UTC-12</option>
|
||||
</select>
|
||||
<label for="timezone">Timezone</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary text-center" style="font-size: 13px;">(UTC) Current offsale time: <span class="text-white">{{asset.offsale_at}}</span> | Note: To remove a offsale date you need to set the item offsale manually</p>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<label for="assetserialamount" class="form-label m-0">Serial Amount</label>
|
||||
<input type="number" name="assetserialamount" id="assetserialamount" class="form-control" placeholder="Serial Amount" value="{{asset.serial_count}}">
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isForsale" type="checkbox" id="isForsaleCheckbox" {% if asset.is_for_sale: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isForsaleCheckbox">
|
||||
<p>For Sale</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isLimited" type="checkbox" id="isLimitedCheckbox" {% if asset.is_limited: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isLimitedCheckbox">
|
||||
<p>Limited</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input class="form-check-input" name="isLimitedUnique" type="checkbox" id="isLimitedUniqueCheckbox" {% if asset.is_limited_unique: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="isLimitedUniqueCheckbox">
|
||||
<p>Limited Unique</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">Created: <span class="text-white">{{asset.created_at}}</span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">Updated: <span class="text-white">{{asset.updated_at}}</span></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">Original Roblox AssetId: <a href="https://www.roblox.com/catalog/{{asset.roblox_asset_id}}/--" target="_blank">{{asset.roblox_asset_id}}</a></p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p clas="m-0 text-secondary">AssetType: <span class="text-white">{{asset.asset_type.name}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary mt-1 w-100 btn-sm fw-bold" value="Save">
|
||||
</form>
|
||||
<form action="/admin/manage-assets/{{asset.id}}/rerender" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-success mt-4 w-100 btn-sm fw-bold" value="Rerender Asset">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,112 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(200,200,200)!important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Asset Moderation</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" class="d-flex" action="/admin/moderate-asset">
|
||||
<input type="text" name="asset-id" class="form-control" placeholder="Asset ID" required {% if AssetObj %}value="{{AssetObj.id}}"{%endif%}>
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary ms-3" value="Search Asset">
|
||||
</form>
|
||||
{% if AssetObj %}
|
||||
<div class="linebreak"></div>
|
||||
<div class="mt-2 p-2">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{AssetObj.id}}&x=512&y=512" width="100%" style="aspect-ratio: 1/1;" class="rounded border">
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="d-flex align-items-center">
|
||||
<h3 class="m-0">{{AssetObj.name}}</h3>
|
||||
<a class="btn btn-light btn-sm ms-auto text-black" href="/catalog/{{AssetObj.id}}/">View Asset Page</a>
|
||||
</div>
|
||||
<div class="border p-2 mt-2 rounded">
|
||||
<h5>Description</h5>
|
||||
<p class="m-0 text-secondary mt-2">{{AssetObj.description}}</p>
|
||||
</div>
|
||||
<div class="mt-2 d-flex" style="flex-wrap: wrap;justify-content: flex-start;gap: 9px;">
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">AssetID: <span class="text-white">{{AssetObj.id}}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Roblox AssetID: <span class="text-white">{{AssetObj.roblox_asset_id}}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Creator: {% if AssetObj.creator_type == 0 %}<a class="text-decoration-none text-primary" href="/admin/manage-users/{{AssetObj.creator_id}}">{{CreatorObj.username}} ( {{CreatorObj.id}} )</a>{%endif%}</p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">CreatorType: <span class="text-white">{% if AssetObj.creator_type == 0 %}User{%else%}Group{%endif%}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Created: <span class="text-white">{{AssetObj.created_at}}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Last Updated: <span class="text-white">{{AssetObj.updated_at}}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">AssetType: <span class="text-white">{{AssetObj.asset_type.name}}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Moderation Status: <span class="text-white">{% if AssetObj.moderation_status == 0: %}Allowed{%elif AssetObj.moderation_status == 2: %}Deleted{%else%}Pending{%endif%}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">IsForSale: <span class="text-white">{{AssetObj.is_for_sale}}</span></p>
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">SaleCount: <span class="text-white">{{AssetObj.sale_count}}</span></p>
|
||||
</div>
|
||||
<h4 class="m-0 mt-3">Quick Moderation Actions</h4>
|
||||
<div class="d-flex border rounded p-2 mt-2">
|
||||
{% if AssetObj.moderation_status != 0 %}
|
||||
<form action="/admin/moderate-asset/{{AssetObj.id}}/quick-approve" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-success" value="Restore Asset to Approved status">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if AssetObj.moderation_status != 1 %}
|
||||
<form action="/admin/moderate-asset/{{AssetObj.id}}/quick-pending" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-warning ms-2" value="Set Asset to Pending status">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if AssetObj.moderation_status != 2 %}
|
||||
<form action="/admin/moderate-asset/{{AssetObj.id}}/quick-delete" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-danger ms-2" value="Content Delete Asset">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h4 class="m-0 mt-3">Linked Assets</h4>
|
||||
<div class="border p-2 mt-2 rounded">
|
||||
{% for asset in RelatedAssets: %}
|
||||
<div class="bg-dark p-2 w-100 rounded mt-1 mb-1">
|
||||
<h5 class="m-0">{{asset.name}}</h5>
|
||||
<div class="d-flex align-items-center w-100" style="flex-wrap: wrap;justify-content: flex-start;gap: 9px;">
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">AssetID: <span class="text-white">{{asset.id}}</span></p>
|
||||
<p class="text-secondary m-0 ms-2" style="font-size: 14px;">AssetType: <span class="text-white">{{asset.asset_type.name}}</span></p>
|
||||
<p class="text-secondary m-0 ms-2" style="font-size: 14px;">Creator: {% if asset.creator_type == 0 %}<a class="text-decoration-none text-primary" href="/admin/manage-users/{{asset.creator_id}}">{{asset.creator_id}}</a>{%endif%}</p>
|
||||
<p class="text-secondary m-0 ms-2" style="font-size: 14px;">CreatorType: <span class="text-white">{% if asset.creator_type == 0 %}User{%else%}Group{%endif%}</span></p>
|
||||
<a class="btn btn-primary ms-auto btn-sm" href="/admin/moderate-asset/{{asset.id}}">View Asset</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if len(RelatedAssets) == 0: %}
|
||||
<p class="text-secondary m-0 w-100 text-center mt-2 mb-2">No Linked Assets</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,129 @@
|
||||
PermissionsDefinition = {
|
||||
'GameServerManager' : {
|
||||
'Name' : 'Server Manager',
|
||||
'Description' : 'View gameserver information and manage them',
|
||||
'route': '/admin/gameservers',
|
||||
'icon': 'bi-database'
|
||||
},
|
||||
'UpdateWebsiteMessage' : {
|
||||
'Name' : 'Website Message',
|
||||
'Description' : 'Update website wide message',
|
||||
'route': '/admin/websitemessage',
|
||||
'icon': 'bi-chat-square-text'
|
||||
},
|
||||
'ManageFFlags' : {
|
||||
'Name' : 'Feature Flags',
|
||||
'Description' : 'Manage feature flags for RCC and Clients',
|
||||
'route': '/admin/fflags',
|
||||
'icon': 'bi-flag'
|
||||
},
|
||||
'CopyAssets' : {
|
||||
'Name' : 'Asset Copier',
|
||||
'Description' : 'Copy assets from the Roblox Catalog',
|
||||
'route': '/admin/asset-copier',
|
||||
'icon': 'bi-files'
|
||||
},
|
||||
'ManageAsset' : {
|
||||
'Name' : 'Manage Asset',
|
||||
'Description' : 'Manage assets under the ROBLOX account',
|
||||
'route': '/admin/manage-assets',
|
||||
'icon': 'bi-file-earmark'
|
||||
},
|
||||
'CreateAsset' : {
|
||||
'Name' : 'Create Asset',
|
||||
'Description' : 'Create assets under the ROBLOX account',
|
||||
'route': '/admin/create-asset',
|
||||
'icon': 'bi-file-earmark-plus'
|
||||
},
|
||||
'AssetModeration' : {
|
||||
'Name' : 'Pending Assets',
|
||||
'Description' : 'View and moderate pending assets',
|
||||
'route': '/admin/pending-assets',
|
||||
'icon': 'bi-hourglass-split'
|
||||
},
|
||||
'ManageUsers' : {
|
||||
'Name' : 'Manage Users',
|
||||
'Description' : 'Manage users and their permissions',
|
||||
'route': '/admin/manage-users',
|
||||
'icon': 'bi-people'
|
||||
},
|
||||
'ManageWebsiteFeatures' : {
|
||||
'Name' : 'Website Features',
|
||||
'Description' : 'Toggle certain features on the website',
|
||||
'route': '/admin/manage-website-features',
|
||||
'icon': 'bi-gear'
|
||||
},
|
||||
'CreateUser' : {
|
||||
'Name' : 'Create User',
|
||||
'Description' : 'Create a user on the website',
|
||||
'route': '/admin/create-user',
|
||||
'icon': 'bi-person-plus'
|
||||
},
|
||||
'CreateGiftcard' : {
|
||||
'Name' : 'Create Giftcard',
|
||||
'Description' : 'Create a giftcard on the website',
|
||||
'route': '/admin/create-giftcard',
|
||||
'icon': 'bi-gift'
|
||||
},
|
||||
'UpdateAssetFile' : {
|
||||
'Name' : 'Update RBXM',
|
||||
'Description' : 'Update the RBXM file of an asset',
|
||||
'route': '/admin/update-asset-file',
|
||||
'icon': 'bi-file-earmark-arrow-up'
|
||||
},
|
||||
'CopyBundle' : {
|
||||
'Name' : 'Copy Bundle',
|
||||
'Description' : 'Copy a bundle from the Roblox Catalog',
|
||||
'route': '/admin/copy-bundle',
|
||||
'icon': 'bi-collection'
|
||||
},
|
||||
'ModerateAsset' : {
|
||||
'Name' : 'Moderate Asset',
|
||||
'Description' : 'Change the status of a user generated asset',
|
||||
'route': '/admin/moderate-asset',
|
||||
'icon': 'bi-file-earmark-check'
|
||||
},
|
||||
'Lottery' : {
|
||||
'Name' : 'Lottery',
|
||||
'Description' : 'Give away limiteds under inactive accounts',
|
||||
'route': '/admin/lottery',
|
||||
'icon': 'bi-cash-stack'
|
||||
},
|
||||
'ManageItemReleases' : {
|
||||
'Name' : 'Item Release Pool',
|
||||
'Description' : 'Manage items in the item release pool',
|
||||
'route': '/admin/item-release-pool',
|
||||
'icon': 'bi-shuffle'
|
||||
},
|
||||
|
||||
'BanUser' : {
|
||||
'Name' : 'Ban User',
|
||||
'Description' : 'Ban a user from the website',
|
||||
'Hidden' : True
|
||||
},
|
||||
'ViewUserLoginHistory' : {
|
||||
'Name' : 'View User Login History',
|
||||
'Description' : 'View the login history of a user',
|
||||
'Hidden' : True
|
||||
},
|
||||
'ViewLoginHistoryDetailed' : {
|
||||
'Name' : 'View Sensitive Login History',
|
||||
'Description' : 'Extra permission to viewing sensitive info in login history like hashed IPs and Session Tokens',
|
||||
'Hidden' : True
|
||||
},
|
||||
'ModifyLimitedAssets' : {
|
||||
'Name' : 'Modify Limited Assets',
|
||||
'Description' : 'Modify limited assets',
|
||||
'Hidden' : True
|
||||
},
|
||||
'ViewItemReleasePoolDrop' : {
|
||||
'Name' : 'View Item Release Pool Drop Time',
|
||||
'Description' : 'Able to see the next item release drop',
|
||||
'Hidden' : True
|
||||
},
|
||||
"ManageAdminPermissions" : {
|
||||
'Name' : 'Manage Admin Permissions',
|
||||
'Description' : 'Manage admin permissions',
|
||||
'Hidden' : True
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Update RBXM</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-floating mt-2">
|
||||
<input type="number" class="form-control" id="floatingNumber" placeholder="Value" name="asset-id" required>
|
||||
<label for="floatingNumber">AssetId</label>
|
||||
</div>
|
||||
|
||||
<input type="file" name="file" id="file" class="form-control mt-2" required/>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-primary mt-2 w-100 btn-sm" type="submit">Update RBXM</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,86 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 text-secondary">Ban <span class="text-white">{{userObj.username}}</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if LastestUserBanObj != None: %}
|
||||
<div class="alert alert-danger">
|
||||
This user currently has a ongoing ban, please revoke the previous ban before banning again.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="linebreak"></div>
|
||||
<form method="post">
|
||||
<div class="form-floating">
|
||||
<select name="ban_type" class="form-control" id="ban-type-select">
|
||||
<option value="0" selected>Warning</option>
|
||||
<option value="1">1 Day Ban</option>
|
||||
<option value="2">3 Day Ban</option>
|
||||
<option value="3">7 Day Ban</option>
|
||||
<option value="4">14 Day Ban</option>
|
||||
<option value="5">30 Day Ban</option>
|
||||
<option value="6">Account Deleted</option>
|
||||
</select>
|
||||
<label for="ban-type-select">Ban Type</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<input type="text" class="form-control" name="ban_reason" id="ban-reason-input" placeholder="Reason" required>
|
||||
<label for="ban-reason-input">Reason</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<input type="text" class="form-control" name="ban_notes" id="ban-notes-input" placeholder="Notes" required>
|
||||
<label for="ban-notes-input">Moderator Note ( Internal )</label>
|
||||
</div>
|
||||
<p class="m-0 text-secondary" style="font-size: 13px;">Please try to include links to evidence that lead to this ban</p>
|
||||
<div class="form-floating mt-2">
|
||||
<input type="password" class="form-control" name="totp_code" id="totp_code-input" placeholder="2FA Code" required>
|
||||
<label for="ban-notes-public-input">2FA Code</label>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<div class="linebreak"></div>
|
||||
{% if not LastestUserBanObj != None: %}
|
||||
<button type="submit" class="btn btn-danger mt-2 w-100 btn-sm">Ban User</button>
|
||||
{% else%}
|
||||
<button class="btn btn-danger mt-2 w-100 btn-sm" disabled>Ban User</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if LastestUserBanObj != None: %}
|
||||
<form method="post" action="/admin/manage-users/{{userObj.id}}/ban-user/revoke-ban?banid={{LastestUserBanObj.id}}">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<button class="btn btn-warning mt-2 w-100 btn-sm">Unban User</button>
|
||||
</form>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Ban History</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="p-1">
|
||||
{% for banRecord in BanHistory %}
|
||||
<div class="w-100 bg-dark p-2 mb-2 mt-2">
|
||||
<h4 class="m-0">{{banRecord.ban_type.name}}</h4>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">Reason: <span class="text-white">{{banRecord.reason}}</span></p>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">Moderator Note: <span class="text-white">{{banRecord.moderator_note}}</span></p>
|
||||
<p class="m-0 text-secondary" style="font-size: 12px;">Ban Author: <a href="/admin/manage-users/{{banRecord.author_userid}}">{{GetBanAuthorName(banRecord)}}</a></p>
|
||||
<div class="linebreak"></div>
|
||||
<div class="d-flex">
|
||||
<p class="m-0 text-secondary me-auto ms-auto" style="font-size: 14px;">Banned At: <span class="text-white">{{banRecord.created_at}}</span></p>
|
||||
<p class="m-0 text-secondary me-auto ms-auto" style="font-size: 14px;">Expires At: <span class="text-white">{{banRecord.expires_at}}</span></p>
|
||||
<p class="m-0 text-secondary me-auto ms-auto" style="font-size: 14px;">Acknowledged: <span class="text-white">{{banRecord.acknowledged}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if len(BanHistory.items) == 0 %}
|
||||
<p class="text-secondary m-5 text-center w-100">No previous bans</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not BanHistory.has_prev %}text-secondary{%endif%}" {% if BanHistory.has_prev %}href="/admin/manage-users/{{userObj.id}}/ban-history?page={{BanHistory.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{BanHistory.page}} of {{BanHistory.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not BanHistory.has_next %}text-secondary{%endif%}" {% if BanHistory.has_next %}href="/admin/manage-users/{{userObj.id}}/ban-history?page={{BanHistory.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 1200px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Game Sessions</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<tr class="rounded-top">
|
||||
<th scope="col" style="width: 5%;">ID</th>
|
||||
<th scope="col" style="width: 27.5%;">JobId</th>
|
||||
<th scope="col" style="width: 22.5%;">Joined At</th>
|
||||
<th scope="col" style="width: 22.5%;">Left At</th>
|
||||
<th scope="col" style="width: 22.5%;">Place</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider" style="border-color: rgb(20,20,20);">
|
||||
{% for session in GameSessions: %}
|
||||
<tr>
|
||||
<th scope="row">{{session.id}}</th>
|
||||
<td>{{session.serveruuid}}</td>
|
||||
<td>{{session.joined_at}} UTC</td>
|
||||
<td>{{session.left_at}} UTC</td>
|
||||
<td><a class="text-primary" href="/games/{{session.place_id}}/--">{{get_place_name(session.place_id)}}</a> ({{session.place_id}})</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if len(GameSessions.items) == 0: %}
|
||||
<div class="text-center">
|
||||
<p class="text-secondary">No Game Session Logs Found</p>
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not GameSessions.has_prev %}text-secondary{%endif%}" {% if GameSessions.has_prev %}href="/admin/manage-users/{{userObj.id}}/game-sessions?page={{GameSessions.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{GameSessions.page}} of {{GameSessions.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not GameSessions.has_next %}text-secondary{%endif%}" {% if GameSessions.has_next %}href="/admin/manage-users/{{userObj.id}}/game-sessions?page={{GameSessions.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 1200px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Invite Keys</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<tr class="rounded-top">
|
||||
<th scope="col" style="width: 5%;">ID</th>
|
||||
<th scope="col" style="width: 20%;">Key</th>
|
||||
<th scope="col" style="width: 15%;">Created</th>
|
||||
<th scope="col" style="width: 15%;">Used On</th>
|
||||
<th scope="col" style="width: 45%;">Used By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider" style="border-color: rgb(20,20,20);">
|
||||
{% for invite in InviteKeys: %}
|
||||
<tr>
|
||||
<th scope="row">{{invite.id}}</th>
|
||||
<td>{{invite.key}}</td>
|
||||
<td>{{invite.created_at}} UTC</td>
|
||||
<td>{% if invite.used_on != None: %}{{invite.used_on}} UTC{%else%}Unused{%endif%}</td>
|
||||
<td>
|
||||
{% if invite.used_by != None: %}
|
||||
<a href="/admin/manage-users/{{invite.used_by}}" class="text-decoration-none text-truncate">
|
||||
<img class="rounded" style="aspect-ratio: 1/1;width: 35px;" src="/Thumbs/Head.ashx?x=48&y=48&userId={{invite.used_by}}">
|
||||
{{invite.user.username}}
|
||||
</a>
|
||||
{% else%}
|
||||
Unused
|
||||
{%endif%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if len(InviteKeys.items) == 0: %}
|
||||
<div class="text-center">
|
||||
<p class="text-secondary">No Invite Keys found</p>
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not InviteKeys.has_prev %}text-secondary{%endif%}" {% if InviteKeys.has_prev %}href="/admin/manage-users/{{userObj.id}}/invite-keys?page={{InviteKeys.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{InviteKeys.page}} of {{InviteKeys.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not InviteKeys.has_next %}text-secondary{%endif%}" {% if InviteKeys.has_next %}href="/admin/manage-users/{{userObj.id}}/invite-keys?page={{InviteKeys.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,105 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Login History</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if len(AlternateAccounts) != 0 %}
|
||||
<div class="border-top border-bottom p-2 ">
|
||||
<h5 class="m-0 mb-1">Possible Alt Accounts ( {{len( AlternateAccounts )}} )</h5>
|
||||
{% for altAccount in AlternateAccounts %}
|
||||
<a class="d-flex align-items-center text-white text-decoration-none" href="/admin/manage-users/{{altAccount.id}}/login-history">
|
||||
<img height="50px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{altAccount.id}}&x=50&y=50">
|
||||
<div>
|
||||
<h5 class="m-0 {% if altAccount.accountstatus == 1 %}text-white{% elif altAccount.accountstatus == 2 %}text-warning{% elif altAccount.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">
|
||||
{{altAccount.username}}
|
||||
{% if altAccount.flags['hwidmatch'] %}
|
||||
<span class="badge rounded-pill text-bg-light ms-1" style="font-size: 12px;font-weight: 400;">HWID</span>
|
||||
{% endif %}
|
||||
{% if altAccount.flags['sessiontokenmatch'] %}
|
||||
<span class="badge rounded-pill text-bg-danger ms-1" style="font-size: 12px;font-weight: 400;">SessionToken</span>
|
||||
{% endif %}
|
||||
{% if altAccount.flags['ipmatch'] %}
|
||||
<span class="badge rounded-pill text-bg-warning ms-1" style="font-size: 12px;font-weight: 400;">IPAddress</span>
|
||||
{% endif %}
|
||||
{% if altAccount.flags['useragentmatch'] %}
|
||||
<span class="badge rounded-pill text-bg-primary ms-1" style="font-size: 12px;font-weight: 400;">UserAgent</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{altAccount.id}}</span></p>
|
||||
</div>
|
||||
</a>
|
||||
{%endfor%}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if canViewSensitiveInfo %}
|
||||
<div class="p-1">
|
||||
{% for loginRecord in LoginHistory %}
|
||||
<div class="w-100 bg-dark p-2 mb-2 mt-2">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<h5 class="m-0">{{loginRecord.ip}}</h5>
|
||||
<p class="m-0 text-secondary" style="font-size: 13px;">Timestamp: <span class="text-white">{{loginRecord.timestamp}} UTC</span></p>
|
||||
</div>
|
||||
<a data-bs-toggle="collapse" href="#additional-record-data-{{loginRecord.id}}" role="button" class="ms-auto mt-auto mb-auto me-2" style="font-size: 20px;"><i class="bi bi-info-circle"></i></a>
|
||||
</div>
|
||||
<div class="collapse mt-1" id="additional-record-data-{{loginRecord.id}}">
|
||||
<div class="border p-1">
|
||||
<p class="m-0 text-secondary" style="font-size: 13px;">Login Record ID: <span class="text-white">{{loginRecord.id}}</span></p>
|
||||
<div class="border mt-1" style="font-size: 13px;padding: 4px;">
|
||||
<p class="m-0 text-secondary">User Agent</p>
|
||||
<p class="text-white m-0" style="word-break: break-all;white-space: normal;">{{loginRecord.useragent}}</p>
|
||||
</div>
|
||||
<div class="border mt-1" style="font-size: 13px;padding: 4px;">
|
||||
<p class="m-0 text-secondary">Session Token</p>
|
||||
<p class="text-white m-0" style="word-break: break-all;white-space: normal;">{{loginRecord.session_token}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not LoginHistory.has_prev %}text-secondary{%endif%}" {% if LoginHistory.has_prev %}href="/admin/manage-users/{{userObj.id}}/login-history?page={{LoginHistory.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{LoginHistory.page}} of {{LoginHistory.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not LoginHistory.has_next %}text-secondary{%endif%}" {% if LoginHistory.has_next %}href="/admin/manage-users/{{userObj.id}}/login-history?page={{LoginHistory.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,208 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main" data-csrf-token="{{ csrf_token() }}" data-userid="{{userObj.id}}">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Admin Permissions</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border rounded p-2 mt-2 mb-2">
|
||||
<p class="mt-5 mb-5 w-100 text-center text-white" id="status-text" style="display: none;"></p>
|
||||
<div id="data-container">
|
||||
<div class="w-100 bg-dark p-2 mb-2 mt-2 admin-permission-template" style="display: none;">
|
||||
<div class="d-flex align-items-center w-100">
|
||||
<a data-bs-toggle="collapse" href="#admin-permission-info-PermissionName" role="button" class="mt-auto mb-auto me-2 permission-expand-info" style="font-size: 20px;"><i class="bi bi-info-circle"></i></a>
|
||||
<div class="d-flex align-items-center">
|
||||
<h5 class="m-0 permission-name-text">Admin Permission name</h5>
|
||||
<i class="bi ms-2 permission-icon" style="display: none;"></i>
|
||||
</div>
|
||||
<div class="form-check form-switch m-0 me-2 ms-auto">
|
||||
<input class="form-check-input" type="checkbox" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse mt-1 permission-info-container" id="admin-permission-info-PermissionName">
|
||||
<div class="border p-1">
|
||||
<p class="m-0 text-secondary" style="font-size: 13px;">Description: <span class="text-white permission-description-text">Permission Description</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="password" class="form-control mb-2" placeholder="2FA Code" id="2fa-code-input">
|
||||
<input type="submit" class="btn btn-primary w-100 mb-2" value="Save Changes" id="save-changes-button">
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
const ActionCSRFToken = document.getElementById("main").getAttribute("data-csrf-token");
|
||||
const userId = document.getElementById("main").getAttribute("data-userid");
|
||||
const statusText = document.getElementById("status-text");
|
||||
const dataContainer = document.getElementById("data-container");
|
||||
const permissionTemplate = document.getElementsByClassName("admin-permission-template")[0];
|
||||
const saveChangesButton = document.getElementById("save-changes-button");
|
||||
const twoFactorCodeInput = document.getElementById("2fa-code-input");
|
||||
|
||||
var isPermissionsLoaded = false;
|
||||
|
||||
async function setStatusText( new_text ) {
|
||||
if ( new_text ) {
|
||||
statusText.innerText = new_text;
|
||||
statusText.style.display = "block";
|
||||
} else {
|
||||
statusText.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserPermissions() {
|
||||
const response = await fetch(`/admin/manage-users/${userId}/manage-admin-permissions/api/fetch-permissions`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": ActionCSRFToken
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadPermission( permission_data ) {
|
||||
const newPermission = permissionTemplate.cloneNode(true);
|
||||
newPermission.style.display = "block";
|
||||
newPermission.classList.remove("admin-permission-template");
|
||||
newPermission.classList.add("admin-permission-container");
|
||||
newPermission.setAttribute("data-permission-internal-name", permission_data.internal_name);
|
||||
|
||||
newPermission.getElementsByClassName("permission-name-text")[0].innerText = permission_data.friendly_name;
|
||||
newPermission.getElementsByClassName("permission-description-text")[0].innerText = permission_data.description;
|
||||
newPermission.getElementsByClassName("form-check-input")[0].checked = permission_data.hasPermission;
|
||||
newPermission.getElementsByClassName("permission-expand-info")[0].setAttribute("href", `#admin-permission-info-${permission_data.internal_name}`);
|
||||
newPermission.getElementsByClassName("permission-info-container")[0].setAttribute("id", `admin-permission-info-${permission_data.internal_name}`);
|
||||
|
||||
if ( permission_data.bi_icon ) {
|
||||
newPermission.getElementsByClassName("permission-icon")[0].style.display = "inline-block";
|
||||
newPermission.getElementsByClassName("permission-icon")[0].classList.add(permission_data.bi_icon);
|
||||
}
|
||||
|
||||
dataContainer.appendChild(newPermission);
|
||||
}
|
||||
|
||||
async function refreshData( new_data ) {
|
||||
dataContainer.innerHTML = "";
|
||||
|
||||
for ( const permission of new_data ) {
|
||||
await loadPermission( permission );
|
||||
}
|
||||
}
|
||||
|
||||
async function attemptFetchAndLoad() {
|
||||
dataContainer.innerHTML = "";
|
||||
isPermissionsLoaded = false;
|
||||
saveChangesButton.disabled = true;
|
||||
twoFactorCodeInput.disabled = true;
|
||||
setStatusText("Fetching Admin Permissions")
|
||||
|
||||
try{
|
||||
const permissionsData = await fetchUserPermissions();
|
||||
await refreshData( permissionsData );
|
||||
setStatusText();
|
||||
isPermissionsLoaded = true;
|
||||
twoFactorCodeInput.disabled = false;
|
||||
} catch (e) {
|
||||
setStatusText("Failed to fetch Admin Permissions");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
twoFactorCodeInput.addEventListener("input", async function () {
|
||||
if ( !isPermissionsLoaded ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( twoFactorCodeInput.value.length != 6 ) {
|
||||
saveChangesButton.disabled = true;
|
||||
}
|
||||
|
||||
if ( twoFactorCodeInput.value.length == 6 ) {
|
||||
saveChangesButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
saveChangesButton.addEventListener("click", async function() {
|
||||
if ( !isPermissionsLoaded ) {
|
||||
return;
|
||||
}
|
||||
if ( twoFactorCodeInput.value.length != 6 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPermissionsLoaded = false;
|
||||
saveChangesButton.disabled = true;
|
||||
|
||||
const permissionsData = [];
|
||||
const permissionContainers = document.getElementsByClassName("admin-permission-container");
|
||||
for ( const permissionContainer of permissionContainers ) {
|
||||
if ( permissionContainer.getElementsByClassName("form-check-input")[0].checked ) {
|
||||
permissionsData.push(permissionContainer.getAttribute("data-permission-internal-name"));
|
||||
}
|
||||
}
|
||||
|
||||
setStatusText("Saving Admin Permissions");
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/manage-users/${userId}/manage-admin-permissions/api/set-permissions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": ActionCSRFToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"permissions": permissionsData,
|
||||
"2fa_code": twoFactorCodeInput.value
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
if ( data.success ) {
|
||||
alert("Successfully saved Admin Permissions");
|
||||
twoFactorCodeInput.value = "";
|
||||
await attemptFetchAndLoad();
|
||||
return;
|
||||
} else {
|
||||
alert(`Failed to save Admin Permissions, reason: ${data.reason}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Failed to save Admin Permissions");
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
isPermissionsLoaded = true;
|
||||
saveChangesButton.disabled = false;
|
||||
twoFactorCodeInput.value = "";
|
||||
twoFactorCodeInput.disabled = false;
|
||||
})
|
||||
|
||||
await attemptFetchAndLoad();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Manage User{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a class="mb-2 d-block" href="/admin/manage-users/{{userObj.id}}">← Return to User Page</a>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div class="ms-3">
|
||||
<h1 class="m-0 text-secondary">Manage Robux & Builders Club for <span class="text-white">{{userObj.username}}</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="linebreak"></div>
|
||||
|
||||
<div class="robux-management">
|
||||
<h3>Robux & Tix Management</h3>
|
||||
<form method="POST" id="robux-form">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="action-select" required>
|
||||
<option value="add">Add</option>
|
||||
<option value="subtract">Subtract</option>
|
||||
</select>
|
||||
<label for="action-select">Action</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-2">
|
||||
<select class="form-control" name="currency_type" id="currency-type-select" required>
|
||||
<option value="robux">Robux</option>
|
||||
<option value="tix">Tix</option>
|
||||
</select>
|
||||
<label for="currency-type-select">Currency</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-2">
|
||||
<input type="number" class="form-control" name="amount" id="amount-input" placeholder="Amount" required>
|
||||
<label for="amount-input">Amount</label>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="linebreak"></div>
|
||||
<button type="submit" class="btn btn-primary mt-2 w-100 btn-sm">Submit Currency Change</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="linebreak"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const actionSelect = document.getElementById("action-select");
|
||||
const amountInput = document.getElementById("amount-input");
|
||||
const robuxForm = document.getElementById("robux-form");
|
||||
|
||||
robuxForm.addEventListener("submit", function(event) {
|
||||
if (actionSelect.value === "subtract") {
|
||||
amountInput.value = -Math.abs(amountInput.value);
|
||||
} else {
|
||||
amountInput.value = Math.abs(amountInput.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Moderator Notes</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% for note in UserModeratorNotes.items %}
|
||||
<div class="rounded border bg-dark p-2 mb-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="40px" style="aspect-ratio: 1/1;" class="rounded-2 border" src="/Thumbs/Head.ashx?userId={{note.note_creator_id}}&x=50&y=50">
|
||||
<div class="ms-1">
|
||||
<h5 class="m-0 text-white">{{ GetUserName(note.note_creator_id) }}</h5>
|
||||
<p class="m-0" style="font-size: 12px;color: rgb(200,200,200);">Created at {{note.created_at}} UTC</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1 border w-100 mt-1">
|
||||
{% for note_line in note.note.split("\n") %}
|
||||
<p class="m-0 text-white">{{note_line}}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if len(UserModeratorNotes.items) == 0 %}
|
||||
<div class="alert alert-danger">
|
||||
This user currently has no moderator notes.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not UserModeratorNotes.has_prev %}text-secondary{%endif%}" {% if UserModeratorNotes.has_prev %}href="/admin/manage-users/{{userObj.id}}/moderator-notes?page={{UserModeratorNotes.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{UserModeratorNotes.page}} of {{UserModeratorNotes.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not UserModeratorNotes.has_next %}text-secondary{%endif%}" {% if UserModeratorNotes.has_next %}href="/admin/manage-users/{{userObj.id}}/moderator-notes?page={{UserModeratorNotes.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="min-width: 1100px;">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>User Manager</h1>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<div class="input-group mb-3">
|
||||
<div class="form-floating" style="width: 52%;">
|
||||
<input type="text" class="form-control" id="user-search-input" name="user-search-input" value="{%if query != None: %}{{query}}{%endif%}" placeholder="ROBLOX">
|
||||
<label for="user-search-input">Search</label>
|
||||
</div>
|
||||
<div class="form-floating" style="min-width: 130px;">
|
||||
<select class="form-select" id="user-search-type" name="user-search-type">
|
||||
<option value="userid">UserID</option>
|
||||
<option value="username" {%if searchType == "username"%}selected{%endif%}>Username</option>
|
||||
<option value="discordid" {%if searchType == "discordid"%}selected{%endif%}>Discord ID</option>
|
||||
</select>
|
||||
<label for="user-search-type">Search With</label>
|
||||
</div>
|
||||
<div class="form-floating" style="min-width: 160px;">
|
||||
<select class="form-select" id="user-order-by" name="user-order-by">
|
||||
<option value="userid">UserID</option>
|
||||
<option value="creation" {%if orderBy == "creation"%}selected{%endif%}>Creation Date</option>
|
||||
<option value="lastonline" {%if orderBy == "lastonline"%}selected{%endif%}>Last Online</option>
|
||||
<option value="robux" {%if orderBy == "robux"%}selected{%endif%}>Robux Balance</option>
|
||||
<option value="tix" {%if orderBy == "tix"%}selected{%endif%}>Tickets Balance</option>
|
||||
</select>
|
||||
<label for="user-order-by">Order By</label>
|
||||
</div>
|
||||
<div class="form-floating" style="min-width: 130px;">
|
||||
<select class="form-select" id="user-order-direction" name="user-order-direction">
|
||||
<option value="asc">Ascending</option>
|
||||
<option value="desc" {%if orderType == "desc"%}selected{%endif%}>Descending</option>
|
||||
</select>
|
||||
<label for="user-order-direction">Order Direction</label>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-outline-primary fw-bold" value="🔍">
|
||||
</div>
|
||||
</form>
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<tr class="rounded-top">
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Last Online</th>
|
||||
<th scope="col">Robux</th>
|
||||
<th scope="col">Tickets</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider" style="border-color: rgb(20,20,20);">
|
||||
{% for user in returnList: %}
|
||||
<tr>
|
||||
<td>{{user.id}}</td>
|
||||
<td><span class="{% if user.accountstatus == 2%}text-warning{% endif %}{% if user.accountstatus > 2 %}text-danger{%endif%}">{{user.username}}</span></td>
|
||||
<td>{{user.creation}}</td>
|
||||
<td>{{user.lastonline}}</td>
|
||||
<td>{{user.robux}}</td>
|
||||
<td>{{user.tix}}</td>
|
||||
<td><a href="/admin/manage-users/{{user.id}}" class="text-decoration-none btn btn-primary btn-sm w-100">View</a></td>
|
||||
</tr>
|
||||
{%endfor%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if len(returnList) == 0: %}
|
||||
<p class="text-white w-100 text-center">No results found</p>
|
||||
{%endif%}
|
||||
<div class="mt-3 d-flex">
|
||||
<a class="text-decoration-none ms-auto {%if page <= 1: %}disabled text-secondary{%else%}text-primary{%endif%}" {% if page > 1:%}href="/admin/manage-users?searchType={{searchType}}&orderBy={{orderBy}}&page={{page-1}}&orderType={{orderType}}{%if query != None: %}&query={{query}}{%endif%}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white">Page {{page}}</p>
|
||||
<a class="text-decoration-none me-auto {%if not isThereNextPage: %}disabled text-secondary{%else%}text-primary{%endif%}" {%if isThereNextPage: %}href="/admin/manage-users?searchType={{searchType}}&orderBy={{orderBy}}&page={{page+1}}&orderType={{orderType}}{%if query != None: %}&query={{query}}{%endif%}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,116 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(32, 32, 32) !important;
|
||||
box-shadow: 0px 0px 5px 0px rgb(0,0,0);
|
||||
}
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.text-tickets {
|
||||
color: rgb(224, 224, 60) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 1000px;">
|
||||
<a class="mb-2" href="/admin/manage-users/{{userObj.id}}"> < Return to User Page</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<img height="100px" style="aspect-ratio: 1/1;" class="rounded-2" src="/Thumbs/Avatar.ashx?userId={{userObj.id}}&x=100&y=100">
|
||||
<div>
|
||||
<h1 class="m-0 {% if userObj.accountstatus == 1 %}text-white{% elif userObj.accountstatus == 2 %}text-warning{% elif userObj.accountstatus == 3%}text-danger{% else %}text-decoration-line-through text-white{%endif%}">{{userObj.username}}<span class="text-secondary"> Transactions</span></h1>
|
||||
<p class="text-secondary m-0" style="font-size: 12px;">UserId: <span class="text-white">{{userObj.id}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-floating mb-2">
|
||||
<select class="form-control" id="change-category-select">
|
||||
<option value="purchase" {%if PageCategory == "purchase"%}selected{%endif%}>Purchases</option>
|
||||
<option value="sale" {%if PageCategory == "sale"%}selected{%endif%}>Sales</option>
|
||||
<option value="group-payout" {%if PageCategory == "group-payout"%}selected{%endif%}>Group Payouts</option>
|
||||
<option value="stipends" {%if PageCategory == "stipends"%}selected{%endif%}>Stipends</option>
|
||||
</select>
|
||||
<label for="change-category-select">Category</label>
|
||||
</div>
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<tr class="rounded-top">
|
||||
<th scope="col" style="width: 15%;">Date</th>
|
||||
<th scope="col" style="width: 20%;">Source</th>
|
||||
<th scope="col" style="width: 45%;">Description</th>
|
||||
<th scope="col" style="width: 20%;">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider" style="border-color: rgb(20,20,20);">
|
||||
{% for Transaction in TransactionInfo: %}
|
||||
<tr>
|
||||
<td>{{Transaction.created_at}}</td>
|
||||
<td>
|
||||
{% if Transaction["source"]["type"] == 0: %}
|
||||
<a href="/users/{{Transaction['source']['id']}}/profile" class="text-decoration-none text-white text-truncate">
|
||||
<img class="rounded-5 overflow-hidden me-2" width="38" height="38" src="/Thumbs/Head.ashx?x=48&y=48&userId={{Transaction['source']['id']}}" alt="{{Transaction['source']['name']}}">
|
||||
{{Transaction["source"]["name"]}}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/groups/{{Transaction['source']['id']}}/--" class="text-decoration-none text-white text-truncate">
|
||||
<img class="rounded-5 overflow-hidden me-2" width="38" height="38" src="/Thumbs/GroupIcon.ashx?x=48&y=48&groupid={{Transaction['source']['id']}}" alt="{{Transaction['source']['name']}}">
|
||||
{{Transaction["source"]["name"]}}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if Transaction['custom_text'] != None %}
|
||||
{{Transaction['custom_text']}}
|
||||
{% else %}
|
||||
{% if Transaction['asset'] != None: %}
|
||||
<a href="/catalog/{{Transaction['asset']['id']}}/--" class="text-decoration-none text-white">
|
||||
<img class="rounded-2 overflow-hidden me-2 border" width="38" height="38" src="/Thumbs/Asset.ashx?x=48&y=48&assetId={{Transaction['asset']['id']}}" alt="{{Transaction['asset']['name']}}">
|
||||
{{Transaction['asset']['name']}}
|
||||
</a>
|
||||
{% else %}
|
||||
Unknown
|
||||
{%endif%}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if Transaction['currency_type'] == 0 %}
|
||||
<span class="text-robux">R$ {{Transaction['currency_amount']}}</span>
|
||||
{% else %}
|
||||
<span class="text-tickets">T$ {{Transaction['currency_amount']}}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if len(TransactionInfo) == 0: %}
|
||||
<div class="text-center">
|
||||
<p class="text-secondary">No transactions found</p>
|
||||
</div>
|
||||
{%endif%}
|
||||
|
||||
<div class="align-items-center d-flex justify-content-center mt-2 mb-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not Pagination.has_prev %}text-secondary{%endif%}" {% if Pagination.has_prev %}href="/admin/manage-users/{{userObj.id}}/transactions?page={{Pagination.prev_num}}&category={{PageCategory}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{Pagination.page}} of {{Pagination.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not Pagination.has_next %}text-secondary{%endif%}" {% if Pagination.has_next %}href="/admin/manage-users/{{userObj.id}}/transactions?page={{Pagination.next_num}}&category={{PageCategory}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>const s = document.getElementById("change-category-select");s.addEventListener("change", function() {var u = new URL(window.location.href);var sb = s.value;u.searchParams.set("category", sb);u.searchParams.set("page", 1);window.location.href = u;});</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,193 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.show-on-hover {
|
||||
filter: blur(4px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.show-on-hover:hover {
|
||||
filter: blur(0px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="min-width: 1100px;">
|
||||
<div class="d-flex">
|
||||
<a href="/admin/manage-users" class="btn border-success btn-sm mb-2">Back to User Manager</a>
|
||||
<a href="/users/{{userObj.id}}/profile" class="btn border-primary btn-sm mb-2 ms-3">View user profile</a>
|
||||
</div>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-danger p-2 text-center">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="border rounded-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<img width="30px" height="30px" class="rounded me-2" src="/Thumbs/Head.ashx?x=48&y=48&userId={{userObj.id}}" style="aspect-ratio: 1/1;">
|
||||
<h5 class="m-0">{{userObj.username}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<img width="100%" class="p-2" src="/Thumbs/Avatar.ashx?x=420&y=420&userId={{userObj.id}}" style="aspect-ratio: 1/1;">
|
||||
{% if isAdministrator: %}
|
||||
<div class="border-top" style="background-color: rgba(24, 220, 255,0.5);font-size:12px;padding:3px;">
|
||||
<p class="m-0 fw-bold text-white text-center">Administrator</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if userObj.accountstatus == 2: %}
|
||||
<div class="border-top" style="background-color: rgba(255, 201, 24, 0.5);font-size:12px;padding:3px;">
|
||||
<p class="m-0 fw-bold text-white text-center">This account is temporarily banned</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if userObj.accountstatus == 3: %}
|
||||
<div class="border-top" style="background-color: rgba(255, 24, 24, 0.5);font-size:12px;padding:3px;">
|
||||
<p class="m-0 fw-bold text-white text-center">This account has been deleted</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if userObj.accountstatus == 4: %}
|
||||
<div class="border-top" style="background-color: rgba(255, 24, 243, 0.5);font-size:12px;padding:3px;">
|
||||
<p class="m-0 fw-bold text-white text-center">This account is GDPR deleted</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-dark p-2 rounded-2 rounded-top-0 border-top text-secondary" style="font-size: 13px;">
|
||||
<span>Created: <span class="text-white">{{userObj.created}}</span></span>
|
||||
<br>
|
||||
<span>Last Online: <span class="text-white">{{userObj.lastonline}}</span></span>
|
||||
<br>
|
||||
<span>2FA Enabled: <span class="text-white">{{userObj.TOTPEnabled}}</span></span>
|
||||
<br>
|
||||
<span>Place Visits: <span class="text-white">{{TotalVisits}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border rounded-2 mt-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<h5 class="m-0">Economy</h5>
|
||||
</div>
|
||||
<div class="text-secondary p-2 pt-1" style="font-size: 13px;">
|
||||
<span>Robux: <span class="text-success fw-bold">R${{UserEconomyObj.robux}}</span></span>
|
||||
<br>
|
||||
<span>Tickets: <span class="text-warning fw-bold">T${{UserEconomyObj.tix}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
{% if LastestUserBanObj != None: %}
|
||||
<div class="border rounded-2 mb-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<h5 class="m-0">Ban Information</h5>
|
||||
</div>
|
||||
<div class="p-2 text-secondary text-truncate" style="font-size: 14px;">
|
||||
<span>Ban Type: <span class="text-white">{{LastestUserBanObj.ban_type.name}}</span></span>
|
||||
<br>
|
||||
<span>Banned On: <span class="text-white">{{LastestUserBanObj.created_at}}</span></span>
|
||||
<br>
|
||||
<span>Expires On: <span class="text-white">{{LastestUserBanObj.expires_at}}</span></span>
|
||||
<br>
|
||||
<span>Reason: <span class="text-white">{{LastestUserBanObj.reason}}</span></span>
|
||||
<br>
|
||||
<span>Moderator Note: <span class="text-white" style="word-break: break-all;white-space: normal;">{{LastestUserBanObj.moderator_note}}</span></span>
|
||||
<br>
|
||||
<span>Banned By: <a class="text-decoration-underline text-primary" href="/admin/manage-users/{{LastestUserBanObj.author_userid}}">{{LastestUserBanObj.author_userid}}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="border rounded-2" style="min-height: 150px;">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<h5 class="m-0">Description</h5>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
{% for line in DescriptionLines %}
|
||||
<p class="text-white m-0">{{line}}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if LastLogin != None: %}
|
||||
<div class="border rounded-2 mt-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<h5 class="m-0">Latest Login</h5>
|
||||
</div>
|
||||
<div class="p-2 text-secondary text-truncate" style="font-size: 14px;">
|
||||
<span>User-Agent: <span class="text-white">{{LastLogin.useragent}}</span></span>
|
||||
<br>
|
||||
<span>Login Timestamp: <span class="text-white">{{LastLogin.timestamp}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
{% if LinkedDiscordObj != None: %}
|
||||
<div class="border rounded-2 mt-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<h5 class="m-0">Discord Account</h5>
|
||||
</div>
|
||||
<div class="p-2 text-secondary text-truncate" style="font-size: 14px;">
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<span>Discord ID: <span class="text-white">{{DiscordUserInfo.UserId}}</span></span>
|
||||
<br>
|
||||
<span>Username: <span class="text-white">{{DiscordUserInfo.Username}}</span></span>
|
||||
<br>
|
||||
<span>Discriminator: <span class="text-white">{{DiscordUserInfo.Discriminator}}</span></span>
|
||||
<br>
|
||||
<span>Linked On: <span class="text-white">{{LinkedDiscordObj.linked_on}}</span></span>
|
||||
<br>
|
||||
<span>Last Refresh: <span class="text-white">{{LinkedDiscordObj.last_updated}}</span></span>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<img width="100%" style="aspect-ratio: 1/1;" class="border rounded" src="{{DiscordUserInfo.GetAvatarURL()}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
{% if InviteKeyUsed != None: %}
|
||||
<div class="border rounded-2 mt-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0 border-bottom">
|
||||
<h5 class="m-0">Invite Key Info</h5>
|
||||
</div>
|
||||
<div class="p-2 text-secondary text-truncate" style="font-size: 14px;">
|
||||
<span>Invite Key: <span class="text-white show-on-hover">{{InviteKeyUsed.key}}</span></span>
|
||||
<br>
|
||||
<span>Creator: {% if InviteKeyUsed.created_by != None: %}<a class="text-decoration-none text-primary" href="/admin/manage-users/{{InviteKeyUsed.created_by}}">{{InviteKeyUsed.creator.username}}</a>{%else%}<span class="text-white">Unknown</span>{%endif%}</span>
|
||||
<br>
|
||||
<span>Created At: <span class="text-white">{{InviteKeyUsed.created_at}}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<div class="border rounded-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0">
|
||||
<h5 class="m-0">Actions</h5>
|
||||
</div>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/ban-user"><i class="bi bi-hammer"></i> Ban User</a>
|
||||
{% if HasAdminPermission("ManageAdminPermissions") %}<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/manage-admin-permissions"><i class="bi bi-shield"></i> Manage Permissions</a>{%endif%}
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/manage-currency"><i class="bi bi-cash-stack"></i> Manage Robux & BC</a>
|
||||
|
||||
</div>
|
||||
<div class="border rounded-2 mt-2">
|
||||
<div class="bg-dark p-2 rounded-2 rounded-bottom-0">
|
||||
<h5 class="m-0">View</h5>
|
||||
</div>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/moderator-notes"><i class="bi bi-journal-text"></i></i> Moderator Notes</a>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/login-history"><i class="bi bi-clock-history"></i></i> Login History</a>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/invite-keys"><i class="bi bi-key"></i></i> Invite Keys</a>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/game-sessions"><i class="bi bi-controller"></i></i> Game Sessions</a>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/transactions"><i class="bi bi-bank"></i></i> Transactions</a>
|
||||
<a class="btn border-top rounded-0 w-100 text-decoration-none" href="/admin/manage-users/{{userObj.id}}/ban-history"><i class="bi bi-exclamation-triangle"></i></i> Ban History</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.bg-dark {
|
||||
background-color: rgb(30,30,30) !important;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.75);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Website Features</h1>
|
||||
<p class="m-0" style="font-size:small;">Only turn off certain features when its an emergency!</p>
|
||||
<div class="row p-2">
|
||||
{% for feature in SitesFeaturesStatus: %}
|
||||
<div class="col-3 p-2">
|
||||
<div class="bg-dark ps-0 pe-0 rounded-top">
|
||||
<div class="{% if feature.enabled: %}bg-success{%else%}bg-danger{%endif%} w-100 rounded-top" style="min-height: 25px;"></div>
|
||||
<div class="p-2 w-100">
|
||||
<h3>{{feature.name}}</h3>
|
||||
{% if feature.enabled: %}
|
||||
<form action="/admin/manage-website-features/{{feature.name}}/disable" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm w-100">Disable</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/admin/manage-website-features/{{feature.name}}/enable" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-outline-success btn-sm w-100">Enable</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,83 @@
|
||||
WebsiteFeaturesDefinition = [
|
||||
{
|
||||
"name": "WebSignup"
|
||||
},
|
||||
{
|
||||
"name": "WebLogin"
|
||||
},
|
||||
{
|
||||
"name": "UsernameChange"
|
||||
},
|
||||
{
|
||||
"name": "PasswordChange"
|
||||
},
|
||||
{
|
||||
"name": "DiscordLinking"
|
||||
},
|
||||
{
|
||||
"name": "GameJoinAPI"
|
||||
},
|
||||
{
|
||||
"name": "ThumbnailRendering"
|
||||
},
|
||||
{
|
||||
"name": "GroupJoining"
|
||||
},
|
||||
{
|
||||
"name": "GroupLeaving"
|
||||
},
|
||||
{
|
||||
"name": "GroupWallPosting"
|
||||
},
|
||||
{
|
||||
"name": "GroupCreation"
|
||||
},
|
||||
{
|
||||
"name": "AssetCreation"
|
||||
},
|
||||
{
|
||||
"name": "AssetEditing"
|
||||
},
|
||||
{
|
||||
"name": "AssetValidationService"
|
||||
},
|
||||
{
|
||||
"name": "EconomyPurchase"
|
||||
},
|
||||
{
|
||||
"name": "MessageSending"
|
||||
},
|
||||
{
|
||||
"name": "ItemReselling"
|
||||
},
|
||||
{
|
||||
"name": "ItemTrading"
|
||||
},
|
||||
{
|
||||
"name": "AvatarUpdate"
|
||||
},
|
||||
{
|
||||
"name": "ClothingMigrator"
|
||||
},
|
||||
{
|
||||
"name": "SendFriendRequest"
|
||||
},
|
||||
{
|
||||
"name": "FollowingUsers"
|
||||
},
|
||||
{
|
||||
"name": "GiftcardRedeem"
|
||||
},
|
||||
{
|
||||
"name": "EmailChange"
|
||||
},
|
||||
{
|
||||
"name": "SendResetPassword"
|
||||
},
|
||||
{
|
||||
"name": "AudioMigrator"
|
||||
},
|
||||
{
|
||||
"name": "CurrencyExchange"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Admin{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/admin.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<a href="/admin" class="btn border-primary btn-sm mb-2">Back to Admin Panel</a>
|
||||
<h1>Website wide message</h1>
|
||||
<p class="m-0">plz do not put anything useless on here thxxx</p>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post">
|
||||
<input type="text" name="message" class="form-control" value="{{message}}" placeholder="Message">
|
||||
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
|
||||
<input type="submit" class="btn btn-primary mt-2" value="Save">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,104 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from app.routes.asset import GetOriginalAssetInfo, AddAudioAssetToAudioMigrationQueue
|
||||
from app.models.asset import Asset
|
||||
from app.models.user import User
|
||||
from app.services import economy
|
||||
from app.util import auth, websiteFeatures, turnstile, transactions
|
||||
from app.extensions import db, redis_controller
|
||||
from app.enums.TransactionType import TransactionType
|
||||
|
||||
AudioMigratorRoute = Blueprint('assetmigrator', __name__, template_folder='pages')
|
||||
|
||||
@AudioMigratorRoute.route('/audiomigrator', methods=['GET'])
|
||||
@auth.authenticated_required
|
||||
def audiomigrator():
|
||||
return render_template('audiomigrator/index.html')
|
||||
|
||||
@AudioMigratorRoute.route('/audiomigrator', methods=['POST'])
|
||||
@auth.authenticated_required
|
||||
def audiomigrator_post():
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if not websiteFeatures.GetWebsiteFeature("AudioMigrator"):
|
||||
flash("Audio migration is temporarily disabled", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
CFTurnstileResponse = request.form.get( key="cf-turnstile-response", default=None, type=str)
|
||||
if CFTurnstileResponse is None:
|
||||
flash("Invalid captcha", "error")
|
||||
return redirect("/audiomigrator")
|
||||
if not turnstile.VerifyToken(CFTurnstileResponse):
|
||||
flash("Invalid captcha", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
PlaceId = request.form.get( key = "placeid", default = None, type = int )
|
||||
if PlaceId is None:
|
||||
flash("Invalid place ID", "error")
|
||||
return redirect("/audiomigrator")
|
||||
RequestedAudioIds = request.form.get( key = "audio-ids", default = None, type = str )
|
||||
if RequestedAudioIds is None:
|
||||
flash("Invalid audio IDs", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
RequestedAudioIds = RequestedAudioIds.split("\n")
|
||||
for i in range(len(RequestedAudioIds)):
|
||||
RequestedAudioIds[i] = RequestedAudioIds[i].strip()
|
||||
if RequestedAudioIds[i].isdigit():
|
||||
RequestedAudioIds[i] = int(RequestedAudioIds[i])
|
||||
else:
|
||||
RequestedAudioIds.remove(RequestedAudioIds[i])
|
||||
|
||||
if len(RequestedAudioIds) > 100:
|
||||
flash("You can only migrate 100 audios at a time", "error")
|
||||
return redirect("/audiomigrator")
|
||||
if len(RequestedAudioIds) < 1:
|
||||
flash("You must migrate at least 1 audio", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
if PlaceId < 1:
|
||||
flash("Invalid place ID", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
RobuxBalance, _ = economy.GetUserBalance(AuthenticatedUser)
|
||||
RequiredAmountRobux = 0
|
||||
AudioIdsToMigrate : list[int] = []
|
||||
for RequestedAudio in RequestedAudioIds:
|
||||
if Asset.query.filter_by(id = RequestedAudio ).first():
|
||||
continue
|
||||
if redis_controller.lrange("migrate_audio_assets_queue", 0, -1).count(str(RequestedAudio)) > 0:
|
||||
continue
|
||||
if RequestedAudio in AudioIdsToMigrate:
|
||||
continue
|
||||
AudioIdsToMigrate.append(RequestedAudio)
|
||||
RequiredAmountRobux += 5
|
||||
|
||||
if len(AudioIdsToMigrate) == 0:
|
||||
flash(f"Queued 0 / {len(RequestedAudioIds)} audios to be migrated", "success")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
if RobuxBalance < RequiredAmountRobux:
|
||||
flash(f"You don't have enough robux to migrate these audios, Required R${RequiredAmountRobux}", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
try:
|
||||
economy.DecrementTargetBalance( AuthenticatedUser, Amount = RequiredAmountRobux, CurrencyType = 0 )
|
||||
except economy.InsufficientFundsException:
|
||||
flash(f"You don't have enough robux to migrate these audios, Required R${RequiredAmountRobux}", "error")
|
||||
return redirect("/audiomigrator")
|
||||
except Exception as e:
|
||||
flash(f"An error occured while deducting robux", "error")
|
||||
return redirect("/audiomigrator")
|
||||
|
||||
transactions.CreateTransaction(
|
||||
Reciever = User.query.filter_by(id = 1).first(),
|
||||
Sender = AuthenticatedUser,
|
||||
CurrencyAmount = RequiredAmountRobux,
|
||||
CurrencyType = 0,
|
||||
TransactionType = TransactionType.Purchase,
|
||||
CustomText = f"Audio migration"
|
||||
)
|
||||
|
||||
for AudioId in AudioIdsToMigrate:
|
||||
AddAudioAssetToAudioMigrationQueue(AudioId, bypassQueueLimit = True ,placeId=PlaceId)
|
||||
|
||||
flash(f"Queued {len(AudioIdsToMigrate)} / {len(RequestedAudioIds)} audios to be migrated", "success")
|
||||
return redirect("/audiomigrator")
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Audio Migrator{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="settings-container" style="max-width: 700px;">
|
||||
<h1 class="mb-2">Audio Migrator</h1>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<div class="form-floating">
|
||||
<input type="text" name="placeid" class="form-control w-100" placeholder="PlaceId associated with Audio" required>
|
||||
<label for="placeid">PlaceID</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<textarea name="audio-ids" class="form-control w-100 mt-2" placeholder="Audio IDs, please enter a new line for each audio id" required style="min-height: 200px;"></textarea>
|
||||
<label for="audio-ids">Audio IDs, separate each audio with a new line</label>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end align-items-top mt-2">
|
||||
<p class="m-0 text-secondary me-1" style="font-size:11px;">Note: As a processing fee, each Audio will cost R$5 even if audio migration fails, this is to prevent spam and abuse of our systems. It can take a few minutes for audios to be processed so please be patient, you will not be charged for any audios that already exist on Vortexi.</p>
|
||||
<div class="cf-turnstile" data-theme="dark" data-sitekey="{{turnstilekey}}"></div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="ms-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Migrate</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,139 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Avatar{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/avatar.css" rel="stylesheet"/>
|
||||
|
||||
<script src="/static/js/AvatarEditor.js?v3"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/OBJLoader.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/loaders/MTLLoader.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
|
||||
<div class="col-3 p-1 asset-card-template" style="display: none;">
|
||||
<div class="position-relative">
|
||||
<img width="100%" src="" class="asset-card-img rounded" alt="Item">
|
||||
<button class="btn-sm btn-primary btn position-absolute p-1 asset-card-button" style="top:10px;right:20px;font-size: 12px;">Wear</button>
|
||||
</div>
|
||||
<a class="text-decoration-none m-0 text-center w-100 asset-card-assetname" href="/catalog/12345676/" style="font-size:12px;">Test Item</a>
|
||||
</div>
|
||||
<style>
|
||||
#toggle-3d-btn, #change-rigtype-btn, #savechanges-btn {
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 900px;">
|
||||
<h1>Avatar</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="position-relative">
|
||||
<div class="avatar-image-container"></div>
|
||||
<img width="100%" style="height: auto;" class="rounded border p-1" id="avatar-image" src="/Thumbs/Avatar.ashx?x=420&y=420&userId={{user.id}}" alt="Your Avatar">
|
||||
<!--<button class="btn btn-primary btn-sm position-absolute" style="bottom: 10px;right: 20px;">Refresh</button>-->
|
||||
<div class="position-absolute w-100" style="bottom: 20px;display:none;" id="render-waiting-text">
|
||||
<p class="fw-bold rounded text-white text-center m-0 ms-auto me-auto w-fit p-1 border" style="width:fit-content;background-color: rgba(31, 31, 31, 0.5);">Rendering in progress</p>
|
||||
</div>
|
||||
<button class="btn-outline-light btn position-absolute" style="top: 20px; right: 60px; aspect-ratio: 1/1; padding: 0px; width: 35px; height: 35px; text-align: center; z-index: 10;" id="toggle-3d-btn">3D</button>
|
||||
<button class="btn-outline-primary btn position-absolute" style="top: 20px;right: 20px;aspect-ratio: 1/1;padding: 0px;width: 35px;height: 35px;text-align: center;" id="change-rigtype-btn">R15</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success fw-bold btn-sm w-100 mt-1" id="savechanges-btn" onclick="UpdateAvatar()" style="display: none;">Save Changes</button>
|
||||
<div class="mt-1">
|
||||
<p class="m-0" style="font-size:small;">Avatar isn't loading correctly? <a class="text-primary text-decoration-none" style="cursor:pointer;" onclick="RedrawAvatar()">Redraw</a></p>
|
||||
<div class="w-100 mt-3 position-relative" style="min-height: 360px;">
|
||||
<h5>Body Colors</h5>
|
||||
<div class="overlay p-1" id="color-picker-overlay" style="display:none;">
|
||||
<div class="d-flex align-items-center">
|
||||
<h5 class="text-center m-0 ms-4">Select Body Color</h5>
|
||||
<div class="ms-auto">
|
||||
<button class="btn btn-close" id="color-picker-close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row p-2" id="color-picker-content"></div>
|
||||
</div>
|
||||
<div class="head" id="head-bodycolor"></div>
|
||||
<div class="torso" id="torso-bodycolor">
|
||||
<div class="leftArm" id="rightarm-bodycolor"></div>
|
||||
<div class="rightArm" id="leftarm-bodycolor"></div>
|
||||
</div>
|
||||
<div class="legs">
|
||||
<div class="leftLeg" id="rightleg-bodycolor"></div>
|
||||
<div class="rightLeg" id="leftleg-bodycolor"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<select class="form-control mb-1" id="select-tab-input">
|
||||
<option data-tab="1">Wear</option>
|
||||
<option data-tab="2">Scaling</option>
|
||||
</select>
|
||||
<div id="equip-assets-tab">
|
||||
<select class="form-control" id="select-asset-type">
|
||||
<option data-type="17">Head</option>
|
||||
<option data-type="18">Faces</option>
|
||||
<option data-type="2">T-Shirts</option>
|
||||
<option data-type="11" selected>Shirts</option>
|
||||
<option data-type="12">Pants</option>
|
||||
<option data-type="19">Gear</option>
|
||||
<option data-type="8">Hats</option>
|
||||
<option data-type="41">Hair Accessory</option>
|
||||
<option data-type="42">Face Accessory</option>
|
||||
<option data-type="43">Neck Accessory</option>
|
||||
<option data-type="44">Shoulder Accessory</option>
|
||||
<option data-type="45">Front Accessory</option>
|
||||
<option data-type="46">Back Accessory</option>
|
||||
<option data-type="47">Waist Accessory</option>
|
||||
<option data-type="27">Torso</option>
|
||||
<option data-type="29">Left Arms</option>
|
||||
<option data-type="28">Right Arms</option>
|
||||
<option data-type="30">Left Legs</option>
|
||||
<option data-type="31">Right Legs</option>
|
||||
</select>
|
||||
<div class="row" id="assets-card-holder">
|
||||
</div>
|
||||
<div class="align-items-center d-flex justify-content-center">
|
||||
<button class="btn btn-sm text-white btn-primary me-2" id="pagination-back-btn" disabled>Previous</button><p class="m-0" id="pagination-page-number">Page 1</p><button id="pagination-next-btn" class="btn btn-sm text-white btn-primary ms-2" disabled>Next</button>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<h5>Currently Wearing</h5>
|
||||
<div class="row" id="currentlywearing-card-holder">
|
||||
</div>
|
||||
</div>
|
||||
<div id="avatar-scaling-tab" style="display: none;">
|
||||
<div id="height-scale-group">
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="height-scaling" class="form-label">Height</label>
|
||||
<p class="m-0 ms-auto text-white scaling-value">100%</p>
|
||||
</div>
|
||||
<input type="range" class="form-range scaling-slider" min="0" max="5" step="0.5">
|
||||
</div>
|
||||
<div id="width-scale-group">
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="width-scaling" class="form-label">Width</label>
|
||||
<p class="m-0 ms-auto text-white scaling-value">100%</p>
|
||||
</div>
|
||||
<input type="range" class="form-range scaling-slider" min="0" max="5" step="0.5">
|
||||
</div>
|
||||
<div id="head-scale-group">
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="head-scaling" class="form-label">Head</label>
|
||||
<p class="m-0 ms-auto text-white scaling-value">100%</p>
|
||||
</div>
|
||||
<input type="range" class="form-range scaling-slider" min="0" max="5" step="0.5">
|
||||
</div>
|
||||
<div id="proportion-scale-group">
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="proportion-scaling" class="form-label">Proportion</label>
|
||||
<p class="m-0 ms-auto text-white scaling-value">100%</p>
|
||||
</div>
|
||||
<input type="range" class="form-range scaling-slider" min="0" max="5" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,277 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify
|
||||
import base64
|
||||
import json
|
||||
from app.extensions import db, redis_controller, limiter, csrf
|
||||
from app.util import auth, websiteFeatures
|
||||
from app.enums.AssetType import AssetType
|
||||
|
||||
from app.models.asset import Asset
|
||||
from app.models.user_avatar_asset import UserAvatarAsset
|
||||
from app.models.user_avatar import UserAvatar
|
||||
from app.models.userassets import UserAsset
|
||||
from app.models.user_thumbnail import UserThumbnail
|
||||
from app.models.user import User
|
||||
|
||||
from app.routes.thumbnailer import TakeUserThumbnail
|
||||
AllowedBodyColors = [361, 192, 217, 153, 359, 352, 5, 101, 1007, 1014, 38, 18, 125, 1030, 133, 106, 105, 1017, 24, 334, 226, 141, 1021, 28, 37, 310, 317, 119, 1011, 1012, 1010, 23, 305, 102, 45, 107, 1018, 1027, 1019, 1013, 11, 1024, 104, 1023, 321, 1015, 1031, 1006, 1026, 21, 1004, 1032, 1016, 330, 9, 1025, 364, 351, 1008, 29, 1022, 151, 135, 1020, 1028, 1009, 1029, 1003, 26, 199, 194, 1002, 208, 1, 1001]
|
||||
ScalingLimits = {
|
||||
"height": {
|
||||
"min": 0.9,
|
||||
"max": 1.05,
|
||||
"increment": 0.01
|
||||
},
|
||||
"width": {
|
||||
"min": 0.7,
|
||||
"max": 1,
|
||||
"increment": 0.01
|
||||
},
|
||||
"head": {
|
||||
"min": 0.95,
|
||||
"max": 1,
|
||||
"increment": 0.01
|
||||
},
|
||||
"proportion": {
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"increment": 0.01
|
||||
},
|
||||
"bodyType": {
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"increment": 0.01
|
||||
}
|
||||
}
|
||||
AvatarRoute = Blueprint('avatar', __name__, template_folder='pages')
|
||||
|
||||
@AvatarRoute.route('/avatar', methods=['GET'])
|
||||
@auth.authenticated_required
|
||||
def avatar():
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
return render_template('avatar/avatar.html', user=AuthenticatedUser)
|
||||
|
||||
@AvatarRoute.route("/avatar/getassets", methods=["GET"])
|
||||
@auth.authenticated_required_api
|
||||
def get_assets():
|
||||
ReqAssetType = request.args.get("type")
|
||||
if ReqAssetType is None:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
try:
|
||||
ReqAssetType = int(ReqAssetType)
|
||||
except:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
if ReqAssetType not in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
PageNumber = request.args.get("page") or 0
|
||||
try:
|
||||
PageNumber = int(PageNumber)
|
||||
except:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
PageNumber += 1
|
||||
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
AssetsList : list[UserAsset] = UserAsset.query.filter_by(userid=AuthenticatedUser.id).join(Asset, Asset.id == UserAsset.assetid).filter(Asset.asset_type == AssetType(ReqAssetType)).distinct(Asset.id).paginate( page=PageNumber, per_page=12, error_out=False)
|
||||
AssetList = []
|
||||
for AssetObj in AssetsList.items:
|
||||
AssetObj : Asset = Asset.query.filter_by(id=AssetObj.assetid).first()
|
||||
if AssetObj is None:
|
||||
continue
|
||||
AssetList.append({
|
||||
"id": AssetObj.id,
|
||||
"name": AssetObj.name,
|
||||
"moderation_status": AssetObj.moderation_status
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"assets": AssetList,
|
||||
"lastPage": not AssetsList.has_next
|
||||
})
|
||||
|
||||
@AvatarRoute.route('/avatar/getavatar', methods=['GET'])
|
||||
@auth.authenticated_required_api
|
||||
def get_avatar():
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
userCurrentlyWearing : list[UserAvatarAsset] = UserAvatarAsset.query.filter_by(user_id=AuthenticatedUser.id).all()
|
||||
userCurrentlyWearingList : list[int] = []
|
||||
for asset in userCurrentlyWearing:
|
||||
AssetObj : Asset = Asset.query.filter_by(id=asset.asset_id).first()
|
||||
userCurrentlyWearingList.append({
|
||||
"id": AssetObj.id,
|
||||
"name": AssetObj.name,
|
||||
"type": AssetObj.asset_type.value,
|
||||
"moderation_status": AssetObj.moderation_status
|
||||
})
|
||||
avatar : UserAvatar = UserAvatar.query.filter_by(user_id=AuthenticatedUser.id).first()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"currentlyWearing": userCurrentlyWearingList,
|
||||
"bodyColors": [
|
||||
avatar.head_color_id,
|
||||
avatar.torso_color_id,
|
||||
avatar.left_arm_color_id,
|
||||
avatar.right_arm_color_id,
|
||||
avatar.left_leg_color_id,
|
||||
avatar.right_leg_color_id
|
||||
],
|
||||
"rigType": "R15" if avatar.r15 else "R6",
|
||||
"scales": {
|
||||
"height": avatar.height_scale,
|
||||
"width": avatar.width_scale,
|
||||
"head": avatar.head_scale,
|
||||
"proportion": avatar.proportion_scale,
|
||||
"bodyType": avatar.body_type_scale
|
||||
}
|
||||
})
|
||||
|
||||
@AvatarRoute.route("/avatar/setavatar", methods=["POST"])
|
||||
@auth.authenticated_required_api
|
||||
@csrf.exempt
|
||||
def set_wearing_assets():
|
||||
if not websiteFeatures.GetWebsiteFeature("AvatarUpdate"):
|
||||
return jsonify({"success": False, "message": "Avatar updating is currently disabled."}), 400
|
||||
|
||||
newAvatarData = request.json
|
||||
if "bodyColors" not in newAvatarData or "assets" not in newAvatarData or "rigType" not in newAvatarData:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
if len(newAvatarData["bodyColors"]) != 6:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if AuthenticatedUser is None:
|
||||
return redirect('/login')
|
||||
if redis_controller.get(f"avatar:lock:{AuthenticatedUser.id}") is not None:
|
||||
return jsonify({"success": False, "message": "You are changing your avatar too fast."}), 429
|
||||
redis_controller.set(f"avatar:lock:{AuthenticatedUser.id}", "1", ex=5)
|
||||
avatar : UserAvatar = UserAvatar.query.filter_by(user_id=AuthenticatedUser.id).first()
|
||||
|
||||
if len(newAvatarData["assets"]) > 32:
|
||||
return jsonify({"success": False, "message": "Too many assets"}), 400
|
||||
AssetTypeCount = {}
|
||||
for asset in newAvatarData["assets"]:
|
||||
AssetObj : Asset = Asset.query.filter_by(id=asset).first()
|
||||
if AssetObj is None:
|
||||
return jsonify({"success": False, "message": "Invalid asset"}), 400
|
||||
if AssetObj.asset_type.value not in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
|
||||
return jsonify({"success": False, "message": "Invalid asset"}), 400
|
||||
if AssetTypeCount.get(AssetObj.asset_type.value) is None:
|
||||
AssetTypeCount[AssetObj.asset_type.value] = 1
|
||||
else:
|
||||
AssetTypeCount[AssetObj.asset_type.value] += 1
|
||||
if AssetTypeCount[AssetObj.asset_type.value] > 1 and AssetObj.asset_type != AssetType.Hat and AssetObj.asset_type != AssetType.HairAccessory and AssetObj.asset_type != AssetType.FaceAccessory:
|
||||
return jsonify({"success": False, "message": "Invalid configuration"}), 400
|
||||
elif AssetTypeCount[AssetObj.asset_type.value] > 5 and AssetObj.asset_type == AssetType.Hat:
|
||||
return jsonify({"success": False, "message": "Invalid configuration"}), 400
|
||||
|
||||
if AssetObj.moderation_status != 0:
|
||||
newAvatarData["assets"].remove(asset)
|
||||
|
||||
UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=asset).first()
|
||||
if UserAssetObj is None:
|
||||
return jsonify({"success": False, "message": "Invalid asset"}), 400
|
||||
for Color in newAvatarData["bodyColors"]:
|
||||
if Color not in AllowedBodyColors:
|
||||
return jsonify({"success": False, "message": "Invalid body color"}), 400
|
||||
|
||||
UserAvatarAsset.query.filter_by(user_id=AuthenticatedUser.id).delete()
|
||||
db.session.commit()
|
||||
for asset in newAvatarData["assets"]:
|
||||
UserAvatarAssetObj = UserAvatarAsset(
|
||||
user_id=AuthenticatedUser.id,
|
||||
asset_id=asset
|
||||
)
|
||||
db.session.add(UserAvatarAssetObj)
|
||||
avatar.head_color_id = newAvatarData["bodyColors"][0]
|
||||
avatar.torso_color_id = newAvatarData["bodyColors"][1]
|
||||
avatar.left_arm_color_id = newAvatarData["bodyColors"][2]
|
||||
avatar.right_arm_color_id = newAvatarData["bodyColors"][3]
|
||||
avatar.left_leg_color_id = newAvatarData["bodyColors"][4]
|
||||
avatar.right_leg_color_id = newAvatarData["bodyColors"][5]
|
||||
avatar.r15 = newAvatarData["rigType"] == "R15"
|
||||
|
||||
if "scales" in newAvatarData:
|
||||
if "height" not in newAvatarData["scales"] or "width" not in newAvatarData["scales"] or "head" not in newAvatarData["scales"] or "proportion" not in newAvatarData["scales"]:
|
||||
return jsonify({"success": False, "message": "Invalid request"}), 400
|
||||
if newAvatarData["scales"]["height"] < ScalingLimits["height"]["min"] or newAvatarData["scales"]["height"] > ScalingLimits["height"]["max"]:
|
||||
return jsonify({"success": False, "message": "Invalid height scale"}), 400
|
||||
if newAvatarData["scales"]["width"] < ScalingLimits["width"]["min"] or newAvatarData["scales"]["width"] > ScalingLimits["width"]["max"]:
|
||||
return jsonify({"success": False, "message": "Invalid width scale"}), 400
|
||||
if newAvatarData["scales"]["head"] < ScalingLimits["head"]["min"] or newAvatarData["scales"]["head"] > ScalingLimits["head"]["max"]:
|
||||
return jsonify({"success": False, "message": "Invalid head scale"}), 400
|
||||
if newAvatarData["scales"]["proportion"] < ScalingLimits["proportion"]["min"] or newAvatarData["scales"]["proportion"] > ScalingLimits["proportion"]["max"]:
|
||||
return jsonify({"success": False, "message": "Invalid proportion scale"}), 400
|
||||
|
||||
avatar.height_scale = round(newAvatarData["scales"]["height"], 2)
|
||||
avatar.width_scale = round(newAvatarData["scales"]["width"], 2)
|
||||
avatar.head_scale = round(newAvatarData["scales"]["head"], 2)
|
||||
avatar.proportion_scale = round(newAvatarData["scales"]["proportion"], 2)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
UserThumbnail.query.filter_by(userid=AuthenticatedUser.id).delete()
|
||||
db.session.commit()
|
||||
TakeUserThumbnail(AuthenticatedUser.id, True, False)
|
||||
# handle 3d within
|
||||
TakeUserThumbnail(AuthenticatedUser.id, bypassCooldown=True, bypassCache=True, Is3D=True)
|
||||
return jsonify({"success": True}), 200
|
||||
|
||||
@AvatarRoute.route("/avatar/forceredraw", methods=["POST"])
|
||||
@auth.authenticated_required_api
|
||||
@csrf.exempt
|
||||
def force_redraw():
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if redis_controller.get(f"avatar:lock:{AuthenticatedUser.id}") is not None:
|
||||
return jsonify({"success": False, "message": "A redraw has recently been started please try again later."}), 429
|
||||
redis_controller.set(f"avatar:lock:{AuthenticatedUser.id}", "1", ex=5)
|
||||
UserThumbnail.query.filter_by(userid=AuthenticatedUser.id).delete()
|
||||
db.session.commit()
|
||||
TakeUserThumbnail(AuthenticatedUser.id, True, True)
|
||||
# handle 3d within
|
||||
TakeUserThumbnail(AuthenticatedUser.id, bypassCooldown=True, bypassCache=True, Is3D=True)
|
||||
return jsonify({"success": True}), 200
|
||||
|
||||
|
||||
|
||||
@AvatarRoute.route("/avatar/3d", methods=["GET"])
|
||||
@auth.authenticated_required_api
|
||||
def get_3d_avatar():
|
||||
AuthenticatedUser: User = auth.GetCurrentUser()
|
||||
|
||||
target_user_id = request.args.get("userId", None)
|
||||
|
||||
if target_user_id:
|
||||
target_id = target_user_id
|
||||
else:
|
||||
target_id = AuthenticatedUser.id
|
||||
|
||||
UserThumbnailObj: UserThumbnail = UserThumbnail.query.filter_by(userid=target_id).first()
|
||||
|
||||
if UserThumbnailObj and UserThumbnailObj.full_3dcontenthash:
|
||||
return jsonify({
|
||||
"targetId": target_id,
|
||||
"state": "Completed",
|
||||
"imageUrl": UserThumbnailObj.full_3dcontenthash,
|
||||
"version": "TN3"
|
||||
})
|
||||
else:
|
||||
TakeUserThumbnail(target_id, bypassCooldown=True, bypassCache=False, Is3D=True)
|
||||
|
||||
return jsonify({
|
||||
"targetId": target_id,
|
||||
"state": "Pending",
|
||||
"imageUrl": "",
|
||||
"version": "TN3"
|
||||
})
|
||||
|
||||
|
||||
@AvatarRoute.route("/avatar/isthumbnailready", methods=["GET"])
|
||||
@auth.authenticated_required_api
|
||||
@csrf.exempt
|
||||
def is_thumbnail_ready():
|
||||
AuthenticateUser : User = auth.GetCurrentUser()
|
||||
UserThumbnailObj : UserThumbnail = UserThumbnail.query.filter_by(userid=AuthenticateUser.id).first()
|
||||
if UserThumbnailObj is None:
|
||||
return jsonify({"success": True, "ready": False}), 200
|
||||
else:
|
||||
if UserThumbnailObj.full_contenthash is None:
|
||||
return jsonify({"success": True, "ready": False}), 200
|
||||
return jsonify({"success": True, "ready": True}), 200
|
||||
@@ -0,0 +1,184 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}{{asset.name}}{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/catalog.css" rel="stylesheet"/>
|
||||
<script src="/static/js/catalog.js?version=5"></script>
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.text-limited {
|
||||
color: rgb(255,255,255) !important;
|
||||
background-color: rgb(9, 136, 62);
|
||||
width: fit-content;
|
||||
padding-left: 10px;
|
||||
font-size: 20px;
|
||||
border-top-right-radius: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
.text-limitedu {
|
||||
color: rgb(0,0,0) !important;
|
||||
background-color: rgb(245, 245, 52);
|
||||
padding-right: 3px;
|
||||
font-weight: 800;
|
||||
border-top-right-radius: 5px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-size: small;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<div id="csrf_token" data-csrf-token="{{ csrf_token() }}"></div>
|
||||
<meta id="asset-info" data-asset-name="{{asset.name}}" data-asset-id="{{asset.id}}" data-asset-onsale="{% if asset.is_for_sale: %}true{%else%}false{%endif%}" {% if doesUserOwnAsset: %}data-user-ownasset="true"{% endif %} {% if asset.is_limited: %}data-islimited="true"{%endif%} {%if OffsaleAt: %}data-offsale="{{OffsaleAt}}"{%endif%}>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container ps-5 pe-5 ps-md-0 pe-md-0" style="max-width: 800px;">
|
||||
<a href="/catalog/" class="text-decoration-none text-white">< Return to Catalog</a>
|
||||
<h1>{{asset.name}}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-5 col-12">
|
||||
<div class="p-2 rounded overflow-hidden position-relative" style="background-color: rgb(36, 36, 36);">
|
||||
<img width="100%" src="/Thumbs/Asset.ashx?assetId={{asset.id}}&x=420&y=420" style="aspect-ratio: 1/1;" alt="{{asset.name}}">
|
||||
{% if asset.is_limited: %}<p class="position-absolute m-0 fw-bold text-limited" style="bottom:-2px;left:0px;">LIMITED {%if asset.is_limited_unique: %}<span class="text-limitedu" style="padding-top:1.5px">U</span>{%else%}<span class="me-2"></span>{%endif%}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 p-1">
|
||||
<div class="d-flex mt-3 mt-md-0">
|
||||
{% if asset.creator_type == 0 :%}
|
||||
<img width="80" height="80" src="/Thumbs/Avatar.ashx?userId={{creator.id}}&x=100&y=100" alt="{{creator.username}}">
|
||||
<div style="font-size:13px;">
|
||||
<p class="m-0 text-secondary">Creator: <a href="/users/{{creator.id}}/profile" class="text-decoration-none">{{creator.username}}</a></p>
|
||||
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
|
||||
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
|
||||
</div>
|
||||
{% else %}
|
||||
<img width="80" height="80" src="/Thumbs/GroupIcon.ashx?groupid={{creator.id}}&x=100&y=100" alt="{{creator.name}}">
|
||||
<div style="font-size:13px;">
|
||||
<p class="m-0 text-secondary">Creator: <a href="/groups/{{creator.id}}/--" class="text-decoration-none">{{creator.name}}</a></p>
|
||||
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
|
||||
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<p>
|
||||
{{asset.description}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3 p-1" >
|
||||
{% if OffsaleAt: %}
|
||||
<p class="m-0 text-secondary mb-2 w-100 text-center" style="font-size: 14px;">Offsale in: <span class="text-danger" id="offsale-countdown">00:00:00</span> <i class="bi bi-alarm"></i></p>
|
||||
{% endif %}
|
||||
<div class="border rounded p-2 text-center">
|
||||
{% if asset.is_for_sale: %}
|
||||
{% if asset.price_robux == 0 and asset.price_tix == 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">FREE</span></p>
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Take One</a>
|
||||
{% endif %}
|
||||
{% if asset.price_robux > 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_robux}} Robux</span></p>
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Buy with Robux</a>
|
||||
{% endif %}
|
||||
{% if asset.price_robux > 0 and asset.price_tix > 0: %}
|
||||
<div class="d-flex align-items-center mt-2 mb-2">
|
||||
<div class="border-top flex-fill me-2"></div>
|
||||
<p class="text-secondary m-0" style="font-size: 13px;">OR</p>
|
||||
<div class="border-top flex-fill ms-2"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if asset.price_tix > 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_tix}} Tickets</span></p>
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="1" data-expected-price="{{asset.price_tix}}">Buy with Tickets</a>
|
||||
{% endif %}
|
||||
{% else: %}
|
||||
{% if asset.is_limited: %}
|
||||
{% if BestPrice != "None": %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Best Price: <span class="text-white">R$ {{BestPrice}}</span></p>
|
||||
{% if BestPriceResult.userid != currentuser.id %}
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{BestPriceResult.userid}}" data-expectedid="{{BestPriceResult.id}}" data-expected-price="{{BestPrice}}">Buy Now</a>
|
||||
{%else%}
|
||||
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
|
||||
{%endif%}
|
||||
{%else%}
|
||||
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">No Resellers Available</btn>
|
||||
{%endif%}
|
||||
{% if userOwnAmountCount > 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:12px;">( {{userOwnAmountCount}} owned )</p>
|
||||
{%endif%}
|
||||
{%else%}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">Offsale</span></p>
|
||||
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
|
||||
{%endif%}
|
||||
{% endif %}
|
||||
<p class="m-0 mt-1 text-secondary" style="font-size:12px;">( {{asset.sale_count}} sold )</p>
|
||||
</div>
|
||||
{% if asset.is_limited and userOwnAmountCount > 0 and not asset.is_for_sale: %}
|
||||
<div class="border rounded p-2 text-center mt-2">
|
||||
<a href="/catalog/resell/{{asset.id}}" class="btn btn-success fw-bold btn-sm w-100">Manage your items</a>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{% if asset.is_limited and not asset.is_for_sale: %}
|
||||
<div class="linebreak mt-3"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Private Sales</h5>
|
||||
{% for sale in PrivateSales: %}
|
||||
<div class="w-100 m-2 d-flex">
|
||||
<img class="rounded-2 overflow-hidden" width="60" height="60" src="/Thumbs/Avatar.ashx?x=60&y=60&userId={{sale.sellerid}}" alt="{{sale.seller}}"/>
|
||||
<div class="ms-2">
|
||||
<a href="/users/{{sale.sellerid}}/profile" class="text-decoration-none text-white m-0">{{sale.seller}}</a>
|
||||
<p class="m-0 text-robux">R$ {{sale.price}}</p>
|
||||
<p class="m-0 text-secondary" style="font-size:13px;">Serial: {%if asset.is_limited_unique: %}{{sale.serial}}{%else%}N/A{%endif%}</p>
|
||||
</div>
|
||||
<div class="ms-auto align-items-center d-flex">
|
||||
{% if sale.sellerid != currentuser.id: %}
|
||||
<button class="btn btn-sm btn-primary purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{sale.sellerid}}" data-expectedid="{{sale.uaid}}" data-expected-price="{{sale.price}}">Buy Now</button>
|
||||
{%else%}
|
||||
<button class="btn btn-sm btn-primary disabled">Buy Now</button>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
<div class="d-flex">
|
||||
{% if PreviousPage > 0: %}
|
||||
<a href="/catalog/{{asset.id}}/?page={{PreviousPage}}" class="ms-auto text-decoration-none">Previous</a>
|
||||
{%else%}
|
||||
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
|
||||
{% endif %}
|
||||
<p class="ms-1 me-1">{{PageNumber}}</p>
|
||||
{% if NextPage > 0: %}
|
||||
<a href="/catalog/{{asset.id}}/?page={{NextPage}}" class="me-auto text-decoration-none">Next</a>
|
||||
{%else%}
|
||||
<a class="me-auto text-decoration-none text-secondary">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="m-0 text-secondary" style="font-size:small;">Recent Average Price: <span class="text-robux">R${{AssetRap}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="purchaseModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="purchase-modal-title">Confirm purchase</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="purchase-modal-content">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="purchase-modal-close" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" id="purchase-modal-btn">Purchase</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}{{badge.name}}{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/catalog.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container ps-5 pe-5 ps-md-0 pe-md-0" style="max-width: 800px;">
|
||||
<h1>{{badge.name}}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-5 col-12">
|
||||
<div class="p-2 rounded overflow-hidden position-relative{% if AssociatedPlaceAssetObj != None %} rounded-bottom-0 {%endif%}" style="background-color: rgb(36, 36, 36);">
|
||||
<img width="100%" src="/Thumbs/Asset.ashx?assetId={{badge.icon_image_id}}&x=420&y=420" style="aspect-ratio: 1/1;" alt="{{badge.name}}">
|
||||
</div>
|
||||
{% if AssociatedPlaceAssetObj != None: %}
|
||||
<div class="rounded-bottom overflow-hidden d-flex p-1" style="background-color: rgb(29, 29, 29);">
|
||||
<p class="m-0 text-secondary me-2" style="font-size: 13px;">Earn this Badge in: <a class="text-decoration-none text-white" href="/games/{{AssociatedPlaceAssetObj.id}}/">{{AssociatedPlaceAssetObj.name}}</a></p>
|
||||
<a class="text-decoration-none ms-auto" href="/games/{{AssociatedPlaceAssetObj.id}}/"><img src="/Thumbs/PlaceIcon.ashx?assetId={{AssociatedPlaceAssetObj.id}}&x=60&y=60" height="60px" width="60px" class="rounded"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 p-1">
|
||||
<div class="d-flex mt-3 mt-md-0">
|
||||
{% if AssociatedPlaceAssetObj.creator_type == 0 :%}
|
||||
<img width="80" height="80" src="/Thumbs/Avatar.ashx?userId={{creator.id}}&x=100&y=100" alt="{{creator.username}}">
|
||||
<div style="font-size:13px;">
|
||||
<p class="m-0 text-secondary">Creator: <a href="/users/{{creator.id}}/profile" class="text-decoration-none">{{creator.username}}</a></p>
|
||||
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
|
||||
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
|
||||
</div>
|
||||
{% else %}
|
||||
<img width="80" height="80" src="/Thumbs/GroupIcon.ashx?groupid={{creator.id}}&x=100&y=100" alt="{{creator.name}}">
|
||||
<div style="font-size:13px;">
|
||||
<p class="m-0 text-secondary">Creator: <a href="/groups/{{creator.id}}/--" class="text-decoration-none">{{creator.name}}</a></p>
|
||||
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
|
||||
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<p>
|
||||
{{badge.description}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,824 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify, abort
|
||||
from app.util import auth, turnstile, websiteFeatures
|
||||
from app.routes.thumbnailer import TakeUserThumbnail
|
||||
from app.util.redislock import acquire_lock, release_lock
|
||||
from app.extensions import db, csrf, limiter, redis_controller, user_limiter
|
||||
from app.models.asset import Asset
|
||||
from app.models.usereconomy import UserEconomy
|
||||
from app.models.userassets import UserAsset
|
||||
from app.models.user_avatar_asset import UserAvatarAsset
|
||||
from app.models.user import User
|
||||
from app.models.package_asset import PackageAsset
|
||||
from app.models.gamepass_link import GamepassLink
|
||||
from app.models.asset_rap import AssetRap
|
||||
from app.models.groups import Group
|
||||
from app.models.place_badge import PlaceBadge, UserBadge
|
||||
from app.models.universe import Universe
|
||||
from app.models.limited_item_transfers import LimitedItemTransfer
|
||||
from app.routes.badgesapi import CalculateBadgeRarity, GetBadgeAwardedPastDay
|
||||
from app.pages.catalog.catalogtypes import CatalogTypes
|
||||
from app.pages.messages.messages import CreateSystemMessage
|
||||
from slugify import slugify
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text, and_, or_
|
||||
import math
|
||||
from app.enums.AssetType import AssetType
|
||||
from app.util.transactions import CreateTransactionForSale
|
||||
import logging
|
||||
import calendar
|
||||
import time
|
||||
import redis_lock
|
||||
from app.util.membership import GetUserMembership
|
||||
from app.enums.MembershipType import MembershipType
|
||||
from app.enums.LimitedItemTransferMethod import LimitedItemTransferMethod
|
||||
from app.services.economy import IncrementTargetBalance, AdjustAssetRap, GetAssetRap, TaxCurrencyAmount, GetUserBalance, GetGroupBalance, DecrementTargetBalance,EconomyLockAcquireException, InsufficientFundsException
|
||||
from app.services.groups import GetUserFromId, GetGroupFromId
|
||||
|
||||
CatalogRoute = Blueprint('catalog', __name__, template_folder="pages", url_prefix="/catalog")
|
||||
LibraryRoute = Blueprint('library', __name__, template_folder="pages", url_prefix="/library")
|
||||
BadgesPageRoute = Blueprint('badges_page', __name__, template_folder="pages", url_prefix="/badges")
|
||||
|
||||
def IncrementAssetCreator( AssetObject : Asset, AmountGiven : int, CurrencyType : int):
|
||||
""" Increments the robux of the asset creator"""
|
||||
CurrencyToGive = math.floor(AmountGiven * 0.7)
|
||||
if CurrencyToGive <= 0:
|
||||
return True
|
||||
|
||||
if AssetObject.creator_type == 0:
|
||||
# User
|
||||
IncrementTargetBalance(GetUserFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
|
||||
return True
|
||||
|
||||
elif AssetObject.creator_type == 1:
|
||||
# Group
|
||||
IncrementTargetBalance(GetGroupFromId(AssetObject.creator_id), CurrencyToGive, CurrencyType)
|
||||
return True
|
||||
|
||||
def ConvertQueryToAsset( queryResult ):
|
||||
"""Converts a query result to a dict with the same keys as the Asset model"""
|
||||
return {
|
||||
"id": queryResult[0],
|
||||
"roblox_asset_id": queryResult[1],
|
||||
"name": queryResult[2],
|
||||
"description": queryResult[3],
|
||||
"created_at": queryResult[4],
|
||||
"updated_at": queryResult[5],
|
||||
"asset_type": queryResult[6],
|
||||
"asset_genre": queryResult[7],
|
||||
"creator_type": queryResult[8],
|
||||
"creator_id": queryResult[9],
|
||||
"moderation_status": queryResult[10],
|
||||
"is_for_sale": queryResult[11],
|
||||
"price_robux": queryResult[12],
|
||||
"price_tix": queryResult[13],
|
||||
"is_limited": queryResult[14],
|
||||
"is_limited_unique": queryResult[15],
|
||||
"serial_count": queryResult[16],
|
||||
"sale_count": queryResult[17],
|
||||
"offsale_at": queryResult[18]
|
||||
}
|
||||
|
||||
OrderTypes = {
|
||||
0: "created_at DESC", # Default
|
||||
1: "price_robux ASC", # Lowest to Highest
|
||||
2: "price_robux DESC", # Highest to Lowest
|
||||
3: "updated_at DESC", # Recently Updated
|
||||
4: "sale_count DESC" # Best Selling
|
||||
}
|
||||
|
||||
import urllib.parse
|
||||
|
||||
@CatalogRoute.route("/", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
@csrf.exempt
|
||||
def catalog_search():
|
||||
CategoryType = int(request.args.get(key="category", default=0, type=int))
|
||||
OrderBy = request.form.get(key="order-by", default=0, type=int)
|
||||
if OrderBy not in OrderTypes:
|
||||
OrderBy = 0
|
||||
SearchQuery = request.form.get(key="search-input", default="", type=str)
|
||||
if len(SearchQuery) < 3:
|
||||
SearchQuery = ""
|
||||
if SearchQuery == "":
|
||||
return redirect(f"/catalog/?sort={OrderBy}&category={str(CategoryType)}")
|
||||
# Make sure the query is url safe
|
||||
SearchQuery = urllib.parse.quote(SearchQuery)
|
||||
|
||||
return redirect(f"/catalog/?q={SearchQuery}&sort={OrderBy}&category={str(CategoryType)}")
|
||||
|
||||
@CatalogRoute.route("/", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def catalog_page():
|
||||
CatalogType = int(request.args.get("category", 0))
|
||||
if CatalogType not in CatalogTypes:
|
||||
return redirect("/catalog/")
|
||||
if request.args.get("page") is None:
|
||||
Page = 1
|
||||
else:
|
||||
try:
|
||||
Page = int(request.args.get("page"))
|
||||
except:
|
||||
Page = 1
|
||||
SearchInput = request.args.get("q", "", type=str)
|
||||
"""
|
||||
Types of sorting:
|
||||
0 - Relevance (Default) [ Just sort by created_at ]
|
||||
1 - Price (Lowest to Highest)
|
||||
2 - Price (Highest to Lowest)
|
||||
3 - Recently Updated
|
||||
4 - Best Selling
|
||||
"""
|
||||
if request.args.get("sort") is None:
|
||||
SortType = 0
|
||||
else:
|
||||
try:
|
||||
SortType = int(request.args.get("sort"))
|
||||
except:
|
||||
SortType = 0
|
||||
if SortType > 4 or SortType < 0:
|
||||
SortType = 0
|
||||
SearchQuery = Asset.query
|
||||
CatalogTypesDict = {
|
||||
0: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
|
||||
AssetType.Hat,
|
||||
AssetType.TShirt,
|
||||
AssetType.Shirt,
|
||||
AssetType.Pants,
|
||||
AssetType.Gear,
|
||||
AssetType.Face,
|
||||
AssetType.Head,
|
||||
AssetType.HairAccessory,
|
||||
AssetType.FaceAccessory,
|
||||
AssetType.NeckAccessory,
|
||||
AssetType.ShoulderAccessory,
|
||||
AssetType.FrontAccessory,
|
||||
AssetType.BackAccessory,
|
||||
AssetType.WaistAccessory,
|
||||
AssetType.Package
|
||||
))).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0)).filter(or_(
|
||||
Asset.is_for_sale == True, Asset.is_limited == True
|
||||
)),
|
||||
1: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
|
||||
AssetType.Hat,
|
||||
AssetType.HairAccessory,
|
||||
AssetType.FaceAccessory,
|
||||
AssetType.NeckAccessory,
|
||||
AssetType.ShoulderAccessory,
|
||||
AssetType.FrontAccessory,
|
||||
AssetType.BackAccessory,
|
||||
AssetType.WaistAccessory,
|
||||
))).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0)).filter(or_(
|
||||
Asset.is_for_sale == True, Asset.is_limited == True
|
||||
)),
|
||||
2: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0, Asset.is_for_sale == True)),
|
||||
3: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter(and_(Asset.creator_id.in_([1]), Asset.creator_type == 0, Asset.is_for_sale == True)),
|
||||
4: lambda queryObj: queryObj.filter_by(is_limited = True),
|
||||
5: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
|
||||
AssetType.Hat,
|
||||
AssetType.HairAccessory,
|
||||
AssetType.FaceAccessory,
|
||||
AssetType.NeckAccessory,
|
||||
AssetType.ShoulderAccessory,
|
||||
AssetType.FrontAccessory,
|
||||
AssetType.BackAccessory,
|
||||
AssetType.WaistAccessory,
|
||||
))).filter_by(is_limited = True),
|
||||
6: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Gear).filter_by(is_limited = True),
|
||||
7: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_limited = True),
|
||||
8: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
|
||||
AssetType.Hat,
|
||||
AssetType.TShirt,
|
||||
AssetType.Shirt,
|
||||
AssetType.Pants,
|
||||
AssetType.Package,
|
||||
))).filter_by(is_for_sale = True),
|
||||
9: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
|
||||
AssetType.Hat,
|
||||
AssetType.HairAccessory,
|
||||
AssetType.FaceAccessory,
|
||||
AssetType.NeckAccessory,
|
||||
AssetType.ShoulderAccessory,
|
||||
AssetType.FrontAccessory,
|
||||
AssetType.BackAccessory,
|
||||
AssetType.WaistAccessory,
|
||||
))).filter_by(is_for_sale = True),
|
||||
10: lambda queryObj: queryObj.filter_by(asset_type = AssetType.Shirt).filter_by(is_for_sale = True),
|
||||
11: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.TShirt).filter_by(is_for_sale = True),
|
||||
12: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Pants).filter_by(is_for_sale = True),
|
||||
13: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Package).filter_by(is_for_sale = True),
|
||||
14: lambda queryObj: queryObj.filter(Asset.asset_type.in_((
|
||||
AssetType.Head,
|
||||
AssetType.Face,
|
||||
AssetType.Package
|
||||
))).filter_by(is_for_sale = True),
|
||||
15: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Head).filter_by(is_for_sale = True),
|
||||
16: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.Face).filter_by(is_for_sale = True),
|
||||
41: lambda queryObj: queryObj.filter(Asset.asset_type == AssetType.HairAccessory).filter_by(is_for_sale = True),
|
||||
}
|
||||
if CatalogType > 41 or CatalogType < 0:
|
||||
CatalogType = 0
|
||||
SearchQuery = CatalogTypesDict[CatalogType](SearchQuery)
|
||||
if SearchInput != "" and len(SearchInput) > 3:
|
||||
SearchQuery = SearchQuery.filter( Asset.name.ilike(f"%{SearchInput}%") )
|
||||
else:
|
||||
SearchInput = ""
|
||||
if SortType == 0:
|
||||
SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
|
||||
elif SortType == 1:
|
||||
SearchQuery = SearchQuery.order_by(Asset.price_robux.asc())
|
||||
elif SortType == 2:
|
||||
SearchQuery = SearchQuery.order_by(Asset.price_robux.desc())
|
||||
elif SortType == 3:
|
||||
SearchQuery = SearchQuery.order_by(Asset.updated_at.desc())
|
||||
elif SortType == 4:
|
||||
SearchQuery = SearchQuery.order_by(Asset.sale_count.desc())
|
||||
else:
|
||||
SearchQuery = SearchQuery.order_by(Asset.created_at.desc())
|
||||
SearchQuery = SearchQuery.paginate(
|
||||
page=Page,
|
||||
per_page=24,
|
||||
error_out=False
|
||||
)
|
||||
for AssetObj in SearchQuery.items:
|
||||
if AssetObj.is_limited and not AssetObj.is_for_sale:
|
||||
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=AssetObj.id, is_for_sale=True).order_by(UserAsset.price.asc()).first()
|
||||
if BestPriceResult is not None:
|
||||
AssetObj.best_price = str(BestPriceResult.price)
|
||||
else:
|
||||
AssetObj.best_price = "--"
|
||||
if Page == 1:
|
||||
PreviousPage = -1
|
||||
else:
|
||||
PreviousPage = Page -1
|
||||
if SearchQuery.has_next:
|
||||
NextPage = Page + 1
|
||||
else:
|
||||
NextPage = -1
|
||||
|
||||
return render_template("catalog/index.html",
|
||||
categoryname=CatalogTypes[CatalogType]["name"],
|
||||
queryResults = SearchQuery.items,
|
||||
categoryid=CatalogType,
|
||||
PreviousPage=PreviousPage,
|
||||
NextPage=NextPage,
|
||||
PageNumber=Page,
|
||||
CatalogType=CatalogType,
|
||||
SortType=SortType,
|
||||
query=SearchInput,
|
||||
totalPages = SearchQuery.pages,
|
||||
totalResults = SearchQuery.total)
|
||||
|
||||
@CatalogRoute.route("/<int:assetid>/", methods=["GET"])
|
||||
@CatalogRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def asset_page(assetid, assetname=None):
|
||||
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
|
||||
if AssetObject is None:
|
||||
return redirect("/catalog")
|
||||
SlugName = slugify(AssetObject.name, lowercase=False)
|
||||
if SlugName is None or SlugName == "":
|
||||
SlugName = "unnamed"
|
||||
if assetname is None:
|
||||
if request.args.get("page") is None:
|
||||
return redirect(f"/catalog/{assetid}/{SlugName}")
|
||||
else:
|
||||
return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
|
||||
if assetname != SlugName:
|
||||
if request.args.get("page") is None:
|
||||
return redirect(f"/catalog/{assetid}/{SlugName}")
|
||||
else:
|
||||
return redirect(f"/catalog/{assetid}/{SlugName}?page={request.args.get('page')}")
|
||||
if AssetObject.asset_type.value not in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
|
||||
if AssetObject.asset_type.value == 9:
|
||||
return redirect(f"/games/{assetid}/")
|
||||
if AssetObject.asset_type.value in [1,3,4,5,10,13,24,34,38,40]:
|
||||
return redirect(f"/library/{assetid}/")
|
||||
return redirect("/catalog/")
|
||||
|
||||
CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
|
||||
Created = AssetObject.created_at.strftime("%d/%m/%Y")
|
||||
Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
|
||||
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
|
||||
BestPrice = "None"
|
||||
if BestPriceResult is not None:
|
||||
BestPrice = str(BestPriceResult.price)
|
||||
UserOwns = 0
|
||||
PrivateSaleList = []
|
||||
NextPage = 0
|
||||
PreviousPage = 0
|
||||
PageNumber = 0
|
||||
AssetRap = 0
|
||||
if AssetObject.is_limited and not AssetObject.is_for_sale:
|
||||
AssetRap = GetAssetRap(assetid)
|
||||
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
|
||||
Page = 1
|
||||
if request.args.get("page"):
|
||||
try:
|
||||
Page = int(request.args.get("page"))
|
||||
except:
|
||||
pass
|
||||
PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5)
|
||||
for sale in PrivateSales.items:
|
||||
SellerUser : User = User.query.filter_by(id=sale.userid).first()
|
||||
PrivateSaleList.append({
|
||||
"price": sale.price,
|
||||
"seller": SellerUser.username,
|
||||
"sellerid": SellerUser.id,
|
||||
"serial": sale.serial,
|
||||
"uaid": sale.id
|
||||
})
|
||||
if PrivateSales.has_next:
|
||||
NextPage = Page + 1
|
||||
else:
|
||||
NextPage = -1
|
||||
if Page > 1:
|
||||
PreviousPage = Page - 1
|
||||
else:
|
||||
PreviousPage = -1
|
||||
PageNumber = Page
|
||||
OffsaleAt = None
|
||||
if AssetObject.offsale_at is not None:
|
||||
if datetime.utcnow() > AssetObject.offsale_at:
|
||||
AssetObject.is_for_sale = False
|
||||
AssetObject.offsale_at = None
|
||||
db.session.commit()
|
||||
else:
|
||||
OffsaleAt = int(calendar.timegm(AssetObject.offsale_at.timetuple()))
|
||||
return render_template("catalog/asset.html", asset=AssetObject, creator=CreatorObj, createddate=Created,
|
||||
updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice,
|
||||
BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
|
||||
NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
|
||||
OffsaleAt=OffsaleAt)
|
||||
|
||||
@CatalogRoute.route("/resell/<int:assetid>", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def resell_page(assetid):
|
||||
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
|
||||
if AssetObject is None:
|
||||
return redirect("/catalog")
|
||||
if not AssetObject.is_limited:
|
||||
return redirect(f"/catalog/{str(AssetObject.id)}/")
|
||||
if AssetObject.is_for_sale:
|
||||
return redirect(f"/catalog/{str(AssetObject.id)}/") # Users cant resell items when it is still for sale
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
|
||||
if UserOwns == 0:
|
||||
return redirect(f"/catalog/{str(AssetObject.id)}/")
|
||||
AllUserAssets = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).all()
|
||||
UserAssetCurrentlyForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=True).count()
|
||||
UserAssetNotForSale = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid, is_for_sale=False).count()
|
||||
return render_template("catalog/resell.html", asset=AssetObject, userassets=AllUserAssets, userassetforsalecount=UserAssetCurrentlyForSale, userassetnotforsalecount=UserAssetNotForSale, isOTPRequired = AuthenticatedUser.TOTPEnabled)
|
||||
|
||||
@CatalogRoute.route("/resell/<int:assetid>", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
@user_limiter.limit("1/second")
|
||||
def resell_page_post(assetid):
|
||||
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
|
||||
if AssetObject is None:
|
||||
return redirect("/catalog")
|
||||
if not AssetObject.is_limited:
|
||||
return redirect(f"/catalog/{str(AssetObject.id)}/")
|
||||
if AssetObject.is_for_sale:
|
||||
return redirect(f"/catalog/{str(AssetObject.id)}/")
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if "uaid" not in request.form or "itemprice" not in request.form:
|
||||
flash("Invalid request", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
try:
|
||||
ItemPrice = int(request.form["itemprice"])
|
||||
UAID = int(request.form["uaid"])
|
||||
except:
|
||||
flash("Invalid request", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
UserAssetObj : UserAsset = UserAsset.query.filter_by(id=UAID).first()
|
||||
if UserAssetObj is None:
|
||||
flash("Invalid request", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
if UserAssetObj.userid != AuthenticatedUser.id:
|
||||
flash("Invalid request", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
if UserAssetObj.assetid != AssetObject.id:
|
||||
flash("Invalid request", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
|
||||
if not websiteFeatures.GetWebsiteFeature("ItemReselling"):
|
||||
flash("Item reselling is currently disabled", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
|
||||
if AuthenticatedUser.TOTPEnabled:
|
||||
if "2fa-code" not in request.form:
|
||||
flash("Invalid request", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
TOTPCode = request.form["2fa-code"]
|
||||
if len(TOTPCode) != 6:
|
||||
flash("Invalid 2FA code", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
if not auth.Validate2FACode(AuthenticatedUser.id, TOTPCode):
|
||||
flash("Invalid 2FA code", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
if ItemPrice < 1 or ItemPrice > 999999999:
|
||||
flash("Invalid price ( 1 - 999999999 )", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
if UserAssetObj.is_for_sale:
|
||||
flash("This asset is already for sale", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
UserCurrentMembership : MembershipType = GetUserMembership(AuthenticatedUser)
|
||||
if UserCurrentMembership == MembershipType.NonBuildersClub:
|
||||
flash("You must be a Builders Club member to sell items", "error")
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
UserAssetObj.is_for_sale = True
|
||||
UserAssetObj.price = ItemPrice
|
||||
db.session.commit()
|
||||
return redirect(f"/catalog/resell/{str(AssetObject.id)}")
|
||||
|
||||
@CatalogRoute.route("/resell/<int:uaid>/takeoff", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
@csrf.exempt
|
||||
def resell_takeoff(uaid):
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
UserAssetObject : UserAsset = UserAsset.query.filter_by(id=uaid).first()
|
||||
if UserAssetObject is None:
|
||||
flash("Invalid asset", "error")
|
||||
return redirect("/catalog")
|
||||
if UserAssetObject.userid != AuthenticatedUser.id:
|
||||
flash("Invalid asset", "error")
|
||||
return redirect("/catalog")
|
||||
if not UserAssetObject.is_for_sale:
|
||||
flash("This asset is not for sale", "error")
|
||||
return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
|
||||
UserAssetObject.is_for_sale = False
|
||||
UserAssetObject.price = 0
|
||||
db.session.commit()
|
||||
return redirect(f"/catalog/resell/{str(UserAssetObject.assetid)}")
|
||||
|
||||
@CatalogRoute.route("/api/purchase", methods=["POST"])
|
||||
@auth.authenticated_required_api
|
||||
@limiter.limit("1/second")
|
||||
@user_limiter.limit("1/second")
|
||||
def api_purchase():
|
||||
JSONData = request.json
|
||||
if "assetId" not in JSONData or "expectedPrice" not in JSONData or "currencyType" not in JSONData:
|
||||
return jsonify({"success": False, "message": "Invalid request"}),400
|
||||
|
||||
if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"):
|
||||
return jsonify({"success": False, "message": "Purchasing is temporarily disabled"}),400
|
||||
|
||||
try:
|
||||
JSONData["assetId"] = int(JSONData["assetId"])
|
||||
JSONData["expectedPrice"] = int(JSONData["expectedPrice"])
|
||||
JSONData["currencyType"] = int(JSONData["currencyType"])
|
||||
except:
|
||||
return jsonify({"success": False, "message": "Invalid request"}),400
|
||||
|
||||
AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
|
||||
if AssetObject is None:
|
||||
return jsonify({"success": False, "message": "Invalid asset"}),400
|
||||
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
|
||||
try:
|
||||
with redis_lock.Lock(redis_client = redis_controller, name=f"asset:{str(AssetObject.id)}", expire=15, auto_renewal=True, strict=True) as lock:
|
||||
if AssetObject.offsale_at is not None:
|
||||
if datetime.utcnow() > AssetObject.offsale_at:
|
||||
AssetObject.is_for_sale = False
|
||||
AssetObject.offsale_at = None
|
||||
db.session.commit()
|
||||
if AssetObject.is_for_sale == False:
|
||||
return jsonify({"success": False, "message": "Asset is not for sale"}),400
|
||||
if JSONData["currencyType"] == 0:
|
||||
if JSONData["expectedPrice"] != AssetObject.price_robux:
|
||||
return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
|
||||
elif JSONData["currencyType"] == 1:
|
||||
if JSONData["expectedPrice"] != AssetObject.price_tix:
|
||||
return jsonify({"success": False, "message": "Expected Price is different from current price"}),400
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Invalid currency type"}),400
|
||||
|
||||
if AssetObject.price_robux == 0 and AssetObject.price_tix != 0 and JSONData["currencyType"] == 0:
|
||||
return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
|
||||
if AssetObject.price_tix == 0 and AssetObject.price_robux != 0 and JSONData["currencyType"] == 1:
|
||||
return jsonify({"success": False, "message": "Asset is not being sold in this currency"}),400
|
||||
|
||||
UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=AssetObject.id).first()
|
||||
if UserAssetObj is not None:
|
||||
return jsonify({"success": False, "message": "User already owns asset"}),400
|
||||
PurchaserRobuxBal, PurchaserTicketsBal = GetUserBalance(AuthenticatedUser)
|
||||
if JSONData["currencyType"] == 0:
|
||||
if PurchaserRobuxBal < AssetObject.price_robux:
|
||||
return jsonify({"success": False, "message": "Insufficient funds"}),400
|
||||
try:
|
||||
DecrementTargetBalance(
|
||||
Target = AuthenticatedUser,
|
||||
Amount = AssetObject.price_robux,
|
||||
CurrencyType = 0
|
||||
)
|
||||
except InsufficientFundsException:
|
||||
return jsonify({"success": False, "message": "Insufficient funds"}),400
|
||||
except EconomyLockAcquireException:
|
||||
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
|
||||
except Exception as e:
|
||||
logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
|
||||
return jsonify({"success": False, "message": "Failed to decrement balance"}),400
|
||||
|
||||
IncrementAssetCreator(
|
||||
AssetObject = AssetObject,
|
||||
AmountGiven = AssetObject.price_robux,
|
||||
CurrencyType = 0
|
||||
)
|
||||
elif JSONData["currencyType"] == 1:
|
||||
if PurchaserTicketsBal < AssetObject.price_tix:
|
||||
return jsonify({"success": False, "message": "Insufficient funds"}),400
|
||||
|
||||
try:
|
||||
DecrementTargetBalance(
|
||||
Target = AuthenticatedUser,
|
||||
Amount = AssetObject.price_tix,
|
||||
CurrencyType = 1
|
||||
)
|
||||
except InsufficientFundsException:
|
||||
return jsonify({"success": False, "message": "Insufficient funds"}),400
|
||||
except EconomyLockAcquireException:
|
||||
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
|
||||
except Exception as e:
|
||||
logging.error(f"/api/purchase : Failed to decrement balance for user {str(AuthenticatedUser.id)}, message: {str(e)}")
|
||||
return jsonify({"success": False, "message": "Failed to decrement balance"}),400
|
||||
|
||||
IncrementAssetCreator(
|
||||
AssetObject = AssetObject,
|
||||
AmountGiven = AssetObject.price_tix,
|
||||
CurrencyType = 1
|
||||
)
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Invalid currency type"}),400
|
||||
|
||||
if AssetObject.is_limited_unique:
|
||||
ItemSerial = AssetObject.sale_count + 1
|
||||
if AssetObject.sale_count + 1 >= AssetObject.serial_count:
|
||||
AssetObject.is_for_sale = False
|
||||
if AssetObject.is_limited_unique:
|
||||
UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id, serial=ItemSerial)
|
||||
else:
|
||||
UserAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=AssetObject.id)
|
||||
|
||||
if AssetObject.asset_type == AssetType.Package:
|
||||
PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObject.id).all()
|
||||
for PackageAssetObj in PackageAssets:
|
||||
NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id)
|
||||
db.session.add(NewPackageAssetObj)
|
||||
|
||||
AssetObject.sale_count += 1
|
||||
db.session.add(UserAssetObj)
|
||||
db.session.commit()
|
||||
|
||||
try:
|
||||
if AssetObject.creator_type == 0:
|
||||
SellerUserObj : User = User.query.filter_by(id=AssetObject.creator_id).first()
|
||||
CreateTransactionForSale(
|
||||
AssetObj = AssetObject,
|
||||
PurchasePrice = JSONData["expectedPrice"],
|
||||
PurchaseCurrencyType = JSONData["currencyType"],
|
||||
Seller = SellerUserObj,
|
||||
Buyer = AuthenticatedUser,
|
||||
ApplyTaxAutomatically = True
|
||||
)
|
||||
else:
|
||||
GroupObj : Group = Group.query.filter_by(id=AssetObject.creator_id).first()
|
||||
CreateTransactionForSale(
|
||||
AssetObj = AssetObject,
|
||||
PurchasePrice = JSONData["expectedPrice"],
|
||||
PurchaseCurrencyType = JSONData["currencyType"],
|
||||
Seller = GroupObj,
|
||||
Buyer = AuthenticatedUser,
|
||||
ApplyTaxAutomatically = True
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
|
||||
pass
|
||||
|
||||
return jsonify({"success": True, "message": "Asset purchased successfully"}),200
|
||||
except AssertionError as e:
|
||||
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
|
||||
|
||||
@CatalogRoute.route("/api/purchase-limited", methods=["POST"])
|
||||
@auth.authenticated_required_api
|
||||
@limiter.limit("1/second")
|
||||
@user_limiter.limit("1/second")
|
||||
def api_purchase_limited():
|
||||
JSONData = request.json
|
||||
if "assetId" not in JSONData or "expectedPrice" not in JSONData or "expectedOwner" not in JSONData or "itemOwnershipId" not in JSONData:
|
||||
return jsonify({"success": False, "message": "Invalid request"}),400
|
||||
# Expected Types: assetId (int), expectedPrice (int), expectedOwner (int), itemOwnershipId (int)
|
||||
if type(JSONData["assetId"]) != int or type(JSONData["expectedPrice"]) != int or type(JSONData["expectedOwner"]) != int or type(JSONData["itemOwnershipId"]) != int:
|
||||
return jsonify({"success": False, "message": "Invalid request"}),400
|
||||
AuthenticatedUser : User = auth.GetAuthenticatedUser(request.cookies.get(".ROBLOSECURITY"))
|
||||
EconomyLock = acquire_lock(f"economy:{str(AuthenticatedUser.id)}", acquire_timeout=5, lock_timeout=1)
|
||||
if EconomyLock is False:
|
||||
return jsonify({"success": False, "message": "Failed to acquire lock"}),400
|
||||
AssetObject : Asset = Asset.query.filter_by(id=JSONData["assetId"]).first()
|
||||
if AssetObject is None:
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": "Invalid asset"}),400
|
||||
if AssetObject.is_limited == False:
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": "Asset is not limited"}),400
|
||||
LimitedAsset : UserAsset = UserAsset.query.filter_by(assetid=AssetObject.id, id=JSONData["itemOwnershipId"]).first()
|
||||
if LimitedAsset is None:
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": "Invalid asset info"}),400
|
||||
ItemLock = acquire_lock(f"item:{str(LimitedAsset.id)}", acquire_timeout=5, lock_timeout=1)
|
||||
if ItemLock is False:
|
||||
return jsonify({"success": False, "message": "Failed to acquire lock ( This is usually caused by too many people purchasing this item at the same time. )"}),400
|
||||
if LimitedAsset.is_for_sale == False:
|
||||
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": "Item is not for sale"}),400
|
||||
if JSONData["expectedOwner"] != LimitedAsset.userid:
|
||||
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": f"Expected Owner ({str(JSONData['expectedOwner'])}) does not match current item owner({str(LimitedAsset.userid)})"}),400
|
||||
if JSONData["expectedPrice"] != LimitedAsset.price:
|
||||
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": f"Expected Price ({str(JSONData['expectedPrice'])}) does not match current item price ({str(LimitedAsset.price)})"}),400
|
||||
if AuthenticatedUser.id == LimitedAsset.userid:
|
||||
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
return jsonify({"success": False, "message": "You cannot purchase your own item"}),400
|
||||
|
||||
UserEconomyObj : UserEconomy = UserEconomy.query.filter_by(userid=AuthenticatedUser.id).first()
|
||||
if UserEconomyObj.robux < LimitedAsset.price:
|
||||
redis_controller.delete(f"economy:{str(AuthenticatedUser.id)}")
|
||||
return jsonify({"success": False, "message": "Insufficient funds"}),400
|
||||
|
||||
UserEconomyObj.robux -= LimitedAsset.price
|
||||
OriginalPrice : int = LimitedAsset.price
|
||||
db.session.commit()
|
||||
IncrementTargetBalance(GetUserFromId(LimitedAsset.userid), TaxCurrencyAmount(LimitedAsset.price), 0)
|
||||
AdjustAssetRap(LimitedAsset.assetid, LimitedAsset.price)
|
||||
LimitedAsset.userid = AuthenticatedUser.id
|
||||
LimitedAsset.is_for_sale = False
|
||||
LimitedAsset.price = 0
|
||||
LimitedAsset.updated = datetime.utcnow()
|
||||
db.session.commit()
|
||||
# We release the locks first as taking a thumbnail can take a while
|
||||
release_lock(f"economy:{str(AuthenticatedUser.id)}", EconomyLock)
|
||||
release_lock(f"item:{str(LimitedAsset.id)}", ItemLock)
|
||||
PreviousOwnerId : int = JSONData["expectedOwner"]
|
||||
# Check how many items the previous owner had of the same asset
|
||||
PreviousOwnerItems = UserAsset.query.filter_by(userid=PreviousOwnerId, assetid=AssetObject.id).count()
|
||||
if PreviousOwnerItems == 0:
|
||||
# Remove the asset from the previous owner avatar if there is any
|
||||
AvatarAsset = UserAvatarAsset.query.filter_by(user_id=PreviousOwnerId, asset_id=AssetObject.id).first()
|
||||
if AvatarAsset is not None:
|
||||
db.session.delete(AvatarAsset)
|
||||
db.session.commit()
|
||||
TakeUserThumbnail(PreviousOwnerId, True, False)
|
||||
CreateSystemMessage(
|
||||
f"Your {AssetObject.name} has been sold!",
|
||||
f"Your item {AssetObject.name} ( UAID: {str(LimitedAsset.id)} / Serial: {str(LimitedAsset.serial)} ) has been sold for R$ {str(OriginalPrice)} to {AuthenticatedUser.username} ( {str(AuthenticatedUser.id)} ).",
|
||||
PreviousOwnerId
|
||||
)
|
||||
SellerUserObj : User = User.query.filter_by(id=PreviousOwnerId).first()
|
||||
|
||||
try:
|
||||
CreateTransactionForSale(
|
||||
AssetObj = AssetObject,
|
||||
PurchasePrice = OriginalPrice,
|
||||
PurchaseCurrencyType = 0,
|
||||
Seller = SellerUserObj,
|
||||
Buyer = AuthenticatedUser,
|
||||
ApplyTaxAutomatically = True
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObject.id)}, message: {str(e)}")
|
||||
pass
|
||||
|
||||
try:
|
||||
newLimitedTransfer = LimitedItemTransfer(
|
||||
original_owner_id = SellerUserObj.id,
|
||||
new_owner_id = AuthenticatedUser.id,
|
||||
asset_id = AssetObject.id,
|
||||
user_asset_id = LimitedAsset.id,
|
||||
transfer_method = LimitedItemTransferMethod.Purchase,
|
||||
purchased_price = OriginalPrice
|
||||
)
|
||||
db.session.add(newLimitedTransfer)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
logging.warn(f"Failed to create limited item transfer log for asset {str(AssetObject.id)}, message: {str(e)}")
|
||||
pass
|
||||
|
||||
return jsonify({"success": True, "message": "Item purchased successfully"}),200
|
||||
|
||||
|
||||
@LibraryRoute.route("/<int:assetid>/", methods=["GET"])
|
||||
@LibraryRoute.route("/<int:assetid>/<assetname>", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def asset_page(assetid, assetname=None):
|
||||
AssetObject : Asset = Asset.query.filter_by(id=assetid).first()
|
||||
if AssetObject is None:
|
||||
return redirect("/library")
|
||||
SlugName = slugify(AssetObject.name, lowercase=False)
|
||||
if SlugName is None or SlugName == "":
|
||||
SlugName = "unnamed"
|
||||
if assetname is None:
|
||||
if request.args.get("page") is None:
|
||||
return redirect(f"/library/{assetid}/{SlugName}")
|
||||
else:
|
||||
return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
|
||||
if assetname != SlugName:
|
||||
if request.args.get("page") is None:
|
||||
return redirect(f"/library/{assetid}/{SlugName}")
|
||||
else:
|
||||
return redirect(f"/library/{assetid}/{SlugName}?page={request.args.get('page')}")
|
||||
if AssetObject.asset_type.value in [2,8,11,12,17,18,19,27,28,29,30,31,32,41,42,43,44,45,46,47,57,58]:
|
||||
return redirect(f"/catalog/{str(AssetObject.id)}/")
|
||||
if AssetObject.asset_type.value == 9:
|
||||
return redirect(f"/games/{assetid}/")
|
||||
CreatorObj : User | Group = User.query.filter_by(id=AssetObject.creator_id).first() if AssetObject.creator_type == 0 else Group.query.filter_by(id=AssetObject.creator_id).first()
|
||||
Created = AssetObject.created_at.strftime("%d/%m/%Y")
|
||||
Updated = AssetObject.updated_at.strftime("%d/%m/%Y")
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
doesUserOwnAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).first() is not None
|
||||
BestPriceResult : UserAsset = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).first()
|
||||
BestPrice = "None"
|
||||
if BestPriceResult is not None:
|
||||
BestPrice = str(BestPriceResult.price)
|
||||
UserOwns = 0
|
||||
PrivateSaleList = []
|
||||
NextPage = 0
|
||||
PreviousPage = 0
|
||||
PageNumber = 0
|
||||
AssetRap = 0
|
||||
if AssetObject.is_limited and not AssetObject.is_for_sale:
|
||||
AssetRap = GetAssetRap(assetid)
|
||||
UserOwns = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=assetid).count()
|
||||
Page = 1
|
||||
if request.args.get("page"):
|
||||
try:
|
||||
Page = int(request.args.get("page"))
|
||||
except:
|
||||
pass
|
||||
PrivateSales = UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page, per_page=5).items
|
||||
for sale in PrivateSales:
|
||||
SellerUser : User = User.query.filter_by(id=sale.userid).first()
|
||||
PrivateSaleList.append({
|
||||
"price": sale.price,
|
||||
"seller": SellerUser.username,
|
||||
"sellerid": SellerUser.id,
|
||||
"serial": sale.serial,
|
||||
"uaid": sale.id
|
||||
})
|
||||
if len(UserAsset.query.filter_by(assetid=assetid, is_for_sale=True).order_by(UserAsset.price.asc()).paginate(page=Page+1, per_page=5, error_out=False).items) > 0:
|
||||
NextPage = Page + 1
|
||||
else:
|
||||
NextPage = -1
|
||||
if Page > 1:
|
||||
PreviousPage = Page - 1
|
||||
else:
|
||||
PreviousPage = -1
|
||||
PageNumber = Page
|
||||
|
||||
GamepassLinkObj = None
|
||||
GamepassRootPlaceAsset = None
|
||||
if AssetObject.asset_type == AssetType.GamePass:
|
||||
GamepassLinkObj : GamepassLink = GamepassLink.query.filter_by(gamepass_id=AssetObject.id).first()
|
||||
GamepassUniverse : Universe = Universe.query.filter_by(id=GamepassLinkObj.universe_id).first()
|
||||
GamepassRootPlaceAsset : Asset = Asset.query.filter_by( id = GamepassUniverse.root_place_id ).first()
|
||||
return render_template("catalog/library.html", asset=AssetObject, creator=CreatorObj, createddate=Created,
|
||||
updateddate=Updated, doesUserOwnAsset=doesUserOwnAsset, BestPrice=BestPrice,
|
||||
BestPriceResult=BestPriceResult, userOwnAmountCount=UserOwns, PrivateSales=PrivateSaleList,
|
||||
NextPage=NextPage, PreviousPage=PreviousPage, PageNumber=PageNumber, AssetRap=AssetRap,
|
||||
GamepassLinkObj=GamepassLinkObj, GamepassRootPlaceAsset=GamepassRootPlaceAsset)
|
||||
|
||||
@BadgesPageRoute.route("/<int:badgeid>/", methods=["GET"])
|
||||
@BadgesPageRoute.route("/<int:badgeid>/<badgename>", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def badge_page( badgeid : int, badgename : str = None ):
|
||||
BadgeObject : PlaceBadge = PlaceBadge.query.filter_by(id=badgeid).first()
|
||||
if BadgeObject is None:
|
||||
return abort(404)
|
||||
|
||||
if badgename is None:
|
||||
return redirect(f"/badges/{badgeid}/{slugify(BadgeObject.name, lowercase=False)}")
|
||||
elif badgename != slugify(BadgeObject.name, lowercase=False):
|
||||
return redirect(f"/badges/{badgeid}/{slugify(BadgeObject.name, lowercase=False)}")
|
||||
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
UserBadgeObj : UserBadge = UserBadge.query.filter_by(user_id=AuthenticatedUser.id, badge_id=badgeid).first()
|
||||
AssociatedPlaceAssetObj : Asset = Asset.query.filter_by(id=BadgeObject.associated_place_id).first()
|
||||
CreatorObj : User | Group = User.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first() if AssociatedPlaceAssetObj.creator_type == 0 else Group.query.filter_by(id=AssociatedPlaceAssetObj.creator_id).first()
|
||||
Created = BadgeObject.created_at.strftime("%d/%m/%Y")
|
||||
Updated = BadgeObject.updated_at.strftime("%d/%m/%Y")
|
||||
|
||||
return render_template(
|
||||
"catalog/badges.html",
|
||||
badge = BadgeObject,
|
||||
userbadge = UserBadgeObj,
|
||||
AssociatedPlaceAssetObj = AssociatedPlaceAssetObj,
|
||||
creator = CreatorObj,
|
||||
createddate = Created,
|
||||
updateddate = Updated
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
from app.models.asset import Asset
|
||||
|
||||
CatalogTypes = {
|
||||
0 : {
|
||||
"name": "Featured Items",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('TShirt','Hat','Shirt','Pants','Decal','Head','Face','Gear','Package','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
|
||||
},
|
||||
1 : {
|
||||
"name": "Featured Hats",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Hat','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
|
||||
},
|
||||
2 : {
|
||||
"name": "Featured Gears",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Gear') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
|
||||
},
|
||||
3 : {
|
||||
"name": "Featured Faces",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Face') AND creator_id = 1 AND creator_type = 0 AND (is_for_sale = true OR is_limited = true)"
|
||||
},
|
||||
4 : {
|
||||
"name": "Collectible Items",
|
||||
"query": "SELECT * FROM asset WHERE is_limited = true"
|
||||
},
|
||||
5 : {
|
||||
"name": "Collectible Hats",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Hat','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND is_limited = true"
|
||||
},
|
||||
6 : {
|
||||
"name": "Collectible Gears",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Gear') AND is_limited = true"
|
||||
},
|
||||
7 : {
|
||||
"name": "Collectible Faces",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Face') AND is_limited = true"
|
||||
},
|
||||
8 : {
|
||||
"name": "All Clothing",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('TShirt','Hat','Shirt','Pants','Package') AND is_for_sale = true"
|
||||
},
|
||||
9 : {
|
||||
"name": "Hats",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Hat','HairAccessory','FaceAccessory','NeckAccessory','ShoulderAccessory','FrontAccessory','FrontAccessory','BackAccessory','WaistAccessory') AND is_for_sale = true"
|
||||
},
|
||||
10 : {
|
||||
"name": "Shirts",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Shirt') AND is_for_sale = true"
|
||||
},
|
||||
11 : {
|
||||
"name": "T-Shirts",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('TShirt') AND is_for_sale = true"
|
||||
},
|
||||
12 : {
|
||||
"name": "Pants",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Pants') AND is_for_sale = true"
|
||||
},
|
||||
13 : {
|
||||
"name": "Packages",
|
||||
"query": "SELECT * FROM asset WHERE asset_type in ('Package') AND is_for_sale = true"
|
||||
},
|
||||
14 : {
|
||||
"name": "Body Parts",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Head','Face') AND is_for_sale = true"
|
||||
},
|
||||
15 : {
|
||||
"name": "Heads",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Head') AND is_for_sale = true"
|
||||
},
|
||||
16 : {
|
||||
"name": "Faces",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('Face') AND is_for_sale = true"
|
||||
},
|
||||
41 : {
|
||||
"name": "Hairs",
|
||||
"query": "SELECT * FROM asset WHERE asset_type IN ('HairAccessory') AND (is_for_sale = true OR is_limited = true)"
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Catalog{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/catalog.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
color: inherit;
|
||||
}
|
||||
.text-tickets {
|
||||
color: rgb(224, 224, 60) !important;
|
||||
font-size: small;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-size: small;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-limited {
|
||||
color: rgb(255,255,255) !important;
|
||||
background-color: rgb(9, 136, 62);
|
||||
width: fit-content;
|
||||
padding-left: 10px;
|
||||
font-size: 10px;
|
||||
border-top-right-radius: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
.text-limitedu {
|
||||
color: rgb(0,0,0) !important;
|
||||
background-color: rgb(245, 245, 52);
|
||||
padding-right: 3px;
|
||||
font-weight: 800;
|
||||
border-top-right-radius: 5px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.text-remaining {
|
||||
color: rgb(197, 21, 21) !important;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<meta id="catalog-info" data-category-id="{{categoryid}}">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 1200px;">
|
||||
<div class="d-md-flex">
|
||||
<h1>Catalog</h1>
|
||||
<div class="ms-md-5 row d-flex w-100 align-items-center">
|
||||
<form method="post">
|
||||
<div class="input-group">
|
||||
<div class="form-floating" style="width: 52%;">
|
||||
<input type="text" class="form-control" id="search-input" name="search-input" {%if query != None: %}value="{{query}}"{%endif%} placeholder="Awesome Shirt">
|
||||
<label for="search-input">Search</label>
|
||||
</div>
|
||||
<div class="form-floating" style="min-width: 160px;">
|
||||
<select class="form-select" id="order-by" name="order-by">
|
||||
<option value="0">Relevance</option>
|
||||
<option value="1" {%if SortType == 1 %}selected{%endif%}>Price (Low to High)</option>
|
||||
<option value="2" {%if SortType == 2 %}selected{%endif%}>Price (High to Low)</option>
|
||||
<option value="3" {%if SortType == 3 %}selected{%endif%}>Recently Updated</option>
|
||||
<option value="4" {%if SortType == 4 %}selected{%endif%}>Best Selling</option>
|
||||
</select>
|
||||
<label for="order-by">Order By</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 p-0 mt-3 pb-3">
|
||||
<div class="border rounded p-2" style="background-color: rgb(19, 19, 19);">
|
||||
<h5 class="text-center m-1" style="font-size: 20px;">Categories</h5>
|
||||
<div class="row">
|
||||
<div class="text-white col-md-12 col-sm-6">
|
||||
<a href="/catalog/?category=0"><b>Featured</b></a>
|
||||
<div class="ms-2 text-secondary">
|
||||
<a href="/catalog/?category=1"><p class="m-0">Featured Hats</p></a>
|
||||
<a href="/catalog/?category=2"><p class="m-0">Featured Gears</p></a>
|
||||
<a href="/catalog/?category=3"><p class="m-0">Featured Faces</p></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-white col-md-12 col-sm-6">
|
||||
<a href="/catalog/?category=4"><b>Collectibles</b></a>
|
||||
<div class="ms-2 text-secondary">
|
||||
<a href="/catalog/?category=5"><p class="m-0">Collectible Hats</p></a>
|
||||
<a href="/catalog/?category=6"><p class="m-0">Collectible Gears</p></a>
|
||||
<a href="/catalog/?category=7"><p class="m-0">Collectible Faces</p></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-white col-md-12 col-sm-6">
|
||||
<a href="/catalog/?category=8"><b>Clothing</b></a>
|
||||
<div class="ms-2 text-secondary">
|
||||
<a href="/catalog/?category=9"><p class="m-0">Hats</p></a>
|
||||
<a href="/catalog/?category=10"><p class="m-0">Shirts</p></a>
|
||||
<a href="/catalog/?category=11"><p class="m-0">T-Shirts</p></a>
|
||||
<a href="/catalog/?category=12"><p class="m-0">Pants</p></a>
|
||||
<a href="/catalog/?category=13"><p class="m-0">Packages</p></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-white col-md-12 col-sm-6">
|
||||
<a href="/catalog/?category=14"><b>Body Parts</b></a>
|
||||
<div class="ms-2 text-secondary">
|
||||
<a href="/catalog/?category=41"><p class="m-0">Hairs</p></a>
|
||||
<a href="/catalog/?category=15"><p class="m-0">Heads</p></a>
|
||||
<a href="/catalog/?category=16"><p class="m-0">Faces</p></a>
|
||||
<a href="/catalog/?category=13"><p class="m-0">Packages</p></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mt-3">
|
||||
<h3 class="mb-1">{{categoryname}}</h3>
|
||||
<div class="row">
|
||||
{% for item in queryResults %}
|
||||
<div class="col-md-2 col-3 p-1">
|
||||
<a href="/catalog/{{item.id}}/">
|
||||
<div class="rounded item-card p-2">
|
||||
<div class="border rounded overflow-hidden position-relative">
|
||||
<img src="/Thumbs/Asset.ashx?x=180&y=180&assetId={{item.id}}" width="100%" style="aspect-ratio: 1/1;" alt="{{item.name}}">
|
||||
{% if item.is_limited: %}<p class="position-absolute m-0 fw-bold text-limited" style="bottom:-1px;left:0px;">LIMITED {%if item.is_limited_unique: %}<span class="text-limitedu">U</span>{%else%}<span class="me-2"></span>{%endif%}</p>{% endif %}
|
||||
{% if item.offsale_at != None %}<p class="position-absolute bg-danger m-0" style="top: 0px;right: 0px;border-bottom-left-radius: 10px;padding: 5px;"><i class="bi bi-alarm text-white"></i></p>{% endif %}
|
||||
</div>
|
||||
<p class="text-secondary m-0" style="font-size:12px;">{{item.name}}</p>
|
||||
{% if item.is_for_sale: %}
|
||||
{% if item.price_robux == 0 and item.price_tix == 0: %}
|
||||
<p class="text-robux m-0 ">FREE</p>
|
||||
{% else %}
|
||||
{% if item.price_robux != 0: %}
|
||||
<p class="text-robux m-0">R$ {{item.price_robux}}</p>
|
||||
{% endif %}
|
||||
{% if item.price_tix != 0: %}
|
||||
<p class="text-tickets m-0">T$ {{item.price_tix}}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if item.is_limited_unique : %}
|
||||
<p class="text-remaining m-0">Remaining: <span class="text-white">{{item.serial_count - item.sale_count}}</span></p>
|
||||
{%endif%}
|
||||
{% else %}
|
||||
{% if item.is_limited: %}
|
||||
{% if item.price_robux == 0 and item.price_tix != 0: %}
|
||||
<p class="text-tickets m-0"><span class="text-secondary fw-normal">was</span> T$ {{item.price_tix or "-"}}</p>
|
||||
{% elif item.price_robux != 0 and item.price_tix != 0: %}
|
||||
<p class="text-robux m-0"><span class="text-secondary fw-normal">was</span> R$ {{item.price_robux or "-"}}</p>
|
||||
{% elif item.price_robux != 0 and item.price_tix == 0: %}
|
||||
<p class="text-robux m-0"><span class="text-secondary fw-normal">was</span> R$ {{item.price_robux or "-"}}</p>
|
||||
{% else %}
|
||||
<p class="text-robux m-0"><span class="text-secondary fw-normal">was</span> FREE</p>
|
||||
{% endif %}
|
||||
<p class="text-robux m-0"><span class="text-secondary fw-normal">now</span> R$ {{item.best_price}}</p>
|
||||
{%else%}
|
||||
<p class="text-secondary m-0 ">Offsale</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if len(queryResults) == 0: %}
|
||||
<p class="w-100 text-center mt-5 mb-5">No results found</p>
|
||||
{% endif %}
|
||||
<div class="d-flex">
|
||||
{% if PreviousPage > 0: %}
|
||||
<a href="/catalog/?category={{CatalogType}}&page={{PreviousPage}}&sort={{SortType}}" class="ms-auto text-decoration-none text-primary">Previous</a>
|
||||
{%else%}
|
||||
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
|
||||
{% endif %}
|
||||
<p class="ms-2 me-2 text-white">Page {{PageNumber}} of {{totalPages}}</p>
|
||||
{% if NextPage > 0: %}
|
||||
<a href="/catalog/?category={{CatalogType}}&page={{NextPage}}&sort={{SortType}}" class="me-auto text-decoration-none text-primary">Next</a>
|
||||
{%else%}
|
||||
<a class="me-auto text-decoration-none text-secondary">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,186 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}{{asset.name}}{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/catalog.css" rel="stylesheet"/>
|
||||
<script src="/static/js/catalog.js"></script>
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.text-limited {
|
||||
color: rgb(255,255,255) !important;
|
||||
background-color: rgb(9, 136, 62);
|
||||
width: fit-content;
|
||||
padding-left: 10px;
|
||||
font-size: 20px;
|
||||
border-top-right-radius: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
.text-limitedu {
|
||||
color: rgb(0,0,0) !important;
|
||||
background-color: rgb(245, 245, 52);
|
||||
padding-right: 3px;
|
||||
font-weight: 800;
|
||||
border-top-right-radius: 5px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-size: small;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<div id="csrf_token" data-csrf-token="{{ csrf_token() }}"></div>
|
||||
<meta id="asset-info" data-asset-name="{{asset.name}}" data-asset-id="{{asset.id}}" data-asset-onsale="{% if asset.is_for_sale: %}true{%else%}false{%endif%}" {% if doesUserOwnAsset: %}data-user-ownasset="true"{% endif %} {% if asset.is_limited: %}data-islimited="true"{%endif%}>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container ps-5 pe-5 ps-md-0 pe-md-0" style="max-width: 800px;">
|
||||
<h1>{{asset.name}}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-5 col-12">
|
||||
<div class="p-2 rounded overflow-hidden position-relative{% if GamepassLinkObj != None: %} rounded-bottom-0 {%endif%}" style="background-color: rgb(36, 36, 36);">
|
||||
<img width="100%" src="/Thumbs/Asset.ashx?assetId={{asset.id}}&x=420&y=420" style="aspect-ratio: 1/1;" alt="{{asset.name}}">
|
||||
{% if asset.is_limited: %}<p class="position-absolute m-0 fw-bold text-limited" style="bottom:-2px;left:0px;">LIMITED {%if asset.is_limited_unique: %}<span class="text-limitedu">U</span>{%else%}<span class="me-2"></span>{%endif%}</p>{% endif %}
|
||||
</div>
|
||||
{% if GamepassLinkObj != None: %}
|
||||
<div class="rounded-bottom overflow-hidden d-flex p-1" style="background-color: rgb(29, 29, 29);">
|
||||
<p class="m-0 text-white me-2" style="font-size: 13px;">Use this Gamepass in: <a class="text-decoration-none" href="/games/{{GamepassRootPlaceAsset.id}}/">{{GamepassRootPlaceAsset.name}}</a></p>
|
||||
<a class="text-decoration-none ms-auto" href="/games/{{GamepassRootPlaceAsset.id}}/"><img src="/Thumbs/PlaceIcon.ashx?assetId={{GamepassRootPlaceAsset.id}}&x=60&y=60" height="60px" width="60px" class="rounded"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 p-1">
|
||||
<div class="d-flex mt-3 mt-md-0">
|
||||
{% if asset.creator_type == 0 :%}
|
||||
<img width="80" height="80" src="/Thumbs/Avatar.ashx?userId={{creator.id}}&x=100&y=100" alt="{{creator.username}}">
|
||||
<div style="font-size:13px;">
|
||||
<p class="m-0 text-secondary">Creator: <a href="/users/{{creator.id}}/profile" class="text-decoration-none">{{creator.username}}</a></p>
|
||||
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
|
||||
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
|
||||
</div>
|
||||
{% else %}
|
||||
<img width="80" height="80" src="/Thumbs/GroupIcon.ashx?groupid={{creator.id}}&x=100&y=100" alt="{{creator.name}}">
|
||||
<div style="font-size:13px;">
|
||||
<p class="m-0 text-secondary">Creator: <a href="/groups/{{creator.id}}/--" class="text-decoration-none">{{creator.name}}</a></p>
|
||||
<p class="m-0 text-secondary">Created: <span class="text-white">{{createddate}}</span></p>
|
||||
<p class="m-0 text-secondary">Updated: <span class="text-white">{{updateddate}}</span></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<p>
|
||||
{{asset.description}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3 p-1" >
|
||||
<div class="border rounded p-2 text-center">
|
||||
{% if asset.is_for_sale: %}
|
||||
{% if asset.price_robux == 0 and asset.price_tix == 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">FREE</span></p>
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Take One</a>
|
||||
{% endif %}
|
||||
{% if asset.price_robux > 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_robux}} Robux</span></p>
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-expected-price="{{asset.price_robux}}">Buy with Robux</a>
|
||||
{% endif %}
|
||||
{% if asset.price_robux > 0 and asset.price_tix > 0: %}
|
||||
<div class="d-flex align-items-center mt-2 mb-2">
|
||||
<div class="border-top flex-fill me-2"></div>
|
||||
<p class="text-secondary m-0" style="font-size: 13px;">OR</p>
|
||||
<div class="border-top flex-fill ms-2"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if asset.price_tix > 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">{{asset.price_tix}} Tickets</span></p>
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="1" data-expected-price="{{asset.price_tix}}">Buy with Tickets</a>
|
||||
{% endif %}
|
||||
{% else: %}
|
||||
{% if asset.is_limited: %}
|
||||
{% if BestPrice != "None": %}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Best Price: <span class="text-white">R$ {{BestPrice}}</span></p>
|
||||
{% if BestPriceResult.userid != currentuser.id %}
|
||||
<a class="btn btn-primary fw-bold w-100 btn-sm purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{BestPriceResult.userid}}" data-expectedid="{{BestPriceResult.id}}" data-expected-price="{{BestPrice}}">Buy Now</a>
|
||||
{%else%}
|
||||
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
|
||||
{%endif%}
|
||||
{%else%}
|
||||
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">No Resellers Available</btn>
|
||||
{%endif%}
|
||||
{% if userOwnAmountCount > 0: %}
|
||||
<p class="m-0 text-secondary" style="font-size:12px;">( {{userOwnAmountCount}} owned )</p>
|
||||
{%endif%}
|
||||
{%else%}
|
||||
<p class="m-0 text-secondary" style="font-size:14px;">Price: <span class="text-white">Offsale</span></p>
|
||||
<btn class="btn btn-primary fw-bold w-100 btn-sm disabled">Buy with Robux</btn>
|
||||
{%endif%}
|
||||
{% endif %}
|
||||
<p class="m-0 mt-1 text-secondary" style="font-size:12px;">( {{asset.sale_count}} sold )</p>
|
||||
</div>
|
||||
{% if asset.is_limited and userOwnAmountCount > 0 and not asset.is_for_sale: %}
|
||||
<div class="border rounded p-2 text-center mt-2">
|
||||
<a href="/catalog/resell/{{asset.id}}" class="btn btn-success fw-bold btn-sm w-100">Manage your items</a>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{% if asset.is_limited and not asset.is_for_sale: %}
|
||||
<div class="linebreak mt-3"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Private Sales</h5>
|
||||
{% for sale in PrivateSales: %}
|
||||
<div class="w-100 m-2 d-flex">
|
||||
<img class="rounded-2 overflow-hidden" width="60" height="60" src="/Thumbs/Avatar.ashx?x=60&y=60&userId={{sale.sellerid}}" alt="{{sale.seller}}"/>
|
||||
<div class="ms-2">
|
||||
<a href="/users/{{sale.sellerid}}/profile" class="text-decoration-none text-white m-0">{{sale.seller}}</a>
|
||||
<p class="m-0 text-robux">R$ {{sale.price}}</p>
|
||||
<p class="m-0 text-secondary" style="font-size:13px;">Serial: {%if asset.is_limited_unique: %}{{sale.serial}}{%else%}N/A{%endif%}</p>
|
||||
</div>
|
||||
<div class="ms-auto align-items-center d-flex">
|
||||
{% if sale.sellerid != currentuser.id: %}
|
||||
<button class="btn btn-sm btn-primary purchase-button" data-currency-type="0" data-islimited="true" data-expectedowner="{{sale.sellerid}}" data-expectedid="{{sale.uaid}}" data-expected-price="{{sale.price}}">Buy Now</button>
|
||||
{%else%}
|
||||
<button class="btn btn-sm btn-primary disabled">Buy Now</button>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
<div class="d-flex">
|
||||
{% if PreviousPage > 0: %}
|
||||
<a href="/catalog/{{asset.id}}/?page={{PreviousPage}}" class="ms-auto text-decoration-none">Previous</a>
|
||||
{%else%}
|
||||
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
|
||||
{% endif %}
|
||||
<p class="ms-1 me-1">{{PageNumber}}</p>
|
||||
{% if NextPage > 0: %}
|
||||
<a href="/catalog/{{asset.id}}/?page={{NextPage}}" class="me-auto text-decoration-none">Next</a>
|
||||
{%else%}
|
||||
<a class="me-auto text-decoration-none text-secondary">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="m-0 text-secondary" style="font-size:small;">Recent Average Price: <span class="text-robux">R${{AssetRap}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="purchaseModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="purchase-modal-title">Confirm purchase</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="purchase-modal-content">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="purchase-modal-close" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" id="purchase-modal-btn">Purchase</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}{{asset.name}}{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/catalog.css" rel="stylesheet"/>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
<a href="/catalog/{{asset.id}}/" class="text-decoration-none text-white">< Return to Item Page</a>
|
||||
<h1>{{asset.name}}</h1>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if userassetnotforsalecount > 0: %}
|
||||
<div class="linebreak"></div>
|
||||
<form method="post">
|
||||
<select class="form-control w-100" name="uaid">
|
||||
{% for userasset in userassets %}
|
||||
{% if not userasset.is_for_sale: %}
|
||||
<option value="{{userasset.id}}">UAID: {{userasset.id}} | Serial: {% if asset.is_limited_unique: %}{{userasset.serial}}{%else%}N/A{%endif%}</option>
|
||||
{%endif%}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="number" class="form-control mt-3" name="itemprice" required placeholder="Price ( min: 1 )">
|
||||
<p class="text-secondary m-0" style="font-size:small;">Note: Once sold you will only recieve 70% of the actual price it was sold for.</p>
|
||||
{%if isOTPRequired: %}
|
||||
<input type="text" class="form-control mt-1" name="2fa-code" required placeholder="2FA Code">
|
||||
<p class="text-secondary m-0" style="font-size:small;">As you have 2FA Enabled it is required to authorise any sale of your items</p>
|
||||
{%endif%}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary mt-4 w-100 fw-bold text-center">Resell Item</button>
|
||||
</form>
|
||||
{%endif%}
|
||||
{% if userassetforsalecount > 0: %}
|
||||
<div class="linebreak"></div>
|
||||
<h5>Manage onsale items</h5>
|
||||
{% for userasset in userassets %}
|
||||
{% if userasset.is_for_sale: %}
|
||||
<div class="border p-2 m-2 rounded">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<h5 class="m-0">UAID: {{userasset.id}} | Serial: {% if asset.is_limited_unique: %}{{userasset.serial}}{%else%}N/A{%endif%}</h5>
|
||||
<p class="m-0 fs-6">Price: {{userasset.price}}</p>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<form method="post" action="/catalog/resell/{{userasset.id}}/takeoff">
|
||||
<button type="submit" class="btn btn-danger btn-sm fw-bold">Take Offsale</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
{%endfor%}
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, jsonify
|
||||
|
||||
ClientPages = Blueprint('clientpages', __name__, template_folder="pages")
|
||||
|
||||
@ClientPages.route("/UploadMedia/PostImage.aspx", methods=["GET"])
|
||||
def UploadMediaPostImage():
|
||||
seostr = request.args.get("seostr")
|
||||
filename = request.args.get("filename")
|
||||
if seostr is None or filename is None:
|
||||
return "Invalid request"
|
||||
return render_template("clientpages/screenshot.html", seostr=seostr, filename=filename)
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Screenshot</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<h2 class="fw-normal">Vortexi Screenshot</h2>
|
||||
<p>Hey, you just took a screenshot in Vortexi! You could:</p>
|
||||
<ul>
|
||||
<li>Go to <a href="#" onclick="window.external.OpenPicFolder()">My Pictures</a> folder to check it out!</li>
|
||||
<li>Paste it to your favorite painting software</li>
|
||||
</ul>
|
||||
<hr class="divider-bottom">
|
||||
<a href="#" onclick="window.external.PostImage(false, 0); window.close();">Not interested, don't bother me again</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Clothing Migrator{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="settings-container">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<h1>Clothing Migrator</h1>
|
||||
<p class="m-0 text-secondary" style="font-size:12px;">This will take a clothing from the Roblox website and will be sold for 5 robux. ( Allowed assets: TShirt, Shirt, Pants )</p>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post">
|
||||
<input type="text" name="asseturl" class="form-control w-100" placeholder="Asset URL ( eg. https://www.roblox.com/catalog/123456789/shirt )" required>
|
||||
<div class="d-flex justify-content-end align-items-top mt-2">
|
||||
<div class="cf-turnstile" data-theme="dark" data-sitekey="{{turnstilekey}}"></div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<div class="ms-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Migrate Clothing</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from app.extensions import db, redis_controller, limiter
|
||||
from app.util import auth, turnstile, websiteFeatures
|
||||
from app.models.asset import Asset
|
||||
from app.models.asset_version import AssetVersion
|
||||
from app.routes.asset import migrateAsset
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
ClothingMigratorRoute = Blueprint('clothingmigrator', __name__, template_folder='pages')
|
||||
|
||||
@ClothingMigratorRoute.route('/clothing-migrator', methods=['GET'])
|
||||
@auth.authenticated_required
|
||||
def clothing_migrator():
|
||||
return render_template('clothingmigrator/index.html')
|
||||
|
||||
@ClothingMigratorRoute.route('/clothing-migrator', methods=['POST'])
|
||||
@auth.authenticated_required
|
||||
@limiter.limit("1/second")
|
||||
def clothing_migrator_post():
|
||||
if not websiteFeatures.GetWebsiteFeature("ClothingMigrator"):
|
||||
flash("Clothing migration is temporarily disabled", "error")
|
||||
return redirect("/clothing-migrator")
|
||||
|
||||
if "asseturl" not in request.form or "cf-turnstile-response" not in request.form:
|
||||
flash("Please fill in all the fields", "error")
|
||||
return redirect("/clothing-migrator")
|
||||
AssetRegex = re.compile(r".com\/catalog\/(\d+)\/")
|
||||
AssetMatch = AssetRegex.search(request.form.get("asseturl"))
|
||||
if AssetMatch is None:
|
||||
flash("Invalid asset URL", "error")
|
||||
return redirect("/clothing-migrator")
|
||||
AssetID = AssetMatch.group(1)
|
||||
if not turnstile.VerifyToken(request.form.get("cf-turnstile-response")):
|
||||
flash("Invalid captcha", "error")
|
||||
return redirect("/clothing-migrator")
|
||||
AssetObject : Asset = Asset.query.filter_by(roblox_asset_id=AssetID).first()
|
||||
if AssetObject is not None:
|
||||
return redirect(f"/catalog/{AssetID}/")
|
||||
AuthenticatedUser = auth.GetCurrentUser()
|
||||
DayOfYear = datetime.utcnow().timetuple().tm_yday
|
||||
RedisResult = redis_controller.get(f"clothingmigrator:{str(AuthenticatedUser.id)}:{str(DayOfYear)}")
|
||||
if RedisResult is None:
|
||||
redis_controller.setex(f"clothingmigrator:{str(AuthenticatedUser.id)}:{str(DayOfYear)}", 86400, "1")
|
||||
else:
|
||||
Count = int(RedisResult)
|
||||
if Count >= 5:
|
||||
flash("You have reached the daily limit of 5 per day, please try again tommorow.", "error")
|
||||
return redirect("/clothing-migrator")
|
||||
redis_controller.setex(f"clothingmigrator:{str(AuthenticatedUser.id)}:{str(DayOfYear)}", 86400, str(Count + 1))
|
||||
MigratedAsset : Asset = migrateAsset(AssetID, forceMigration=False, allowedTypes=[2, 11, 12], creatorId=2, keepRobloxId=False, migrateInfo=True)
|
||||
if MigratedAsset is None:
|
||||
flash("Unable to migrate asset", "error")
|
||||
return redirect("/clothing-migrator")
|
||||
if MigratedAsset.creator_id == 2 and MigratedAsset.creator_type == 0:
|
||||
if (datetime.utcnow() - MigratedAsset.created_at).total_seconds() < 2:
|
||||
MigratedAsset.is_for_sale = True
|
||||
MigratedAsset.price_robux = 5
|
||||
MigratedAsset.description = f"This item was taken from the Roblox Catalog using the Clothing Migrator. This item was requested by {AuthenticatedUser.username} ({AuthenticatedUser.id})."
|
||||
db.session.commit()
|
||||
return redirect(f"/catalog/{str(MigratedAsset.id)}/")
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Cryptomus Dashboard{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div style="min-height: 100vh;width: 100%;padding: 20px;margin-top: 100px;max-width: 1200px;" class="ms-auto me-auto">
|
||||
<a href="/membership/payment_methods" class="text-white text-decoration-none"><i class="bi bi-caret-left"></i> Return to payment methods</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<h1 class="m-0">Pay with Cryptomus</h1>
|
||||
<button class="btn btn-light btn-lg ms-auto" id="pay-button" data-csrf-token="{{ csrf_token() }}"><i class="bi bi-coin"></i> Donate $5</button>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<div>
|
||||
<div class="p-1 mt-1">
|
||||
{% if len(UserInvoices.items) == 0 %}
|
||||
<p class="text-secondary w-100 text-center mt-5 mb-5" style="font-size: 14px;">No past payments</p>
|
||||
{% else %}
|
||||
{% for invoice in UserInvoices.items %}
|
||||
<div class="p-2 d-flex align-items-center mb-2" style="background-color: rgb(15,15,15);border: 1px solid rgb(60,60,60);border-radius: 4px;">
|
||||
<div>
|
||||
<h5 class="m-0">{{invoice.cryptomus_invoice_id}}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">{{invoice.created_at.strftime("%d/%m/%Y %H:%M:%S UTC")}}</p>
|
||||
<p class="text-secondary m-0 ms-2" style="font-size: 14px;"> | {{invoice.status.name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/cryptomus_service/view_payment/{{invoice.id}}" class="btn btn-light ms-auto" style="font-weight: 600;">View Payment</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex w-100 mt-2" style="font-size: 14px;">
|
||||
<a class="text-decoration-none ms-auto {% if not UserInvoices.has_prev: %}text-secondary{%endif%}" {% if UserInvoices.has_prev: %}href="/cryptomus_service/dashboard?page={{UserInvoices.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="m-0 ms-2 me-2 text-white">Page {{UserInvoices.page}} of {{UserInvoices.pages}}</p>
|
||||
<a class="text-decoration-none me-auto {% if not UserInvoices.has_next: %}text-secondary{%endif%}" {% if UserInvoices.has_next: %}href="/cryptomus_service/dashboard?page={{UserInvoices.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const payButton = document.getElementById('pay-button');
|
||||
payButton.addEventListener('click', function() {
|
||||
if (payButton.classList.contains('disabled')) {
|
||||
return;
|
||||
}
|
||||
var csrfToken = payButton.getAttribute('data-csrf-token');
|
||||
|
||||
payButton.classList.add('disabled');
|
||||
fetch('/cryptomus_service/create_payment/membership', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
},
|
||||
})
|
||||
.then(async response => {
|
||||
if (!response.ok) {
|
||||
alert(`An error occurred while trying to create a payment, please try again later. Status: ${response.status}`)
|
||||
return;
|
||||
} else {
|
||||
var data = await response.json();
|
||||
console.log(data);
|
||||
if (data.status == 'success') {
|
||||
window.location.href = data.payment_url;
|
||||
} else {
|
||||
alert(`An error occurred while trying to create a payment, please try again later.`)
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
payButton.classList.remove('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}View Payment{% endblock %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div style="min-height: 100vh;width: 100%;padding: 20px;margin-top: 100px;max-width: 1200px;min-width: 900px;" class="ms-auto me-auto">
|
||||
<a href="/cryptomus_service/dashboard" class="text-white text-decoration-none"><i class="bi bi-caret-left"></i> Return to payment dashboard</a>
|
||||
<div class="w-100 p-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<h2 class="m-0">Invoice</h2>
|
||||
<p class="text-secondary m-0">ID: {{InvoiceObj.cryptomus_invoice_id}}</p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="/cryptomus_service/pay_invoice/{{ InvoiceObj.id }}" class="btn btn-lg btn-light ms-auto {% if InvoiceObj.is_final %}disabled{% endif %}" style="font-weight: 600;"><i class="bi bi-coin"></i> Pay Invoice</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<div class="d-flex">
|
||||
<div class="p-1" style="width: 50%;">
|
||||
<div class="border-bottom" style="border-color: rgb(60,60,60) !important;">
|
||||
<h4 class="m-0">Invoice Details</h4>
|
||||
<div class="p-1">
|
||||
<p class="text-secondary m-0">Created at: <span class="text-white">{{InvoiceObj.created_at.strftime("%d/%m/%Y %H:%M:%S UTC")}}</span></p>
|
||||
<p class="text-secondary m-0">Expires at: <span class="text-white">{{InvoiceObj.expires_at.strftime("%d/%m/%Y %H:%M:%S UTC")}}</span></p>
|
||||
<p class="text-secondary m-0">Last Status Update: <span class="text-white">{{InvoiceObj.updated_at.strftime("%d/%m/%Y %H:%M:%S UTC")}}</span></p>
|
||||
<p class="text-secondary m-0">Invoice Status: <span class="text-white">{{InvoiceObj.status.name}}</span></p>
|
||||
<p class="text-secondary m-0">Is Finalised: <span class="text-white">{% if InvoiceObj.is_final %}Yes{% else %}No{% endif %}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h4 class="m-0">Payment Details</h4>
|
||||
<div class="p-1">
|
||||
<p class="text-secondary m-0">Required Amount: <span class="text-white">{{ round( InvoiceObj.required_amount, 2 )}} {{InvoiceObj.currency}}</span></p>
|
||||
<p class="text-secondary m-0">USD received: <span class="text-white">{{ round( InvoiceObj.paid_amount_usd, 2 )}} USD</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1" style="width: 50%;">
|
||||
<div>
|
||||
<h4 class="m-0">Additional Details</h4>
|
||||
<div class="p-1">
|
||||
{% if InvoiceObj.assigned_key == None %}
|
||||
<p class="text-secondary w-100 text-center m-2" style="font-size: 14px;">No additional details</p>
|
||||
{% else %}
|
||||
<div class="p-2" style="background-color: rgb(15,15,15);border: 1px solid rgb(60,60,60);border-radius: 4px;">
|
||||
<p class="text-secondary m-0 mb-1" style="font-size: 13px;">Outrageous Builders Club 1 Month</p>
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="assigned-key" value="{{InvoiceObj.assigned_key}}" readonly>
|
||||
<label for="assigned-key">Assigned Key</label>
|
||||
</div>
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 13px;">Redeem this key at <a href="/giftcard-redeem" class="text-decoration-none">www.vortexi.cc/giftcard-redeem</a> or give it to a friend</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<p class="text-secondary" style="font-size: 14px;">If you have any issues with paying, viewing or receving your item please open a billing ticket in our <a href="https://discord.gg/spUbRdSa5J" class="text-decoration-none">Discord Server</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,378 @@
|
||||
from app.services.economy import GetUserBalance, DecrementTargetBalance, IncrementTargetBalance
|
||||
from app.enums.TransactionType import TransactionType
|
||||
from app.models.exchange_offer import ExchangeOffer
|
||||
from app.models.user import User
|
||||
from app.util import auth, transactions, redislock, websiteFeatures
|
||||
from app.extensions import db, limiter, redis_controller
|
||||
from datetime import datetime, timedelta
|
||||
from app.pages.messages.messages import CreateSystemMessage
|
||||
from sqlalchemy import case
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, make_response, flash, abort
|
||||
|
||||
import redis_lock
|
||||
CurrencyExchangeRoute = Blueprint("currencyexchange", __name__, url_prefix="/currency-exchange")
|
||||
|
||||
@CurrencyExchangeRoute.errorhandler(429)
|
||||
def ratelimit_handler(e):
|
||||
flash("You are being ratelimited.", "danger")
|
||||
return redirect(request.referrer)
|
||||
|
||||
@CurrencyExchangeRoute.route("/", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def index():
|
||||
CategoryType : str = request.args.get(
|
||||
key = "category",
|
||||
default = "all",
|
||||
type = str
|
||||
)
|
||||
SortType : str = request.args.get(
|
||||
key = "sort",
|
||||
default = "best",
|
||||
type = str
|
||||
)
|
||||
PageNumber : int = request.args.get(
|
||||
key = "page",
|
||||
default = 1,
|
||||
type = int
|
||||
)
|
||||
|
||||
if CategoryType not in ["tr", "rt", "all"]:
|
||||
CategoryType = "all"
|
||||
if SortType not in ["best", "worst", "created"]:
|
||||
SortType = "best"
|
||||
if PageNumber < 1:
|
||||
PageNumber = 1
|
||||
|
||||
QueryObj = ExchangeOffer.query
|
||||
QueryObj = QueryObj.filter_by(
|
||||
reciever_id = None
|
||||
)
|
||||
if CategoryType == "tr":
|
||||
QueryObj = QueryObj.filter_by(
|
||||
offer_currency_type = 1
|
||||
)
|
||||
elif CategoryType == "rt":
|
||||
QueryObj = QueryObj.filter_by(
|
||||
offer_currency_type = 0
|
||||
)
|
||||
|
||||
if SortType == "best":
|
||||
if CategoryType == "rt":
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.ratio.asc()
|
||||
)
|
||||
elif CategoryType == "tr":
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.ratio.desc()
|
||||
)
|
||||
else:
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.worth.desc()
|
||||
)
|
||||
elif SortType == "worst":
|
||||
if CategoryType == "rt":
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.ratio.desc()
|
||||
)
|
||||
elif CategoryType == "tr":
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.ratio.asc()
|
||||
)
|
||||
else:
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.worth.asc()
|
||||
)
|
||||
else:
|
||||
QueryObj = QueryObj.order_by(
|
||||
ExchangeOffer.created_at.desc()
|
||||
)
|
||||
|
||||
QueryObj = QueryObj.paginate(
|
||||
page = PageNumber,
|
||||
per_page = 15,
|
||||
error_out = False
|
||||
)
|
||||
|
||||
for Offer in QueryObj.items:
|
||||
Offer : ExchangeOffer
|
||||
if Offer.expires_at < datetime.utcnow():
|
||||
IncrementTargetBalance(Offer.creator, Offer.offer_value, Offer.offer_currency_type)
|
||||
transactions.CreateTransaction(
|
||||
Reciever = Offer.creator,
|
||||
Sender = User.query.filter_by(id = 1).first(),
|
||||
CurrencyAmount = Offer.offer_value,
|
||||
CurrencyType = Offer.offer_currency_type,
|
||||
TransactionType = TransactionType.Sale,
|
||||
AssetId = None,
|
||||
CustomText = "Offer expired."
|
||||
)
|
||||
|
||||
db.session.delete(Offer)
|
||||
db.session.commit()
|
||||
continue
|
||||
|
||||
return render_template(
|
||||
"currencyexchange/index.html",
|
||||
offers = QueryObj.items,
|
||||
hasNextPage = QueryObj.has_next,
|
||||
hasPrevPage = QueryObj.has_prev,
|
||||
PageCategory = CategoryType,
|
||||
SortType = SortType,
|
||||
PageNumber = PageNumber
|
||||
)
|
||||
|
||||
@CurrencyExchangeRoute.route("/view/<int:OfferId>", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def viewOffer(OfferId : int):
|
||||
OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by(
|
||||
id = OfferId
|
||||
).first()
|
||||
if OfferObj is None:
|
||||
return abort(404)
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
|
||||
return render_template(
|
||||
"currencyexchange/view.html",
|
||||
offer = OfferObj,
|
||||
AuthenticatedUser = AuthenticatedUser
|
||||
)
|
||||
|
||||
@CurrencyExchangeRoute.route("/delete/<int:OfferId>", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
def deleteOffer(OfferId : int):
|
||||
OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by(
|
||||
id = OfferId
|
||||
).first()
|
||||
if OfferObj is None:
|
||||
return abort(404)
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if OfferObj.creator_id != AuthenticatedUser.id:
|
||||
return abort(403)
|
||||
|
||||
if websiteFeatures.GetWebsiteFeature("CurrencyExchange") is False:
|
||||
flash("Currency exchange is currently disabled.", "error")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
|
||||
try:
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"currency_exchange_order:{OfferId}", expire = 15, auto_renewal = True ):
|
||||
OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by(
|
||||
id = OfferId
|
||||
).first()
|
||||
if OfferObj.reciever_id is not None:
|
||||
return abort(403)
|
||||
|
||||
IncrementTargetBalance(OfferObj.creator, OfferObj.offer_value, OfferObj.offer_currency_type)
|
||||
transactions.CreateTransaction(
|
||||
Reciever = OfferObj.creator,
|
||||
Sender = User.query.filter_by(id = 1).first(),
|
||||
CurrencyAmount = OfferObj.offer_value,
|
||||
CurrencyType = OfferObj.offer_currency_type,
|
||||
TransactionType = TransactionType.Sale,
|
||||
AssetId = None,
|
||||
CustomText = "Offer cancelled."
|
||||
)
|
||||
|
||||
db.session.delete(OfferObj)
|
||||
db.session.commit()
|
||||
|
||||
flash("Offer successfully deleted.", "success")
|
||||
return redirect("/currency-exchange/")
|
||||
except AssertionError:
|
||||
flash("Failed to delete offer, try again later.", "error")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
|
||||
@CurrencyExchangeRoute.route("/fill/<int:OfferId>", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
@limiter.limit("40/minute")
|
||||
def fillOffer(OfferId : int):
|
||||
OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by(
|
||||
id = OfferId
|
||||
).first()
|
||||
if OfferObj is None:
|
||||
return abort(404)
|
||||
|
||||
if websiteFeatures.GetWebsiteFeature("CurrencyExchange") is False:
|
||||
flash("Currency exchange is currently disabled.", "error")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
|
||||
LockName = f"currency_exchange_order:{str(OfferId)}"
|
||||
try:
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = LockName, expire = 15, auto_renewal = True ):
|
||||
OfferObj : ExchangeOffer = ExchangeOffer.query.filter_by(
|
||||
id = OfferId
|
||||
).first()
|
||||
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
if OfferObj.creator_id == AuthenticatedUser.id:
|
||||
return abort(403)
|
||||
if OfferObj.reciever_id is not None:
|
||||
return abort(403)
|
||||
|
||||
if OfferObj.expires_at < datetime.utcnow():
|
||||
return abort(403)
|
||||
|
||||
SendingCurrencyType : int = 0 if OfferObj.offer_currency_type == 1 else 1
|
||||
UserRobuxBal, UserTicketsBal = GetUserBalance(AuthenticatedUser)
|
||||
if SendingCurrencyType == 0:
|
||||
if UserRobuxBal < OfferObj.receive_value:
|
||||
flash("You do not have enough Robux to fill this offer.", "error")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
else:
|
||||
if UserTicketsBal < OfferObj.receive_value:
|
||||
flash("You do not have enough Tickets to fill this offer.", "error")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
|
||||
transactions.CreateTransaction(
|
||||
Reciever = OfferObj.creator,
|
||||
Sender = User.query.filter_by(id = 1).first(), # Remain anonymous
|
||||
CurrencyAmount = OfferObj.receive_value,
|
||||
CurrencyType = SendingCurrencyType,
|
||||
TransactionType = TransactionType.Sale,
|
||||
AssetId = None,
|
||||
CustomText = f"Currency Exchange Order ({OfferId}) Filled"
|
||||
)
|
||||
transactions.CreateTransaction(
|
||||
Reciever = AuthenticatedUser,
|
||||
Sender = User.query.filter_by(id = 1).first(), # Remain anonymous
|
||||
CurrencyAmount = OfferObj.offer_value,
|
||||
CurrencyType = OfferObj.offer_currency_type,
|
||||
TransactionType = TransactionType.Sale,
|
||||
AssetId = None,
|
||||
CustomText = f"Currency Exchange Order ({OfferId}) Filled"
|
||||
)
|
||||
transactions.CreateTransaction(
|
||||
Reciever = User.query.filter_by(id = 1).first(), # Remain anonymous
|
||||
Sender = AuthenticatedUser,
|
||||
CurrencyAmount = OfferObj.receive_value,
|
||||
CurrencyType = SendingCurrencyType,
|
||||
TransactionType = TransactionType.Purchase,
|
||||
AssetId = None,
|
||||
CustomText = f"Currency Exchange Order ({OfferId}) Filled"
|
||||
)
|
||||
DecrementTargetBalance(AuthenticatedUser, OfferObj.receive_value, SendingCurrencyType)
|
||||
IncrementTargetBalance(AuthenticatedUser, OfferObj.offer_value, OfferObj.offer_currency_type)
|
||||
IncrementTargetBalance(OfferObj.creator, OfferObj.receive_value, SendingCurrencyType)
|
||||
|
||||
OfferObj.reciever_id = AuthenticatedUser.id
|
||||
db.session.commit()
|
||||
|
||||
CreateSystemMessage(
|
||||
subject = "Offer Filled",
|
||||
message = f"Your currency exchange offer ({OfferId}) has been filled, you have received {OfferObj.receive_value} {'Robux' if OfferObj.offer_currency_type == 1 else 'Tickets'} in exchange for {OfferObj.offer_value} {'Robux' if OfferObj.offer_currency_type == 0 else 'Tickets'}.",
|
||||
userid = OfferObj.creator_id
|
||||
)
|
||||
|
||||
flash("Offer successfully filled.", "success")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
except AssertionError:
|
||||
flash("Failed to fill offer, try again later.", "error")
|
||||
return redirect(f"/currency-exchange/view/{OfferId}")
|
||||
|
||||
@CurrencyExchangeRoute.route("/create", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def createOfferPage():
|
||||
return render_template("currencyexchange/create.html")
|
||||
|
||||
@CurrencyExchangeRoute.route("/create", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
@limiter.limit("40/minute")
|
||||
def createOffer():
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
|
||||
OfferCurrencyType : int = request.form.get(
|
||||
key = "offer-currency-type",
|
||||
default = None,
|
||||
type = int
|
||||
)
|
||||
OfferValue : int = request.form.get(
|
||||
key = "offer-currency-amount",
|
||||
default = None,
|
||||
type = int
|
||||
)
|
||||
ReceiveValue : int = request.form.get(
|
||||
key = "exchange-currency-amount",
|
||||
default = None,
|
||||
type = int
|
||||
)
|
||||
if OfferCurrencyType is None or OfferValue is None or ReceiveValue is None:
|
||||
flash("Invalid request", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
|
||||
if OfferValue <= 0 or ReceiveValue <= 0:
|
||||
flash("Invalid request", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
|
||||
if websiteFeatures.GetWebsiteFeature("CurrencyExchange") is False:
|
||||
flash("Currency exchange is currently disabled.", "error")
|
||||
return redirect(f"/currency-exchange/create")
|
||||
|
||||
try:
|
||||
with redis_lock.Lock( redis_client = redis_controller, name = f"currency_exchange_create_offer:{AuthenticatedUser.id}", expire = 15, auto_renewal = True ):
|
||||
ActiveOffers : int = ExchangeOffer.query.filter_by(
|
||||
creator_id = AuthenticatedUser.id,
|
||||
reciever_id = None
|
||||
).filter(
|
||||
ExchangeOffer.expires_at > datetime.utcnow()
|
||||
).count()
|
||||
|
||||
if ActiveOffers >= 25:
|
||||
flash("You have too many active offers, please cancel them to create a new offer.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
|
||||
if OfferCurrencyType == 0:
|
||||
if OfferValue < 5:
|
||||
flash("You can only create offers of 5 Robux or more.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
if OfferValue > 10000:
|
||||
flash("You can only create offers up to 10,000 Robux.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
Ratio = ReceiveValue / OfferValue
|
||||
else:
|
||||
if OfferValue < 10:
|
||||
flash("You can only create offers of 10 Tickets or more.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
if OfferValue > 100000:
|
||||
flash("You can only create offers up to 100,000 Tickets.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
Ratio = OfferValue / ReceiveValue
|
||||
|
||||
if Ratio < 2 or Ratio > 20:
|
||||
flash("Bad exchange ratio", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
|
||||
UserRobuxBal, UserTicketsBal = GetUserBalance(AuthenticatedUser)
|
||||
if OfferCurrencyType == 0 and UserRobuxBal < OfferValue:
|
||||
flash("You do not have enough Robux to create this offer.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
elif OfferCurrencyType == 1 and UserTicketsBal < OfferValue:
|
||||
flash("You do not have enough Tickets to create this offer.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
|
||||
DecrementTargetBalance(AuthenticatedUser, OfferValue, OfferCurrencyType)
|
||||
transactions.CreateTransaction(
|
||||
Reciever = User.query.filter_by(id=1).first(),
|
||||
Sender = AuthenticatedUser,
|
||||
CurrencyAmount = OfferValue,
|
||||
CurrencyType = OfferCurrencyType,
|
||||
TransactionType = TransactionType.Purchase,
|
||||
CustomText = "Created Exchange Order"
|
||||
)
|
||||
|
||||
NewOffer : ExchangeOffer = ExchangeOffer(
|
||||
creator_id = AuthenticatedUser.id,
|
||||
offer_value = OfferValue,
|
||||
receive_value = ReceiveValue,
|
||||
offer_currency_type = OfferCurrencyType,
|
||||
created_at = datetime.utcnow(),
|
||||
expires_at = datetime.utcnow() + timedelta(days=31),
|
||||
ratio = Ratio,
|
||||
worth = abs( 20 - Ratio ) if OfferCurrencyType == 0 else ( 20 - abs( Ratio - 20 ) )
|
||||
)
|
||||
db.session.add(NewOffer)
|
||||
db.session.commit()
|
||||
|
||||
flash("Offer created successfully", "success")
|
||||
return redirect("/currency-exchange/")
|
||||
except AssertionError:
|
||||
flash("Failed to create offer, try again later.", "error")
|
||||
return redirect("/currency-exchange/create")
|
||||
@@ -0,0 +1,168 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Create Offer{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bg-dark {
|
||||
background-color: rgb(30,30,30) !important;
|
||||
}
|
||||
.form-control:disabled ~ label {
|
||||
color: --bs-secondary-bg;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container position-relative" style="min-height: 100vh;margin-top: 200px;max-width: 900px;">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<form method="post">
|
||||
<div class="d-flex align-items-center mb-1">
|
||||
<div class="border-top flex-fill me-2"></div>
|
||||
<h5 class="text-secondary">You are offering</h5>
|
||||
<div class="border-top flex-fill ms-2"></div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="offer-currency-type" name="offer-currency-type" style="min-width: 100px;">
|
||||
<option value="0">Robux</option>
|
||||
<option value="1">Tickets</option>
|
||||
</select>
|
||||
<label for="offer-currency-type">Currency</label>
|
||||
</div>
|
||||
<div class="form-floating ms-2 flex-fill">
|
||||
<input type="number" class="form-control" id="offer-currency-amount" name="offer-currency-amount" placeholder="0" value="0">
|
||||
<label for="offer-currency-amount">Amount</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-3 mb-1">
|
||||
<div class="border-top flex-fill me-2"></div>
|
||||
<h5 class="text-secondary">In exchange for</h5>
|
||||
<div class="border-top flex-fill ms-2"></div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="exchange-currency-type" name="exchange-currency-type" style="min-width: 100px;" disabled>
|
||||
<option value="0">Robux</option>
|
||||
<option value="1" selected>Tickets</option>
|
||||
</select>
|
||||
<label for="exchange-currency-type disabled">Currency</label>
|
||||
</div>
|
||||
<div class="form-floating ms-2 flex-fill">
|
||||
<input type="number" class="form-control" id="exchange-currency-amount" name="exchange-currency-amount" placeholder="0" value="0">
|
||||
<label for="exchange-currency-amount">Amount</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 mt-2 text-secondary">Robux to Tix Ratio: <span class="fw-bold text-white" id="robux-to-tix-ratio">0:0</span></p>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm mt-3 w-100" id="submit-offer">Create Offer</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-4 border-start">
|
||||
<a class="text-decoration-none w-100 text-end" style="font-size: 12px;" href="/currency-exchange/">Return to Currency Exchange</a>
|
||||
<h3 class="w-100">New Exchange Offer</h3>
|
||||
<p class="text-secondary m-0 ms-2">You can create an offer to be placed onto the currency exchange system, this will allow other users to trade for example Robux to Tickets or vice versa.</p>
|
||||
|
||||
<p class="text-secondary m-0 mt-2 ms-2">Note: The minimum for Robux to Tix is 1:2 while the maximum is 1:20</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-absolute w-100 h-100" id="transparent-background-top" style="top: 0;left: 0;background-color: rgba(0,0,0,0.5);display: none;"></div>
|
||||
<script>
|
||||
const OfferCurrencyTypeSelect = document.getElementById('offer-currency-type');
|
||||
const ExchangeCurrencyTypeSelect = document.getElementById('exchange-currency-type');
|
||||
|
||||
OfferCurrencyTypeSelect.addEventListener('change', () => {
|
||||
if (OfferCurrencyTypeSelect.value == 0) {
|
||||
ExchangeCurrencyTypeSelect.value = 1;
|
||||
} else {
|
||||
ExchangeCurrencyTypeSelect.value = 0;
|
||||
}
|
||||
UpdateRatio();
|
||||
});
|
||||
|
||||
const OfferCurrencyAmountInput = document.getElementById('offer-currency-amount');
|
||||
const ExchangeCurrencyAmountInput = document.getElementById('exchange-currency-amount');
|
||||
const RobuxToTixRatio = document.getElementById('robux-to-tix-ratio');
|
||||
const SubmitButton = document.getElementById('submit-offer');
|
||||
const TransparentBackgroundTop = document.getElementById('transparent-background-top');
|
||||
|
||||
const DisableButton = () => {
|
||||
if ( SubmitButton.disabled ) return;
|
||||
|
||||
SubmitButton.disabled = true;
|
||||
SubmitButton.classList.add('disabled');
|
||||
}
|
||||
const EnableButton = () => {
|
||||
if ( !SubmitButton.disabled ) return;
|
||||
|
||||
SubmitButton.disabled = false;
|
||||
SubmitButton.classList.remove('disabled');
|
||||
}
|
||||
|
||||
const UpdateRatio = () => {
|
||||
if (OfferCurrencyTypeSelect.value == 0) {
|
||||
var ratio = ExchangeCurrencyAmountInput.value / OfferCurrencyAmountInput.value;
|
||||
// Round to 3 decimal places
|
||||
ratio = Math.round(ratio * 1000) / 1000;
|
||||
RobuxToTixRatio.innerText = `1:${ratio}`;
|
||||
|
||||
if (ratio < 1 || ratio > 50) {
|
||||
DisableButton();
|
||||
} else {
|
||||
EnableButton();
|
||||
}
|
||||
|
||||
} else {
|
||||
var ratio = ExchangeCurrencyAmountInput.value / OfferCurrencyAmountInput.value;
|
||||
// Round to 3 decimal places
|
||||
ratio = Math.round(ratio * 1000) / 1000;
|
||||
RobuxToTixRatio.innerText = `${ratio}:1`;
|
||||
|
||||
var ActualRatio = OfferCurrencyAmountInput.value / ExchangeCurrencyAmountInput.value;
|
||||
|
||||
if ( ActualRatio < 1 || ActualRatio > 50 ) {
|
||||
DisableButton();
|
||||
} else {
|
||||
EnableButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SubmitButton.addEventListener('click', () => {
|
||||
TransparentBackgroundTop.style.display = 'block';
|
||||
})
|
||||
|
||||
OfferCurrencyAmountInput.addEventListener('input', () => {
|
||||
UpdateRatio();
|
||||
});
|
||||
|
||||
ExchangeCurrencyAmountInput.addEventListener('input', () => {
|
||||
UpdateRatio();
|
||||
});
|
||||
DisableButton()
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,114 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Currency Exchange{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-tickets {
|
||||
color: rgb(224, 224, 60) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container position-relative" style="min-height: 100vh;margin-top: 100px;max-width: 1200px;">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<h1 class="m-0">Currency Exchange</h1>
|
||||
<div class="d-flex">
|
||||
<a class="text-decoration-none mt-1 ms-1" href="/currency-exchange/create">Create Exchange Offer</a>
|
||||
<a class="text-decoration-none mt-1 ms-2" href="/transactions/">My Transactions</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto form-floating">
|
||||
<select class="form-control" id="change-category-select">
|
||||
<option value="rt" {%if PageCategory == "rt"%}selected{%endif%} selected>Tix to Robux</option>
|
||||
<option value="tr" {%if PageCategory == "tr"%}selected{%endif%}>Robux to Tix</option>
|
||||
<option value="all" {%if PageCategory == "all"%}selected{%endif%}>All</option>
|
||||
</select>
|
||||
<label for="change-category-select">Category</label>
|
||||
</div>
|
||||
<div class="ms-2 form-floating">
|
||||
<select class="form-control" id="change-sort-select">
|
||||
<option value="best" {%if SortType == "best"%}selected{%endif%}>Best Value</option>
|
||||
<option value="worst" {%if SortType == "worst"%}selected{%endif%}>Worst Value</option>
|
||||
<option value="created" {%if SortType == "created"%}selected{%endif%}>Created</option>
|
||||
</select>
|
||||
<label for="change-sort-select">Sort</label>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-dark table-striped mt-2">
|
||||
<thead>
|
||||
<tr class="rounded-top">
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Expires</th>
|
||||
<th scope="col">Offering</th>
|
||||
<th scope="col">Requesting</th>
|
||||
<th scope="col">R:T Ratio</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider" style="border-color: rgb(20,20,20);">
|
||||
{% for OfferObj in offers: %}
|
||||
<tr>
|
||||
<td>{{ OfferObj.created_at.strftime("%d/%m/%Y") }}</td>
|
||||
<td>{{ OfferObj.expires_at.strftime("%d/%m/%Y") }}</td>
|
||||
<td>
|
||||
{% if OfferObj.offer_currency_type == 0 %}
|
||||
<span class="text-robux">R$ {{OfferObj.offer_value}}</span>
|
||||
{% else %}
|
||||
<span class="text-tickets">T$ {{OfferObj.offer_value}}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if OfferObj.offer_currency_type == 1 %}
|
||||
<span class="text-robux">R$ {{OfferObj.receive_value}}</span>
|
||||
{% else %}
|
||||
<span class="text-tickets">T$ {{OfferObj.receive_value}}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
1:{{round(OfferObj.ratio, 3)}}
|
||||
</td>
|
||||
<td><a class="text-decoration-none" href="/currency-exchange/view/{{OfferObj.id}}">View</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if len(offers) == 0: %}
|
||||
<div class="text-center">
|
||||
<p class="text-secondary">No offers found</p>
|
||||
</div>
|
||||
{%endif%}
|
||||
<div class="align-items-center d-flex justify-content-center mt-2">
|
||||
<a class="btn btn-sm text-white btn-primary me-2 pagination-back-btn {% if not hasPrevPage: %}disabled {% endif %}" {% if hasPrevPage: %}href="/currency-exchange/?category={{PageCategory}}&sort={{SortType}}&page={{PageNumber-1}}"{%endif%}>Previous</a>
|
||||
<p class="m-0 pagination-page-number">Page {{PageNumber}}</p>
|
||||
<a class="btn btn-sm text-white btn-primary ms-2 pagination-next-btn {% if not hasNextPage: %}disabled {% endif %}" {% if hasNextPage: %}href="/currency-exchange/?category={{PageCategory}}&sort={{SortType}}&page={{PageNumber+1}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>const s = document.getElementById("change-category-select");s.addEventListener("change", function() {var u = new URL(window.location.href);var sb = s.value;u.searchParams.set("category", sb);;window.location.href = u;});</script>
|
||||
<script>const aa = document.getElementById("change-sort-select");aa.addEventListener("change", function() {var u = new URL(window.location.href);var ab = aa.value;u.searchParams.set("sort", ab);;window.location.href = u;});</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,78 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}View Offer{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.text-robux {
|
||||
color: rgb(26, 212, 103) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text-tickets {
|
||||
color: rgb(224, 224, 60) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container position-relative" style="min-height: 100vh;margin-top: 200px;max-width: 900px;">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<a class="text-decoration-none mb-3" href="/currency-exchange/">Return to Currency Exchange</a>
|
||||
<h1 class="m-0">Viewing Offer</h1>
|
||||
{% if offer.creator_id == AuthenticatedUser.id: %}
|
||||
<p class="text-secondary m-0">This offer was created by you</p>
|
||||
{% endif %}
|
||||
<div class="row mt-2 p-2">
|
||||
<div class="col border p-2">
|
||||
<h5 class="text-center">Offering</h5>
|
||||
<h2 style="font-size: 3.5rem;" class="{% if offer.offer_currency_type == 0: %}text-robux{%else%}text-tickets{%endif%} w-100 text-center">{% if offer.offer_currency_type == 0: %}R${%else%}T${%endif%}{{offer.offer_value}}</h2>
|
||||
</div>
|
||||
<div class="col border p-2">
|
||||
<h5 class="text-center">R:T Ratio</h5>
|
||||
<h2 style="font-size: 3.5rem;" class="text-secondary w-100 text-center">1:{{round(offer.ratio,3)}}</h2>
|
||||
</div>
|
||||
<div class="col border p-2">
|
||||
<h5 class="text-center">Requesting</h5>
|
||||
<h2 style="font-size: 3.5rem;" class="{% if offer.offer_currency_type == 1: %}text-robux{%else%}text-tickets{%endif%} w-100 text-center">{% if offer.offer_currency_type == 1: %}R${%else%}T${%endif%}{{offer.receive_value}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex mt-2">
|
||||
<p class="text-secondary m-0">Created: <span class="text-white">{{offer.created_at.strftime("%b %d, %Y %I:%M %p")}} UTC</span></p>
|
||||
<p class="text-secondary m-0 ms-3">Expires: <span class="text-white">{{offer.expires_at.strftime("%b %d, %Y %I:%M %p")}} UTC</span></p>
|
||||
{% if offer.reciever_id != None: %}
|
||||
<p class="text-danger m-0 ms-auto">This order has already been filled</p>
|
||||
{% else: %}
|
||||
{% if offer.creator_id == AuthenticatedUser.id: %}
|
||||
<form method="post" action="/currency-exchange/delete/{{offer.id}}" class="ms-auto">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-danger">Delete Offer</button>
|
||||
</form>
|
||||
{%else%}
|
||||
<form method="post" action="/currency-exchange/fill/{{offer.id}}" class="ms-auto">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-primary">Fufill Offer</button>
|
||||
</form>
|
||||
{%endif%}
|
||||
{%endif%}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Edit Item{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/develop.css" rel="stylesheet"/>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 800px;">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="d-flex align-items-center ps-4 pe-4 ps-md-1 pe-md-1">
|
||||
<div>
|
||||
<a class="text-decoration-none text-secondary m-0" href="/develop?type={{AssetObj.asset_type.value}}" style="font-size: 12px;">< Return to develop page</a>
|
||||
<h1 class="m-0">Edit Item</h1>
|
||||
</div>
|
||||
{% if AssetObj.asset_type.value != 3 and AssetObj.asset_type.value != 1: %}
|
||||
<a class="text-decoration-none ms-auto" href="/catalog/{{AssetObj.id}}/">View on Catalog</a>
|
||||
{%else%}
|
||||
<a class="text-decoration-none ms-auto" href="/catalog/{{AssetObj.id}}/">View on Library</a>
|
||||
{%endif%}
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<img style="aspect-ratio:1/1; max-width: 400px; max-height: 400px;" width="100%" src="/Thumbs/Asset.ashx?assetId={{AssetObj.id}}&x=512&y=512" class="rounded">
|
||||
</div>
|
||||
<div class="col-md-7 border-start pt-2 pb-2 ps-4 pe-4 ps-md-1 pe-md-1">
|
||||
<form method="post">
|
||||
<div class="form-floating mb-2">
|
||||
<input type="text" class="form-control" id="item-name" name="item-name" placeholder="Awesome Item!" required value="{{AssetObj.name}}">
|
||||
<label for="item-name">Name</label>
|
||||
</div>
|
||||
<div class="form-floating mb-2">
|
||||
<textarea class="form-control mt-2" placeholder="Description" style="min-height: 150px;" name="item-description">{{AssetObj.description}}</textarea>
|
||||
<label for="item-description">Description</label>
|
||||
</div>
|
||||
{% if AssetObj.asset_type.value != 3 and AssetObj.asset_type.value != 1: %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" {% if AssetObj.is_for_sale: %}checked{%endif%} id="isForSaleCheckbox" name="is-for-sale">
|
||||
<label class="form-check-label" for="isForSaleCheckbox">
|
||||
For Sale
|
||||
</label>
|
||||
</div>
|
||||
<div class="p-1 row">
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" id="basic-addon1">R$</span>
|
||||
<input type="number" class="form-control" placeholder="Robux" aria-label="Robux" name="robux-cost" value="{{AssetObj.price_robux}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" id="basic-addon1">T$</span>
|
||||
<input type="number" class="form-control" placeholder="Tickets" aria-label="Tickets" name="tix-cost" value="{{AssetObj.price_tix}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p style="font-size:13px;">* Vortexi will take a 30% fee from the sale</p>
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 text-center">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'develop/games/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<h4>Access</h4>
|
||||
<form method="post">
|
||||
<label for="MaxPlayersInput" class="form-label">Max Players ( 2 - 50 )</label>
|
||||
<input class="form-control mb-1" type="number" name="maxplayers" id="MaxPlayersInput" placeholder="Max Players" required value="{{PlaceObj.maxplayers}}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mt-2">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Manage Place{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/develop.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.page-tabs a {
|
||||
color: #dadada !important;
|
||||
font-size: medium;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 0 1px 0 rgba(0,0,0,.2), 0 1px 2px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
.page-tabs a:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 1000px;">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div>
|
||||
<h1 class="m-0">{{PlaceAssetObj.name}}</h1>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/places" class="text-decoration-none">Return to Manage Universe</a>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 border-end page-tabs">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/manage" class="w-100">Basic Settings</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/access" class="w-100">Access</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/upload-version" class="w-100">Upload File</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/placeicon" class="w-100">Game Icon</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/thumbnails" class="w-100">Thumbnails</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/version-history" class="w-100">Version History</a>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
{% block subcontent %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
{% extends 'develop/games/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<h4>Basic Settings</h4>
|
||||
<form method="post">
|
||||
<input type="text" name="name" class="form-control" placeholder="Name" value="{{PlaceAssetObj.name}}" required>
|
||||
<textarea name="description" class="form-control mt-2" placeholder="Description" style="min-height: 150px;" required>{{PlaceAssetObj.description}}</textarea>
|
||||
<h5 class="mt-3">Chat Style</h5>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="chat-style-type" id="chat-style-type-radio1" value="0" {% if PlaceObj.chat_style.value == 0: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="chat-style-type-radio1">
|
||||
Classic
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="chat-style-type" id="chat-style-type-radio2" value="1" {% if PlaceObj.chat_style.value == 1: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="chat-style-type-radio2">
|
||||
Bubble
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="chat-style-type" id="chat-style-type-radio3" value="2" {% if PlaceObj.chat_style.value == 2: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="chat-style-type-radio3">
|
||||
Classic and Bubble
|
||||
</label>
|
||||
</div>
|
||||
{% if UniverseObj.place_year.value == 2018 or UniverseObj.place_year.value == 2020 or UniverseObj.place_year.value == 2021 %}
|
||||
<h5 class="mt-3">Avatar Rig Type</h5>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="avatar-rig-type" id="avatar-rig-type-radio1" value="0" {% if PlaceObj.rig_choice.value == 0: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="avatar-rig-type-radio1">
|
||||
User Choice
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="avatar-rig-type" id="avatar-rig-type-radio2" value="1" {% if PlaceObj.rig_choice.value == 1: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="avatar-rig-type-radio2">
|
||||
Force R6
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="avatar-rig-type" id="avatar-rig-type-radio3" value="2" {% if PlaceObj.rig_choice.value == 2: %}checked{% endif %}>
|
||||
<label class="form-check-label" for="avatar-rig-type-radio3">
|
||||
Force R15
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mt-2">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends 'develop/games/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<img src="/Thumbs/PlaceIcon.ashx?assetId={{PlaceAssetObj.id}}&x=420&y=420&refresh={{RandomNumber}}" class="mt-2 rounded" width="100%" style="aspect-ratio: 1/1;">
|
||||
<p class="m-0" style="font-size: 10px;">Note: Image may be cached so you may see the previous image after uploading</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h4 class="m-0">Upload Place Icon</h4>
|
||||
<p class="m-0 mb-2">Must have a 1:1 Aspect Ratio, Lesser than 2mb and be at max 1024 x 1024</p>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Upload Place Icon</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="usegenerated" value="on">
|
||||
<button type="submit" class="btn btn-primary mt-2 btn-sm w-100">Use Generated Icon</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends 'develop/games/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{PlaceAssetObj.id}}&x=1280&y=720&refresh={{RandomNumber}}" class="mt-2 rounded" width="100%" style="aspect-ratio: 16/9;">
|
||||
<p class="m-0" style="font-size: 10px;">Note: Image may be cached so you may see the previous image after uploading</p>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<h4 class="m-0">Upload Thumbnail</h4>
|
||||
<p class="m-0 mb-2">Must have a 16:9 Aspect Ratio, Lesser than 2mb and be at max 1920x1080</p>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Upload Thumbnail</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="usegenerated" value="on">
|
||||
<button type="submit" class="btn btn-primary mt-2 btn-sm w-100">Use Generated Thumbnail</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends 'develop/games/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<h4 class="m-0">Upload Place File</h4>
|
||||
<p class="m-0 mb-2">Supported file types: .rbxl and .rbxlx Max File Size: 30mb</p>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Upload File</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends 'develop/games/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<h4 class="m-0">Version History</h4>
|
||||
<div class="linebreak"></div>
|
||||
<div>
|
||||
{% for AssetVer in AssetVersions.items: %}
|
||||
<div class="bg-dark p-2 rounded w-100 mt-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<h5 class="m-0">Version {{AssetVer.version}}</h5>
|
||||
<p class="text-secondary m-0" style="font-size: 13px;">Created on {{AssetVer.created_at.strftime("%d/%m/%Y at %H:%M:%S UTC")}} | ID: {{AssetVer.id}}</p>
|
||||
</div>
|
||||
<a href="{{CDN_URL}}/{{AssetVer.content_hash}}" class="ms-auto text-white me-1" style="font-size: 20px;">
|
||||
<i class="bi bi-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="align-items-center d-flex justify-content-center mt-2">
|
||||
<a class="ms-auto m-0 text-decoration-none {% if not AssetVersions.has_prev %}text-secondary{%endif%}" {% if AssetVersions.has_prev %}href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/version-history?page={{AssetVersions.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="ms-2 me-2 text-white m-0">Page {{AssetVersions.page}} of {{AssetVersions.pages}}</p>
|
||||
<a class="me-auto m-0 text-decoration-none {% if not AssetVersions.has_next %}text-secondary{%endif%}" {% if AssetVersions.has_next %}href="/develop/universes/{{UniverseObj.id}}/place/{{PlaceAssetObj.id}}/version-history?page={{AssetVersions.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends 'develop/template.html' %}
|
||||
{% block subcontent %}
|
||||
<form action="/develop/create/9" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if GroupIdContext != None %}<input type="hidden" name="groupIdContext" value="{{GroupIdContext}}">{%endif%}
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create Game</button>
|
||||
</form>
|
||||
<div class="mt-2">
|
||||
{% for place in UserPlaces: %}
|
||||
<div class="w-100 m-1 p-1 d-flex">
|
||||
<img src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a class="text-white text-decoration-none" href="/games/{{place.id}}/"><h5 class="mb-1">{{place.name}}</h5></a>
|
||||
<p class="text-secondary m-0" style="font-size:13px;">Visits: <span class="text-white">{{place.place.visitcount}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto">
|
||||
<a class="btn btn-sm border-primary text-white" href="/develop/universes/{{GetPlaceUniverse(place).id}}/manage">Manage</a>
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
{% if len(UserPlaces) == 0 %}
|
||||
<p class="mt-5 mb-5 text-secondary w-100 text-center">No games found</p>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends 'develop/template.html' %}
|
||||
{% block subcontent %}
|
||||
<form action="/develop/create/1" method="post" enctype="multipart/form-data">
|
||||
<div>
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<p class="m-0 text-secondary mb-2" style="font-size:12px;">Image has to be smaller than 3MB</p>
|
||||
<div class="mb-2">
|
||||
<input class="form-control" type="text" id="name" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if GroupIdContext != None %}<input type="hidden" name="groupIdContext" value="{{GroupIdContext}}">{%endif%}
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create Image</button>
|
||||
</form>
|
||||
<div class="mt-2">
|
||||
{% for image in UserImages.items: %}
|
||||
<div class="w-100 m-1 p-1 d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{image.id}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a class="text-white text-decoration-none" href="/library/{{image.id}}/"><h5 class="m-0">{{image.name}}</h5></a>
|
||||
<p class="text-secondary m-0" style="font-size:13px;">Sales: <span class="text-white">{{image.sale_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto">
|
||||
<a class="btn btn-sm border-primary text-white" href="/develop/{{image.id}}/edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
{% if len(UserImages.items) == 0: %}
|
||||
<h5 class="text-center mt-3 mb-3">No items found</h5>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends 'develop/template.html' %}
|
||||
{% block subcontent %}
|
||||
<p class="m-0 text-secondary mb-2">Want to create Pants? <a class="text-decoration-none" href="/static/img/TemplatePants.png">Download</a> the template</p>
|
||||
<form action="/develop/create/12" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="text" id="name" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if GroupIdContext != None %}<input type="hidden" name="groupIdContext" value="{{GroupIdContext}}">{%endif%}
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create Pants</button>
|
||||
</form>
|
||||
<div class="mt-2">
|
||||
{% for pants in UserClothing: %}
|
||||
<div class="w-100 m-1 p-1 d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{pants.id}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a class="text-white text-decoration-none" href="/catalog/{{pants.id}}/"><h5 class="m-0">{{pants.name}}</h5></a>
|
||||
<p class="text-secondary m-0" style="font-size:13px;">Sales: <span class="text-white">{{pants.sale_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto">
|
||||
<a class="btn btn-sm border-primary text-white" href="/develop/{{pants.id}}/edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
{% if len(UserClothing) == 0: %}
|
||||
<h5 class="text-center mt-3 mb-3">No items found</h5>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends 'develop/template.html' %}
|
||||
{% block subcontent %}
|
||||
<p class="m-0 text-secondary mb-2">Want to create Shirts? <a class="text-decoration-none" href="/static/img/TemplateShirt.png">Download</a> the template</p>
|
||||
<form action="/develop/create/11" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="text" id="name" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if GroupIdContext != None %}<input type="hidden" name="groupIdContext" value="{{GroupIdContext}}">{%endif%}
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create Shirt</button>
|
||||
</form>
|
||||
<div class="mt-2">
|
||||
{% for shirt in UserClothing: %}
|
||||
<div class="w-100 m-1 p-1 d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{shirt.id}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a class="text-white text-decoration-none" href="/catalog/{{shirt.id}}/"><h5 class="m-0">{{shirt.name}}</h5></a>
|
||||
<p class="text-secondary m-0" style="font-size:13px;">Sales: <span class="text-white">{{shirt.sale_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto">
|
||||
<a class="btn btn-sm border-primary text-white" href="/develop/{{shirt.id}}/edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
{% if len(UserClothing) == 0: %}
|
||||
<h5 class="text-center mt-3 mb-3">No items found</h5>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends 'develop/template.html' %}
|
||||
{% block subcontent %}
|
||||
<form action="/develop/create/3" method="post" enctype="multipart/form-data">
|
||||
<div>
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<p class="m-0 text-secondary mb-2" style="font-size:12px;">Sound has to be a MP3 File, Lesser than 8MB and shorter than 8 minutes. Minimum: 20 Robux, every second of audio costs 0.5 robux, will round down if total is decimal point. </p>
|
||||
<div class="mb-2">
|
||||
<input class="form-control" type="text" id="name" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if GroupIdContext != None %}<input type="hidden" name="groupIdContext" value="{{GroupIdContext}}">{%endif%}
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create Sound</button>
|
||||
</form>
|
||||
<div class="mt-2">
|
||||
{% for sound in UserSounds.items: %}
|
||||
<div class="w-100 m-1 p-1 d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{sound.id}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a class="text-white text-decoration-none" href="/library/{{sound.id}}/"><h5 class="m-0">{{sound.name}}</h5></a>
|
||||
<p class="text-secondary m-0" style="font-size:13px;">Sales: <span class="text-white">{{sound.sale_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto">
|
||||
<a class="btn btn-sm border-primary text-white" href="/develop/{{sound.id}}/edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
{% if len(UserSounds.items) == 0: %}
|
||||
<h5 class="text-center mt-3 mb-3">No items found</h5>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends 'develop/template.html' %}
|
||||
{% block subcontent %}
|
||||
<form action="/develop/create/2" method="post" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="file" id="file" name="file" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input class="form-control" type="text" id="name" name="name" placeholder="Name" required>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if GroupIdContext != None %}<input type="hidden" name="groupIdContext" value="{{GroupIdContext}}">{%endif%}
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create T-Shirt</button>
|
||||
</form>
|
||||
<div class="mt-2">
|
||||
{% for tshirt in UserClothing: %}
|
||||
<div class="w-100 m-1 p-1 d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?assetId={{tshirt.id}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a class="text-white text-decoration-none" href="/catalog/{{tshirt.id}}/"><h5 class="m-0">{{tshirt.name}}</h5></a>
|
||||
<p class="text-secondary m-0" style="font-size:13px;">Sales: <span class="text-white">{{tshirt.sale_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto mt-auto mb-auto">
|
||||
<a class="btn btn-sm border-primary text-white" href="/develop/{{tshirt.id}}/edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{%endfor%}
|
||||
{% if len(UserClothing.items) == 0: %}
|
||||
<h5 class="text-center mt-3 mb-3">No items found</h5>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Develop{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/develop.css" rel="stylesheet"/>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 900px;">
|
||||
<h1>Develop</h1>
|
||||
<select class="form-control w-100 mb-2" id="select-creator">
|
||||
<option value="-1">Created by you</option>
|
||||
{% for group in UserGroups: %}
|
||||
<option value="{{group.id}}" {% if GroupIdContext == group.id: %}selected{%endif%}>{{group.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select class="form-control w-100 mb-2" id="select-asset-type">
|
||||
<option value="9" {% if PageType == 9: %}selected{%endif%}>Games</option>
|
||||
<option value="1" {% if PageType == 1: %}selected{%endif%}>Image</option>
|
||||
<option value="3" {% if PageType == 3: %}selected{%endif%}>Audio</option>
|
||||
<option value="11" {% if PageType == 11: %}selected{%endif%}>Shirts</option>
|
||||
<option value="12" {% if PageType == 12: %}selected{%endif%}>Pants</option>
|
||||
<option value="2" {% if PageType == 2: %}selected{%endif%}>T-Shirts</option>
|
||||
</select>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<div class="alert border border-danger p-2 text-center messagealerts">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="linebreak"></div>
|
||||
{% block subcontent %}{% endblock %}
|
||||
<div class="d-flex">
|
||||
{% if PreviousPage > 0: %}
|
||||
<a href="/develop?type={{PageType}}&page={{PreviousPage}}{% if GroupIdContext != None %}&groupId={{GroupIdContext}}{%endif%}" class="ms-auto text-decoration-none text-primary">Previous</a>
|
||||
{%else%}
|
||||
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
|
||||
{% endif %}
|
||||
<p class="ms-1 me-1">{{PageNumber}}</p>
|
||||
{% if NextPage > 0: %}
|
||||
<a href="/develop?type={{PageType}}&page={{NextPage}}{% if GroupIdContext != None %}&groupId={{GroupIdContext}}{%endif%}" class="me-auto text-decoration-none text-primary">Next</a>
|
||||
{%else%}
|
||||
<a class="me-auto text-decoration-none text-secondary">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>const s = document.getElementById("select-asset-type");s.addEventListener("change", function() {var u = new URL(window.location.href);var sb = s.value;u.searchParams.set("type", sb);;window.location.href = u;});</script>
|
||||
<script>const c = document.getElementById("select-creator");c.addEventListener("change", function() {var u = new URL(window.location.href);var sb = c.value;if (sb < 1 ){ u.searchParams.delete("groupId") } else { u.searchParams.set("groupId", sb) };u.searchParams.delete("type");window.location.href = u;});</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<h4>Access</h4>
|
||||
<form method="post">
|
||||
<label for="MinAccountAgeInput" class="form-label">Minimum Account Age ( 0 - 365 )</label>
|
||||
<input class="form-control mb-1" type="number" name="minaccountage" id="MinAccountAgeInput" placeholder="Min Account Age" required value="{{UniverseObj.minimum_account_age}}">
|
||||
<div class="w-100">
|
||||
<input class="form-check-input" type="checkbox" name="ispublic" id="isPublicCheckbox" {%if UniverseObj.is_public%}checked{%endif%}>
|
||||
<label class="form-check-label" for="isPublicCheckbox">
|
||||
<p class="text-white m-0">Public</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-100 mt-1">
|
||||
<input class="form-check-input" type="checkbox" name="bcrequired" id="BCRequiredCheckbox" {%if UniverseObj.bc_required%}checked{%endif%}>
|
||||
<label class="form-check-label" for="BCRequiredCheckbox">
|
||||
<p class="text-white m-0">Builders Club members only</p>
|
||||
</label>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mt-2">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Badges</h4>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-primary btn-sm" href="/develop/universes/{{UniverseObj.id}}/create-badge">Create Badge</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 mt-2">
|
||||
{% for badgeObj in Badges: %}
|
||||
<div class="bg-dark p-2 rounded w-100 mt-2">
|
||||
<div class="d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?x=180&y=180&assetId={{badgeObj.icon_image_id}}" width="100px" height="100px" class="border rounded">
|
||||
<div class="ms-2 h-100">
|
||||
<h5 class="m-0">{{badgeObj.name}}</h5>
|
||||
<p class="m-0 mt-1 text-secondary" style="font-size: 14px;">Badge ID: <span class="text-white">{{badgeObj.id}}</span></p>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">Awarded Yesterday: <span class="text-white">{{GetBadgeAwardedPastDay(badgeObj.id)}}</span></p>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">Total Awarded: <span class="text-white">{{GetTotalBadgeAwardedCount(badgeObj.id)}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-light btn-sm" href="/develop/universes/{{UniverseObj.id}}/badges/{{badgeObj.id}}">Edit Badge</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if len(Badges) == 0: %}
|
||||
<p class="text-secondary text-center w-100 mt-5 mb-5">No Badges found, create one by clicking the button above</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Create Badge</h4>
|
||||
<div class="ms-auto">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/badges" class="text-decoration-none m-0">Manage Badges</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="Badge Name" required>
|
||||
<label for="name">Badge Name</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<textarea class="form-control" id="description" name="description" placeholder="Badge Description" style="height: 100px;" required></textarea>
|
||||
<label for="description">Badge Description</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h5 class="m-0 mb-1">Badge Icon</h5>
|
||||
<input type="file" class="form-control" name="icon-file" id="file" class="mt-2" required>
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 12px;">Must be a PNG, minimum 128x128 and maximum 1024x1024, less than 1mb</p>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 mt-2">Create Badge</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Create Gamepass</h4>
|
||||
<div class="ms-auto">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/gamepasses" class="text-decoration-none m-0">Manage Gamepasses</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="Gamepass Name" required>
|
||||
<label for="name">Gamepass Name</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<textarea class="form-control" id="description" name="description" placeholder="Gamepass Description" style="height: 100px;" required></textarea>
|
||||
<label for="description">Gamepass Description</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h5 class="m-0 mb-1">Gamepass Icon</h5>
|
||||
<input type="file" class="form-control" name="file" id="file" class="mt-2" required>
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 12px;">Must be a PNG, minimum 128x128 and maximum 1024x1024, less than 1mb</p>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 mt-2">Create Gamepass</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Create new place</h4>
|
||||
<p class="text-secondary m-0 ms-2" style="font-size: 13px;">{{TotalAmountPlacesCreated}} / 10 Places used</p>
|
||||
</div>
|
||||
<div class="w-100 mt-2">
|
||||
<form method="post">
|
||||
<p class="text-secondary m-0 mb-1">Are you sure you want to create a new place under <span class="text-white">{{RootPlaceAssetObj.name}}</span>?</p>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Create Place</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Create Developer Product</h4>
|
||||
<div class="ms-auto">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/developer-products" class="text-decoration-none m-0">Manage Developer Products</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="Product Name" required>
|
||||
<label for="name">Product Name</label>
|
||||
</div>
|
||||
<div class="form-floating mt-2">
|
||||
<textarea class="form-control" id="description" name="description" placeholder="Gamepass Description" style="height: 100px;" required></textarea>
|
||||
<label for="description">Product Description</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<h5 class="m-0 mb-1">Product Icon</h5>
|
||||
<input type="file" class="form-control" name="file" id="file" class="mt-2" required>
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 12px;">Must be a PNG, minimum 128x128 and maximum 1024x1024, less than 1mb</p>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 mt-2">Create Developer Product</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Developer Products</h4>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-primary btn-sm" href="/develop/universes/{{UniverseObj.id}}/create-product">Create Developer Product</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 mt-2">
|
||||
{% for product in DeveloperProducts: %}
|
||||
<div class="bg-dark p-2 rounded w-100 mt-2">
|
||||
<div class="d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?x=180&y=180&assetId={{product.iconimage_assetid}}" width="100px" height="100px" class="border rounded">
|
||||
<div class="ms-2 h-100">
|
||||
<h5 class="m-0">{{product.name}}</h5>
|
||||
<p class="m-0 mt-1 text-secondary" style="font-size: 14px;">Product ID: <span class="text-white">{{product.productid}}</span></p>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">Total sold: <span class="text-white">{{product.sales_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-light btn-sm" href="/develop/universes/{{UniverseObj.id}}/developer-products/{{product.productid}}">Edit Product</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if len(DeveloperProducts) == 0: %}
|
||||
<p class="text-secondary text-center w-100 mt-5 mb-5">No Developer Products found, create one by clicking the button above</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Edit Badge</h4>
|
||||
<div class="ms-auto">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/badges" class="text-decoration-none m-0">Manage Badges</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<img style="aspect-ratio:1/1; max-width: 400px; max-height: 400px;" width="100%" src="/Thumbs/Asset.ashx?assetId={{BadgeObj.icon_image_id}}&x=352&y=352" class="rounded">
|
||||
<div class="mt-2">
|
||||
<input type="file" class="form-control" name="icon-file" id="file" class="mt-2">
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 12px;">Must be a PNG, minimum 128x128 and maximum 1024x1024, less than 1mb</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7 pt-2 pb-2 ps-4 pe-4 ps-md-1 pe-md-1">
|
||||
<form method="post">
|
||||
<div class="form-floating mb-2">
|
||||
<input type="text" class="form-control" id="item-name" name="badge-name" placeholder="Awesome Badge!" required value="{{BadgeObj.name}}">
|
||||
<label for="item-name">Name</label>
|
||||
</div>
|
||||
<div class="form-floating mb-2">
|
||||
<textarea class="form-control mt-2" placeholder="Description" style="min-height: 150px;" name="badge-description">{{BadgeObj.description}}</textarea>
|
||||
<label for="item-description">Description</label>
|
||||
</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 text-center">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Edit Gamepass</h4>
|
||||
<div class="ms-auto">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/gamepasses" class="text-decoration-none m-0">Manage Gamepasses</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<img style="aspect-ratio:1/1; max-width: 400px; max-height: 400px;" width="100%" src="/Thumbs/Asset.ashx?assetId={{GamepassObj.gamepass_id}}&x=352&y=352" class="rounded">
|
||||
<div class="mt-2">
|
||||
<input type="file" class="form-control" name="icon-file" id="file" class="mt-2">
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 12px;">Must be a PNG, minimum 128x128 and maximum 1024x1024, less than 1mb</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7 pt-2 pb-2 ps-4 pe-4 ps-md-1 pe-md-1">
|
||||
<form method="post">
|
||||
<div class="form-floating mb-2">
|
||||
<input type="text" class="form-control" id="item-name" name="pass-name" placeholder="Awesome Item!" required value="{{GamepassObj.gamepass.name}}">
|
||||
<label for="item-name">Name</label>
|
||||
</div>
|
||||
<div class="form-floating mb-2">
|
||||
<textarea class="form-control mt-2" placeholder="Description" style="min-height: 150px;" name="pass-description">{{GamepassObj.gamepass.description}}</textarea>
|
||||
<label for="item-description">Description</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" {% if GamepassObj.gamepass.is_for_sale: %}checked{%endif%} id="isForSaleCheckbox" name="is-for-sale">
|
||||
<label class="form-check-label" for="isForSaleCheckbox">
|
||||
For Sale
|
||||
</label>
|
||||
</div>
|
||||
<div class="p-1 row">
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" id="basic-addon1">R$</span>
|
||||
<input type="number" class="form-control" placeholder="Robux" aria-label="Robux" name="robux-cost" value="{{GamepassObj.gamepass.price_robux}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p style="font-size:13px;">* Vortexi will take a 30% fee from the sale</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 text-center">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Edit Developer Product</h4>
|
||||
<div class="ms-auto">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/developer-products" class="text-decoration-none m-0">Manage Developer Products</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linebreak"></div>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<img style="aspect-ratio:1/1; max-width: 400px; max-height: 400px;" width="100%" src="/Thumbs/Asset.ashx?assetId={{ProductObj.iconimage_assetid}}&x=352&y=352" class="rounded">
|
||||
<div class="mt-2">
|
||||
<input type="file" class="form-control" name="icon-file" id="file" class="mt-2">
|
||||
<p class="text-secondary m-0 mt-1" style="font-size: 12px;">Must be a PNG, minimum 128x128 and maximum 1024x1024, less than 1mb</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7 pt-2 pb-2 ps-4 pe-4 ps-md-1 pe-md-1">
|
||||
<form method="post">
|
||||
<div class="form-floating mb-2">
|
||||
<input type="text" class="form-control" id="item-name" name="product-name" placeholder="Awesome Item!" required value="{{ProductObj.name}}">
|
||||
<label for="item-name">Name</label>
|
||||
</div>
|
||||
<div class="form-floating mb-2">
|
||||
<textarea class="form-control mt-2" placeholder="Description" style="min-height: 150px;" name="product-description">{{ProductObj.description}}</textarea>
|
||||
<label for="item-description">Description</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" {% if ProductObj.is_for_sale: %}checked{%endif%} id="isForSaleCheckbox" name="is-for-sale">
|
||||
<label class="form-check-label" for="isForSaleCheckbox">
|
||||
For Sale
|
||||
</label>
|
||||
</div>
|
||||
<div class="p-1 row">
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" id="basic-addon1">R$</span>
|
||||
<input type="number" class="form-control" placeholder="Robux" aria-label="Robux" name="robux-cost" value="{{ProductObj.robux_price}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<p style="font-size:13px;">* Vortexi will take a 30% fee from the sale</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100 text-center">Save Changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,29 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<h4 class="m-0">Gamepasses</h4>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-primary btn-sm" href="/develop/universes/{{UniverseObj.id}}/create-gamepass">Create Gamepass</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 mt-2">
|
||||
{% for gamepassobj in Gamepasses: %}
|
||||
<div class="bg-dark p-2 rounded w-100 mt-2">
|
||||
<div class="d-flex">
|
||||
<img src="/Thumbs/Asset.ashx?x=180&y=180&assetId={{gamepassobj.gamepass_id}}" width="100px" height="100px" class="border rounded">
|
||||
<div class="ms-2 h-100">
|
||||
<h5 class="m-0">{{gamepassobj.gamepass.name}}</h5>
|
||||
<p class="m-0 mt-1 text-secondary" style="font-size: 14px;">Gamepass ID: <span class="text-white">{{gamepassobj.gamepass_id}}</span></p>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">Total sold: <span class="text-white">{{gamepassobj.gamepass.sale_count}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-light btn-sm" href="/develop/universes/{{UniverseObj.id}}/gamepass/{{gamepassobj.gamepass_id}}">Edit Gamepass</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if len(Gamepasses) == 0: %}
|
||||
<p class="text-secondary text-center w-100 mt-5 mb-5">No gamepasses found, create one by clicking the button above</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Manage Universe{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/develop.css" rel="stylesheet"/>
|
||||
<style>
|
||||
.page-tabs a {
|
||||
color: #dadada !important;
|
||||
font-size: medium;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 0 1px 0 rgba(0,0,0,.2), 0 1px 2px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
.page-tabs a:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="container" style="max-width: 1000px;">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for category, message in messages %}
|
||||
{% if category == 'error': %}
|
||||
<div class="alert border p-2 text-center alert-danger border-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if category == 'success': %}
|
||||
<div class="alert border p-2 text-center alert-success border-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<h1 class="m-0">{{RootPlaceAssetObj.name}}</h1>
|
||||
<p class="text-secondary m-0 mb-1 ms-1" style="font-size: 12px;">Universe ID: <span class="text-white">{{UniverseObj.id}}</span> Starting Place ID: <span class="text-white">{{RootPlaceAssetObj.id}}</span></p>
|
||||
<div class="linebreak"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 border-end page-tabs">
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/manage" class="w-100">Basic Settings</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/access" class="w-100">Access</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/places" class="w-100">Places</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/gamepasses" class="w-100">Gamepasses</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/developer-products" class="w-100">Developer Products</a>
|
||||
<a href="/develop/universes/{{UniverseObj.id}}/badges" class="w-100">Badges</a>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
{% block subcontent %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<h4>Basic Settings</h4>
|
||||
<form method="post">
|
||||
<input type="text" name="name" class="form-control" placeholder="Name" value="{{RootPlaceAssetObj.name}}" required>
|
||||
<textarea name="description" class="form-control mt-2" placeholder="Description" style="min-height: 150px;" required>{{RootPlaceAssetObj.description}}</textarea>
|
||||
<p class="text-secondary m-0 mb-1" style="font-size: 13px;">Updating name and description will update the Starting Place name and description</p>
|
||||
<h5 class="mt-2">Place Year</h5>
|
||||
<select name="place_year" class="form-control">
|
||||
<!-- <option value="2014" {% if UniverseObj.place_year.value == 2014 %}selected{%endif%}>2014</option>-->
|
||||
<option value="2016" {% if UniverseObj.place_year.value == 2016 %}selected{%endif%}>2016</option>
|
||||
<option value="2018" {% if UniverseObj.place_year.value == 2018 %}selected{%endif%}>2018</option>
|
||||
<!-- <option value="2020" {% if UniverseObj.place_year.value == 2020 %}selected{%endif%}>2020</option>
|
||||
<option value="2021" {% if UniverseObj.place_year.value == 2021 %}selected{%endif%}>2021</option>-->
|
||||
</select>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="mt-2">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'develop/universes/manage-template.html' %}
|
||||
{% block subcontent %}
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<h4 class="m-0">Places</h4>
|
||||
<p class="text-secondary m-0" style="font-size: 13px;">{{TotalAmountPlacesCreated}} / 10 Places used</p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-light btn-sm" href="/develop/universes/{{UniverseObj.id}}/create-place">Create Place</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 mt-2">
|
||||
{% for placeObj in universePlaces: %}
|
||||
<div class="p-1 w-100 d-flex mb-1">
|
||||
<img src="/Thumbs/PlaceIcon.ashx?assetId={{placeObj.placeid}}&x=100&y=100" class="rounded" height="100px" width="100px">
|
||||
<div class="ms-2">
|
||||
<a href="/games/{{placeObj.placeid}}/--" class="text-decoration-none"><h5 class="m-0">{{placeObj.assetObj.name}}</h5></a>
|
||||
{% if UniverseObj.root_place_id == placeObj.placeid %}<p class="text-secondary m-0" style="font-size: 14px;"><i class="bi bi-star-fill text-warning"></i> Start Place</p>{% endif %}
|
||||
<p class="text-secondary m-0" style="font-size: 14px;">Place ID: <span class="text-white">{{placeObj.placeid}}</span></p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-outline-light btn-sm" href="/develop/universes/{{UniverseObj.id}}/place/{{placeObj.placeid}}/manage">Manage Place</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Leaving Vortexi{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(200,200,200) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="settings-container p-3 d-flex" style="background-color: rgb(26, 26, 26);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);max-width: 600px;">
|
||||
<div class="w-100 flex-grow-1">
|
||||
<h1 class="m-0 mb-1">You are leaving Vortexi</h1>
|
||||
<p class="text-secondary m-0">
|
||||
Vortexi Forums is real asf cuh.
|
||||
</p>
|
||||
<a href="{{baseurl}}" class="btn btn-sm btn-light w-100 text-decoration-none mt-2">Take me there</a>
|
||||
<p class="text-secondary m-0 mt-2" style="font-size: 12px;">You will be redirected to <span class="text-white" style="background-color: rgb(44, 44, 44);padding: 2px;">{{baseurl}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}SSO Login{% endblock %}
|
||||
{% block head %}
|
||||
<link href="/static/css/settings.css" rel="stylesheet"/> <!-- Its just easier to resuse this-->
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(200,200,200) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="main">
|
||||
<div class="settings-container p-3 d-flex" style="background-color: rgb(26, 26, 26);box-shadow: 0px 0px 10px 0px rgb(0,0,0,0.5);max-width: 600px;">
|
||||
<div>
|
||||
<img src="/Thumbs/Head.ashx?x=100&y=100&userId={{currentuser.id}}" style="aspect-ratio: 1/1;" class="rounded border" width="100px">
|
||||
<p class="m-0 text-secondary text-center" style="font-size: 12px;">Logged in as <b class="text-white">{{currentuser.username}}</b></p>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<h1 class="m-0 mb-1">Authorise SYNTAX Forums</h1>
|
||||
<p class="text-secondary m-0">
|
||||
SYNTAX Forums is a official website hosted by vortexi.cc, authorising will allow SYNTAX Forums to access the following information:
|
||||
</p>
|
||||
<ul class="text-secondary m-0">
|
||||
<li>Username</li>
|
||||
<li>User ID</li>
|
||||
</ul>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<button type="submit" class="btn btn-primary w-100 text-center btn-sm mt-2">Authorise Login</button>
|
||||
</form>
|
||||
<p class="text-secondary m-0 mt-2" style="font-size: 12px;">You will be redirected to <span class="text-white" style="background-color: rgb(44, 44, 44);padding: 2px;">{{baseurl}}/session/sso_login</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Downloads{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container" style="margin-top:128px;min-height: 100vh;max-width: 1000px;">
|
||||
<div class="d-flex mb-2 align-items-center">
|
||||
<h1>Downloads</h1>
|
||||
<div class="ms-auto">
|
||||
<a class="btn btn-outline-light fw-bold" href="https://github.com/PrintedScript/syntax-bootstrapper" target="_blank"><i class="bi bi-github"></i> Github Repo</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border d-flex rounded">
|
||||
<div>
|
||||
<h4 class="mb-0"><i class="bi bi-windows"></i> Windows Client Bootstrapper</h4>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">MD5: <span class="text-white">790df486f2721208e06bd05dbb9d95f8</span></p>
|
||||
<div class="d-flex mt-1">
|
||||
<a href="https://www.virustotal.com/gui/file/386cdfecd4f4c80cedcd9a9176b6938eddf4883545f11826ead6bfce0339d667" target="_blank" class="text-decoration-none m-0" style="font-size: 11px;">Bootstrapper VirusTotal</a>
|
||||
<p class="text-secondary m-0 ms-1" style="font-size: 11px;">
|
||||
| Windows 10 or newer required
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="https://setup.vortexi.cc/VortexiPlayerLauncher.exe" target="_blank" class="text-decoration-none m-0 btn btn-outline-primary btn fw-bold"><i class="bi bi-download me-2"></i>Download</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border d-flex mt-3 rounded">
|
||||
<div>
|
||||
<h4 class="mb-0 text-white"><i class="bi bi-hammer"></i></i> Studio (2016)</h4>
|
||||
<p class="m-0 text-secondary" style="font-size: 14px;">MD5: <span class="text-white">3d01030e5bb37f7ab0b6ad05c3e7e5ca</span></p>
|
||||
<div class="d-flex mt-1">
|
||||
<p class="text-secondary m-0" style="font-size: 11px;">
|
||||
Note: You currently can not publish / update your games through the studio.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="https://setup.vortexi.cc/Studio16.zip" target="_blank" class="text-decoration-none m-0 btn btn-outline-primary btn fw-bold"><i class="bi bi-download me-2"></i>Download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Drivers{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container" style="margin-top:128px;min-height: 100vh;max-width: 1000px;">
|
||||
<div class="d-flex mb-2 align-items-center">
|
||||
<h1>Drivers</h1>
|
||||
</div>
|
||||
<p class="text-secondary">If you have issues with Vortexi not starting because of graphics issues you should seriously consider buying a new computer or graphics card</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,298 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, make_response, abort, flash
|
||||
from app.util import auth, placeinfo
|
||||
from app.models.asset import Asset
|
||||
from app.models.placeservers import PlaceServer
|
||||
from app.models.placeserver_players import PlaceServerPlayer
|
||||
from app.models.gameservers import GameServer
|
||||
from app.models.place import Place
|
||||
from app.models.gamepass_link import GamepassLink
|
||||
from app.models.userassets import UserAsset
|
||||
from app.models.groups import Group, GroupRole, GroupRolePermission
|
||||
from app.models.place_badge import PlaceBadge, UserBadge
|
||||
from app.models.universe import Universe
|
||||
from slugify import slugify
|
||||
from app.pages.develop.develop import ShutdownServer
|
||||
from app.enums.AssetType import AssetType
|
||||
from app.models.user import User
|
||||
from app.util.membership import GetUserMembership
|
||||
from app.enums.MembershipType import MembershipType
|
||||
from app.routes.rate import GetAssetLikesAndDislikes, GetUserVoteStatus, GetAssetVotePercentage, GetAssetFavoriteCount, GetUserFavoriteStatus
|
||||
from app.routes.badgesapi import CalculateBadgeRarity, GetBadgeAwardedPastDay
|
||||
from app.services import groups
|
||||
from sqlalchemy import and_, func
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
GamePagesRoute = Blueprint("games", __name__, template_folder="pages")
|
||||
|
||||
def DoesUserownGamepass( gamepass_id, userid ):
|
||||
GamepassObj : Asset = Asset.query.filter_by(id=gamepass_id, asset_type = AssetType.GamePass).first()
|
||||
if GamepassObj is None:
|
||||
return False
|
||||
UserGamepass : UserAsset = UserAsset.query.filter_by(userid=userid, assetid=GamepassObj.id).first()
|
||||
if UserGamepass is None:
|
||||
return False
|
||||
return True
|
||||
def DoesUserOwnBadge( badge_id, userid ):
|
||||
BadgeObj : PlaceBadge = PlaceBadge.query.filter_by(id=badge_id).first()
|
||||
if BadgeObj is None:
|
||||
return False
|
||||
UserBadgeObj : UserBadge = UserBadge.query.filter_by(badge_id=badge_id, user_id=userid).first()
|
||||
if UserBadgeObj is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def GetTotalBadgeAwardedCount( badge_id ) -> int:
|
||||
return UserBadge.query.filter_by( badge_id = badge_id ).count()
|
||||
|
||||
@GamePagesRoute.route("/games/<int:placeid>/", methods=["GET"])
|
||||
@GamePagesRoute.route("/games/<int:placeid>/<string:slug>", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def game_page(placeid : int, slug=None):
|
||||
AssetObject : Asset = Asset.query.filter_by(id=placeid).first()
|
||||
if AssetObject is None:
|
||||
return abort(404)
|
||||
if AssetObject.asset_type != AssetType.Place:
|
||||
# Let catalog handle the redirect
|
||||
return redirect(f"/catalog/{str(placeid)}/")
|
||||
SlugName = slugify(AssetObject.name, lowercase=False)
|
||||
if SlugName is None or SlugName == "":
|
||||
SlugName = "unnamed"
|
||||
if slug is None:
|
||||
return redirect(f"/games/{str(placeid)}/{SlugName}")
|
||||
if slug != SlugName:
|
||||
return redirect(f"/games/{str(placeid)}/{SlugName}")
|
||||
if AssetObject.creator_type == 0:
|
||||
CreatorObject : User = User.query.filter_by(id=AssetObject.creator_id).first()
|
||||
else:
|
||||
CreatorObject : Group = Group.query.filter_by(id=AssetObject.creator_id).first()
|
||||
PlaceObj : Place = Place.query.filter_by(placeid=placeid).first()
|
||||
if PlaceObj is None:
|
||||
return abort(404)
|
||||
UniverseObj : Universe = Universe.query.filter_by(id=PlaceObj.parent_universe_id).first()
|
||||
ActualRootPlace : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
|
||||
|
||||
try:
|
||||
SplittedDescription = AssetObject.description.split("\n")
|
||||
LikeCount, DislikeCount = GetAssetLikesAndDislikes(AssetObject.id)
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
|
||||
PlaceServerObjs : list[PlaceServer] = PlaceServer.query.filter_by(serverPlaceId=placeid).all()
|
||||
ActiveServers = []
|
||||
for PlaceServerObj in PlaceServerObjs:
|
||||
PlayersInServer : list[PlaceServerPlayer] = PlaceServerPlayer.query.filter_by(serveruuid=PlaceServerObj.serveruuid).all()
|
||||
GameServerObj : GameServer = GameServer.query.filter_by(serverId=PlaceServerObj.originServerId).first()
|
||||
if GameServerObj is None:
|
||||
continue
|
||||
ActiveServers.append({
|
||||
"id": PlaceServerObj.serveruuid,
|
||||
"playercount": PlaceServerObj.playerCount,
|
||||
"maxplayercount": PlaceServerObj.maxPlayerCount,
|
||||
"players": PlayersInServer,
|
||||
"host": GameServerObj.serverName,
|
||||
"is_reserved_server": PlaceServerObj.reservedServerAccessCode is not None
|
||||
})
|
||||
|
||||
Gamepasses : list[GamepassLink] = GamepassLink.query.filter_by(universe_id = PlaceObj.parent_universe_id).all()
|
||||
UserDoesNotHaveBC : bool = UniverseObj.bc_required and GetUserMembership(AuthenticatedUser) == MembershipType.NonBuildersClub
|
||||
CanShutdownServer = False if AuthenticatedUser.id not in (1, 49) else True
|
||||
if AssetObject.creator_type == 0:
|
||||
if AuthenticatedUser.id == AssetObject.creator_id:
|
||||
CanShutdownServer = True
|
||||
else:
|
||||
GroupObj : Group = groups.GetGroupFromId( AssetObject.creator_id )
|
||||
UserGroupRole : GroupRole = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupObj )
|
||||
if UserGroupRole is None:
|
||||
CanShutdownServer = False
|
||||
UserGroupRolePermission : GroupRolePermission = groups.GetRolesetPermission( UserGroupRole )
|
||||
if UserGroupRolePermission.manage_group_games:
|
||||
CanShutdownServer = True
|
||||
|
||||
GameBadges : list[PlaceBadge] = PlaceBadge.query.filter_by( universe_id = PlaceObj.parent_universe_id ).all()
|
||||
|
||||
return render_template("games/view.html", PlaceAssetObj=AssetObject,
|
||||
CreatorObj=CreatorObject,
|
||||
PlaceObj=PlaceObj,
|
||||
SplittedDescription=SplittedDescription,
|
||||
PlayerCount=placeinfo.GetPlayingCount(PlaceObj),
|
||||
LikeCount=LikeCount,
|
||||
DislikeCount=DislikeCount,
|
||||
UserVoteStatus=GetUserVoteStatus(AssetObject.id, AuthenticatedUser.id),
|
||||
UserFavoriteStatus=GetUserFavoriteStatus(AssetObject.id, AuthenticatedUser.id),
|
||||
FavoriteCount=GetAssetFavoriteCount(AssetObject.id),
|
||||
ActiveServers=ActiveServers,
|
||||
ActiveServerCount=len(ActiveServers),
|
||||
Gamepasses=Gamepasses,
|
||||
DoesUserownGamepass=DoesUserownGamepass,
|
||||
UserDoesNotHaveBC=UserDoesNotHaveBC,
|
||||
IsTooYoung = (datetime.utcnow() - AuthenticatedUser.created).days < UniverseObj.minimum_account_age,
|
||||
MinAccountAge = UniverseObj.minimum_account_age,
|
||||
CanShutdownServer = CanShutdownServer,
|
||||
GameBadges = GameBadges,
|
||||
CalculateBadgeRarity = CalculateBadgeRarity,
|
||||
DoesUserOwnBadge = DoesUserOwnBadge,
|
||||
GetTotalBadgeAwardedCount = GetTotalBadgeAwardedCount,
|
||||
GetBadgeAwardedPastDay = GetBadgeAwardedPastDay,
|
||||
UniverseObj = UniverseObj,
|
||||
UniverseRootPlace = ActualRootPlace
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error during rendering game page [{placeid}]: {str(e)}")
|
||||
return redirect(f"/games/{str(placeid)}/{SlugName}")
|
||||
|
||||
@GamePagesRoute.route("/games/<int:placeid>/shutdown-server/<jobid>", methods=["POST"])
|
||||
@auth.authenticated_required
|
||||
def game_shutdown_server(placeid : int, jobid : str):
|
||||
AuthenticatedUser : User = auth.GetCurrentUser()
|
||||
AssetObject : Asset = Asset.query.filter_by(id=placeid).first()
|
||||
if AssetObject is None:
|
||||
return abort(404)
|
||||
if AssetObject.asset_type != AssetType.Place:
|
||||
return abort(404)
|
||||
TargetPlaceServer : PlaceServer = PlaceServer.query.filter_by(serveruuid=jobid).first()
|
||||
if TargetPlaceServer is None:
|
||||
return abort(404)
|
||||
if TargetPlaceServer.serverPlaceId != placeid:
|
||||
return abort(403)
|
||||
if (AssetObject.creator_id != AuthenticatedUser.id and AssetObject.creator_type != 1) and AuthenticatedUser.id != 1:
|
||||
return abort(403)
|
||||
if AssetObject.creator_type == 1:
|
||||
GroupObj : Group = groups.GetGroupFromId( AssetObject.creator_id )
|
||||
UserGroupRole : GroupRole = groups.GetUserRolesetInGroup( AuthenticatedUser, GroupObj )
|
||||
if UserGroupRole is None:
|
||||
return abort(403)
|
||||
UserGroupRolePermission : GroupRolePermission = groups.GetRolesetPermission( UserGroupRole )
|
||||
if not UserGroupRolePermission.manage_group_games:
|
||||
return abort(403)
|
||||
ShutdownServer(TargetPlaceServer.serveruuid)
|
||||
flash("Requested server shutdown", "success")
|
||||
return redirect(f"/games/{str(placeid)}/")
|
||||
|
||||
@GamePagesRoute.route("/games", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def games_page():
|
||||
TopPlacesInfo = []
|
||||
PageNumber = max( request.args.get('page', default = 1, type = int ), 1 )
|
||||
SearchQuery = request.args.get('q', default = None, type = str)
|
||||
GameLookupResultsBackendResult = None
|
||||
GameLookupResults = []
|
||||
if SearchQuery:
|
||||
SearchQuery = SearchQuery.strip().replace('%', '')
|
||||
if len(SearchQuery) > 0 and len(SearchQuery) <= 32:
|
||||
GameLookupResultsBackendResult = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join( Asset, Asset.id == Place.placeid ).filter( Asset.name.ilike(f"%{SearchQuery}%") ).filter_by( moderation_status = 0 ).order_by( Universe.visit_count.desc() ).paginate(page=PageNumber, per_page=36, error_out=False)
|
||||
else:
|
||||
flash('Invalid search query.', 'danger')
|
||||
|
||||
if GameLookupResultsBackendResult:
|
||||
for UniverseObj in GameLookupResultsBackendResult:
|
||||
UniverseObj : Universe
|
||||
AssetObj : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
|
||||
if AssetObj is None:
|
||||
continue
|
||||
|
||||
PlaceObjData = {
|
||||
"id": UniverseObj.root_place_id,
|
||||
"name": AssetObj.name,
|
||||
"playingcount": placeinfo.GetUniversePlayingCount(UniverseObj),
|
||||
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
|
||||
"likePercentage": GetAssetVotePercentage(AssetObj.id),
|
||||
"placeyear": UniverseObj.place_year
|
||||
}
|
||||
GameLookupResults.append(PlaceObjData)
|
||||
|
||||
PopularGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join( Asset, Asset.id == Place.placeid ).filter_by( moderation_status = 0 ).outerjoin( PlaceServer, PlaceServer.serverPlaceId == Place.placeid ).group_by( Universe.id )
|
||||
PopularGames = PopularGames.order_by( func.coalesce( func.sum( PlaceServer.playerCount ), 0 ).desc() ).order_by( Universe.visit_count.desc() )
|
||||
|
||||
UniverseObjsList = PopularGames.limit(24).all()
|
||||
for UniverseObj in UniverseObjsList:
|
||||
UniverseObj : Universe
|
||||
AssetObj : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
|
||||
if AssetObj is None:
|
||||
continue
|
||||
PlaceObjData = {
|
||||
"id": UniverseObj.root_place_id,
|
||||
"name": AssetObj.name,
|
||||
"playingcount": placeinfo.GetUniversePlayingCount( UniverseObj ),
|
||||
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
|
||||
"likePercentage": GetAssetVotePercentage(UniverseObj.root_place_id),
|
||||
"placeyear": UniverseObj.place_year
|
||||
}
|
||||
TopPlacesInfo.append(PlaceObjData)
|
||||
|
||||
RecentlyUpdatedUniverses : list[Universe] = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).join( Asset, Asset.id == Universe.root_place_id ).filter_by( moderation_status = 0 ).order_by(Universe.updated_at.desc()).limit(24).all()
|
||||
RecentlyUpdatedUniversesInfo = []
|
||||
for UniverseObj in RecentlyUpdatedUniverses:
|
||||
PlaceObj : Place = Place.query.filter_by(placeid=UniverseObj.root_place_id).first()
|
||||
if PlaceObj is None:
|
||||
continue
|
||||
AssetObj : Asset = Asset.query.filter_by(id=PlaceObj.placeid).first()
|
||||
if len(RecentlyUpdatedUniversesInfo) >= 24:
|
||||
break
|
||||
PlaceObjData = {
|
||||
"id": AssetObj.id,
|
||||
"name": AssetObj.name,
|
||||
"playingcount": placeinfo.GetUniversePlayingCount( UniverseObj ),
|
||||
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
|
||||
"likePercentage": GetAssetVotePercentage(AssetObj.id),
|
||||
"placeyear": UniverseObj.place_year
|
||||
}
|
||||
RecentlyUpdatedUniversesInfo.append(PlaceObjData)
|
||||
|
||||
FeaturedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0, Universe.is_featured == True ) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join( Asset, Asset.id == Place.placeid ).filter_by( moderation_status = 0 ).outerjoin( PlaceServer, PlaceServer.serverPlaceId == Place.placeid ).group_by( Universe.id )
|
||||
FeaturedGames = FeaturedGames.order_by( func.coalesce( func.sum( PlaceServer.playerCount ), 0 ).desc() ).order_by( Universe.visit_count.desc() )
|
||||
FeaturedPlaceObjs = FeaturedGames.limit(24).all()
|
||||
FeaturedPlacesInfo = []
|
||||
for UniverseObj in FeaturedPlaceObjs:
|
||||
UniverseObj : Universe
|
||||
AssetObj : Asset = Asset.query.filter_by(id=UniverseObj.root_place_id).first()
|
||||
if AssetObj is None:
|
||||
continue
|
||||
PlaceObjData = {
|
||||
"id": UniverseObj.root_place_id,
|
||||
"name": AssetObj.name,
|
||||
"playingcount": placeinfo.GetUniversePlayingCount( UniverseObj ),
|
||||
"slug": slugify(AssetObj.name, lowercase=False) if AssetObj.name is not None else "",
|
||||
"likePercentage": GetAssetVotePercentage(AssetObj.id),
|
||||
"placeyear": UniverseObj.place_year
|
||||
}
|
||||
FeaturedPlacesInfo.append(PlaceObjData)
|
||||
|
||||
return render_template("games/index.html", PopularPlaces=TopPlacesInfo, RecentlyUpdatedPlaces=RecentlyUpdatedUniversesInfo, FeaturedPlacesInfo=FeaturedPlacesInfo, GameLookupResults = GameLookupResults, PageNumber=PageNumber, SearchQuery = SearchQuery, GameLookupResultsBackendResult = GameLookupResultsBackendResult)
|
||||
|
||||
from sqlalchemy import func
|
||||
@GamePagesRoute.route("/games/<genre>/view", methods=["GET"])
|
||||
@auth.authenticated_required
|
||||
def games_genre_page(genre : str):
|
||||
pageNumber = max( request.args.get("page", default=1, type=int), 1 )
|
||||
if genre == "popular":
|
||||
PopularGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join(Asset, Asset.id == Place.placeid).outerjoin(PlaceServer, PlaceServer.serverPlaceId == Place.placeid).group_by(Universe.id)
|
||||
PopularGames = PopularGames.filter( Asset.moderation_status == 0).order_by(func.coalesce(func.sum(PlaceServer.playerCount), 0).desc()).order_by( Universe.visit_count.desc() )
|
||||
PopularGames = PopularGames.paginate(page=pageNumber, per_page=30, error_out=False)
|
||||
ViewerFacingGenre = "Popular Games"
|
||||
pageResults = PopularGames
|
||||
elif genre == "updated":
|
||||
UpdatedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0) ).join( Asset, Asset.id == Universe.root_place_id ).filter( Asset.moderation_status == 0 ).order_by(Universe.updated_at.desc())
|
||||
UpdatedGames = UpdatedGames.paginate(page=pageNumber, per_page=30, error_out=False)
|
||||
ViewerFacingGenre = "Recently Updated"
|
||||
pageResults = UpdatedGames
|
||||
elif genre == "featured":
|
||||
FeaturedGames = Universe.query.filter( and_( Universe.is_public == True, Universe.moderation_status == 0, Universe.is_featured == True ) ).outerjoin( Place, Place.parent_universe_id == Universe.id ).join(Asset, Asset.id == Place.placeid).outerjoin(PlaceServer, PlaceServer.serverPlaceId == Place.placeid).group_by(Universe.id)
|
||||
FeaturedGames = FeaturedGames.filter( Asset.moderation_status == 0).order_by(func.coalesce(func.sum(PlaceServer.playerCount), 0).desc()).order_by( Universe.visit_count.desc() )
|
||||
FeaturedGames = FeaturedGames.paginate(page=pageNumber, per_page=30, error_out=False)
|
||||
ViewerFacingGenre = "Featured Games"
|
||||
pageResults = FeaturedGames
|
||||
else:
|
||||
return abort(404)
|
||||
|
||||
if pageResults is None:
|
||||
return abort(404)
|
||||
|
||||
def getRootPlace( UniverseObj : Universe ) -> Place :
|
||||
try:
|
||||
return UniverseObj.root_place_obj
|
||||
except:
|
||||
UniverseObj.root_place_obj = Place.query.filter_by(placeid=UniverseObj.root_place_id).first()
|
||||
return UniverseObj.root_place_obj
|
||||
|
||||
return render_template("games/genre.html", Games=pageResults, Genre=genre, getPlayerCount = placeinfo.GetUniversePlayingCount, getAssetVotePercentage = GetAssetVotePercentage, ViewerFacingGenre=ViewerFacingGenre, getRootPlace=getRootPlace)
|
||||
@@ -0,0 +1,47 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Games{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.wide-place-card {
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 8px 0px rgb(0 0 0 / 50%);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.wide-place-card:hover {
|
||||
transform: scale(1.05);
|
||||
transition: 0.2s;
|
||||
z-index: 999;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="w-100 ps-4 pe-4 ms-auto me-auto" style="margin-top:100px;min-height: 100vh;max-width: 1950px;">
|
||||
<h1>{{ViewerFacingGenre}}</h1>
|
||||
<div class="p-2 w-100 row">
|
||||
{% for universe in Games: %}
|
||||
<a class="text-decoration-none p-1 col-xxl-2 col-lg-3 col-md-4 col-sm-6" href="/games/{{universe.root_place_id}}/">
|
||||
<div class="overflow-hidden wide-place-card w-100 position-relative">
|
||||
<img width="100%" style="aspect-ratio: 16/9;" src="/Thumbs/Asset.ashx?assetId={{universe.root_place_id}}&x=352&y=352" alt="{{getRootPlace(universe).assetObj.name}}">
|
||||
<div class="position-absolute" style="bottom: 0;padding: 5px;width: 100%;background: linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0));">
|
||||
<h5 style="font-size: 17px;font-weight: 700;color:rgb(230,230,230);" class="text-truncate m-0">{{getRootPlace(universe).assetObj.name}}</h5>
|
||||
<div class="d-flex">
|
||||
<p class="text-secondary m-0 ms-1 me-auto" style="font-size: 12px;"><span class="bi bi-person"></span> <span class="text-secondary">{{getPlayerCount(universe)}}</span> Playing</p>
|
||||
<p class="text-secondary m-0 ms-auto me-auto" style="font-size: 12px;"><span class="bi bi-eye"></span> <span class="text-secondary">{{universe.visit_count}}</span> Visits</p>
|
||||
<p class="text-secondary m-0 me-1 ms-auto" style="font-size: 12px;"><span class="bi bi-hand-thumbs-up"></span> <span class="text-secondary">{{getAssetVotePercentage(universe.root_place_id)}}%</span> Rating</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{%endfor%}
|
||||
</div>
|
||||
<div class="mt-2 mb-2 w-100 d-flex">
|
||||
<a class="m-0 text-decoration-none ms-auto {% if not Games.has_prev %}text-secondary{%endif%}" {% if Games.has_prev %}href="/games/{{Genre}}/view?page={{Games.prev_num}}"{%endif%}>Previous</a>
|
||||
<p class="m-0 ms-2 me-2 text-white">Page {{Games.page}} of {{Games.pages}}</p>
|
||||
<a class="m-0 text-decoration-none me-auto {% if not Games.has_next %}text-secondary{%endif%}" {% if Games.has_next %}href="/games/{{Genre}}/view?page={{Games.next_num}}"{%endif%}>Next</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,203 @@
|
||||
{% extends '__layout__.html' %}
|
||||
{% block title %}Games{% endblock %}
|
||||
{% block head %}
|
||||
<style>
|
||||
.text-secondary {
|
||||
color: rgb(199, 199, 199) !important;
|
||||
}
|
||||
.place-card {
|
||||
height: 220px !important;
|
||||
aspect-ratio: 0.640909/1;
|
||||
}
|
||||
.place-card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 0px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="w-100 ps-4 pe-4 ms-auto me-auto" style="margin-top:100px;min-height: 100vh;max-width: 1950px;">
|
||||
{% if GameLookupResults %}
|
||||
<form method="get" class="ms-md-2 me-md-2" style="min-width: 250px;max-width: 500px;">
|
||||
<div class="input-group">
|
||||
<div class="form-floating" style="width: 52%;">
|
||||
<input type="text" class="form-control" id="form-control" name="q" value="{% if SearchQuery != None %}{{SearchQuery}}{% endif %}" placeholder="Awesome Game">
|
||||
<label for="search-input">Search</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="p-2 w-100 place-card-container">
|
||||
{% for place in GameLookupResults: %}
|
||||
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
|
||||
<div class="overflow-hidden place-card w-100">
|
||||
<div class="position-relative">
|
||||
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
|
||||
<div class="position-absolute" style="bottom: 0px;left: 0px;">
|
||||
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
|
||||
</div>
|
||||
<div class="w-100 votePercentageBackground">
|
||||
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
|
||||
<div>
|
||||
<div class="segment" style="left: 18%;"></div>
|
||||
<div class="segment" style="left: 38%;"></div>
|
||||
<div class="segment" style="left: 58%;"></div>
|
||||
<div class="segment" style="left: 78%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{%endfor%}
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
{% if GameLookupResultsBackendResult.has_prev: %}
|
||||
<a href="/games?q={{SearchQuery}}&page={{GameLookupResultsBackendResult.prev_num}}" class="ms-auto text-decoration-none text-primary">Previous</a>
|
||||
{%else%}
|
||||
<a class="ms-auto text-decoration-none text-secondary">Previous</a>
|
||||
{% endif %}
|
||||
<p class="ms-2 me-2 text-white">Page {{GameLookupResultsBackendResult.page}} of {{GameLookupResultsBackendResult.pages}}</p>
|
||||
{% if GameLookupResultsBackendResult.has_next: %}
|
||||
<a href="/games?q={{SearchQuery}}&page={{GameLookupResultsBackendResult.next_num}}" class="me-auto text-decoration-none text-primary">Next</a>
|
||||
{%else%}
|
||||
<a class="me-auto text-decoration-none text-secondary">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif not GameLookupResults and SearchQuery %}
|
||||
<form method="get" class="ms-md-2 me-md-2" style="min-width: 250px;max-width: 500px;">
|
||||
<div class="input-group">
|
||||
<div class="form-floating" style="width: 52%;">
|
||||
<input type="text" class="form-control" id="form-control" name="q" value="{% if SearchQuery != None %}{{SearchQuery}}{% endif %}" placeholder="Awesome Game">
|
||||
<label for="search-input">Search</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="w-100 p-2">
|
||||
<p class="text-white m-auto">No results found for "{{SearchQuery}}"</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-md-flex align-items-center w-100">
|
||||
<div class="d-flex align-items-center">
|
||||
<h1>Popular</h1>
|
||||
<a class="ms-3 text-decoration-none" href="/games/popular/view">View All</a>
|
||||
</div>
|
||||
<form method="get" class="ms-md-auto me-md-4" style="min-width: 300px;">
|
||||
<div class="input-group">
|
||||
<div class="form-floating" style="width: 52%;">
|
||||
<input type="text" class="form-control" id="form-control" name="q" value="{% if SearchQuery != None %}{{SearchQuery}}{% endif %}" placeholder="Awesome Game">
|
||||
<label for="search-input">Search</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-outline-primary fw-bold" style="min-width: 50px;"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="p-2 w-100 place-card-container">
|
||||
{% for place in PopularPlaces: %}
|
||||
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
|
||||
<div class="overflow-hidden place-card w-100">
|
||||
<div class="position-relative">
|
||||
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
|
||||
<div class="position-absolute" style="bottom: 0px;left: 0px;">
|
||||
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
|
||||
</div>
|
||||
<div class="w-100 votePercentageBackground">
|
||||
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
|
||||
<div>
|
||||
<div class="segment" style="left: 18%;"></div>
|
||||
<div class="segment" style="left: 38%;"></div>
|
||||
<div class="segment" style="left: 58%;"></div>
|
||||
<div class="segment" style="left: 78%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{%endfor%}
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<h1>Featured</h1>
|
||||
<a class="ms-3 text-decoration-none" href="/games/featured/view">View All</a>
|
||||
</div>
|
||||
<div class="p-2 w-100 place-card-container">
|
||||
{% for place in FeaturedPlacesInfo: %}
|
||||
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
|
||||
<div class="overflow-hidden place-card w-100">
|
||||
<div class="position-relative">
|
||||
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
|
||||
<div class="position-absolute" style="bottom: 0px;left: 0px;">
|
||||
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
|
||||
</div>
|
||||
<div class="w-100 votePercentageBackground">
|
||||
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
|
||||
<div>
|
||||
<div class="segment" style="left: 18%;"></div>
|
||||
<div class="segment" style="left: 38%;"></div>
|
||||
<div class="segment" style="left: 58%;"></div>
|
||||
<div class="segment" style="left: 78%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{%endfor%}
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<h1>Recently Updated</h1>
|
||||
<a class="ms-3 text-decoration-none" href="/games/updated/view">View All</a>
|
||||
</div>
|
||||
<div class="p-2 w-100 place-card-container">
|
||||
{% for place in RecentlyUpdatedPlaces: %}
|
||||
<a class="text-decoration-none p-1" href="/games/{{place.id}}/{{place.slug}}">
|
||||
<div class="overflow-hidden place-card w-100">
|
||||
<div class="position-relative">
|
||||
<img width="100%" style="aspect-ratio: 1/1;" src="/Thumbs/PlaceIcon.ashx?assetId={{place.id}}&x=150&y=150" alt="{{place.name}}">
|
||||
<div class="position-absolute" style="bottom: 0px;left: 0px;">
|
||||
<div class="fw-bold bg-dark text-white" style="font-size: 12px;padding: 3px;border-top-right-radius: 4px;">{{place.placeyear.value}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<h5 class="mb-0" style="font-size: 17px;text-overflow: ellipsis;">{{place.name}}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<p style="margin:0;font-size: 14px;" class="text-secondary"><i class="bi bi-people"></i> {{place.playingcount}} <span style="font-size: 13px;">Playing</span></p>
|
||||
</div>
|
||||
<div class="w-100 votePercentageBackground">
|
||||
<div class="votePercentageFill" style="width: {{place.likePercentage}}%;"></div>
|
||||
<div>
|
||||
<div class="segment" style="left: 18%;"></div>
|
||||
<div class="segment" style="left: 38%;"></div>
|
||||
<div class="segment" style="left: 58%;"></div>
|
||||
<div class="segment" style="left: 78%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{%endfor%}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user