Asset changes and bugfixes.
- Squashed a bug that prevented sales from being displayed on the transaction overview page. - Split up auto uploader and xml uploader on admin panel. - Manual asset uploader on admin panel. - General package AssetType support. - Manual BodyPart upload support. - Manual Face upload support. - Trusted, user creatable, and admin creatable options added to asset types. - Vertical tabbed navs. - Moved all routes for "shop.asset" to a method in the asset model itself. - Added a way to view a list of the most recent assets uploaded by admins. - Added Face render support. - Indev outfits page.
This commit is contained in:
parent
8b3f940fc6
commit
426778c340
|
|
@ -15,6 +15,7 @@ use App\Helpers\CdnHelper;
|
|||
use App\Models\Asset;
|
||||
use App\Models\AssetVersion;
|
||||
use App\Models\RobloxAsset;
|
||||
use App\Models\UserAsset;
|
||||
|
||||
class AssetHelper
|
||||
{
|
||||
|
|
@ -24,23 +25,33 @@ class AssetHelper
|
|||
'.ROBLOXSECURITY' => env('app.robloxcookie')
|
||||
], '.roblox.com');
|
||||
|
||||
return Http::withOptions(['cookies' => $cookieJar, 'headers' => ['User-Agent' => 'Roblox/WinInet']]);
|
||||
return Http::withOptions(['cookies' => $cookieJar, 'headers' => ['User-Agent' => 'Roblox/WinInet', 'Requester' => 'Server']]);
|
||||
}
|
||||
|
||||
public static function uploadRobloxAsset($id, $uploadToHolder = false)
|
||||
public static function newAsset($props, $hash)
|
||||
{
|
||||
{
|
||||
$uploadedAsset = RobloxAsset::where('robloxAssetId', $id)->first();
|
||||
if($uploadedAsset)
|
||||
return $uploadedAsset->asset;
|
||||
}
|
||||
$asset = Asset::create($props);
|
||||
$assetVersion = AssetVersion::create([
|
||||
'parentAsset' => $asset->id,
|
||||
'localVersion' => 1,
|
||||
'contentURL' => $hash
|
||||
]);
|
||||
$asset->assetVersionId = $assetVersion->id;
|
||||
$asset->save();
|
||||
|
||||
$marketplaceResult = self::cookiedRequest()->get('https://api.roblox.com/marketplace/productinfo?assetId=' . $id);
|
||||
$assetResult = self::cookiedRequest()->get('https://assetdelivery.roblox.com/v2/asset?id=' . $id);
|
||||
UserAsset::createSerialed($asset->creatorId, $asset->id);
|
||||
|
||||
if(!$marketplaceResult->ok() || !$assetResult->ok())
|
||||
return $asset;
|
||||
}
|
||||
|
||||
private static function getRBXMarketplaceInfo($assetId)
|
||||
{
|
||||
$marketplaceResult = self::cookiedRequest()->get('https://api.roblox.com/marketplace/productinfo?assetId=' . $assetId);
|
||||
|
||||
if(!$marketplaceResult->ok())
|
||||
return false;
|
||||
|
||||
$marketplaceResult = $marketplaceResult->json();
|
||||
$assetTypeId = $marketplaceResult['AssetTypeId'];
|
||||
if(
|
||||
$assetTypeId == 41 || // Hair Accessory
|
||||
|
|
@ -54,25 +65,37 @@ class AssetHelper
|
|||
$assetTypeId = 8;
|
||||
}
|
||||
|
||||
$marketplaceResult['AssetTypeId'] = $assetTypeId;
|
||||
|
||||
return $marketplaceResult;
|
||||
}
|
||||
|
||||
public static function uploadRobloxAsset($id, $uploadToHolder = false)
|
||||
{
|
||||
{
|
||||
$uploadedAsset = RobloxAsset::where('robloxAssetId', $id)->first();
|
||||
if($uploadedAsset)
|
||||
return $uploadedAsset->asset;
|
||||
}
|
||||
|
||||
$marketplaceResult = self::getRBXMarketplaceInfo($id);
|
||||
$assetResult = self::cookiedRequest()->get('https://assetdelivery.roblox.com/v2/asset?id=' . $id);
|
||||
|
||||
if(!$marketplaceResult || !$assetResult->ok())
|
||||
return false;
|
||||
|
||||
$assetContent = Http::get($assetResult['locations'][0]['location']);
|
||||
$hash = CdnHelper::SaveContent($assetContent->body(), $assetContent->header('Content-Type'));
|
||||
$asset = Asset::create([
|
||||
$asset = self::newAsset([
|
||||
'creatorId' => ($uploadToHolder ? 1 : Auth::user()->id),
|
||||
'name' => $marketplaceResult['Name'],
|
||||
'description' => $marketplaceResult['Description'],
|
||||
'approved' => true,
|
||||
'priceInTokens' => $marketplaceResult['PriceInRobux'] ?: 0,
|
||||
'onSale' => $marketplaceResult['IsForSale'],
|
||||
'assetTypeId' => $assetTypeId,
|
||||
'assetTypeId' => $marketplaceResult['AssetTypeId'],
|
||||
'assetVersionId' => 0
|
||||
]);
|
||||
$assetVersion = AssetVersion::create([
|
||||
'parentAsset' => $asset->id,
|
||||
'localVersion' => 1,
|
||||
'contentURL' => $hash
|
||||
]);
|
||||
$asset->assetVersionId = $assetVersion->id;
|
||||
$asset->save();
|
||||
], $hash);
|
||||
|
||||
RobloxAsset::create([
|
||||
'robloxAssetId' => $id,
|
||||
|
|
@ -90,42 +113,22 @@ class AssetHelper
|
|||
return $uploadedAsset->asset;
|
||||
}
|
||||
|
||||
$marketplaceResult = self::cookiedRequest()->get('https://api.roblox.com/marketplace/productinfo?assetId=' . $id);
|
||||
$marketplaceResult = self::getRBXMarketplaceInfo($id);
|
||||
|
||||
if(!$marketplaceResult->ok())
|
||||
if(!$marketplaceResult)
|
||||
return false;
|
||||
|
||||
$assetTypeId = $marketplaceResult['AssetTypeId'];
|
||||
if(
|
||||
$assetTypeId == 41 || // Hair Accessory
|
||||
$assetTypeId == 42 || // Face Accessory
|
||||
$assetTypeId == 43 || // Neck Accessory
|
||||
$assetTypeId == 44 || // Shoulder Accessory
|
||||
$assetTypeId == 45 || // Front Accessory
|
||||
$assetTypeId == 46 || // Back Accessory
|
||||
$assetTypeId == 47 // Waist Accessory
|
||||
) {
|
||||
$assetTypeId = 8;
|
||||
}
|
||||
|
||||
$hash = CdnHelper::SaveContentB64($b64Content, 'application/octet-stream');
|
||||
$asset = Asset::create([
|
||||
$asset = self::newAsset([
|
||||
'creatorId' => ($uploadToHolder ? 1 : Auth::user()->id),
|
||||
'name' => $marketplaceResult['Name'],
|
||||
'description' => $marketplaceResult['Description'],
|
||||
'approved' => true,
|
||||
'priceInTokens' => $marketplaceResult['PriceInRobux'] ?: 0,
|
||||
'onSale' => $marketplaceResult['IsForSale'],
|
||||
'assetTypeId' => $assetTypeId,
|
||||
'assetTypeId' => $marketplaceResult['AssetTypeId'],
|
||||
'assetVersionId' => 0
|
||||
]);
|
||||
$assetVersion = AssetVersion::create([
|
||||
'parentAsset' => $asset->id,
|
||||
'localVersion' => 1,
|
||||
'contentURL' => $hash
|
||||
]);
|
||||
$asset->assetVersionId = $assetVersion->id;
|
||||
$asset->save();
|
||||
], $hash);
|
||||
|
||||
RobloxAsset::create([
|
||||
'robloxAssetId' => $id,
|
||||
|
|
|
|||
|
|
@ -144,10 +144,9 @@ class GridHelper
|
|||
]);
|
||||
}
|
||||
|
||||
public static function getBodyColorsXML()
|
||||
private static function getXMLFromGameDisk($fileName)
|
||||
{
|
||||
$disk = self::getGameDisk();
|
||||
$fileName = 'BodyColors.xml';
|
||||
|
||||
if(!$disk->exists($fileName))
|
||||
throw new Exception('Unable to locate template file.');
|
||||
|
|
@ -155,6 +154,21 @@ class GridHelper
|
|||
return $disk->get($fileName);
|
||||
}
|
||||
|
||||
public static function getBodyColorsXML()
|
||||
{
|
||||
return self::getXMLFromGameDisk('BodyColors.xml');
|
||||
}
|
||||
|
||||
public static function getBodyPartXML()
|
||||
{
|
||||
return self::getXMLFromGameDisk('BodyPart.xml');
|
||||
}
|
||||
|
||||
public static function getFaceXML()
|
||||
{
|
||||
return self::getXMLFromGameDisk('Face.xml');
|
||||
}
|
||||
|
||||
public static function getArbiter($name)
|
||||
{
|
||||
$query = DynamicWebConfiguration::where('name', sprintf('%sArbiterIP', $name))->first();
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@
|
|||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
use App\Helpers\AssetHelper;
|
||||
use App\Helpers\CdnHelper;
|
||||
use App\Helpers\GridHelper;
|
||||
use App\Helpers\ValidationHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\AppDeployment;
|
||||
use App\Models\AssetType;
|
||||
use App\Models\Deployment;
|
||||
use App\Models\RobloxAsset;
|
||||
use App\Rules\AppDeploymentFilenameRule;
|
||||
|
||||
class AdminController extends Controller
|
||||
|
|
@ -19,7 +23,173 @@ class AdminController extends Controller
|
|||
|
||||
|
||||
// Admin+
|
||||
function manualAssetUpload(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'asset-type-id' => ['required', 'int'],
|
||||
'name' => ['required', 'string'],
|
||||
'description' => ['string', 'nullable'],
|
||||
'roblox-id' => ['int', 'min:0', 'nullable'],
|
||||
'on-sale' => ['required', 'boolean'],
|
||||
'price' => ['required_if:on-sale,true', 'int', 'min:0'],
|
||||
'content' => ['nullable'],
|
||||
'mesh-id' => ['int', 'nullable'],
|
||||
'base-id' => ['int', 'nullable'],
|
||||
'overlay-id' => ['int', 'nullable'],
|
||||
],[
|
||||
'asset-type-id.required' => 'An asset type ID must be provided.',
|
||||
'roblox-id.integer' => 'Roblox ID must be an integer.'
|
||||
]);
|
||||
|
||||
if($validator->fails())
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
|
||||
$valid = $validator->valid();
|
||||
$isRobloxAsset = ($request->has('roblox-id') && $valid['roblox-id'] > 0);
|
||||
|
||||
if($isRobloxAsset)
|
||||
{
|
||||
$uploadedAsset = RobloxAsset::where('robloxAssetId', $valid['roblox-id'])->first();
|
||||
if($uploadedAsset)
|
||||
{
|
||||
$validator->errors()->add('roblox-id', 'This asset has already been uploaded!');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
}
|
||||
|
||||
$assetType = AssetType::where('id', $valid['asset-type-id'])
|
||||
->where('adminCreatable', 1)
|
||||
->first();
|
||||
if(!$assetType)
|
||||
{
|
||||
$validator->errors()->add('asset-type-id', 'Invalid asset type for admin upload.');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
$assetFunction = 'Unknown';
|
||||
$assetFunctionArgs = [];
|
||||
switch($assetType->id)
|
||||
{
|
||||
case 27: // Torso
|
||||
case 28: // Right Arm
|
||||
case 29: // Left Arm
|
||||
case 30: // Left Leg
|
||||
case 31: // Right Leg
|
||||
$assetFunctionArgs = [$assetType->id, $valid];
|
||||
$assetFunction = 'BodyPart';
|
||||
break;
|
||||
case 18: // Face
|
||||
$assetFunctionArgs = [$validator, $valid];
|
||||
$assetFunction = 'Face';
|
||||
break;
|
||||
default:
|
||||
$assetFunctionArgs = [$validator];
|
||||
$assetFunction = 'Generic';
|
||||
break;
|
||||
}
|
||||
|
||||
$assetContent = $this->{ 'manualAssetUpload' . $assetFunction }($request, ...$assetFunctionArgs);
|
||||
|
||||
$hash = CdnHelper::SaveContent($assetContent, 'application/octet-stream');
|
||||
$asset = AssetHelper::newAsset([
|
||||
'creatorId' => 1,
|
||||
'name' => $valid['name'],
|
||||
'description' => $valid['description'],
|
||||
'approved' => true,
|
||||
'priceInTokens' => $valid['price'],
|
||||
'onSale' => $valid['on-sale'] == 1 ? true : false,
|
||||
'assetTypeId' => $assetType->id,
|
||||
'assetVersionId' => 0
|
||||
], $hash);
|
||||
$asset->logAdminUpload(Auth::user()->id);
|
||||
|
||||
if($isRobloxAsset)
|
||||
{
|
||||
RobloxAsset::create([
|
||||
'robloxAssetId' => $valid['roblox-id'],
|
||||
'localAssetId' => $asset->id
|
||||
]);
|
||||
}
|
||||
|
||||
return response([
|
||||
'success' => true,
|
||||
'message' => 'Your asset has been successfully uploaded!',
|
||||
'assetId' => $asset->id
|
||||
]);
|
||||
}
|
||||
|
||||
function manualAssetUploadBodyPart(Request $request, $assetTypeId, $valid)
|
||||
{
|
||||
$bodyParts = [
|
||||
27 => 1, // Torso
|
||||
28 => 3, // Right Arm
|
||||
29 => 2, // Left Arm
|
||||
30 => 4, // Left Leg
|
||||
31 => 5 // Right Leg
|
||||
];
|
||||
|
||||
$document = simplexml_load_string(GridHelper::getBodyPartXML());
|
||||
$document->xpath('//int[@name="BaseTextureId"]')[0][0] = $valid['base-id'] ?: 0;
|
||||
$document->xpath('//token[@name="BodyPart"]')[0][0] = $bodyParts[$assetTypeId];
|
||||
$document->xpath('//int[@name="MeshId"]')[0][0] = $valid['mesh-id'];
|
||||
$document->xpath('//string[@name="Name"]')[0][0] = $valid['name'];
|
||||
$document->xpath('//int[@name="OverlayTextureId"]')[0][0] = $valid['overlay-id'] ?: 0;
|
||||
|
||||
$domXML = dom_import_simplexml($document);
|
||||
$assetContent = $domXML->ownerDocument->saveXML($domXML->ownerDocument->documentElement);
|
||||
|
||||
return $assetContent;
|
||||
}
|
||||
|
||||
function manualAssetUploadFace(Request $request, $validator, $valid)
|
||||
{
|
||||
if(!$request->has('content'))
|
||||
{
|
||||
$validator->errors()->add('content', 'Asset content cannot be blank!');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
$hash = CdnHelper::SaveContent(
|
||||
file_get_contents($request->file('content')->path()),
|
||||
'application/octet-stream'
|
||||
);
|
||||
$imageAsset = AssetHelper::newAsset([
|
||||
'creatorId' => 1,
|
||||
'name' => $valid['name'],
|
||||
'approved' => true,
|
||||
'onSale' => false,
|
||||
'assetTypeId' => 1, // Image
|
||||
'assetVersionId' => 0
|
||||
], $hash);
|
||||
$imageAsset->logAdminUpload(Auth::user()->id);
|
||||
$imageAssetUrl = route('client.asset', ['id' => $imageAsset->id]);
|
||||
|
||||
$document = simplexml_load_string(GridHelper::getFaceXML());
|
||||
$document->xpath('//Content[@name="Texture"]')[0][0]->addChild('url', $imageAssetUrl);
|
||||
|
||||
$domXML = dom_import_simplexml($document);
|
||||
$assetContent = $domXML->ownerDocument->saveXML($domXML->ownerDocument->documentElement);
|
||||
|
||||
return $assetContent;
|
||||
}
|
||||
|
||||
function manualAssetUploadGeneric(Request $request, $validator)
|
||||
{
|
||||
if(!$request->has('content'))
|
||||
{
|
||||
$validator->errors()->add('content', 'Asset content cannot be blank!');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
$assetContent = file_get_contents($request->file('content')->path());
|
||||
|
||||
return $assetContent;
|
||||
}
|
||||
|
||||
function manualAssetUploadUnknown(Request $request)
|
||||
{
|
||||
throw new \BadMethodCallException('Not implemented');
|
||||
}
|
||||
|
||||
// Owner+
|
||||
function deploy(Request $request)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class AvatarController extends Controller
|
|||
|
||||
array_push($data, [
|
||||
'id' => $asset->id,
|
||||
'Url' => route('shop.asset', ['asset' => $asset, 'assetName' => Str::slug($asset->name, '-')]),
|
||||
'Url' => $asset->getShopUrl(),
|
||||
'Thumbnail' => $asset->getThumbnail(),
|
||||
'Name' => $asset->name,
|
||||
'Wearing' => Auth::user()->isWearing($asset->id)
|
||||
|
|
@ -100,7 +100,7 @@ class AvatarController extends Controller
|
|||
|
||||
array_push($data, [
|
||||
'id' => $asset->id,
|
||||
'Url' => route('shop.asset', ['asset' => $asset, 'assetName' => Str::slug($asset->name, '-')]),
|
||||
'Url' => $asset->getShopUrl(),
|
||||
'Thumbnail' => $asset->getThumbnail(),
|
||||
'Name' => $asset->name,
|
||||
'Wearing' => true
|
||||
|
|
@ -160,11 +160,7 @@ class AvatarController extends Controller
|
|||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
AvatarAsset::Create([
|
||||
'owner_id' => Auth::user()->id,
|
||||
'asset_id' => $valid['id']
|
||||
]);
|
||||
|
||||
Auth::user()->wearAsset($valid['id']);
|
||||
Auth::user()->redraw();
|
||||
|
||||
return response(['success' => true]);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ class MoneyController extends Controller
|
|||
|
||||
foreach($dataPoint['Points'] as $transactionType)
|
||||
{
|
||||
$newColumn['total'] += Transaction::where('user_id', Auth::user()->id)
|
||||
$column = $transactionType == 'Sales' ? 'seller_id' : 'user_id';
|
||||
|
||||
$newColumn['total'] += Transaction::where($column, Auth::user()->id)
|
||||
->where('transaction_type_id', TransactionType::IDFromType($transactionType))
|
||||
->where(function($query) use($request) {
|
||||
if(!$request->has('filter'))
|
||||
|
|
@ -110,6 +112,7 @@ class MoneyController extends Controller
|
|||
return $query->where('user_id', Auth::user()->id);
|
||||
})
|
||||
->where('transaction_type_id', $transactionType->id)
|
||||
->with('asset')
|
||||
->orderByDesc('id')
|
||||
->cursorPaginate(30);
|
||||
$prevCursor = $transactions->previousCursor();
|
||||
|
|
@ -126,7 +129,7 @@ class MoneyController extends Controller
|
|||
$asset = null;
|
||||
if($transactionType->format != '')
|
||||
$asset = [
|
||||
'url' => route('shop.asset', ['asset' => $transaction->asset, 'assetName' => Str::slug($transaction->asset->name, '-')]),
|
||||
'url' => $transaction->asset->getShopUrl(),
|
||||
'name' => $transaction->asset->name
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class ShopController extends Controller
|
|||
'Thumbnail' => $asset->getThumbnail(),
|
||||
'OnSale' => $asset->onSale,
|
||||
'Price' => $asset->priceInTokens,
|
||||
'Url' => route('shop.asset', ['asset' => $asset->id, 'assetName' => Str::slug($asset->name, '-')])
|
||||
'Url' => $asset->getShopUrl()
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ class ThumbnailController extends Controller
|
|||
break;
|
||||
}
|
||||
} elseif($renderType == 'Asset') {
|
||||
if($model->renderId)
|
||||
$model = Asset::where('id', $model->renderId)->first();
|
||||
|
||||
if($model->moderated)
|
||||
return response(['status' => 'success', 'data' => '/thumbs/DeletedThumbnail.png']);
|
||||
|
||||
|
|
@ -108,13 +111,13 @@ class ThumbnailController extends Controller
|
|||
if($renderType == 'User' && $valid['position'] == 'bust')
|
||||
$trackerType .= 'bust';
|
||||
$tracker = RenderTracker::where('type', $trackerType)
|
||||
->where('target', $valid['id'])
|
||||
->where('target', $model->id)
|
||||
->where('created_at', '>', Carbon::now()->subMinute());
|
||||
|
||||
if(!$tracker->exists()) {
|
||||
$tracker = RenderTracker::create([
|
||||
'type' => $trackerType,
|
||||
'target' => $valid['id']
|
||||
'target' => $model->id
|
||||
]);
|
||||
|
||||
ArbiterRender::dispatch(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Validator;
|
|||
use Illuminate\Validation\Rule;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AdminUpload;
|
||||
use App\Models\DynamicWebConfiguration;
|
||||
use App\Models\PunishmentType;
|
||||
use App\Models\Username;
|
||||
|
|
@ -264,13 +265,30 @@ class AdminController extends Controller
|
|||
return view('web.admin.userlookup')->with('users', $users)->with('input', $request->get('lookup'));
|
||||
}
|
||||
|
||||
// Admin+
|
||||
|
||||
// GET admin.autoupload
|
||||
function autoUpload()
|
||||
{
|
||||
return view('web.admin.autoupload');
|
||||
return view('web.admin.catalog.autoupload');
|
||||
}
|
||||
|
||||
// GET admin.assetupload
|
||||
function assetUpload()
|
||||
{
|
||||
return view('web.admin.catalog.assetupload');
|
||||
}
|
||||
|
||||
// GET admin.adminuploads
|
||||
function getAdminUploads(Request $request)
|
||||
{
|
||||
$uploads = AdminUpload::query()
|
||||
->orderByDesc('id')
|
||||
->paginate(25);
|
||||
|
||||
return view('web.admin.catalog.adminuploads')->with('uploads', $uploads);
|
||||
}
|
||||
|
||||
// Admin+
|
||||
function metricsVisualization()
|
||||
{
|
||||
return view('web.admin.metricsvisualization');
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ class ArbiterRender implements ShouldQueue
|
|||
case 'Avatar':
|
||||
$arguments[0] = route('client.characterFetch', ['userId' => $this->assetId]);
|
||||
break;
|
||||
case 'Face':
|
||||
$this->type = 'Decal';
|
||||
break;
|
||||
case 'Torso':
|
||||
case 'Right Arm':
|
||||
case 'Left Arm':
|
||||
|
|
@ -116,8 +119,9 @@ class ArbiterRender implements ShouldQueue
|
|||
break;
|
||||
case 'Package':
|
||||
// TODO: XlXi: Move these to config, as it could be different from prod in a testing environment. Also move these to their own assets (not loading from roblox).
|
||||
$arguments[0] = $this->tracker->targetObj->getPackageAssetUrls();
|
||||
array_push($arguments, 'https://www.roblox.com/asset/?id=1785197'); // Rig
|
||||
array_push($arguments, '27113661;25251154'); // Custom Texture URLs (shirt and pands)
|
||||
array_push($arguments, 'https://www.roblox.com/asset/?id=27113661;https://www.roblox.com/asset/?id=25251154'); // Custom Texture URLs (shirt and pands)
|
||||
break;
|
||||
case 'Place':
|
||||
$arguments[2] = 768*4; // XlXi: These get scaled down by 4.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminUpload extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'asset_id',
|
||||
'uploader_id'
|
||||
];
|
||||
|
||||
public function asset()
|
||||
{
|
||||
return $this->belongsTo(Asset::class, 'asset_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -229,7 +229,9 @@ class User extends Authenticatable
|
|||
|
||||
foreach($oldHashes as $hash)
|
||||
{
|
||||
if(!User::where('thumbnailBustHash', $hash)->orWhere('thumbnail2DHash', $hash)->orWhere('thumbnail3DHash', $hash)->exists())
|
||||
$userThumbExists = User::where('thumbnailBustHash', $hash)->orWhere('thumbnail2DHash', $hash)->orWhere('thumbnail3DHash', $hash)->exists();
|
||||
$assetThumbExists = Asset::where('thumbnail2DHash', $hash)->orWhere('thumbnail3DHash', $hash)->exists();
|
||||
if(!$userThumbExists && !$assetThumbExists)
|
||||
CdnHelper::Delete($hash);
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +249,27 @@ class User extends Authenticatable
|
|||
->whereRelation('asset', 'moderated', 0);
|
||||
}
|
||||
|
||||
public function wearAsset($assetId)
|
||||
{
|
||||
$asset = Asset::where('id', $assetId)->first();
|
||||
if($asset->assetType->id == 32)
|
||||
{
|
||||
foreach(explode(';', $asset->getPackageAssetIds()) as $asset)
|
||||
{
|
||||
if($this->isWearing($asset)) continue;
|
||||
|
||||
$this->wearAsset($asset);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AvatarAsset::Create([
|
||||
'owner_id' => $this->id,
|
||||
'asset_id' => $assetId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBodyColors()
|
||||
{
|
||||
$colors = AvatarColor::user($this->id);
|
||||
|
|
|
|||
|
|
@ -25,12 +25,34 @@ class UserAsset extends Model
|
|||
return $this->belongsTo(Asset::class, 'asset_id');
|
||||
}
|
||||
|
||||
public function owner()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_id');
|
||||
}
|
||||
|
||||
public static function createSerialed($ownerId, $assetId)
|
||||
{
|
||||
return self::create([
|
||||
$userAsset = self::create([
|
||||
'owner_id' => $ownerId,
|
||||
'asset_id' => $assetId,
|
||||
'serial' => self::where('asset_id', $assetId)->count()+1
|
||||
]);
|
||||
$userAsset->postSerialize();
|
||||
|
||||
return $userAsset;
|
||||
}
|
||||
|
||||
public function postSerialize()
|
||||
{
|
||||
// Grant package assets.
|
||||
if($this->asset->assetType->id == 32)
|
||||
{
|
||||
foreach(explode(';', $this->asset->getPackageAssetIds()) as $asset)
|
||||
{
|
||||
if($this->owner->hasAsset($asset)) continue;
|
||||
|
||||
self::createSerialed($this->owner_id, $asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ use Carbon\Carbon;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Helpers\CdnHelper;
|
||||
use App\Models\AssetType;
|
||||
|
||||
/*
|
||||
|
|
@ -41,7 +43,7 @@ class Asset extends Model
|
|||
'creatorId',
|
||||
'name',
|
||||
'description',
|
||||
'parentAssetId',
|
||||
'renderId',
|
||||
'approved',
|
||||
'priceInTokens',
|
||||
'onSale',
|
||||
|
|
@ -139,6 +141,12 @@ class Asset extends Model
|
|||
|
||||
public function getThumbnail()
|
||||
{
|
||||
if($this->renderId)
|
||||
{
|
||||
$asset = Asset::where('id', $this->renderId)->first();
|
||||
return $asset->getThumbnail();
|
||||
}
|
||||
|
||||
if($this->moderated)
|
||||
return '/thumbs/DeletedThumbnail.png';
|
||||
|
||||
|
|
@ -160,6 +168,29 @@ class Asset extends Model
|
|||
return route('content', $this->thumbnail2DHash);
|
||||
}
|
||||
|
||||
// XlXi: For packages. https://abc.com/asset?id=1;https://abc.com/asset?id=2
|
||||
public function getPackageAssetUrls()
|
||||
{
|
||||
$result = '';
|
||||
$assets = $this->getPackageAssetIds();
|
||||
if(!$assets)
|
||||
return $result;
|
||||
|
||||
foreach(explode(';', $assets) as $key=>$asset)
|
||||
{
|
||||
$result .= ($key > 0 ? ';' : '') . route('client.asset', ['id' => $asset]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getPackageAssetIds()
|
||||
{
|
||||
$disk = CdnHelper::GetDisk();
|
||||
|
||||
return $disk->get($this->getContent());
|
||||
}
|
||||
|
||||
public function set2DHash($hash)
|
||||
{
|
||||
$this->thumbnail2DHash = $hash;
|
||||
|
|
@ -174,6 +205,19 @@ class Asset extends Model
|
|||
$this->save();
|
||||
}
|
||||
|
||||
public function getShopUrl()
|
||||
{
|
||||
return route('shop.asset', ['asset' => $this, 'assetName' => Str::slug($this->name, '-')]);
|
||||
}
|
||||
|
||||
public function logAdminUpload($uploaderId)
|
||||
{
|
||||
return AdminUpload::create([
|
||||
'asset_id' => $this->id,
|
||||
'uploader_id' => $uploaderId
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCreated()
|
||||
{
|
||||
$date = $this['created_at'];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components\Admin\Navigation;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class AssetUploader extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View|\Closure|string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('components.admin.navigation.asset-uploader');
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ return new class extends Migration
|
|||
$table->unsignedBigInteger('creatorId');
|
||||
$table->string('name');
|
||||
$table->longText('description')->nullable();
|
||||
$table->unsignedBigInteger('parentAssetId')->nullable()->comment('Used by things like images that were created because of something else.');
|
||||
$table->unsignedBigInteger('renderId')->nullable();
|
||||
|
||||
$table->boolean('approved')->default(false);
|
||||
$table->boolean('moderated')->default(false);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ return new class extends Migration
|
|||
$table->boolean('copyable')->default(false); // Can be downloaded through /asset
|
||||
$table->boolean('sellable')->default(false); // If false, can be made on sale for free only.
|
||||
$table->boolean('locked')->default(false); // Cannot be put on sale
|
||||
$table->boolean('trusted')->default(false); // Skips text filtering for name and description.
|
||||
$table->boolean('userCreatable')->default(false); // Can be uploaded by a user.
|
||||
$table->boolean('adminCreatable')->default(false); // Can be uploaded via the admin panel.
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('admin_uploads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedBigInteger('asset_id');
|
||||
$table->unsignedBigInteger('uploader_id');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('admin_uploads');
|
||||
}
|
||||
};
|
||||
|
|
@ -19,14 +19,17 @@ class AssetTypeSeeder extends Seeder
|
|||
'name' => 'Image',
|
||||
'renderable' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'trusted' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'T-Shirt',
|
||||
'renderable' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Audio',
|
||||
|
|
@ -37,22 +40,26 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Lua',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'HTML',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Text',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Hat',
|
||||
|
|
@ -60,15 +67,18 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Place',
|
||||
'renderable' => true
|
||||
'renderable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Model',
|
||||
'renderable' => true
|
||||
'renderable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Shirt',
|
||||
|
|
@ -76,7 +86,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Pants',
|
||||
|
|
@ -84,7 +95,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Decal',
|
||||
|
|
@ -103,13 +115,16 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Face',
|
||||
'renderable' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Gear',
|
||||
|
|
@ -117,7 +132,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
null,
|
||||
[
|
||||
|
|
@ -129,28 +145,22 @@ class AssetTypeSeeder extends Seeder
|
|||
'name' => 'Group Emblem',
|
||||
'renderable' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'trusted' => true
|
||||
],
|
||||
null,
|
||||
[
|
||||
'name' => 'Animation',
|
||||
'copyable' => true
|
||||
'copyable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Arms',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Legs',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Torso',
|
||||
|
|
@ -158,7 +168,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Right Arm',
|
||||
|
|
@ -166,7 +177,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Left Arm',
|
||||
|
|
@ -174,7 +186,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Left Leg',
|
||||
|
|
@ -182,7 +195,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Right Leg',
|
||||
|
|
@ -190,7 +204,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Package',
|
||||
|
|
@ -198,7 +213,8 @@ class AssetTypeSeeder extends Seeder
|
|||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'wearable' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'YouTubeVideo',
|
||||
|
|
@ -206,7 +222,8 @@ class AssetTypeSeeder extends Seeder
|
|||
],
|
||||
[
|
||||
'name' => 'Game Pass',
|
||||
'sellable' => true
|
||||
'sellable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'App',
|
||||
|
|
@ -216,11 +233,13 @@ class AssetTypeSeeder extends Seeder
|
|||
[
|
||||
'name' => 'Code',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
'locked' => true,
|
||||
'adminCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Plugin',
|
||||
'sellable' => true
|
||||
'sellable' => true,
|
||||
'userCreatable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'SolidModel',
|
||||
|
|
@ -236,59 +255,31 @@ class AssetTypeSeeder extends Seeder
|
|||
],
|
||||
[
|
||||
'name' => 'Hair Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Face Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Neck Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Shoulder Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Front Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Back Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Waist Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Climb Animation',
|
||||
|
|
|
|||
|
|
@ -408,7 +408,12 @@ class OutfitsTab extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return (<><p>outfits</p></>);
|
||||
return (<>
|
||||
<div className="mb-1 d-flex">
|
||||
<button className="btn btn-sm btn-primary ms-auto">Create New</button>
|
||||
</div>
|
||||
<p>outfits</p>
|
||||
</>);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,401 @@
|
|||
/*
|
||||
Copyright © XlXi 2022
|
||||
*/
|
||||
|
||||
import { Component, createElement, createRef } from 'react';
|
||||
import axios from 'axios';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { buildGenericApiUrl } from '../util/HTTP.js';
|
||||
import Loader from './Loader';
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
const assetTypes = [
|
||||
{
|
||||
assetTypeId: 1,
|
||||
name: 'Image',
|
||||
type: 'fileupload',
|
||||
extra: <p>PNG, JPG, JPEG, and other common image formats are supported.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 4,
|
||||
name: 'Mesh',
|
||||
type: 'fileupload',
|
||||
extra: <p>Your mesh can be <b>obj</b> or Roblox's <b>mesh</b> format.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 5,
|
||||
name: 'Lua',
|
||||
type: 'text',
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 6,
|
||||
name: 'HTML',
|
||||
type: 'text',
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 7,
|
||||
name: 'Text',
|
||||
type: 'text',
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 8,
|
||||
name: 'Hat',
|
||||
type: 'fileupload',
|
||||
sellable: true
|
||||
},
|
||||
{
|
||||
assetTypeId: 17,
|
||||
name: 'Head',
|
||||
type: 'fileupload',
|
||||
extra: <p>Heads are SpecialMeshes. Export it from studio, do not upload the mesh file here.</p>,
|
||||
sellable: true
|
||||
},
|
||||
{
|
||||
assetTypeId: 18,
|
||||
name: 'Face',
|
||||
type: 'fileupload',
|
||||
extra: <p>Faces are image files. The XML will be automatically generated.</p>,
|
||||
sellable: true
|
||||
},
|
||||
{
|
||||
assetTypeId: 19,
|
||||
name: 'Gear',
|
||||
type: 'fileupload',
|
||||
sellable: true
|
||||
},
|
||||
{
|
||||
assetTypeId: 27,
|
||||
name: 'Torso',
|
||||
type: 'packagepart',
|
||||
extra: <p>Overlay ID displays atop clothing, base ID displays under clothing.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 28,
|
||||
name: 'Right Arm',
|
||||
type: 'packagepart',
|
||||
extra: <p>Overlay ID displays atop clothing, base ID displays under clothing.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 29,
|
||||
name: 'Left Arm',
|
||||
type: 'packagepart',
|
||||
extra: <p>Overlay ID displays atop clothing, base ID displays under clothing.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 30,
|
||||
name: 'Left Leg',
|
||||
type: 'packagepart',
|
||||
extra: <p>Overlay ID displays atop clothing, base ID displays under clothing.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 31,
|
||||
name: 'Right Leg',
|
||||
type: 'packagepart',
|
||||
extra: <p>Overlay ID displays atop clothing, base ID displays under clothing.</p>,
|
||||
sellable: false
|
||||
},
|
||||
{
|
||||
assetTypeId: 32,
|
||||
name: 'Package',
|
||||
type: 'text',
|
||||
extra: <p>Asset IDs for package. Example: 1;2;3;4;5</p>,
|
||||
sellable: true
|
||||
},
|
||||
{
|
||||
assetTypeId: 37,
|
||||
name: 'Code',
|
||||
type: 'text',
|
||||
sellable: false
|
||||
}
|
||||
];
|
||||
|
||||
class ManualAssetUploadModel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
forSale: false,
|
||||
requestInputs: {
|
||||
name: 'New Asset',
|
||||
description: this.props.TypeName + ' Asset',
|
||||
'roblox-id': 0,
|
||||
'on-sale': false,
|
||||
price: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.forSaleRef = createRef();
|
||||
|
||||
this.pushInput = this.pushInput.bind(this);
|
||||
this.updateContentInputFile = this.updateContentInputFile.bind(this);
|
||||
this.updateContentInputText = this.updateContentInputText.bind(this);
|
||||
this.updateInput = this.updateInput.bind(this);
|
||||
this.uploadAsset = this.uploadAsset.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.pushInput('asset-type-id', this.props.TypeId);
|
||||
|
||||
if(this.props.UploadType == 'packagepart')
|
||||
{
|
||||
this.pushInput('mesh-id', 0);
|
||||
this.pushInput('overlay-id', 0);
|
||||
this.pushInput('base-id', 0);
|
||||
}
|
||||
}
|
||||
|
||||
pushInput(name, value) {
|
||||
this.setState((state, props) => ({
|
||||
requestInputs: { ...state.requestInputs, [name]: value }
|
||||
}));
|
||||
}
|
||||
|
||||
updateContentInputFile(value) {
|
||||
this.pushInput('content', value);
|
||||
}
|
||||
|
||||
updateContentInputText(value) {
|
||||
const file = (value != '' && new Blob([ value ], { type: 'text/plain' }));
|
||||
this.pushInput('content', file);
|
||||
}
|
||||
|
||||
updateInput(e, prop='value') {
|
||||
this.pushInput(e.target.id, e.target[prop]);
|
||||
}
|
||||
|
||||
uploadAsset() {
|
||||
let { requestInputs } = this.state;
|
||||
|
||||
this.props.setLoad(true);
|
||||
|
||||
if(!requestInputs.content && this.props.UploadType != 'packagepart')
|
||||
{
|
||||
this.props.flashError('Asset content cannot be blank!');
|
||||
this.props.setLoad(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let bodyFormData = new FormData();
|
||||
Object.keys(requestInputs).map(key => {
|
||||
let val = requestInputs[key];
|
||||
if(typeof(val) == 'boolean')
|
||||
val = val ? 1 : 0;
|
||||
|
||||
bodyFormData.append(key, val);
|
||||
});
|
||||
|
||||
axios.post(buildGenericApiUrl('api', 'admin/v1/manual-asset-upload'), bodyFormData)
|
||||
.then(res => {
|
||||
const data = res.data;
|
||||
|
||||
this.props.flashSuccess(data.message, data.assetId);
|
||||
this.props.setLoad(false);
|
||||
})
|
||||
.catch(err => {
|
||||
const data = err.response.data;
|
||||
|
||||
this.props.flashError(data.errors ? data.errors[0].message : 'An unknown error occurred.');
|
||||
this.props.setLoad(false);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let { TypeName, UploadType, Extra, Sellable } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>Create a { TypeName }</h4>
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
{ Extra }
|
||||
{
|
||||
UploadType == 'fileupload'
|
||||
?
|
||||
<>
|
||||
<label for="content" className="form-label">Find your { TypeName }:</label>
|
||||
<input className="form-control mb-2" type="file" id="content" onChange={ (e) => this.updateContentInputFile(e.target.files[0]) } />
|
||||
</>
|
||||
:
|
||||
UploadType == 'packagepart'
|
||||
?
|
||||
<>
|
||||
<label for="mesh-id" className="form-label">Mesh ID:</label>
|
||||
<input className="form-control mb-2" type="number" id="mesh-id" value={ this.state.requestInputs['mesh-id'] } onChange={ this.updateInput } />
|
||||
<label for="overlay-id" className="form-label">Overlay Texture ID (if applicable):</label>
|
||||
<input className="form-control mb-2" type="number" id="overlay-id" value={ this.state.requestInputs['overlay-id'] } onChange={ this.updateInput } />
|
||||
<label for="base-id" className="form-label">Base Texture ID (if applicable):</label>
|
||||
<input className="form-control mb-2" type="number" id="base-id" value={ this.state.requestInputs['base-id'] } onChange={ this.updateInput } />
|
||||
</>
|
||||
:
|
||||
UploadType == 'text'
|
||||
&&
|
||||
<>
|
||||
<label for="content" className="form-label">{ TypeName } Contents:</label>
|
||||
<textarea className="form-control mb-2" type="text" id="content" onChange={ (e) => this.updateContentInputText(e.target.value) }></textarea>
|
||||
</>
|
||||
}
|
||||
<label for="name" className="form-label">{ TypeName } Name:</label>
|
||||
<input className="form-control mb-2" type="text" id="name" value={ this.state.requestInputs.name } onChange={ this.updateInput } />
|
||||
<label for="description" className="form-label">{ TypeName } Description:</label>
|
||||
<textarea className="form-control mb-2" type="text" id="description" value={ this.state.requestInputs.description } onChange={ this.updateInput }></textarea>
|
||||
<label for="roblox-id" className="form-label">Roblox Asset ID (if applicable):</label>
|
||||
<input className="form-control" type="number" id="roblox-id" value={ this.state.requestInputs['roblox-id'] } onChange={ this.updateInput } />
|
||||
{
|
||||
Sellable
|
||||
&&
|
||||
<>
|
||||
<hr />
|
||||
<h4>Sell this Item</h4>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" value={ this.state.requestInputs['on-sale'] } id="on-sale" onChange={ (e) => this.updateInput(e, 'checked') } />
|
||||
<label className="form-check-label" for="on-sale">Sell this Item</label>
|
||||
</div>
|
||||
{
|
||||
this.state.requestInputs['on-sale']
|
||||
&&
|
||||
<div className="input-group mb-2">
|
||||
<span className="input-group-text px-1 pe-0">
|
||||
<p className="virtubrick-tokens"> </p>
|
||||
</span>
|
||||
<input className="form-control" type="number" id="price" value={ this.state.requestInputs.price } min="0" max="999999999" placeholder="Price" onChange={ this.updateInput } />
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<button className="btn btn-success px-5" onClick={ this.uploadAsset }>Upload</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ManualAssetUpload extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
createModelLoaded: false,
|
||||
currentTabTypeId: 0,
|
||||
tabKey: 0,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.findAssetType = this.findAssetType.bind(this);
|
||||
this.setLoad = this.setLoad.bind(this);
|
||||
this.flashError = this.flashError.bind(this);
|
||||
this.flashSuccess = this.flashSuccess.bind(this);
|
||||
this.navigateAssetType = this.navigateAssetType.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount()
|
||||
{
|
||||
this.navigateAssetType(1);
|
||||
}
|
||||
|
||||
findAssetType(typeId) {
|
||||
return assetTypes.find(obj => {
|
||||
return obj.assetTypeId === typeId
|
||||
});
|
||||
}
|
||||
|
||||
setLoad(loading) {
|
||||
this.setState({ loading: loading });
|
||||
}
|
||||
|
||||
flashError(message) {
|
||||
this.setState({ errorMessage: message });
|
||||
setTimeout(function(){
|
||||
this.setState({ errorMessage: null });
|
||||
}.bind(this), 3000);
|
||||
}
|
||||
|
||||
flashSuccess(message, assetId) {
|
||||
this.setState({ successMessage: message, successId: assetId });
|
||||
setTimeout(function(){
|
||||
this.setState({ successMessage: null, successId: null });
|
||||
}.bind(this), 10000);
|
||||
}
|
||||
|
||||
navigateAssetType(typeId)
|
||||
{
|
||||
if(this.state.loading) return;
|
||||
|
||||
let data = this.findAssetType(typeId);
|
||||
|
||||
this.activeModel = createElement(
|
||||
ManualAssetUploadModel,
|
||||
{
|
||||
TypeId: data.assetTypeId,
|
||||
TypeName: data.name,
|
||||
UploadType: data.type,
|
||||
Extra: data.extra,
|
||||
Sellable: data.sellable,
|
||||
setLoad: this.setLoad,
|
||||
flashError: this.flashError,
|
||||
flashSuccess: this.flashSuccess,
|
||||
key: this.state.tabKey
|
||||
}
|
||||
);
|
||||
|
||||
this.setState({ createModelLoaded: true, currentTabTypeId: data.assetTypeId, tabKey: this.state.tabKey+1 });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="col-2 pe-0">
|
||||
<ul className="nav nav-tabs flex-column">
|
||||
{
|
||||
assetTypes.map(({ assetTypeId, name }) =>
|
||||
<li className="nav-item">
|
||||
<button className={classNames({ 'nav-link': true, 'active': (assetTypeId == this.state.currentTabTypeId) })} disabled={ this.state.loading } onClick={ () => this.navigateAssetType(assetTypeId) }>{ name }</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="col-10 ps-0">
|
||||
{
|
||||
this.state.successMessage
|
||||
&&
|
||||
<div className="alert alert-success virtubrick-alert virtubrick-error-popup">{ this.state.successMessage } <a className="text-decoration-none" href={ buildGenericApiUrl('www', `shop/${ this.state.successId }`) }>Click Here</a></div>
|
||||
}
|
||||
{
|
||||
this.state.errorMessage
|
||||
&&
|
||||
<div className="alert alert-danger virtubrick-alert virtubrick-error-popup">{ this.state.errorMessage }</div>
|
||||
}
|
||||
<div className="card p-3 vb-card-navconnector">
|
||||
{
|
||||
this.state.loading
|
||||
&&
|
||||
<div className="virtubrick-shop-overlay">
|
||||
<Loader />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!this.state.createModelLoaded
|
||||
?
|
||||
<Loader />
|
||||
:
|
||||
this.activeModel
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ManualAssetUpload;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Copyright © XlXi 2023
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import ManualAssetUpload from '../components/ManualAssetUpload';
|
||||
|
||||
const assetUploadId = 'vb-manual-assetupload';
|
||||
|
||||
$(document).ready(function() {
|
||||
if (document.getElementById(assetUploadId)) {
|
||||
render(<ManualAssetUpload />, document.getElementById(assetUploadId));
|
||||
}
|
||||
});
|
||||
|
|
@ -1098,6 +1098,29 @@ input {
|
|||
&.nav-justified > li {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
&.flex-column {
|
||||
.nav-link {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-top: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
|
||||
&:not(.disabled):hover,
|
||||
&:not(.disabled):focus,
|
||||
&.active {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card.vb-card-navconnector {
|
||||
border-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
@keyframes dropdownEase {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<x-admin.navigation-tabs>
|
||||
<x-admin.navigation-tab-link label="Upload Asset" :route="route('admin.assetupload')" />
|
||||
<x-admin.navigation-tab-link label="Auto Uploader" :route="route('admin.autoupload')" />
|
||||
<x-admin.navigation-tab-link label="Uploaded Assets" :route="route('admin.adminuploads')" />
|
||||
</x-admin.navigation-tabs>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Auto Uploader')
|
||||
|
||||
@push('content')
|
||||
<div class="container-md">
|
||||
<h4>Auto Uploader</h4>
|
||||
<div class="card p-3">
|
||||
<label for="vb-rbx-asset" class="form-label">Roblox Asset ID</label>
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Roblox Asset ID Here"
|
||||
aria-label="Roblox Asset ID Here"
|
||||
name="rbx-asset" id="vb-rbx-asset"
|
||||
aria-describedby="vb-rbx-asset"
|
||||
>
|
||||
<button type="submit" class="btn btn-primary" type="button" name="rbx-asset-button" id="vb-rbx-asset-btn">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Uploaded Assets')
|
||||
|
||||
@push('content')
|
||||
<div class="container-md">
|
||||
<x-admin.navigation.asset-uploader />
|
||||
<h4>Uploaded Assets</h4>
|
||||
<div class="card">
|
||||
@if(isset($uploads) && count($uploads) > 0)
|
||||
<table class="table virtubrick-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Asset</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Uploader</th>
|
||||
<th scope="col">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($uploads as $upload)
|
||||
@php
|
||||
$asset = $upload->asset;
|
||||
@endphp
|
||||
<tr class="align-middle">
|
||||
<th scope="col"><a href="{{ $asset->getShopUrl() }}" class="text-decoration-none">{{ $asset->name }}</th>
|
||||
<th scope="col">{{ $asset->typeString() }}</th>
|
||||
<th scope="col">
|
||||
<a href="{{ route('admin.useradmin', ['ID' => $asset->user->id]) }}" class="text-decoration-none">
|
||||
<x-user-circle :user="$asset->user" :size=40 />
|
||||
</a>
|
||||
</th>
|
||||
<th scope="col">{{ $upload->created_at->isoFormat('l LT') }}</th>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $uploads->links('pagination.virtubrick') }}
|
||||
@else
|
||||
<p class="text-muted p-3">No assets found.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Upload Asset')
|
||||
|
||||
@section('page-specific')
|
||||
<!-- Secure Page JS -->
|
||||
<script src="{{ mix('js/adm/ManualAssetUpload.js') }}"></script>
|
||||
@endsection
|
||||
|
||||
@push('content')
|
||||
<div class="container-md">
|
||||
<x-admin.navigation.asset-uploader />
|
||||
<h4>Upload Asset</h4>
|
||||
<div class="row" id="vb-manual-assetupload">
|
||||
<x-loader />
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'Auto Uploader')
|
||||
|
||||
@push('content')
|
||||
<div class="container-md">
|
||||
<x-admin.navigation.asset-uploader />
|
||||
<h4>Auto Uploader</h4>
|
||||
<div class="card p-3">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||
|
|
@ -44,7 +44,11 @@ Route::middleware('auth')->group(function () {
|
|||
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
|
||||
Route::middleware('roleset:owner')->group(function () {
|
||||
Route::get('/deploy', 'AdminController@deploy')->name('deploy');
|
||||
Route::post('/deploy/{version}', 'AdminController@deployVersion')->name('deploy');
|
||||
Route::post('/deploy/{version}', 'AdminController@deployVersion')->name('deployVersion');
|
||||
});
|
||||
|
||||
Route::middleware('roleset:administrator')->group(function () {
|
||||
Route::post('/manual-asset-upload', 'AdminController@manualAssetUpload')->name('manualAssetUpload')->middleware('throttle:6,2,adminassetupload');
|
||||
});
|
||||
|
||||
// RCC Only
|
||||
|
|
|
|||
|
|
@ -62,11 +62,15 @@ Route::group(['as' => 'admin.', 'prefix' => 'admin'], function() {
|
|||
Route::get('/userlookuptool', 'AdminController@userLookup')->name('userlookup');
|
||||
Route::post('/userlookuptool', 'AdminController@userLookupQuery')->name('userlookupquery');
|
||||
});
|
||||
|
||||
Route::get('/auto-uploader', 'AdminController@autoUpload')->name('autoupload');
|
||||
});
|
||||
|
||||
Route::middleware('roleset:administrator')->group(function () {
|
||||
Route::group(['prefix' => 'catalog'], function() {
|
||||
Route::get('/autoassetupload', 'AdminController@autoUpload')->name('autoupload');
|
||||
Route::get('/manualassetupload', 'AdminController@assetUpload')->name('assetupload');
|
||||
Route::get('/uploadedassets', 'AdminController@getAdminUploads')->name('adminuploads');
|
||||
});
|
||||
|
||||
Route::get('/metrics', 'AdminController@metricsVisualization')->name('metricsvisualization');
|
||||
|
||||
Route::get('/arbiter-diag/{arbiterType?}', 'AdminController@arbiterDiag')->name('diag');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||
<External>null</External>
|
||||
<External>nil</External>
|
||||
<Item class="CharacterMesh" referent="RBX0">
|
||||
<Properties>
|
||||
<int name="BaseTextureId"></int>
|
||||
<token name="BodyPart"></token>
|
||||
<int name="MeshId"></int>
|
||||
<string name="Name"></string>
|
||||
<int name="OverlayTextureId"></int>
|
||||
<bool name="archivable">true</bool>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
|
||||
<External>null</External>
|
||||
<External>nil</External>
|
||||
<Item class="Decal" referent="RBX0">
|
||||
<Properties>
|
||||
<token name="Face">5</token>
|
||||
<string name="Name">face</string>
|
||||
<float name="Shiny">20</float>
|
||||
<float name="Specular">0</float>
|
||||
<Content name="Texture"></Content>
|
||||
<bool name="archivable">true</bool>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
|
@ -23,10 +23,6 @@
|
|||
<category name="Private Servers">
|
||||
<link name="Private Server Transactions" route="admin.dashboard" />
|
||||
</category>
|
||||
|
||||
<category name="Catalog">
|
||||
<link name="Auto Uploader" route="admin.autoupload" />
|
||||
</category>
|
||||
</list>
|
||||
|
||||
<list name="Administration" color="primary" roleset="Administrator">
|
||||
|
|
@ -73,6 +69,12 @@
|
|||
<category name="Universe Status">
|
||||
<link name="Universe Status" route="admin.dashboard" />
|
||||
</category>
|
||||
|
||||
<category name="Catalog">
|
||||
<link name="Upload Asset" route="admin.assetupload" />
|
||||
<link name="Auto Uploader" route="admin.autoupload" />
|
||||
<link name="Uploaded Assets" route="admin.adminuploads" />
|
||||
</category>
|
||||
</list>
|
||||
|
||||
<list name="Owner" color="danger" roleset="Owner">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ mix.js('resources/js/app.js', 'public/js')
|
|||
.js('resources/js/pages/AvatarEditor.js', 'public/js')
|
||||
.js('resources/js/pages/Item.js', 'public/js')
|
||||
.js('resources/js/pages/Place.js', 'public/js')
|
||||
.js('resources/js/pages/ManualAssetUpload.js', 'public/js/adm')
|
||||
.js('resources/js/pages/ManualUserModeration.js', 'public/js/adm')
|
||||
.js('resources/js/pages/AppDeployer.js', 'public/js/adm')
|
||||
.js('resources/js/pages/SiteConfiguration.js', 'public/js/adm')
|
||||
|
|
|
|||
Loading…
Reference in New Issue