From 426778c3404a98163203222577420125c84eb486 Mon Sep 17 00:00:00 2001 From: Graphictoria Date: Sun, 29 Jan 2023 02:43:37 -0500 Subject: [PATCH] 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. --- web/app/Helpers/AssetHelper.php | 93 ++-- web/app/Helpers/GridHelper.php | 18 +- .../Http/Controllers/Api/AdminController.php | 170 ++++++++ .../Http/Controllers/Api/AvatarController.php | 10 +- .../Http/Controllers/Api/MoneyController.php | 7 +- .../Http/Controllers/Api/ShopController.php | 2 +- .../Controllers/Api/ThumbnailController.php | 7 +- .../Http/Controllers/Web/AdminController.php | 22 +- web/app/Jobs/ArbiterRender.php | 6 +- web/app/Models/AdminUpload.php | 36 ++ web/app/Models/User.php | 25 +- web/app/Models/UserAsset.php | 24 +- web/app/Models/asset.php | 46 +- .../Admin/Navigation/AssetUploader.php | 28 ++ .../2022_06_05_140351_create_assets_table.php | 2 +- ..._11_15_201917_create_asset_types_table.php | 3 + ...2_12_225210_create_admin_uploads_table.php | 35 ++ web/database/seeders/AssetTypeSeeder.php | 131 +++--- web/resources/js/components/AvatarEditor.js | 7 +- .../js/components/ManualAssetUpload.js | 401 ++++++++++++++++++ web/resources/js/pages/ManualAssetUpload.js | 18 + web/resources/sass/VirtuBrick.scss | 23 + .../admin/navigation/asset-uploader.blade.php | 5 + .../views/web/admin/autoupload.blade.php | 23 - .../web/admin/catalog/adminuploads.blade.php | 44 ++ .../web/admin/catalog/assetupload.blade.php | 18 + .../web/admin/catalog/autoupload.blade.php | 13 + web/routes/api.php | 6 +- web/routes/web.php | 8 +- web/storage/app/grid/game/BodyPart.xml | 14 + web/storage/app/grid/game/Face.xml | 14 + web/storage/app/layouts/Admin.xml | 10 +- web/webpack.mix.js | 1 + 33 files changed, 1103 insertions(+), 167 deletions(-) create mode 100644 web/app/Models/AdminUpload.php create mode 100644 web/app/View/Components/Admin/Navigation/AssetUploader.php create mode 100644 web/database/migrations/2023_02_12_225210_create_admin_uploads_table.php create mode 100644 web/resources/js/components/ManualAssetUpload.js create mode 100644 web/resources/js/pages/ManualAssetUpload.js create mode 100644 web/resources/views/components/admin/navigation/asset-uploader.blade.php delete mode 100644 web/resources/views/web/admin/autoupload.blade.php create mode 100644 web/resources/views/web/admin/catalog/adminuploads.blade.php create mode 100644 web/resources/views/web/admin/catalog/assetupload.blade.php create mode 100644 web/resources/views/web/admin/catalog/autoupload.blade.php create mode 100644 web/storage/app/grid/game/BodyPart.xml create mode 100644 web/storage/app/grid/game/Face.xml diff --git a/web/app/Helpers/AssetHelper.php b/web/app/Helpers/AssetHelper.php index d3afcc0..b72975b 100644 --- a/web/app/Helpers/AssetHelper.php +++ b/web/app/Helpers/AssetHelper.php @@ -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, diff --git a/web/app/Helpers/GridHelper.php b/web/app/Helpers/GridHelper.php index 5dedef3..ec80888 100644 --- a/web/app/Helpers/GridHelper.php +++ b/web/app/Helpers/GridHelper.php @@ -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(); diff --git a/web/app/Http/Controllers/Api/AdminController.php b/web/app/Http/Controllers/Api/AdminController.php index ccce65e..1c7d571 100644 --- a/web/app/Http/Controllers/Api/AdminController.php +++ b/web/app/Http/Controllers/Api/AdminController.php @@ -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) diff --git a/web/app/Http/Controllers/Api/AvatarController.php b/web/app/Http/Controllers/Api/AvatarController.php index 5d7a8e7..7e4d0cb 100644 --- a/web/app/Http/Controllers/Api/AvatarController.php +++ b/web/app/Http/Controllers/Api/AvatarController.php @@ -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]); diff --git a/web/app/Http/Controllers/Api/MoneyController.php b/web/app/Http/Controllers/Api/MoneyController.php index f06e439..db65652 100644 --- a/web/app/Http/Controllers/Api/MoneyController.php +++ b/web/app/Http/Controllers/Api/MoneyController.php @@ -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 ]; diff --git a/web/app/Http/Controllers/Api/ShopController.php b/web/app/Http/Controllers/Api/ShopController.php index 47d5cde..49b94aa 100644 --- a/web/app/Http/Controllers/Api/ShopController.php +++ b/web/app/Http/Controllers/Api/ShopController.php @@ -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() ]); } diff --git a/web/app/Http/Controllers/Api/ThumbnailController.php b/web/app/Http/Controllers/Api/ThumbnailController.php index 1bcd402..8fcd924 100644 --- a/web/app/Http/Controllers/Api/ThumbnailController.php +++ b/web/app/Http/Controllers/Api/ThumbnailController.php @@ -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( diff --git a/web/app/Http/Controllers/Web/AdminController.php b/web/app/Http/Controllers/Web/AdminController.php index 7ebff96..e61defb 100644 --- a/web/app/Http/Controllers/Web/AdminController.php +++ b/web/app/Http/Controllers/Web/AdminController.php @@ -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'); diff --git a/web/app/Jobs/ArbiterRender.php b/web/app/Jobs/ArbiterRender.php index 9507eef..c77edac 100644 --- a/web/app/Jobs/ArbiterRender.php +++ b/web/app/Jobs/ArbiterRender.php @@ -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. diff --git a/web/app/Models/AdminUpload.php b/web/app/Models/AdminUpload.php new file mode 100644 index 0000000..4cbcf71 --- /dev/null +++ b/web/app/Models/AdminUpload.php @@ -0,0 +1,36 @@ + + */ + 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'); + } +} diff --git a/web/app/Models/User.php b/web/app/Models/User.php index 7264c5b..2588661 100644 --- a/web/app/Models/User.php +++ b/web/app/Models/User.php @@ -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); diff --git a/web/app/Models/UserAsset.php b/web/app/Models/UserAsset.php index a190090..e45e114 100644 --- a/web/app/Models/UserAsset.php +++ b/web/app/Models/UserAsset.php @@ -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); + } + } } } diff --git a/web/app/Models/asset.php b/web/app/Models/asset.php index 8f0a7cc..582176f 100644 --- a/web/app/Models/asset.php +++ b/web/app/Models/asset.php @@ -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']; diff --git a/web/app/View/Components/Admin/Navigation/AssetUploader.php b/web/app/View/Components/Admin/Navigation/AssetUploader.php new file mode 100644 index 0000000..9cce5be --- /dev/null +++ b/web/app/View/Components/Admin/Navigation/AssetUploader.php @@ -0,0 +1,28 @@ +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); diff --git a/web/database/migrations/2022_11_15_201917_create_asset_types_table.php b/web/database/migrations/2022_11_15_201917_create_asset_types_table.php index f9228c3..39862ef 100644 --- a/web/database/migrations/2022_11_15_201917_create_asset_types_table.php +++ b/web/database/migrations/2022_11_15_201917_create_asset_types_table.php @@ -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(); }); } diff --git a/web/database/migrations/2023_02_12_225210_create_admin_uploads_table.php b/web/database/migrations/2023_02_12_225210_create_admin_uploads_table.php new file mode 100644 index 0000000..909b438 --- /dev/null +++ b/web/database/migrations/2023_02_12_225210_create_admin_uploads_table.php @@ -0,0 +1,35 @@ +id(); + + $table->unsignedBigInteger('asset_id'); + $table->unsignedBigInteger('uploader_id'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('admin_uploads'); + } +}; diff --git a/web/database/seeders/AssetTypeSeeder.php b/web/database/seeders/AssetTypeSeeder.php index 4fa89d0..9faa835 100644 --- a/web/database/seeders/AssetTypeSeeder.php +++ b/web/database/seeders/AssetTypeSeeder.php @@ -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', diff --git a/web/resources/js/components/AvatarEditor.js b/web/resources/js/components/AvatarEditor.js index cea89da..fcb1922 100644 --- a/web/resources/js/components/AvatarEditor.js +++ b/web/resources/js/components/AvatarEditor.js @@ -408,7 +408,12 @@ class OutfitsTab extends Component { } render() { - return (<>

outfits

); + return (<> +
+ +
+

outfits

+ ); } } diff --git a/web/resources/js/components/ManualAssetUpload.js b/web/resources/js/components/ManualAssetUpload.js new file mode 100644 index 0000000..eeb97a6 --- /dev/null +++ b/web/resources/js/components/ManualAssetUpload.js @@ -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:

