add web
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user