PNG, JPG, JPEG, and other common image formats are supported.

, + sellable: false + }, + { + assetTypeId: 4, + name: 'Mesh', + type: 'fileupload', + extra:

Your mesh can be obj or Roblox's mesh format.

, + 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:

Heads are SpecialMeshes. Export it from studio, do not upload the mesh file here.

, + sellable: true + }, + { + assetTypeId: 18, + name: 'Face', + type: 'fileupload', + extra:

Faces are image files. The XML will be automatically generated.

, + sellable: true + }, + { + assetTypeId: 19, + name: 'Gear', + type: 'fileupload', + sellable: true + }, + { + assetTypeId: 27, + name: 'Torso', + type: 'packagepart', + extra:

Overlay ID displays atop clothing, base ID displays under clothing.

, + sellable: false + }, + { + assetTypeId: 28, + name: 'Right Arm', + type: 'packagepart', + extra:

Overlay ID displays atop clothing, base ID displays under clothing.

, + sellable: false + }, + { + assetTypeId: 29, + name: 'Left Arm', + type: 'packagepart', + extra:

Overlay ID displays atop clothing, base ID displays under clothing.

, + sellable: false + }, + { + assetTypeId: 30, + name: 'Left Leg', + type: 'packagepart', + extra:

Overlay ID displays atop clothing, base ID displays under clothing.

, + sellable: false + }, + { + assetTypeId: 31, + name: 'Right Leg', + type: 'packagepart', + extra:

Overlay ID displays atop clothing, base ID displays under clothing.

, + sellable: false + }, + { + assetTypeId: 32, + name: 'Package', + type: 'text', + extra:

Asset IDs for package. Example: 1;2;3;4;5

, + 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 ( + <> +

Create a { TypeName }

+
+
+ { Extra } + { + UploadType == 'fileupload' + ? + <> + + this.updateContentInputFile(e.target.files[0]) } /> + + : + UploadType == 'packagepart' + ? + <> + + + + + + + + : + UploadType == 'text' + && + <> + + + + } + + + + + + + { + Sellable + && + <> +
+

Sell this Item

+
+ this.updateInput(e, 'checked') } /> + +
+ { + this.state.requestInputs['on-sale'] + && +
+ +

 

+
+ +
+ } + + } +
+ +
+ + ) + } +} + +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 ( + <> +
+ +
+
+ { + this.state.successMessage + && +
{ this.state.successMessage } Click Here
+ } + { + this.state.errorMessage + && +
{ this.state.errorMessage }
+ } +
+ { + this.state.loading + && +
+ +
+ } + { + !this.state.createModelLoaded + ? + + : + this.activeModel + } +
+
+ + ); + } +} + +export default ManualAssetUpload; \ No newline at end of file diff --git a/web/resources/js/pages/ManualAssetUpload.js b/web/resources/js/pages/ManualAssetUpload.js new file mode 100644 index 0000000..11efa1a --- /dev/null +++ b/web/resources/js/pages/ManualAssetUpload.js @@ -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(, document.getElementById(assetUploadId)); + } +}); \ No newline at end of file diff --git a/web/resources/sass/VirtuBrick.scss b/web/resources/sass/VirtuBrick.scss index 7ff1185..a66538b 100644 --- a/web/resources/sass/VirtuBrick.scss +++ b/web/resources/sass/VirtuBrick.scss @@ -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 { diff --git a/web/resources/views/components/admin/navigation/asset-uploader.blade.php b/web/resources/views/components/admin/navigation/asset-uploader.blade.php new file mode 100644 index 0000000..87654aa --- /dev/null +++ b/web/resources/views/components/admin/navigation/asset-uploader.blade.php @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/resources/views/web/admin/autoupload.blade.php b/web/resources/views/web/admin/autoupload.blade.php deleted file mode 100644 index cecf564..0000000 --- a/web/resources/views/web/admin/autoupload.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -@extends('layouts.admin') - -@section('title', 'Auto Uploader') - -@push('content') -
-

Auto Uploader

-
- -
- - -
-
-
-@endpush \ No newline at end of file diff --git a/web/resources/views/web/admin/catalog/adminuploads.blade.php b/web/resources/views/web/admin/catalog/adminuploads.blade.php new file mode 100644 index 0000000..cc69d84 --- /dev/null +++ b/web/resources/views/web/admin/catalog/adminuploads.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.admin') + +@section('title', 'Uploaded Assets') + +@push('content') +
+ +

Uploaded Assets

+
+ @if(isset($uploads) && count($uploads) > 0) + + + + + + + + + + + @foreach($uploads as $upload) + @php + $asset = $upload->asset; + @endphp + + + + + + + @endforeach + +
AssetTypeUploaderCreated
{{ $asset->name }}{{ $asset->typeString() }} + + + + {{ $upload->created_at->isoFormat('l LT') }}
+ {{ $uploads->links('pagination.virtubrick') }} + @else +

No assets found.

+ @endif +
+
+@endpush \ No newline at end of file diff --git a/web/resources/views/web/admin/catalog/assetupload.blade.php b/web/resources/views/web/admin/catalog/assetupload.blade.php new file mode 100644 index 0000000..996c65e --- /dev/null +++ b/web/resources/views/web/admin/catalog/assetupload.blade.php @@ -0,0 +1,18 @@ +@extends('layouts.admin') + +@section('title', 'Upload Asset') + +@section('page-specific') + + +@endsection + +@push('content') +
+ +

Upload Asset

+
+ +
+
+@endpush \ No newline at end of file diff --git a/web/resources/views/web/admin/catalog/autoupload.blade.php b/web/resources/views/web/admin/catalog/autoupload.blade.php new file mode 100644 index 0000000..8e56738 --- /dev/null +++ b/web/resources/views/web/admin/catalog/autoupload.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.admin') + +@section('title', 'Auto Uploader') + +@push('content') +
+ +

Auto Uploader

+
+ +
+
+@endpush \ No newline at end of file diff --git a/web/routes/api.php b/web/routes/api.php index 8a7fd6a..43b3e74 100644 --- a/web/routes/api.php +++ b/web/routes/api.php @@ -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 diff --git a/web/routes/web.php b/web/routes/web.php index 44cda9e..9f5e5de 100644 --- a/web/routes/web.php +++ b/web/routes/web.php @@ -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'); diff --git a/web/storage/app/grid/game/BodyPart.xml b/web/storage/app/grid/game/BodyPart.xml new file mode 100644 index 0000000..90f7554 --- /dev/null +++ b/web/storage/app/grid/game/BodyPart.xml @@ -0,0 +1,14 @@ + + null + nil + + + + + + + + true + + + \ No newline at end of file diff --git a/web/storage/app/grid/game/Face.xml b/web/storage/app/grid/game/Face.xml new file mode 100644 index 0000000..7c7dbe3 --- /dev/null +++ b/web/storage/app/grid/game/Face.xml @@ -0,0 +1,14 @@ + + null + nil + + + 5 + face + 20 + 0 + + true + + + \ No newline at end of file diff --git a/web/storage/app/layouts/Admin.xml b/web/storage/app/layouts/Admin.xml index 4800526..366e0f1 100644 --- a/web/storage/app/layouts/Admin.xml +++ b/web/storage/app/layouts/Admin.xml @@ -23,10 +23,6 @@ - - - - @@ -73,6 +69,12 @@ + + + + + + diff --git a/web/webpack.mix.js b/web/webpack.mix.js index 90027e7..0d455f2 100644 --- a/web/webpack.mix.js +++ b/web/webpack.mix.js @@ -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')