I've been busy back here.
- Added setup endpoint. - Added cdn endpoint. - Indev games page and place page. - Universes for places. These are pretty much just a collection of places with shared things like datastores, player points, etc. - Moved asset types to the database. - Indev negotiation tickets. Only the migration has been added. - Indev client/studio deployments. Mostly complete. - Upgraded to fontawesome pro. - Moved the admin usage bars to their own component for cleanliness. - Created default place thumbnails. Not used at the moment. - Updated /asset to behave with the migration of asset types to the database. - "Busy" icon for a rendering game icon.
|
|
@ -0,0 +1,33 @@
|
|||
Deployer
|
||||
|
||||
Deploy or Revert switch
|
||||
|
||||
If deploy:
|
||||
- Generate a new version hash for studio and/or client.
|
||||
- Able to deploy both client/studio at the same time.
|
||||
- If no launcher provided, use launcher from previous version.
|
||||
- Set client/studio version in dynamic web config.
|
||||
|
||||
If revert:
|
||||
- Provide a dropdown list of recent versions with their version numbers
|
||||
i.e.: (Studio 1.0.0.0) version-abcdefghijk
|
||||
(Client 1.0.0.0) version-bbcdefghijk
|
||||
- Reset client/studio version in dynamic web config.
|
||||
|
||||
Studio/Client deployment buttons.
|
||||
- Create a new card for deploying the new studio/client.
|
||||
- Disable button if one exists on the view already.
|
||||
- Allow Xing out of a deployment box.
|
||||
- Standardized deployment interface.
|
||||
- Allow one or both.
|
||||
- On upload both should be pushed at the same time, not as they upload.
|
||||
|
||||
|
||||
Deployment Api
|
||||
|
||||
On deploy:
|
||||
- Create and return version hash.
|
||||
- /admin/v1/deploy should return a status, such as the ones thumbnails return. These should have a percentage and a message.
|
||||
- Message/percentage to be displayed on modal in deployment JS.
|
||||
- Call /admin/v1/deploy/version-hash
|
||||
- Validate if the version is supposed to be deployed.
|
||||
|
|
@ -62,17 +62,21 @@
|
|||
|
||||
# Api endpoints.
|
||||
<VirtualHost *:80 *:443>
|
||||
ServerName gtoria.net
|
||||
|
||||
ServerAlias api.gtoria.net
|
||||
ServerAlias apis.gtoria.net
|
||||
ServerAlias assetgame.gtoria.net
|
||||
ServerAlias data.gtoria.net
|
||||
ServerAlias gamepersistence.gtoria.net
|
||||
ServerAlias cdn.gtoria.net
|
||||
ServerAlias clientsettings.api.gtoria.net
|
||||
ServerAlias ephemeralcounters.api.gtoria.net
|
||||
ServerAlias versioncompatibility.api.gtoria.net
|
||||
ServerAlias logging.service.gtoria.net
|
||||
ServerAlias data.gtoria.net
|
||||
ServerAlias ecsv2.gtoria.net
|
||||
ServerAlias ephemeralcounters.api.gtoria.net
|
||||
ServerAlias gamepersistence.gtoria.net
|
||||
ServerAlias logging.service.gtoria.net
|
||||
ServerAlias setup.gtoria.net
|
||||
ServerAlias test.public.ecs.gtoria.net
|
||||
ServerAlias versioncompatibility.api.gtoria.net
|
||||
|
||||
SSLEngine on
|
||||
SSLCertificateFile "C:/graphictoria/COMMIT_HASH/etc/cert/cloudflare.crt"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
use App\Helpers\ValidationHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\AppDeployment;
|
||||
use App\Models\Deployment;
|
||||
use App\Rules\AppDeploymentFilenameRule;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
// Moderator+
|
||||
|
||||
|
||||
// Admin+
|
||||
|
||||
|
||||
// Owner+
|
||||
function deploy(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'version' => ['regex:/version\\-[a-fA-F0-9]{16}/'],
|
||||
'type' => ['required_without:version', 'regex:/(Deploy|Revert)/i'],
|
||||
'app' => ['required_without:version', 'regex:/(Client|Studio)/i']
|
||||
]);
|
||||
|
||||
if($validator->fails())
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
|
||||
$valid = $validator->valid();
|
||||
|
||||
$response = [
|
||||
'status' => 'Loading',
|
||||
'version' => null,
|
||||
'message' => 'Please wait...',
|
||||
'progress' => 0
|
||||
];
|
||||
|
||||
if(!$request->has('version'))
|
||||
{
|
||||
$deployment = Deployment::newVersionHash($valid);
|
||||
|
||||
$response['version'] = $deployment->version;
|
||||
$response['message'] = 'Created deployment.';
|
||||
$response['progress'] = 0;
|
||||
|
||||
return response($response);
|
||||
}
|
||||
|
||||
$deployment = Deployment::where('version', $valid['version'])->first();
|
||||
if($deployment === null || !$deployment->isValid()) {
|
||||
$validator->errors()->add('version', 'Unknown version deployment hash.');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
$response['version'] = $deployment->version;
|
||||
|
||||
if($deployment->error != null)
|
||||
{
|
||||
$response['status'] = 'Error';
|
||||
$response['message'] = sprintf('Failed to deploy %s. Error: %s', $deployment->version, $deployment->error);
|
||||
$response['progress'] = 1;
|
||||
return response($response);
|
||||
}
|
||||
|
||||
$steps = 5;
|
||||
$response['progress'] = $deployment->step/$steps;
|
||||
switch($deployment->step)
|
||||
{
|
||||
case 0:
|
||||
$response['message'] = 'Files uploading.';
|
||||
break;
|
||||
case 1:
|
||||
$response['message'] = 'Batching deployment.';
|
||||
break;
|
||||
case 2:
|
||||
$response['message'] = 'Unpacking files.';
|
||||
break;
|
||||
case 3:
|
||||
$response['message'] = 'Updating version security.';
|
||||
break;
|
||||
case 4:
|
||||
$response['message'] = 'Pushing deployment to setup.';
|
||||
break;
|
||||
case 5:
|
||||
$response['status'] = 'Success';
|
||||
$response['message'] = sprintf('Deploy completed. Successfully deployed %s %s', $deployment->app, $deployment->version);
|
||||
break;
|
||||
}
|
||||
|
||||
return response($response);
|
||||
}
|
||||
|
||||
function deployVersion(Request $request, string $version)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'file.*' => ['required']
|
||||
]);
|
||||
|
||||
if($validator->fails())
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
|
||||
$valid = $validator->valid();
|
||||
|
||||
$deployment = Deployment::where('version', $version)->first();
|
||||
if($deployment === null || !$deployment->isValid() || $deployment->step != 0) {
|
||||
$validator->errors()->add('version', 'Unknown version deployment hash.');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
$deploymentRule = new AppDeploymentFilenameRule($deployment->app);
|
||||
if(!$deploymentRule->passes('file', $request->file('file')))
|
||||
{
|
||||
$deployment->error = 'Missing files.';
|
||||
$deployment->save();
|
||||
|
||||
$validator->errors()->add('file', $deployment->error);
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
foreach($request->file('file') as $file)
|
||||
{
|
||||
$file->storeAs(
|
||||
'setuptmp',
|
||||
sprintf('%s-%s', $version, $file->getClientOriginalName())
|
||||
);
|
||||
}
|
||||
|
||||
$deployment->step = 1; // Batching deployment.
|
||||
$deployment->save();
|
||||
|
||||
AppDeployment::dispatch($deployment);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NegotiationTicket;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
function generateAuthTicket()
|
||||
{
|
||||
$ticket = Str::random(100);
|
||||
|
||||
NegotiationTicket::create([
|
||||
'ticket' => $ticket,
|
||||
'userId' => Auth::user()->id
|
||||
]);
|
||||
|
||||
return response($ticket)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\ValidationHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Universe;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GamesController extends Controller
|
||||
{
|
||||
protected static function getAssets()
|
||||
{
|
||||
// TODO: XlXi: sort also based on how many people are in open servers
|
||||
return Universe::where('public', true)
|
||||
->whereRelation('starterPlace', 'moderated', false)
|
||||
->join('assets', 'assets.id', '=', 'universes.startPlaceId')
|
||||
->orderBy('assets.created_at', 'desc')
|
||||
->orderBy('assets.visits', 'desc');
|
||||
}
|
||||
|
||||
protected function listJson(Request $request)
|
||||
{
|
||||
$assets = self::getAssets()->paginate(30);
|
||||
|
||||
$data = [];
|
||||
foreach($assets as $asset) {
|
||||
$asset = $asset->starterPlace;
|
||||
$creator = $asset->user;
|
||||
|
||||
array_push($data, [
|
||||
'Name' => $asset->universe->name,
|
||||
'Creator' => [
|
||||
'Name' => $creator->username,
|
||||
'Url' => $creator->getProfileUrl()
|
||||
],
|
||||
'Playing' => 0,
|
||||
'Ratio' => 0,
|
||||
'Url' => route('games.asset', ['asset' => $asset->id, 'assetName' => Str::slug($asset->name, '-')])
|
||||
]);
|
||||
}
|
||||
|
||||
return response([
|
||||
'pages' => ($assets->hasPages() ? $assets->lastPage() : 1),
|
||||
'data' => $data
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
|
|||
use App\Helpers\ValidationHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Shout;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ class ThumbnailController extends Controller
|
|||
|
||||
// TODO: XlXi: Turn this into a switch case and fill in the rest of the unrenderables.
|
||||
// Things like HTML assets should just have a generic "default" image.
|
||||
if($model->assetTypeId == 1)
|
||||
$model = Asset::where('id', $model->parentAsset)->first();
|
||||
//if($model->assetTypeId == 1)
|
||||
// $model = Asset::where('id', $model->parentAsset)->first();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Setup;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DynamicWebConfiguration;
|
||||
|
||||
class SetupController extends Controller
|
||||
{
|
||||
public function getFile(Request $request, $file)
|
||||
{
|
||||
$file = basename($file);
|
||||
$filePath = Storage::path('setup/' . $file);
|
||||
|
||||
if(!file_exists($filePath) || strtolower($file) == '.gitignore' || str_ends_with(strtolower($file), 'pdb.zip'))
|
||||
return response('404 not found.', 404)
|
||||
->header('Content-Type', 'text/plain');
|
||||
|
||||
return response()->file($filePath);
|
||||
}
|
||||
|
||||
public function getClientVersion()
|
||||
{
|
||||
return response(DynamicWebConfiguration::where('name', 'ClientUploadVersion')->first()->value)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
public function getStudioVersion()
|
||||
{
|
||||
return response(DynamicWebConfiguration::where('name', 'StudioUploadVersion')->first()->value)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,16 @@ use App\Models\DynamicWebConfiguration;
|
|||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
function getJs(Request $request, string $jsFile)
|
||||
{
|
||||
$filePath = public_path('js/adm/' . basename($jsFile));
|
||||
|
||||
if(!file_exists($filePath))
|
||||
abort(404);
|
||||
|
||||
return response()->file($filePath);
|
||||
}
|
||||
|
||||
// Moderator+
|
||||
function dashboard()
|
||||
{
|
||||
|
|
@ -16,6 +26,11 @@ class AdminController extends Controller
|
|||
}
|
||||
|
||||
// Admin+
|
||||
function metricsVisualization()
|
||||
{
|
||||
return view('web.admin.metricsvisualization');
|
||||
}
|
||||
|
||||
function arbiterDiag(Request $request, string $arbiterType = null)
|
||||
{
|
||||
return view('web.admin.arbiter.diag')->with([
|
||||
|
|
@ -39,4 +54,9 @@ class AdminController extends Controller
|
|||
'values' => DynamicWebConfiguration::get()
|
||||
]);
|
||||
}
|
||||
|
||||
function deployer()
|
||||
{
|
||||
return view('web.admin.deployer');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ class ClientController extends Controller
|
|||
if(
|
||||
!($asset->onSale || (Auth::check() && Auth::user()->id == $asset->creatorId)) // not on sale and not the creator
|
||||
&&
|
||||
!($asset->copyable()) // asset isn't defaulted to open source
|
||||
&&
|
||||
!GridHelper::hasAllAccess() // not grid
|
||||
) {
|
||||
$validator->errors()->add('id', 'You do not have access to this asset.');
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GamesController extends Controller
|
||||
{
|
||||
|
|
@ -11,4 +13,23 @@ class GamesController extends Controller
|
|||
{
|
||||
return view('web.games.index');
|
||||
}
|
||||
|
||||
public function showGame(Request $request, Asset $asset, string $assetName = null)
|
||||
{
|
||||
$assetSlug = Str::slug($asset->name, '-');
|
||||
|
||||
if($asset->moderated)
|
||||
abort(404);
|
||||
|
||||
if($asset->assetTypeId != 9) // Place
|
||||
return redirect()->route('shop.asset', ['asset' => $asset->id, 'assetName' => $assetSlug]);
|
||||
|
||||
if ($assetName != $assetSlug)
|
||||
return redirect()->route('games.asset', ['asset' => $asset->id, 'assetName' => $assetSlug]);
|
||||
|
||||
return view('web.games.asset')->with([
|
||||
'title' => sprintf('%s by %s', $asset->universe->name, $asset->user->username),
|
||||
'asset' => $asset
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,14 @@ class ShopController extends Controller
|
|||
|
||||
public function showAsset(Request $request, Asset $asset, string $assetName = null)
|
||||
{
|
||||
if ($asset->moderated)
|
||||
$assetSlug = Str::slug($asset->name, '-');
|
||||
|
||||
if($asset->moderated)
|
||||
abort(404);
|
||||
|
||||
$assetSlug = Str::slug($asset->name, '-');
|
||||
if($asset->assetTypeId == 9) // Place
|
||||
return redirect()->route('games.asset', ['asset' => $asset->id, 'assetName' => $assetSlug]);
|
||||
|
||||
if ($assetName != $assetSlug)
|
||||
return redirect()->route('shop.asset', ['asset' => $asset->id, 'assetName' => $assetSlug]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use COM;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use ZipArchive;
|
||||
|
||||
use App\Models\Deployment;
|
||||
use App\Models\DynamicWebConfiguration;
|
||||
|
||||
class AppDeployment implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $deployment;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Deployment $deployment)
|
||||
{
|
||||
$this->deployment = $deployment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->deployment->step = 2; // Unpacking files.
|
||||
$this->deployment->save();
|
||||
|
||||
$workingDirectory = storage_path(sprintf('app/setuptmp/%s', $this->deployment->version));
|
||||
Storage::makeDirectory($workingDirectory);
|
||||
|
||||
$appArchive = '';
|
||||
$appName = '';
|
||||
$bootstrapperName = '';
|
||||
$bootstrapperVersionName = '';
|
||||
switch($this->deployment->app)
|
||||
{
|
||||
case 'client':
|
||||
$appArchive = 'GraphictoriaApp.zip';
|
||||
$appName = 'GraphictoriaPlayer.exe';
|
||||
$bootstrapperName = 'GraphictoriaPlayerLauncher.exe';
|
||||
$bootstrapperVersionName = 'BootstrapperVersion.txt';
|
||||
break;
|
||||
case 'studio':
|
||||
$appArchive = 'GraphictoriaStudio.zip';
|
||||
$appName = 'GraphictoriaStudio.exe';
|
||||
$bootstrapperName = 'GraphictoriaStudioLauncherBeta.exe';
|
||||
$bootstrapperVersionName = 'BootstrapperQTStudioVersion.txt';
|
||||
break;
|
||||
}
|
||||
$bootstrapperLocation = sprintf('%s/../%s-%s', $workingDirectory, $this->deployment->version, $bootstrapperName);
|
||||
|
||||
$zip = new ZipArchive();
|
||||
$zip->open(sprintf(
|
||||
'%s/../%s-%s',
|
||||
$workingDirectory,
|
||||
$this->deployment->version,
|
||||
$appArchive
|
||||
));
|
||||
$zip->extractTo($workingDirectory);
|
||||
$zip->close();
|
||||
|
||||
$this->deployment->step = 3; // Updating version security.
|
||||
$this->deployment->save();
|
||||
|
||||
// XlXi: this will not work on linux.
|
||||
$fso = new COM("Scripting.FileSystemObject");
|
||||
$appVersion = $fso->GetFileVersion(sprintf('%s/%s', $workingDirectory, $appName));
|
||||
$bootstrapperVersion = $fso->GetFileVersion($bootstrapperLocation);
|
||||
|
||||
$hashConfig = DynamicWebConfiguration::where('name', sprintf('%sUploadVersion', $this->deployment->app))->first();
|
||||
$versionConfig = DynamicWebConfiguration::where('name', sprintf('%sDeployVersion', $this->deployment->app))->first();
|
||||
$launcherConfig = DynamicWebConfiguration::where('name', sprintf('%sLauncherDeployVersion', $this->deployment->app))->first();
|
||||
|
||||
$hashConfig->value = $this->deployment->version;
|
||||
$versionConfig->value = $appVersion;
|
||||
$launcherConfig->value = $bootstrapperVersion;
|
||||
$hashConfig->save();
|
||||
$versionConfig->save();
|
||||
$launcherConfig->save();
|
||||
|
||||
$this->deployment->step = 4; // Pushing to setup.
|
||||
$this->deployment->save();
|
||||
|
||||
Storage::copy(sprintf('setuptmp/%s-%s', $this->deployment->version, $bootstrapperName), sprintf('setup/%s', $bootstrapperName));
|
||||
Storage::put(sprintf('setup/%s-%s', $this->deployment->version, $bootstrapperVersionName), str_replace('.', ', ', $bootstrapperVersion));
|
||||
Storage::put(sprintf('setup/%s-gtManifest.txt', $this->deployment->version), '');
|
||||
|
||||
$files = Storage::files('setuptmp');
|
||||
foreach($files as $file)
|
||||
{
|
||||
$fileName = str_replace('setuptmp/', '', $file);
|
||||
|
||||
if(str_starts_with($fileName, $this->deployment->version))
|
||||
Storage::move($file, sprintf('setup/%s', $fileName));
|
||||
}
|
||||
|
||||
Storage::deleteDirectory(sprintf('setuptmp/%s', $this->deployment->version));
|
||||
|
||||
$this->deployment->step = 5; // Success.
|
||||
$this->deployment->save();
|
||||
}
|
||||
|
||||
public function failed($exception)
|
||||
{
|
||||
$this->deployment->error = $exception->getMessage();
|
||||
$this->deployment->save();
|
||||
}
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ class ArbiterRender implements ShouldQueue
|
|||
{
|
||||
// TODO: XlXi: User avatar/closeup render support.
|
||||
$arguments = [
|
||||
url(sprintf('/asset?id=%d', $this->assetId)), // TODO: XlXi: Move url() to route once the route actually exists.
|
||||
url(sprintf('/asset?id=%d', $this->assetId)), // TODO: XlXi: Move url() to route() once the route actually exists.
|
||||
($this->is3D ? 'OBJ' : 'PNG'),
|
||||
840, // Width
|
||||
840, // Height
|
||||
|
|
@ -103,6 +103,8 @@ class ArbiterRender implements ShouldQueue
|
|||
array_push($arguments, '27113661;25251154'); // Custom Texture URLs (shirt and pands)
|
||||
break;
|
||||
case 'Place':
|
||||
$arguments[2] = 768*4; // XlXi: These get scaled down by 4.
|
||||
$arguments[3] = 432*4; // XlXi: These get scaled down by 4.
|
||||
array_push($arguments, '0'); // TODO: XlXi: Universe IDs
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AssetType extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Deployment 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 = [
|
||||
'version',
|
||||
'app',
|
||||
'type'
|
||||
];
|
||||
|
||||
static function newVersionHash($config)
|
||||
{
|
||||
// XlXi: We want a GUID here, not a UUID. This is why we're not using Str::uuid().
|
||||
$hash = preg_replace('/[^a-z0-9]+/i', '', com_create_guid());
|
||||
$hash = substr($hash, 0, 16);
|
||||
$hash = strtolower($hash);
|
||||
|
||||
return self::create([
|
||||
'version' => sprintf('version-%s', $hash),
|
||||
'app' => strtolower($config['app']),
|
||||
'type' => strtolower($config['type'])
|
||||
]);
|
||||
}
|
||||
|
||||
function isValid()
|
||||
{
|
||||
$isValid = $this->created_at > Carbon::now()->subMinute(3);
|
||||
if(!$isValid)
|
||||
{
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NegotiationTicket extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'ticket',
|
||||
'userId'
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Universe extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function starterPlace()
|
||||
{
|
||||
return $this->belongsTo(Asset::class, 'startPlaceId');
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,17 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
use App\Models\AssetType;
|
||||
|
||||
/*
|
||||
TODO: XlXi: game performance priority system
|
||||
where games can be chosen to have a higher
|
||||
thread count on RCC.
|
||||
|
||||
TODO: XlXi: game reccomendations, split words of title
|
||||
SELECT articleID, COUNT(keyword) FROM keyword WHERE keyword IN (A, B, C) GROUP BY articleID ORDER BY COUNT(keyword) DESC
|
||||
*/
|
||||
|
||||
class Asset extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
|
@ -21,85 +32,7 @@ class Asset extends Model
|
|||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $assetTypes = [
|
||||
/* 0 */ 'Product',
|
||||
/* 1 */ 'Image',
|
||||
/* 2 */ 'T-Shirt',
|
||||
/* 3 */ 'Audio',
|
||||
/* 4 */ 'Mesh',
|
||||
/* 5 */ 'Lua',
|
||||
/* 6 */ 'HTML',
|
||||
/* 7 */ 'Text',
|
||||
/* 8 */ 'Hat',
|
||||
/* 9 */ 'Place',
|
||||
/* 10 */ 'Model',
|
||||
/* 11 */ 'Shirt',
|
||||
/* 12 */ 'Pants',
|
||||
/* 13 */ 'Decal',
|
||||
/* 14 */ null, // Doesn't exist on Roblox.
|
||||
/* 15 */ null, // Doesn't exist on Roblox.
|
||||
/* 16 */ 'Avatar',
|
||||
/* 17 */ 'Head',
|
||||
/* 18 */ 'Face',
|
||||
/* 19 */ 'Gear',
|
||||
/* 20 */ null, // Doesn't exist on Roblox.
|
||||
/* 21 */ 'Badge',
|
||||
/* 22 */ 'Group Emblem',
|
||||
/* 23 */ null, // Doesn't exist on Roblox.
|
||||
/* 24 */ 'Animation',
|
||||
/* 25 */ 'Arms',
|
||||
/* 26 */ 'Legs',
|
||||
/* 27 */ 'Torso',
|
||||
/* 28 */ 'Right Arm',
|
||||
/* 29 */ 'Left Arm',
|
||||
/* 30 */ 'Left Leg',
|
||||
/* 31 */ 'Right Leg',
|
||||
/* 32 */ 'Package',
|
||||
/* 33 */ 'YouTubeVideo',
|
||||
/* 34 */ 'Game Pass',
|
||||
/* 35 */ 'App',
|
||||
/* 36 */ null, // Doesn't exist on Roblox.
|
||||
/* 37 */ 'Code',
|
||||
/* 38 */ 'Plugin',
|
||||
/* 39 */ 'SolidModel',
|
||||
/* 40 */ 'MeshPart',
|
||||
/* 41 */ 'Hair Accessory',
|
||||
/* 42 */ 'Face Accessory',
|
||||
/* 43 */ 'Neck Accessory',
|
||||
/* 44 */ 'Shoulder Accessory',
|
||||
/* 45 */ 'Front Accessory',
|
||||
/* 46 */ 'Back Accessory',
|
||||
/* 47 */ 'Waist Accessory',
|
||||
/* 48 */ 'Climb Animation',
|
||||
/* 49 */ 'Death Animation',
|
||||
/* 50 */ 'Fall Animation',
|
||||
/* 51 */ 'Idle Animation',
|
||||
/* 52 */ 'Jump Animation',
|
||||
/* 53 */ 'Run Animation',
|
||||
/* 54 */ 'Swim Animation',
|
||||
/* 55 */ 'Walk Animation',
|
||||
/* 56 */ 'Pose Animation',
|
||||
/* 57 */ 'Ear Accessory',
|
||||
/* 58 */ 'Eye Accessory',
|
||||
/* 59 */ 'LocalizationTableManifest',
|
||||
/* 60 */ 'LocalizationTableTranslation',
|
||||
/* 61 */ 'Emote Animation',
|
||||
/* 62 */ 'Video',
|
||||
/* 63 */ 'TexturePack',
|
||||
/* 64 */ 'T-Shirt Accessory',
|
||||
/* 65 */ 'Shirt Accessory',
|
||||
/* 66 */ 'Pants Accessory',
|
||||
/* 67 */ 'Jacket Accessory',
|
||||
/* 68 */ 'Sweater Accessory',
|
||||
/* 69 */ 'Shorts Accessory',
|
||||
/* 70 */ 'Left Shoe Accessory',
|
||||
/* 71 */ 'Right Shoe Accessory',
|
||||
/* 72 */ 'Dress Skirt Accessory',
|
||||
/* 73 */ 'Font Family',
|
||||
/* 74 */ 'Font Face',
|
||||
/* 75 */ 'MeshHiddenSurfaceRemoval'
|
||||
];
|
||||
|
||||
/* TODO: XlXi: move to db */
|
||||
protected $assetGenres = [
|
||||
/* 0 */ 'All',
|
||||
/* 1 */ 'Town And City',
|
||||
|
|
@ -119,6 +52,7 @@ class Asset extends Model
|
|||
/* 15 */ 'RPG'
|
||||
];
|
||||
|
||||
/* TODO: XlXi: move to db */
|
||||
protected $gearAssetGenres = [
|
||||
/* 0 */ 'Melee Weapon',
|
||||
/* 1 */ 'Ranged Weapon',
|
||||
|
|
@ -131,6 +65,11 @@ class Asset extends Model
|
|||
/* 8 */ 'Personal Transport'
|
||||
];
|
||||
|
||||
public function assetType()
|
||||
{
|
||||
return $this->belongsTo(AssetType::class, 'assetTypeId');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creatorId');
|
||||
|
|
@ -138,85 +77,44 @@ class Asset extends Model
|
|||
|
||||
public function parentAsset()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'parentAssetId');
|
||||
return $this->belongsTo(Asset::class, 'parentAssetId');
|
||||
}
|
||||
|
||||
public function typeString()
|
||||
{
|
||||
return $this->assetTypes[$this->assetTypeId];
|
||||
}
|
||||
public function universe()
|
||||
{
|
||||
return $this->belongsTo(Universe::class, 'universeId');
|
||||
}
|
||||
|
||||
public function latestVersion()
|
||||
{
|
||||
return $this->belongsTo(AssetVersion::class, 'assetVersionId');
|
||||
}
|
||||
|
||||
public function typeString()
|
||||
{
|
||||
return $this->assetType->name;
|
||||
}
|
||||
|
||||
public function isWearable()
|
||||
{
|
||||
switch($this->assetTypeId)
|
||||
{
|
||||
case 2: // T-Shirt
|
||||
case 8: // Hat
|
||||
case 11: // Shirt
|
||||
case 12: // Pants
|
||||
case 17: // Head
|
||||
case 18: // Face
|
||||
case 19: // Gear
|
||||
case 25: // Arms
|
||||
case 26: // Legs
|
||||
case 27: // Torso
|
||||
case 28: // Right Arm
|
||||
case 29: // Left Arm
|
||||
case 30: // Left Leg
|
||||
case 31: // Right Leg
|
||||
case 32: // Package
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->assetType->wearable;
|
||||
}
|
||||
|
||||
public function isRenderable()
|
||||
{
|
||||
switch($this->assetTypeId)
|
||||
{
|
||||
case 2: // T-Shirt
|
||||
case 4: // Mesh
|
||||
case 8: // Hat
|
||||
case 9: // Place
|
||||
case 10: // Model
|
||||
case 11: // Shirt
|
||||
case 12: // Pants
|
||||
case 13: // Decal
|
||||
case 17: // Head
|
||||
case 18: // Face
|
||||
case 19: // Gear
|
||||
case 25: // Arms
|
||||
case 26: // Legs
|
||||
case 27: // Torso
|
||||
case 28: // Right Arm
|
||||
case 29: // Left Arm
|
||||
case 30: // Left Leg
|
||||
case 31: // Right Leg
|
||||
case 32: // Package
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->assetType->renderable;
|
||||
}
|
||||
|
||||
public function canRender3D()
|
||||
{
|
||||
switch($this->assetTypeId)
|
||||
{
|
||||
case 9: // Place
|
||||
case 10: // Model
|
||||
case 13: // Decal
|
||||
case 18: // Face
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isRenderable();
|
||||
return $this->assetType->renderable3d;
|
||||
}
|
||||
|
||||
// XlXi: copyable() is when an asset is freely available for download
|
||||
// on /asset regardless of whether or not its on sale or owned.
|
||||
public function copyable()
|
||||
{
|
||||
return $this->assetType->copyable;
|
||||
}
|
||||
|
||||
public function getThumbnail()
|
||||
|
|
@ -225,12 +123,12 @@ class Asset extends Model
|
|||
|
||||
// TODO: XlXi: Turn this into a switch case and fill in the rest of the unrenderables.
|
||||
// Things like HTML assets should just have a generic "default" image.
|
||||
if($this->assetTypeId == 1) // Image
|
||||
$renderId = $this->parentAsset->id;
|
||||
//if($this->assetTypeId == 1) // Image
|
||||
// $renderId = $this->parentAsset->id;
|
||||
|
||||
$thumbnail = Http::get(route('thumbnails.v1.asset', ['id' => $renderId, 'type' => '2d']));
|
||||
if($thumbnail->json('status') == 'loading')
|
||||
return 'https://gtoria.local/images/busy/asset.png';
|
||||
return ($this->assetTypeId == 9 ? 'https://gtoria.local/images/busy/game.png' : 'https://gtoria.local/images/busy/asset.png');
|
||||
|
||||
return $thumbnail->json('data');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,14 @@ class RouteServiceProvider extends ServiceProvider
|
|||
->middleware('api')
|
||||
->namespace('App\Http\Controllers\Cdn')
|
||||
->group(base_path('routes/cdn.php'));
|
||||
|
||||
//
|
||||
// Domain: setup.gtoria.net
|
||||
//
|
||||
Route::domain('setup.' . DomainHelper::TopLevelDomain())
|
||||
->middleware('api')
|
||||
->namespace('App\Http\Controllers\Setup')
|
||||
->group(base_path('routes/setup.php'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class AppDeploymentFilenameRule implements Rule
|
||||
{
|
||||
protected $appType;
|
||||
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
*
|
||||
* @param string $appType
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $appType)
|
||||
{
|
||||
$this->appType = $appType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
if(!$value)
|
||||
return false;
|
||||
|
||||
$files = [
|
||||
'content-fonts.zip',
|
||||
'content-music.zip',
|
||||
'content-particles.zip',
|
||||
'content-sky.zip',
|
||||
'content-sounds.zip',
|
||||
'content-terrain.zip',
|
||||
'content-textures.zip',
|
||||
'content-textures2.zip',
|
||||
'content-textures3.zip',
|
||||
'shaders.zip',
|
||||
'redist.zip',
|
||||
'libraries.zip'
|
||||
];
|
||||
|
||||
if($this->appType == 'client')
|
||||
{
|
||||
array_push($files, ...[
|
||||
'playerpdb.zip',
|
||||
'graphictoria.zip',
|
||||
'graphictoriaplayerlauncher.exe'
|
||||
]);
|
||||
}
|
||||
elseif($this->appType == 'studio')
|
||||
{
|
||||
array_push($files, ...[
|
||||
'builtinplugins.zip',
|
||||
'imageformats.zip',
|
||||
'content-scripts.zip',
|
||||
'studiopdb.zip',
|
||||
'graphictoriastudio.zip',
|
||||
'graphictoriastudiolauncherbeta.exe'
|
||||
]);
|
||||
}
|
||||
|
||||
$neededFiles = count($files);
|
||||
if(count($value) != $neededFiles)
|
||||
return false;
|
||||
|
||||
foreach($value as $file)
|
||||
{
|
||||
if(!in_array(strtolower($file->getClientOriginalName()), $files))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return 'Missing files.';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components\Admin;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class UsageBar 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.usage-bar');
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ return new class extends Migration
|
|||
$table->text('user_agent')->nullable();
|
||||
$table->text('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
$table->boolean('clientAuthenticated')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ return new class extends Migration
|
|||
$table->boolean('approved')->default(false);
|
||||
$table->boolean('moderated')->default(false);
|
||||
|
||||
$table->unsignedBigInteger('favorites')->default(0);
|
||||
$table->unsignedBigInteger('upVotes')->default(0);
|
||||
$table->unsignedBigInteger('downVotes')->default(0);
|
||||
|
||||
$table->unsignedBigInteger('priceInTokens')->default(15);
|
||||
$table->unsignedBigInteger('sales')->default(0);
|
||||
$table->boolean('onSale')->default(false);
|
||||
|
|
@ -32,6 +36,14 @@ return new class extends Migration
|
|||
$table->unsignedSmallInteger('assetAttributeId')->nullable();
|
||||
$table->unsignedBigInteger('assetVersionId')->comment('The most recent version id for the asset. This is used internally as asset version 0 when using the /asset api.');
|
||||
|
||||
$table->unsignedBigInteger('universeId')->nullable();
|
||||
$table->unsignedBigInteger('onlinePlayers')->default(0);
|
||||
$table->unsignedBigInteger('visits')->default(0);
|
||||
$table->unsignedBigInteger('maxPlayers')->default(10);
|
||||
$table->unsignedTinyInteger('chatStyleEnum')->default(2);
|
||||
$table->boolean('uncopylocked')->default(false);
|
||||
|
||||
$table->string('iconHash')->nullable();
|
||||
$table->string('thumbnail2DHash')->nullable();
|
||||
$table->string('thumbnail3DHash')->nullable();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?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('universes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->unsignedBigInteger('creatorId');
|
||||
$table->string('name');
|
||||
|
||||
$table->unsignedBigInteger('startPlaceId');
|
||||
|
||||
$table->boolean('public')->default(false);
|
||||
$table->boolean('studioApiServices')->default(false);
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('universes');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?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('asset_types', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('id')->unique()->nullable();
|
||||
$table->string('name')->nullable();
|
||||
$table->boolean('wearable')->default(false);
|
||||
$table->boolean('renderable')->default(false);
|
||||
$table->boolean('renderable3d')->default(false);
|
||||
$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->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('asset_types');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?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('negotiation_tickets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('ticket');
|
||||
$table->unsignedBigInteger('userId');
|
||||
$table->boolean('used')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('negotiation_tickets');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?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('deployments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('version');
|
||||
$table->string('app');
|
||||
$table->string('type');
|
||||
$table->int('step')->default(0);
|
||||
$table->string('error')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('deployments');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
use App\Models\AssetType;
|
||||
|
||||
class AssetTypeSeeder extends Seeder
|
||||
{
|
||||
/* Default asset types */
|
||||
protected $assetTypes = [
|
||||
[
|
||||
'name' => 'Product',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Image',
|
||||
'renderable' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'T-Shirt',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Audio',
|
||||
'copyable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Mesh',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Lua',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'HTML',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Text',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Hat',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Place',
|
||||
'renderable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Model',
|
||||
'renderable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Shirt',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Pants',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Decal',
|
||||
'renderable' => true,
|
||||
'copyable' => true
|
||||
],
|
||||
null,
|
||||
null,
|
||||
[
|
||||
'name' => 'Avatar',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Head',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Face',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Gear',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
null,
|
||||
[
|
||||
'name' => 'Badge',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Group Emblem',
|
||||
'renderable' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
null,
|
||||
[
|
||||
'name' => 'Animation',
|
||||
'copyable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Arms',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Legs',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Torso',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Right Arm',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Left Arm',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Left Leg',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Right Leg',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Package',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'YouTubeVideo',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Game Pass',
|
||||
'sellable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'App',
|
||||
'locked' => true
|
||||
],
|
||||
null,
|
||||
[
|
||||
'name' => 'Code',
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Plugin',
|
||||
'sellable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'SolidModel',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'MeshPart',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Hair Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Face Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Neck Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Shoulder Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Front Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Back Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Waist Accessory',
|
||||
'renderable' => true,
|
||||
'renderable3d' => true,
|
||||
'copyable' => true,
|
||||
'sellable' => true,
|
||||
'wearable' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Climb Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Death Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Fall Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Idle Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Jump Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Run Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Swim Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Walk Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Pose Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Ear Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Eye Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'LocalizationTableManifest',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'LocalizationTableTranslation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Emote Animation',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Video',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'TexturePack',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'T-Shirt Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Shirt Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Pants Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Jacket Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Sweater Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Shorts Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Left Shoe Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Right Shoe Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Dress Skirt Accessory',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Font Family',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Font Face',
|
||||
'locked' => true
|
||||
],
|
||||
[
|
||||
'name' => 'MeshHiddenSurfaceRemoval',
|
||||
'locked' => true
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
foreach($this->assetTypes as $typeId => $assetType) {
|
||||
AssetType::create($assetType != null ? array_merge(['id' => $typeId], $assetType) : ['name'=>null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
|
|||
{
|
||||
$this->call([
|
||||
WebConfigurationSeeder::class,
|
||||
AssetTypeSeeder::class,
|
||||
UsageCounterSeeder::class,
|
||||
RolesetSeeder::class
|
||||
//FFlagSeeder::class
|
||||
|
|
|
|||
|
|
@ -53,5 +53,35 @@ class WebConfigurationSeeder extends Seeder
|
|||
'name' => 'ThumbnailArbiterIP',
|
||||
'value' => '127.0.0.1'
|
||||
]);
|
||||
|
||||
DynamicWebConfiguration::create([
|
||||
'name' => 'ClientUploadVersion',
|
||||
'value' => 'version-unknown'
|
||||
]);
|
||||
|
||||
DynamicWebConfiguration::create([
|
||||
'name' => 'ClientDeployVersion',
|
||||
'value' => '0.0.0.0'
|
||||
]);
|
||||
|
||||
DynamicWebConfiguration::create([
|
||||
'name' => 'ClientLauncherDeployVersion',
|
||||
'value' => '0, 0, 0, 0'
|
||||
]);
|
||||
|
||||
DynamicWebConfiguration::create([
|
||||
'name' => 'StudioUploadVersion',
|
||||
'value' => 'version-unknown'
|
||||
]);
|
||||
|
||||
DynamicWebConfiguration::create([
|
||||
'name' => 'StudioDeployVersion',
|
||||
'value' => '0.0.0.0'
|
||||
]);
|
||||
|
||||
DynamicWebConfiguration::create([
|
||||
'name' => 'StudioLauncherDeployVersion',
|
||||
'value' => '0, 0, 0, 0'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -10,8 +10,6 @@ import Twemoji from 'react-twemoji';
|
|||
import { buildGenericApiUrl } from '../util/HTTP.js';
|
||||
import Loader from './Loader';
|
||||
|
||||
const commentsId = 'gt-comments'; // XlXi: Keep this in sync with the Item page.
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
class Comments extends Component {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,635 @@
|
|||
/*
|
||||
Graphictoria 5 (https://gtoria.net)
|
||||
Copyright © XlXi 2022
|
||||
*/
|
||||
|
||||
import { Component, createRef } from 'react';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { buildGenericApiUrl } from '../util/HTTP.js';
|
||||
import Loader from './Loader';
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
class DeploymentUploadModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
deployments: []
|
||||
};
|
||||
|
||||
this.updateInterval = null;
|
||||
|
||||
this.ModalRef = createRef();
|
||||
this.Modal = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.Modal = new Bootstrap.Modal(
|
||||
this.ModalRef.current,
|
||||
{
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}
|
||||
);
|
||||
this.Modal.show();
|
||||
|
||||
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
||||
this.props.setModal(null);
|
||||
})
|
||||
|
||||
this.setState({ deployments: this.props.deployments });
|
||||
this.updateInterval = setInterval(function() {
|
||||
let deployments = this.state.deployments;
|
||||
deployments.map((component, index) => {
|
||||
axios.get(buildGenericApiUrl('api', `admin/v1/deploy?version=${ component.version }`))
|
||||
.then(res => {
|
||||
deployments[index] = { ...component, ...res.data };
|
||||
|
||||
// XlXi: -_-
|
||||
//deployment['status'] = res.data['status'];
|
||||
//deployment['message'] = res.data['message'];
|
||||
//deployment['progress'] = res.data['progress'];
|
||||
});
|
||||
});
|
||||
this.setState({ deployments: deployments });
|
||||
}.bind(this), 2000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.Modal.dispose();
|
||||
|
||||
clearInterval(this.updateInterval);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.ModalRef} className="modal fade">
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Deployment Status</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{
|
||||
this.state.deployments.map((deployment, index) => (
|
||||
<>
|
||||
<h5 className="mb-0">Deploying { capitalizeFirstLetter(deployment.key) } { deployment.version }</h5>
|
||||
<p className="text-muted mb-2">{ deployment.message }</p>
|
||||
<div className="progress">
|
||||
<div
|
||||
className={classNames({
|
||||
'progress-bar': true,
|
||||
'progress-bar-striped': true,
|
||||
'progress-bar-animated': true,
|
||||
'bg-primary': ( deployment.status == 'Loading' ),
|
||||
'bg-success': ( deployment.status == 'Success' ),
|
||||
'bg-danger': ( deployment.status == 'Error' )
|
||||
})}
|
||||
style={ {width: `${deployment.progress * 100}%`} }
|
||||
></div>
|
||||
</div>
|
||||
{ index != this.state.deployments.length-1 ? <hr /> : null }
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeploymentCard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="card mb-2">
|
||||
<div className="card-header d-flex">
|
||||
<span>{ this.props.name }</span>
|
||||
<button className="ms-auto btn-close" onClick={ ()=>this.props.removeDeployment(this.props.index) }></button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RevertDeploymentCard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
deployments: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let deployType = 'Unknown';
|
||||
switch(this.props.index)
|
||||
{
|
||||
case 'client':
|
||||
deployType = 'WindowsPlayer'
|
||||
break;
|
||||
case 'studio':
|
||||
deployType = 'Studio'
|
||||
break;
|
||||
}
|
||||
|
||||
let deployHistoryRegex = new RegExp(`New ${deployType} version-[a-zA-Z0-9]{16} at \\d{1,2}\\/\\d{1,2}\\/\\d{4} \\d{1,2}:\\d{1,2}:\\d{1,2} (AM|PM), file version: (\\d+(, )?){4}\\.{3}Done!`, 'g');
|
||||
|
||||
axios.get(buildGenericApiUrl('setup', 'DeployHistory.txt'))
|
||||
.then(res => {
|
||||
let deployments = res.data.split(/\r?\n/).reverse();
|
||||
deployments = deployments.filter((deployment) => deployHistoryRegex.test(deployment)).slice(0, 30);
|
||||
|
||||
let realDeployments = [];
|
||||
|
||||
deployments.map((deployment) => {
|
||||
let newDeployment = {
|
||||
type: deployType,
|
||||
hash: deployment.match(/version-[a-zA-Z0-9]{16}/)[0],
|
||||
version: deployment.match(/file version: (\d+(, )?){4}/)[0].replace('file version: ', ''),
|
||||
date: deployment.match(/at \d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{1,2}:\d{1,2} (AM|PM)/)[0].replace('at ', '')
|
||||
};
|
||||
|
||||
realDeployments.push(newDeployment);
|
||||
});
|
||||
|
||||
this.setState({ loading: false, deployments: realDeployments });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DeploymentCard name={ this.props.name } index={ this.props.index } removeDeployment={ this.props.removeDeployment }>
|
||||
<h5 className="mb-0">Revert Deployment</h5>
|
||||
<p className="text-muted">Select a previous deployment below to roll back the { this.props.index } version.</p>
|
||||
<select className="form-select mt-2" id="gt-revert-deployment" disabled={ this.state.loading }>
|
||||
<option selected>{ this.state.loading ? 'Loading...' : 'None Selected' }</option>
|
||||
{
|
||||
this.state.deployments.map((deployment, index) => (
|
||||
<option value={ deployment.hash } key={ index }>[{ deployment.type } { deployment.version }] [{ deployment.date }] { deployment.hash }</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</DeploymentCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PushDeploymentCard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
drag: false,
|
||||
showRequiredFiles: false,
|
||||
files: [],
|
||||
options: []
|
||||
};
|
||||
|
||||
this.dragDropBox = createRef();
|
||||
this.dragCounter = 0;
|
||||
|
||||
this.neededFiles = [
|
||||
'content-fonts.zip',
|
||||
'content-music.zip',
|
||||
'content-particles.zip',
|
||||
'content-sky.zip',
|
||||
'content-sounds.zip',
|
||||
'content-terrain.zip',
|
||||
'content-textures.zip',
|
||||
'content-textures2.zip',
|
||||
'content-textures3.zip',
|
||||
'shaders.zip',
|
||||
'redist.zip',
|
||||
'Libraries.zip'
|
||||
];
|
||||
|
||||
this.handleDrag = this.handleDrag.bind(this);
|
||||
this.handleDragIn = this.handleDragIn.bind(this);
|
||||
this.handleDragOut = this.handleDragOut.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
this.handleDropUi = this.handleDropUi.bind(this);
|
||||
this.fileExists = this.fileExists.bind(this);
|
||||
this.removeFile = this.removeFile.bind(this);
|
||||
this.setOptions = this.setOptions.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
switch(this.props.index)
|
||||
{
|
||||
case 'client':
|
||||
this.neededFiles = this.neededFiles.concat([
|
||||
'PlayerPdb.zip',
|
||||
'Graphictoria.zip',
|
||||
'GraphictoriaPlayerLauncher.exe'
|
||||
]);
|
||||
break;
|
||||
case 'studio':
|
||||
this.neededFiles = this.neededFiles.concat([
|
||||
'BuiltInPlugins.zip',
|
||||
'imageformats.zip',
|
||||
'content-scripts.zip',
|
||||
'StudioPdb.zip',
|
||||
'GraphictoriaStudio.zip',
|
||||
'GraphictoriaStudioLauncherBeta.exe'
|
||||
]);
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({ showRequiredFiles: true });
|
||||
|
||||
let ddb = this.dragDropBox.current
|
||||
ddb.addEventListener('dragenter', this.handleDragIn)
|
||||
ddb.addEventListener('dragleave', this.handleDragOut)
|
||||
ddb.addEventListener('dragover', this.handleDrag)
|
||||
ddb.addEventListener('drop', this.handleDrop)
|
||||
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let ddb = this.dragDropBox.current
|
||||
ddb.removeEventListener('dragenter', this.handleDragIn)
|
||||
ddb.removeEventListener('dragleave', this.handleDragOut)
|
||||
ddb.removeEventListener('dragover', this.handleDrag)
|
||||
ddb.removeEventListener('drop', this.handleDrop)
|
||||
}
|
||||
|
||||
handleDrag(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
handleDragIn(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.dragCounter++;
|
||||
if (evt.dataTransfer.items && evt.dataTransfer.items.length > 0) {
|
||||
this.setState({drag: true});
|
||||
}
|
||||
}
|
||||
|
||||
handleDragOut(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.dragCounter--;
|
||||
if (this.dragCounter === 0) {
|
||||
this.setState({drag: false});
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.setState({drag: false});
|
||||
if (evt.dataTransfer.files && evt.dataTransfer.files.length > 0) {
|
||||
this.handleDropUi(evt.dataTransfer.files);
|
||||
evt.dataTransfer.clearData();
|
||||
this.dragCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
handleDropUi(files) {
|
||||
let fileList = this.state.files;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
let file = files[i];
|
||||
|
||||
if (!file)
|
||||
continue;
|
||||
|
||||
if(this.fileExists(file.name))
|
||||
continue;
|
||||
|
||||
fileList.push(file);
|
||||
}
|
||||
|
||||
this.setState({files: fileList});
|
||||
this.props.updateDeploymentFiles(fileList);
|
||||
}
|
||||
|
||||
fileExists(fileName) {
|
||||
return this.state.files.some(file => file.name.toLowerCase() === fileName.toLowerCase());
|
||||
}
|
||||
|
||||
removeFile(fileName) {
|
||||
this.setState(prevState => ({
|
||||
files: prevState.files.filter((file) => file.name.toLowerCase() !== fileName.toLowerCase())
|
||||
}));
|
||||
|
||||
this.props.updateDeploymentFiles(this.state.files);
|
||||
}
|
||||
|
||||
setOptions(key, value) {
|
||||
let options = this.state.options;
|
||||
if(value != '')
|
||||
options[key] = value;
|
||||
else
|
||||
delete options[key];
|
||||
|
||||
this.setState({ options: options });
|
||||
|
||||
this.props.updateDeploymentOptions(options)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DeploymentCard name={ this.props.name } index={ this.props.index } removeDeployment={ this.props.removeDeployment }>
|
||||
<h5 className="mb-0">Deployment Files</h5>
|
||||
<p className="text-muted">Drag-and-Drop the necessary files into the box below. Any unneeded files will be discarded when uploading.</p>
|
||||
<div className="card bg-secondary mt-3 p-3" ref={ this.dragDropBox }>
|
||||
<div>
|
||||
{/* XlXi: Reusing game cards here because they were already exactly what I wanted. */}
|
||||
{
|
||||
this.state.files.length == 0
|
||||
?
|
||||
<p className="text-muted">Drop files here.</p>
|
||||
:
|
||||
this.state.files.map((file, index) => {
|
||||
let fileType = /(?:\.([^.]+))?$/.exec(file.name)[1].toLowerCase();
|
||||
let fileIconClasses = {
|
||||
'm-auto': true,
|
||||
'fs-1': true,
|
||||
'fa-regular': true
|
||||
};
|
||||
switch(fileType)
|
||||
{
|
||||
case 'exe':
|
||||
fileIconClasses['fa-browser'] = true;
|
||||
break;
|
||||
case 'zip':
|
||||
fileIconClasses['fa-file-zipper'] = true;
|
||||
break;
|
||||
default:
|
||||
fileIconClasses['fa-file'] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="graphictoria-item-card graphictoria-game-card">
|
||||
<div className="card m-2" data-bs-toggle="tooltip" data-bs-placement="top" title={ file.name }>
|
||||
<div className="bg-light d-flex p-3">
|
||||
<i className={classNames(fileIconClasses)}></i>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<p className="text-truncate">{ file.name }</p>
|
||||
<button className="btn btn-sm btn-danger mt-1 w-100" onClick={ ()=>this.removeFile(file.name) }>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{
|
||||
this.state.showRequiredFiles
|
||||
?
|
||||
<>
|
||||
<hr />
|
||||
<h5>Needed Files</h5>
|
||||
<div className="small">
|
||||
{
|
||||
this.neededFiles.map((fileName) => {
|
||||
let fileExists = this.fileExists(fileName);
|
||||
|
||||
return (
|
||||
<p className={classNames({
|
||||
'text-success': fileExists,
|
||||
'text-danger': !fileExists
|
||||
})}>{ fileName }</p>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
this.props.index == 'client'
|
||||
?
|
||||
<>
|
||||
<h5 className="mb-0 mt-3">Optional Configuration</h5>
|
||||
<p className="text-muted mb-3">Only change if you've updated the security settings on the client/rcc. Shutting down game servers will delay deployment by 10 minutes.</p>
|
||||
<div className="form-check form-switch">
|
||||
<input className="form-check-input" type="checkbox" role="switch" id="gt-shut-down-servers" />
|
||||
<label className="form-check-label" htmlFor="gt-shut-down-servers">Shut down game servers.</label>
|
||||
</div>
|
||||
<label htmlFor="gt-rcc-security-key" className="form-label mt-2">Update RCC Security Key</label>
|
||||
<input type="text" id="gt-rcc-security-key" className="form-control" placeholder="New RCC Security Key" onChange={ changeEvent => this.setOptions('rccAccessKey', changeEvent.target.value) } />
|
||||
<label htmlFor="gt-rcc-security-key" className="form-label mt-2">Update Version Compatibility Salt</label>
|
||||
<input type="text" id="gt-rcc-security-key" className="form-control" placeholder="New Version Compatibility Salt" onChange={ changeEvent => this.setOptions('versionCompatiblityFuzzyKey', changeEvent.target.value) } />
|
||||
</>
|
||||
:
|
||||
null
|
||||
}
|
||||
</DeploymentCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
class Deployer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showModal: false,
|
||||
deployType: 'deploy',
|
||||
showTypeSwitchError: false,
|
||||
deploying: false,
|
||||
deployments: []
|
||||
};
|
||||
|
||||
this.visibleModal = null;
|
||||
|
||||
this.updateDeploymentFiles = this.updateDeploymentFiles.bind(this);
|
||||
this.deploymentExists = this.deploymentExists.bind(this);
|
||||
this.getDeployment = this.getDeployment.bind(this);
|
||||
this.createDeployment = this.createDeployment.bind(this);
|
||||
this.removeDeployment = this.removeDeployment.bind(this);
|
||||
this.trySetDeployType = this.trySetDeployType.bind(this);
|
||||
this.uploadDeploymentFiles = this.uploadDeploymentFiles.bind(this);
|
||||
this.uploadDeployments = this.uploadDeployments.bind(this);
|
||||
this.setModal = this.setModal.bind(this);
|
||||
}
|
||||
|
||||
updateDeploymentFiles(key, files) {
|
||||
if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
|
||||
return;
|
||||
|
||||
let deployment = this.getDeployment(key);
|
||||
deployment['files'] = files;
|
||||
}
|
||||
|
||||
updateDeploymentOptions(key, options) {
|
||||
if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
|
||||
return;
|
||||
|
||||
let deployment = this.getDeployment(key);
|
||||
deployment['extraOptions'] = options;
|
||||
}
|
||||
|
||||
deploymentExists(key) {
|
||||
return this.state.deployments.some(deployment => deployment.key === key);
|
||||
}
|
||||
|
||||
getDeployment(key) {
|
||||
return this.state.deployments.find((deployment) => deployment.key === key);
|
||||
}
|
||||
|
||||
createDeployment(key) {
|
||||
if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
|
||||
return;
|
||||
|
||||
let newElement = {
|
||||
key: key,
|
||||
name: capitalizeFirstLetter(this.state.deployType) + ' ' + capitalizeFirstLetter(key),
|
||||
files: [],
|
||||
extraOptions: []
|
||||
};
|
||||
|
||||
this.setState(prevState => ({
|
||||
deployments: [...prevState.deployments, newElement]
|
||||
}));
|
||||
}
|
||||
|
||||
removeDeployment(key) {
|
||||
if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
|
||||
return;
|
||||
|
||||
this.setState(prevState => ({
|
||||
deployments: prevState.deployments.filter((deployment) => deployment.key !== key)
|
||||
}));
|
||||
}
|
||||
|
||||
trySetDeployType(evt, type) {
|
||||
if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
|
||||
return;
|
||||
|
||||
if(!evt.target.checked)
|
||||
return;
|
||||
|
||||
if(this.state.deployType != type && this.state.deployments.length != 0)
|
||||
{
|
||||
this.setState({ showTypeSwitchError: true });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ deployType: type, showTypeSwitchError: false });
|
||||
}
|
||||
|
||||
uploadDeploymentFiles(key, version) {
|
||||
let deployment = this.getDeployment(key);
|
||||
let bodyFormData = new FormData();
|
||||
|
||||
deployment.files.map((file) => {
|
||||
bodyFormData.append('file[]', file);
|
||||
});
|
||||
|
||||
Object.keys(deployment.extraOptions).forEach(function(key, index) {
|
||||
bodyFormData.append(key, deployment.extraOptions[key]);
|
||||
});
|
||||
|
||||
axios.post(
|
||||
buildGenericApiUrl('api', `admin/v1/deploy/${ deployment.version }`),
|
||||
bodyFormData
|
||||
);
|
||||
}
|
||||
|
||||
uploadDeployments() {
|
||||
if(this.state.deploying) // XlXi: Prevent multiple deployments of the same files.
|
||||
return;
|
||||
|
||||
this.setState({ deploying: true });
|
||||
|
||||
let requests = 0;
|
||||
this.state.deployments.map((component, index) => {
|
||||
axios.get(buildGenericApiUrl('api', `admin/v1/deploy?type=deploy&app=${ component.key }`))
|
||||
.then(res => {
|
||||
requests++;
|
||||
|
||||
this.state.deployments[index] = {...component, ...res.data};
|
||||
|
||||
if(requests == this.state.deployments.length)
|
||||
this.setModal(<DeploymentUploadModal setModal={ this.setModal } deployments={ this.state.deployments } />);
|
||||
|
||||
this.uploadDeploymentFiles(component.key, component.version);
|
||||
});
|
||||
});
|
||||
|
||||
//this.setState({ deployments: [] });
|
||||
}
|
||||
|
||||
setModal(modal = null) {
|
||||
this.visibleModal = modal;
|
||||
|
||||
if(modal) {
|
||||
this.setState({'showModal': true});
|
||||
} else {
|
||||
this.setState({'showModal': false});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h5>Deployment Options</h5>
|
||||
<div className="d-block">
|
||||
<div className="btn-group mb-1">
|
||||
<input type="radio" className="btn-check" name="gt-deployment-type" id="gt-deployment-deploy" autoComplete="off" onChange={ (e)=>this.trySetDeployType(e, 'deploy') } checked={ this.state.deployType == 'deploy' } />
|
||||
<label className="btn btn-sm btn-outline-primary" htmlFor="gt-deployment-deploy">Deploy</label>
|
||||
<input type="radio" className="btn-check" name="gt-deployment-type" id="gt-deployment-revert" autoComplete="off" onChange={ (e)=>this.trySetDeployType(e, 'revert') } checked={ this.state.deployType == 'revert' } />
|
||||
<label className="btn btn-sm btn-outline-primary" htmlFor="gt-deployment-revert">Revert</label>
|
||||
</div>
|
||||
<br />
|
||||
<button className="btn btn-sm btn-success" onClick={ ()=>this.createDeployment('client') } disabled={ this.deploymentExists('client') }>Deploy Client</button>
|
||||
<button className="btn btn-sm btn-primary" onClick={ ()=>this.createDeployment('studio') } disabled={ this.deploymentExists('studio') }>Deploy Studio</button>
|
||||
</div>
|
||||
<hr />
|
||||
{
|
||||
this.state.showTypeSwitchError ?
|
||||
<div className="alert alert-danger graphictoria-alert graphictoria-error-popup">Remove your { this.state.deployType == 'deploy' ? 'deployments' : 'reversions' } to change the uploader type.</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<h5>{ this.state.deployType == 'deploy' ? 'Staged Deployments' : 'Revert Deployments' }</h5>
|
||||
{
|
||||
this.state.deployments.length == 0 ?
|
||||
<p className="text-muted">No deployments are selected.</p>
|
||||
:
|
||||
this.state.deployments.map((component) => {
|
||||
// XlXi: surely theres a better way to do this.....
|
||||
switch(this.state.deployType)
|
||||
{
|
||||
case 'deploy':
|
||||
return <PushDeploymentCard name={ component.name } index={ component.key } key={ component.key } removeDeployment={ this.removeDeployment } updateDeploymentFiles={ (files)=>this.updateDeploymentFiles(component.key, files) } updateDeploymentOptions={ (options)=>this.updateDeploymentOptions(component.key, options) } />
|
||||
case 'revert':
|
||||
return <RevertDeploymentCard name={ component.name } index={ component.key } key={ component.key } removeDeployment={ this.removeDeployment } />
|
||||
}
|
||||
})
|
||||
}
|
||||
<hr />
|
||||
<button className="btn btn-primary" onClick={ this.uploadDeployments } disabled={ this.state.deploying || this.state.deployments.length == 0 }>{ this.state.deployType == 'deploy' ? 'Deploy' : 'Revert Deployment' }</button>
|
||||
{ this.state.showModal ? this.visibleModal : null }
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Deployer;
|
||||
|
|
@ -34,21 +34,21 @@ class GameItemCard extends Component {
|
|||
render() {
|
||||
return (
|
||||
<a
|
||||
className="graphictoria-item-card"
|
||||
href="#"
|
||||
className="graphictoria-item-card graphictoria-game-card"
|
||||
href={ this.props.item.Url }
|
||||
onMouseEnter={() => this.setState({hovered: true})}
|
||||
onMouseLeave={() => this.setState({hovered: false})}
|
||||
>
|
||||
<span className="card m-2">
|
||||
<ProgressiveImage
|
||||
src={ buildGenericApiUrl('www', 'images/busy/game.png') }
|
||||
placeholderImg={ buildGenericApiUrl('www', 'images/busy/game.png') }
|
||||
alt="Game todo"
|
||||
src={ buildGenericApiUrl('www', 'images/busy/game-icon.png') }
|
||||
placeholderImg={ buildGenericApiUrl('www', 'images/busy/game-icon.png') }
|
||||
alt={ this.props.item.Name }
|
||||
className='img-fluid'
|
||||
/>
|
||||
<div className="p-2">
|
||||
<p>Todo</p>
|
||||
<p className="text-muted small">{commaSeparate(1337)} Playing</p>
|
||||
<p>{ this.props.item.Name }</p>
|
||||
<p className="text-muted small">{commaSeparate(this.props.item.Playing)} Playing</p>
|
||||
<div className="d-flex mt-1">
|
||||
<i className={classNames({
|
||||
'fa-solid': true,
|
||||
|
|
@ -73,7 +73,7 @@ class GameItemCard extends Component {
|
|||
'bg-dark': !this.state.hovered,
|
||||
'bg-success': this.state.hovered,
|
||||
})}
|
||||
style={{width: '80%', height: '8px'}}></div>
|
||||
style={{width: (this.props.item.Ratio * 100) + '%', height: '8px'}}></div>
|
||||
</div>
|
||||
<i className={classNames({
|
||||
'fa-solid': true,
|
||||
|
|
@ -89,7 +89,7 @@ class GameItemCard extends Component {
|
|||
<div className="card px-2">
|
||||
<hr className="m-0" />
|
||||
<p className="text-truncate my-1">
|
||||
<span className="text-muted">By </span><a href="#" className="text-decoration-none fw-normal">Todo</a>
|
||||
<span className="text-muted">By </span><a href={ this.props.item.Creator.Url } className="text-decoration-none fw-normal">{ this.props.item.Creator.Name }</a>
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
|
|
@ -104,14 +104,96 @@ class GameItemCard extends Component {
|
|||
class Games extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
pageItems: [],
|
||||
pageLoaded: true,
|
||||
pageNumber: null,
|
||||
pageCount: null,
|
||||
error: false
|
||||
};
|
||||
|
||||
this.navigate = this.navigate.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.navigate();
|
||||
}
|
||||
|
||||
navigate(data = {}) {
|
||||
this.setState({pageLoaded: false});
|
||||
|
||||
let url = buildGenericApiUrl('api', 'games/v1/list-json');
|
||||
let paramIterator = 0;
|
||||
Object.keys(data).map(key => {
|
||||
url += ((paramIterator++ == 0 ? '?' : '&') + `${key}=${data[key]}`);
|
||||
});
|
||||
|
||||
axios.get(url)
|
||||
.then(res => {
|
||||
const items = res.data;
|
||||
|
||||
this.setState({ pageItems: items.data, pageCount: items.pages, pageNumber: 1, pageLoaded: true, error: false });
|
||||
}).catch(err => {
|
||||
const data = err.response.data;
|
||||
|
||||
let errorMessage = 'An error occurred while processing your request.';
|
||||
if(data.errors)
|
||||
errorMessage = data.errors[0].message;
|
||||
|
||||
this.setState({ pageItems: [], pageCount: 1, pageNumber: 1, pageLoaded: true, error: errorMessage });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container-lg my-2 d-flex flex-column">
|
||||
<div className="container-sm my-2 d-flex flex-column">
|
||||
<h4 className="my-auto">Games</h4>
|
||||
<Loader />
|
||||
<GameItemCard />
|
||||
<div className="card p-3 mt-2">
|
||||
{
|
||||
this.state.error ?
|
||||
<div className="alert alert-danger p-2 mb-0 text-center">{this.state.error}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
!this.state.pageLoaded ?
|
||||
<div className="graphictoria-shop-overlay">
|
||||
<Loader />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
(this.state.pageItems.length == 0 && !this.state.error) ?
|
||||
<p className="text-muted text-center">Nothing found.</p>
|
||||
:
|
||||
<div>
|
||||
{
|
||||
this.state.pageItems.map((item, index) =>
|
||||
<GameItemCard item={ item } key={ index } />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.pageCount > 1 ?
|
||||
<ul className="list-inline mx-auto mt-3">
|
||||
<li className="list-inline-item">
|
||||
<button className="btn btn-secondary" disabled={(this.state.pageNumber <= 1) ? true : null}><i className="fa-solid fa-angle-left"></i></button>
|
||||
</li>
|
||||
<li className="list-inline-item graphictoria-paginator">
|
||||
<span>Page </span>
|
||||
<input type="text" value={ this.state.pageNumber || '' } className="form-control" disabled={this.state.pageLoaded ? null : true} />
|
||||
<span> of { this.state.pageCount || '???' }</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<button className="btn btn-secondary" disabled={(this.state.pageNumber >= this.state.pageCount) ? true : null}><i className="fa-solid fa-angle-right"></i></button>
|
||||
</li>
|
||||
</ul>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
Graphictoria 5 (https://gtoria.net)
|
||||
Copyright © XlXi 2022
|
||||
*/
|
||||
|
||||
import { Component, createRef } from 'react';
|
||||
import axios from 'axios';
|
||||
import Twemoji from 'react-twemoji';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { buildGenericApiUrl, getCurrentDomain } from '../util/HTTP.js';
|
||||
import Loader from './Loader';
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
const playerProtocol = 'graphictoria-player';
|
||||
|
||||
class PlaceLoadingModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDownloadScreen: false
|
||||
};
|
||||
|
||||
this.ModalRef = createRef();
|
||||
this.Modal = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
|
||||
this.Modal.show();
|
||||
|
||||
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
||||
this.props.setModal(null);
|
||||
});
|
||||
|
||||
setTimeout(function(){
|
||||
this.setState({showDownloadScreen: true});
|
||||
}.bind(this), 10000)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.Modal.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.ModalRef} className="modal fade">
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className={classNames({
|
||||
'modal-content': true,
|
||||
'text-center': true,
|
||||
'mx-5': (!this.state.showDownloadScreen)
|
||||
})}>
|
||||
<div className={classNames({
|
||||
'modal-body': true,
|
||||
'd-flex': true,
|
||||
'flex-column': true,
|
||||
'pb-5': (!this.state.showDownloadScreen)
|
||||
})}>
|
||||
<button type="button" className="ms-auto btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
{
|
||||
this.state.showDownloadScreen ?
|
||||
<>
|
||||
<h5>Download Graphictoria</h5>
|
||||
<p>Download Graphictoria to get access to thousands of community-driven games.</p>
|
||||
<a href={ buildGenericApiUrl('setup', 'GraphictoriaPlayerLauncher.exe') } target="_blank" className="btn btn-success mt-3">Download</a>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<Loader />
|
||||
<p>Starting Graphictoria</p>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceLoadingErrorModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
};
|
||||
|
||||
this.ModalRef = createRef();
|
||||
this.Modal = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
|
||||
this.Modal.show();
|
||||
|
||||
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
||||
this.props.setModal(null);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.Modal.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.ModalRef} className="modal fade">
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<div className="modal-content text-center">
|
||||
<div className="modal-body d-flex flex-column pb-4">
|
||||
<button type="button" className="ms-auto btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<h5>An error occurred while starting Graphictoria.</h5>
|
||||
<p>Error Detail: { this.props.message }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceButtons extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
playDebounce: false,
|
||||
showModal: false
|
||||
};
|
||||
|
||||
this.playGame = this.playGame.bind(this);
|
||||
this.setModal = this.setModal.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let placeElement = this.props.element;
|
||||
if (placeElement) {
|
||||
this.placeId = parseInt(placeElement.getAttribute('data-place-id'))
|
||||
}
|
||||
}
|
||||
|
||||
setModal(modal = null) {
|
||||
this.visibleModal = modal;
|
||||
|
||||
if(modal) {
|
||||
this.setState({'showModal': true});
|
||||
} else {
|
||||
this.setState({'showModal': false});
|
||||
}
|
||||
}
|
||||
|
||||
playGame() {
|
||||
this.setState({ playDebounce: true });
|
||||
setTimeout(function(){
|
||||
this.setState({ playDebounce: false });
|
||||
}.bind(this), 1000);
|
||||
|
||||
this.setModal(<PlaceLoadingModal setModal={ this.setModal } />);
|
||||
|
||||
let protocol = playerProtocol;
|
||||
let domainSplit = getCurrentDomain().split('.');
|
||||
if(getCurrentDomain() == 'gtoria.local')
|
||||
{
|
||||
protocol += '-dev';
|
||||
}
|
||||
else if(domainSplit.length > 2)
|
||||
{
|
||||
protocol += '-' + domainSplit.at(-3); // XlXi: Third to last
|
||||
}
|
||||
|
||||
axios.get(buildGenericApiUrl('api', `auth/v1/generate-token`))
|
||||
.then(res => {
|
||||
window.location = protocol
|
||||
+ ':1'
|
||||
+ '+launchmode:play'
|
||||
+ '+gameinfo:' + res.data
|
||||
+ '+placelauncherurl:' + encodeURIComponent(buildGenericApiUrl('www', `Game/PlaceLauncher?request=RequestGame&placeId=&${this.placeId}&isPlayTogetherGame=false`));
|
||||
})
|
||||
.catch(function(error) {
|
||||
this.setModal(<PlaceLoadingErrorModal setModal={ this.setModal } message={ error.message } />);
|
||||
|
||||
//alert('Error while starting Graphictoria: ' + error.message);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<button className="btn btn-lg btn-success fs-3" onClick={ this.playGame } disabled={ this.state.playDebounce }>
|
||||
<i className="fa-solid fa-play"></i>
|
||||
</button>
|
||||
{ this.state.showModal ? this.visibleModal : null }
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PlaceButtons;
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
Copyright © XlXi 2022
|
||||
*/
|
||||
|
||||
import { Component, createRef } from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class ThumbnailTool extends Component {
|
|||
|
||||
this.setState({ initialLoading: false });
|
||||
|
||||
if(localStorage.getItem('gt-use-3d-thumbnails') === 'true')
|
||||
if(this.renderable3d && localStorage.getItem('gt-use-3d-thumbnails') === 'true')
|
||||
this.toggle3D();
|
||||
}
|
||||
}
|
||||
|
|
@ -207,9 +207,11 @@ class ThumbnailTool extends Component {
|
|||
:
|
||||
<ProgressiveImage
|
||||
src={ this.thumbnail2d }
|
||||
placeholderImg={ buildGenericApiUrl('www', 'images/busy/asset.png') }
|
||||
placeholderImg={ buildGenericApiUrl('www', (this.props.placeholder == null ? 'images/busy/asset.png' : this.props.placeholder)) }
|
||||
alt={ this.assetName }
|
||||
className='img-fluid'
|
||||
width={ this.props.width }
|
||||
height={ this.props.height }
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Graphictoria 5 (https://gtoria.net)
|
||||
Copyright © XlXi 2022
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import Deployer from '../components/Deployer';
|
||||
|
||||
const deployerId = 'gt-deployer';
|
||||
|
||||
$(document).ready(function() {
|
||||
if (document.getElementById(deployerId)) {
|
||||
render(<Deployer />, document.getElementById(deployerId));
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
Graphictoria 5 (https://gtoria.net)
|
||||
Copyright © XlXi 2022
|
||||
*/
|
||||
|
||||
import $ from 'jquery';
|
||||
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import Comments from '../components/Comments';
|
||||
import ThumbnailTool from '../components/ThumbnailTool';
|
||||
import PlaceButtons from '../components/PlaceButtons';
|
||||
|
||||
const thumbnailId = 'gt-thumbnail';
|
||||
const buttonsId = 'gt-place-buttons';
|
||||
const commentsId = 'gt-comments';
|
||||
|
||||
$(document).ready(function() {
|
||||
if (document.getElementById(thumbnailId)) {
|
||||
let tElem = document.getElementById(thumbnailId);
|
||||
render(<ThumbnailTool element={ tElem } placeholder="/images/busy/game.png" width="640" height="360" />, tElem);
|
||||
}
|
||||
if (document.getElementById(buttonsId)) {
|
||||
let bElem = document.getElementById(buttonsId);
|
||||
render(<PlaceButtons element={ bElem } />, bElem);
|
||||
}
|
||||
if (document.getElementById(commentsId)) {
|
||||
let cElem = document.getElementById(commentsId);
|
||||
render(<Comments element={ cElem } />, cElem);
|
||||
}
|
||||
});
|
||||
|
|
@ -9,8 +9,12 @@
|
|||
@import "Variables";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "./scss/fontawesome.scss";
|
||||
@import "./scss/solid.scss";
|
||||
@import "./scss/brands.scss";
|
||||
@import "./scss/duotone.scss";
|
||||
@import "./scss/light.scss";
|
||||
@import "./scss/regular.scss";
|
||||
@import "./scss/solid.scss";
|
||||
@import "./scss/thin.scss";
|
||||
|
||||
// Variables
|
||||
|
||||
|
|
@ -73,6 +77,11 @@ img.twemoji {
|
|||
height: 420px;
|
||||
}
|
||||
|
||||
.graphictoria-game-thumbnail {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.graphictoria-smaller-page {
|
||||
max-width: 1096px;
|
||||
margin: 0 auto 0 auto;
|
||||
|
|
@ -140,6 +149,28 @@ img.twemoji {
|
|||
background-size: cover;
|
||||
}
|
||||
|
||||
.graphictoria-game-card {
|
||||
@media (max-width: 576px) {
|
||||
width: math.div(100%, 2)
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
width: math.div(100%, 3)
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
width: math.div(100%, 4)
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
width: math.div(100%, 5)
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
width: 176px;
|
||||
}
|
||||
}
|
||||
|
||||
.graphictoria-shop-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
|
@ -750,6 +781,75 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-favorite {
|
||||
--bs-text-opacity: 1;
|
||||
transition: .1s;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
html.gtoria-light & {
|
||||
color: rgba(var(--bs-black-rgb),var(--bs-text-opacity));
|
||||
}
|
||||
|
||||
html.gtoria-dark & {
|
||||
color: rgba(var(--bs-light-rgb),var(--bs-text-opacity));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #e59800!important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: #e59800!important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-upvote {
|
||||
--bs-text-opacity: 1;
|
||||
transition: .1s;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
html.gtoria-light & {
|
||||
color: rgba(var(--bs-black-rgb),var(--bs-text-opacity));
|
||||
}
|
||||
|
||||
html.gtoria-dark & {
|
||||
color: rgba(var(--bs-light-rgb),var(--bs-text-opacity));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-downvote {
|
||||
--bs-text-opacity: 1;
|
||||
transition: .1s;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
html.gtoria-light & {
|
||||
color: rgba(var(--bs-black-rgb),var(--bs-text-opacity));
|
||||
}
|
||||
|
||||
html.gtoria-dark & {
|
||||
color: rgba(var(--bs-light-rgb),var(--bs-text-opacity));
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important;
|
||||
}
|
||||
}
|
||||
|
||||
// Typography
|
||||
|
||||
.text-secondary {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@
|
|||
|
||||
.#{$fa-css-prefix},
|
||||
.fas,
|
||||
.fa-solid,
|
||||
.#{$fa-css-prefix}-solid,
|
||||
.far,
|
||||
.fa-regular,
|
||||
.#{$fa-css-prefix}-regular,
|
||||
.fal,
|
||||
.fa-light,
|
||||
.#{$fa-css-prefix}-light,
|
||||
.fat,
|
||||
.fa-thin,
|
||||
.#{$fa-css-prefix}-thin,
|
||||
.fad,
|
||||
.fa-duotone,
|
||||
.#{$fa-css-prefix}-duotone,
|
||||
.fab,
|
||||
.fa-brands {
|
||||
.#{$fa-css-prefix}-brands {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: var(--#{$fa-css-prefix}-display, #{$fa-display});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
// specific duotone icon class definition
|
||||
// -------------------------
|
||||
|
||||
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
|
||||
readers do not read off random characters that represent icons */
|
||||
|
||||
@each $name, $icon in $fa-icons {
|
||||
.fad.#{$fa-css-prefix}-#{$name}::after, .#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-#{$name}::after {
|
||||
content: unquote("\"#{ $icon }#{ $icon }\"");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
// functions
|
||||
// --------------------------
|
||||
|
||||
// Originally obtained from the Bootstrap https://github.com/twbs/bootstrap
|
||||
// fa-content: convenience function used to set content property
|
||||
@function fa-content($fa-var) {
|
||||
@return unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
|
||||
// fa-divide: Originally obtained from the Bootstrap https://github.com/twbs/bootstrap
|
||||
//
|
||||
// Licensed under: The MIT License (MIT)
|
||||
//
|
||||
|
|
@ -25,6 +30,7 @@
|
|||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
@function fa-divide($dividend, $divisor, $precision: 10) {
|
||||
$sign: if($dividend > 0 and $divisor > 0, 1, -1);
|
||||
$dividend: abs($dividend);
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
readers do not read off random characters that represent icons */
|
||||
|
||||
@each $name, $icon in $fa-icons {
|
||||
.#{$fa-css-prefix}-#{$name}::before { content: fa-content($icon); }
|
||||
.#{$fa-css-prefix}-#{$name}::before { content: unquote("\"#{ $icon }\""); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
// Icon Sizes
|
||||
// -------------------------
|
||||
|
||||
// makes the font 33% larger relative to the icon container
|
||||
.#{$fa-css-prefix}-lg {
|
||||
font-size: (4em / 3);
|
||||
line-height: (3em / 4);
|
||||
vertical-align: -.0667em;
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-xs {
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-sm {
|
||||
font-size: .875em;
|
||||
}
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
.#{$fa-css-prefix}-#{$i}x {
|
||||
font-size: $i * 1em;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-light($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-light;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-thin($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-thin;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-duotone($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-duotone;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
&::after {
|
||||
content: unquote("\"#{ $fa-var }#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
|
||||
@mixin fa-icon-brands($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-brands;
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
// only display content to screen readers
|
||||
.sr-only,
|
||||
.fa-sr-only {
|
||||
@include fa-sr-only;
|
||||
.#{$fa-css-prefix}-sr-only {
|
||||
@include fa-sr-only;
|
||||
}
|
||||
|
||||
// use in conjunction with .sr-only to only display content when it's focused
|
||||
.sr-only-focusable,
|
||||
.fa-sr-only-focusable {
|
||||
@include fa-sr-only-focusable;
|
||||
.#{$fa-css-prefix}-sr-only-focusable {
|
||||
@include fa-sr-only-focusable;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
/*!
|
||||
* Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{ $fa-css-prefix }-font-brands: normal 400 1em/1 "Font Awesome 6 Brands";
|
||||
--#{$fa-css-prefix}-font-brands: normal 400 1em/1 "Font Awesome 6 Brands";
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
|
@ -20,11 +20,11 @@
|
|||
}
|
||||
|
||||
.fab,
|
||||
.fa-brands {
|
||||
.#{$fa-css-prefix}-brands {
|
||||
font-family: 'Font Awesome 6 Brands';
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@each $name, $icon in $fa-brand-icons {
|
||||
.#{$fa-css-prefix}-#{$name}:before { content: fa-content($icon); }
|
||||
.#{$fa-css-prefix}-#{$name}:before { content: unquote("\"#{ $icon }\""); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/*!
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-font-duotone: normal 900 1em/1 "Font Awesome 6 Duotone";
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Duotone';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-duotone-900.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-duotone-900.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fad,
|
||||
.#{$fa-css-prefix}-duotone {
|
||||
position: relative;
|
||||
font-family: 'Font Awesome 6 Duotone';
|
||||
font-weight: 900;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.fad::before,
|
||||
.#{$fa-css-prefix}-duotone::before {
|
||||
position: absolute;
|
||||
color: var(--#{$fa-css-prefix}-primary-color, inherit);
|
||||
opacity: var(--#{$fa-css-prefix}-primary-opacity, #{$fa-primary-opacity});
|
||||
}
|
||||
|
||||
.fad::after,
|
||||
.#{$fa-css-prefix}-duotone::after {
|
||||
color: var(--#{$fa-css-prefix}-secondary-color, inherit);
|
||||
opacity: var(--#{$fa-css-prefix}-secondary-opacity, #{$fa-secondary-opacity});
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-swap-opacity .fad::before,
|
||||
.#{$fa-css-prefix}-swap-opacity .fa-duotone::before,
|
||||
.fad.#{$fa-css-prefix}-swap-opacity::before,
|
||||
.fa-duotone.#{$fa-css-prefix}-swap-opacity::before {
|
||||
opacity: var(--#{$fa-css-prefix}-secondary-opacity, #{$fa-secondary-opacity});
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-swap-opacity .fad::after,
|
||||
.#{$fa-css-prefix}-swap-opacity .fa-duotone::after,
|
||||
.fad.#{$fa-css-prefix}-swap-opacity::after,
|
||||
.fa-duotone.#{$fa-css-prefix}-swap-opacity::after {
|
||||
opacity: var(--#{$fa-css-prefix}-primary-opacity, #{$fa-primary-opacity});
|
||||
}
|
||||
|
||||
.fad.#{$fa-css-prefix}-inverse,
|
||||
.#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-inverse {
|
||||
color: var(--#{$fa-css-prefix}-inverse, $fa-inverse);
|
||||
}
|
||||
|
||||
.fad.#{$fa-css-prefix}-stack-1x, .fad.#{$fa-css-prefix}-stack-2x,
|
||||
.#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-duotone.#{$fa-css-prefix}-stack-2x {
|
||||
position: absolute;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*!
|
||||
* Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
// Font Awesome core compile (Web Fonts-based)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*!
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-font-light: normal 300 1em/1 "#{ $fa-style-family }";
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-light-300.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-light-300.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fal,
|
||||
.#{$fa-css-prefix}-light {
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-weight: 300;
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
/*!
|
||||
* Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{ $fa-css-prefix }-font-regular: normal 400 1em/1 "#{ $fa-style-family }";
|
||||
--#{$fa-css-prefix}-font-regular: normal 400 1em/1 "#{ $fa-style-family }";
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: $fa-font-display;
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
}
|
||||
|
||||
.far,
|
||||
.fa-regular {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
.#{$fa-css-prefix}-regular {
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-weight: 400;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
/*!
|
||||
* Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{ $fa-css-prefix }-font-solid: normal 900 1em/1 "#{ $fa-style-family }";
|
||||
--#{$fa-css-prefix}-font-solid: normal 900 1em/1 "#{ $fa-style-family }";
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: $fa-font-display;
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
}
|
||||
|
||||
.fas,
|
||||
.fa-solid {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
.#{$fa-css-prefix}-solid {
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-weight: 900;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/*!
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-font-thin: normal 100 1em/1 "#{ $fa-style-family }";
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-thin-100.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-thin-100.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fat,
|
||||
.#{$fa-css-prefix}-thin {
|
||||
font-family: 'Font Awesome 6 Pro';
|
||||
font-weight: 100;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*!
|
||||
* Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license (Commercial License)
|
||||
* Copyright 2022 Fonticons, Inc.
|
||||
*/
|
||||
// V4 shims compile (Web Fonts-based)
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 730 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
|
@ -0,0 +1,21 @@
|
|||
@props([
|
||||
'stat'
|
||||
])
|
||||
|
||||
<div class="my-auto rounded-1 bg-secondary border border-light right-0 me-1 position-relative graphictoria-admin-usagebar">
|
||||
@php
|
||||
$usage_bar_color = 'bg-primary';
|
||||
$usage_bar_usage = $stat * 100;
|
||||
|
||||
if($usage_bar_usage <= 25)
|
||||
$usage_bar_color = 'bg-success'; // Green
|
||||
elseif($usage_bar_usage > 25 && $usage_bar_usage <= 75)
|
||||
$usage_bar_color = 'bg-warning'; // Orange
|
||||
elseif($usage_bar_usage > 75)
|
||||
$usage_bar_color = 'bg-danger'; // Red
|
||||
@endphp
|
||||
<div
|
||||
class="{{ $usage_bar_color }} rounded-1 position-absolute graphictoria-admin-usagebar"
|
||||
style="width:{{ $usage_bar_usage }}%!important;height:8px!important;"
|
||||
></div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Not Implemented')
|
||||
|
||||
@section('content')
|
||||
<div class="container graphictoria-center-vh">
|
||||
<x-card title="NOT IMPLEMENTED">
|
||||
<x-slot name="body">
|
||||
todo
|
||||
</x-slot>
|
||||
<x-slot name="footer">
|
||||
<div class="mt-2">
|
||||
<a class="btn btn-primary px-4 me-2" href="{{ url('/') }}">Home</a>
|
||||
<a class="btn btn-secondary px-4" onclick="history.back();return false;">Back</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-card>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
color: #eee;
|
||||
}
|
||||
|
||||
.graphictoria-admin-memorybar {
|
||||
.graphictoria-admin-usagebar {
|
||||
width: 100px;
|
||||
height: 10px;
|
||||
}
|
||||
|
|
@ -62,61 +62,19 @@
|
|||
<div class="collapse navbar-collapse" id="graphictoria-admin-nav">
|
||||
<ul class="navbar-nav graphictoria-admin-nav ms-auto">
|
||||
@yield('quick-admin')
|
||||
@admin
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.diag') }}" class="nav-link py-0">Arbiter Diag</a>
|
||||
</li>
|
||||
@endadmin
|
||||
@owner
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.arbitermanagement') }}" class="nav-link py-0">Arbiter Management</a>
|
||||
</li>
|
||||
@endowner
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.dashboard') }}" class="nav-link py-0"><i class="fa-solid fa-gavel"></i></a>
|
||||
</li>
|
||||
@admin
|
||||
<li class="nav-item" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true" title="<strong>Updates every minute</strong><br/>{{ \App\Helpers\QAaMBHelper::getMemoryUsage() }}">
|
||||
<span class="px-md-2 d-flex" style="height:24px;">
|
||||
<div class="my-auto rounded-1 bg-secondary border border-light right-0 me-1 position-relative graphictoria-admin-memorybar">
|
||||
@php
|
||||
$admin_memorybar_color = 'bg-primary';
|
||||
$admin_memorybar_usage = \App\Helpers\QAaMBHelper::getMemoryPercentage() * 100;
|
||||
|
||||
if($admin_memorybar_usage <= 25)
|
||||
$admin_memorybar_color = 'bg-success'; // Green
|
||||
elseif($admin_memorybar_usage > 25 && $admin_memorybar_usage <= 75)
|
||||
$admin_memorybar_color = 'bg-warning'; // Orange
|
||||
elseif($admin_memorybar_usage > 75)
|
||||
$admin_memorybar_color = 'bg-danger'; // Red
|
||||
@endphp
|
||||
<div
|
||||
class="{{ $admin_memorybar_color }} rounded-1 position-absolute graphictoria-admin-memorybar"
|
||||
style="width:{{ $admin_memorybar_usage }}%!important;height:8px!important;"
|
||||
></div>
|
||||
</div>
|
||||
<x-admin.usage-bar :stat="\App\Helpers\QAaMBHelper::getMemoryPercentage()" />
|
||||
<i class="my-auto fa-solid fa-gear"></i>
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-html="true" title="<strong>Updates every minute</strong><br/>{{ \App\Helpers\QAaMBHelper::getCpuUsage() }}">
|
||||
<span class="px-md-2 d-flex" style="height:24px;">
|
||||
<div class="my-auto rounded-1 bg-secondary border border-light right-0 me-1 position-relative graphictoria-admin-memorybar">
|
||||
@php
|
||||
$admin_cpubar_color = 'bg-primary';
|
||||
$admin_cpubar_usage = \App\Helpers\QAaMBHelper::getCpuPercentage() * 100;
|
||||
|
||||
if($admin_cpubar_usage <= 25)
|
||||
$admin_cpubar_color = 'bg-success'; // Green
|
||||
elseif($admin_cpubar_usage > 25 && $admin_cpubar_usage <= 75)
|
||||
$admin_cpubar_color = 'bg-warning'; // Orange
|
||||
elseif($admin_cpubar_usage > 75)
|
||||
$admin_cpubar_color = 'bg-danger'; // Red
|
||||
@endphp
|
||||
<div
|
||||
class="{{ $admin_cpubar_color }} rounded-1 position-absolute graphictoria-admin-memorybar"
|
||||
style="width:{{ $admin_cpubar_usage }}%!important;height:8px!important;"
|
||||
></div>
|
||||
</div>
|
||||
<x-admin.usage-bar :stat="\App\Helpers\QAaMBHelper::getCpuPercentage()" />
|
||||
<i class="my-auto fa-solid fa-microchip"></i>
|
||||
</span>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,20 @@
|
|||
|
||||
@section('page-specific')
|
||||
<style>
|
||||
.gt-conf-row > .row {
|
||||
padding: 0.5rem!important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gt-conf-row:nth-of-type(even)>* {
|
||||
background-color: #0000000d;
|
||||
}
|
||||
|
||||
.gt-conf-row > .row > div:not(:last-child) {
|
||||
border-right: 1px solid #00000020;
|
||||
}
|
||||
|
||||
/* old stuff */
|
||||
.gt-small-row {
|
||||
width: 0;
|
||||
text-align: center;
|
||||
|
|
@ -19,13 +33,29 @@
|
|||
</style>
|
||||
|
||||
<!-- Secure Page JS -->
|
||||
<script src="data:text/javascript;base64,{{ base64_encode(File::get(public_path('js/adm/SiteConfiguration.js'))) }}"></script>
|
||||
<script src="{{ mix('js/adm/SiteConfiguration.js') }}"></script>
|
||||
@endsection
|
||||
|
||||
@push('content')
|
||||
<div class="container-md">
|
||||
<h4>Site Configuration</h4>
|
||||
<div class="card p-2">
|
||||
<div class="card">
|
||||
<div class="gt-conf-row">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<p>Name</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<p>Value</p>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<p>Last Modified</p>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<button class="btn btn-sm btn-primary mx-auto" disabled>Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-sm table-bordered table-striped mb-2">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
@extends('layouts.admin')
|
||||
|
||||
@section('title', 'App Deployer')
|
||||
|
||||
@section('page-specific')
|
||||
<!-- Secure Page JS -->
|
||||
<script src="{{ mix('js/adm/AppDeployer.js') }}"></script>
|
||||
@endsection
|
||||
|
||||
@push('content')
|
||||
<div class="container">
|
||||
<div class="alert alert-warning graphictoria-alert graphictoria-error-popup">Ensure the RCC version compatibility security settings and api keys are synced before deploying, else you may completely brick games or api calls.</div>
|
||||
<h4 class="mt-1">App Deployer</h4>
|
||||
<div class="card p-2" id="gt-deployer">
|
||||
<x-loader />
|
||||
|
||||
@if(false)
|
||||
<h5>Deployment Options</h5>
|
||||
<div class="d-block">
|
||||
<div class="btn-group mb-1">
|
||||
<input type="radio" class="btn-check" name="gt-deployment-type" id="gt-deployment-deploy" autocomplete="off" checked>
|
||||
<label class="btn btn-sm btn-outline-primary" for="gt-deployment-deploy">Deploy</label>
|
||||
<input type="radio" class="btn-check" name="gt-deployment-type" id="gt-deployment-revert" autocomplete="off">
|
||||
<label class="btn btn-sm btn-outline-primary" for="gt-deployment-revert">Revert</label>
|
||||
</div>
|
||||
<br />
|
||||
<button class="btn btn-sm btn-success" disabled>Deploy Client</button>
|
||||
<button class="btn btn-sm btn-primary">Deploy Studio</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="alert alert-danger graphictoria-alert graphictoria-error-popup">Remove your [deployments/reversions] to change the uploader type.</div>
|
||||
<h5>Revert Deployments</h5>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex">
|
||||
<span>Revert Client</span>
|
||||
<button class="ms-auto btn-close"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="mb-0">Revert Deployment</h5>
|
||||
<p class="text-muted">Select a previous deployment below to roll back the [client/studio] version.</p>
|
||||
<select class="form-select mt-2" id="gt-revert-deployment">
|
||||
<option selected>None Selected</option>
|
||||
<option value="version-abcdefghijk">[Client 1.0.0.2] [11/23/2022] version-abcdefghijk</option>
|
||||
<option value="version-bbcdefghijk">[Client 1.0.0.1] [11/22/2022] version-bbcdefghijk</option>
|
||||
<option value="version-cbcdefghijk">[Client 1.0.0.0] [11/21/2022] version-cbcdefghijk</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted">No deployments are selected.</p>
|
||||
|
||||
<h5>Staged Deployments</h5>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex">
|
||||
<span>Deploy Client</span>
|
||||
<button class="ms-auto btn-close"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="mb-0">Deployment Files</h5>
|
||||
<p class="text-muted">Drag-and-Drop the necessary files into the box below.</p>
|
||||
<div class="card bg-secondary mt-3 p-3">
|
||||
<div>
|
||||
{{-- XlXi: Reusing game cards here because they were already exactly what I wanted. --}}
|
||||
<div class="graphictoria-item-card graphictoria-game-card">
|
||||
<div class="card m-2" data-bs-toggle="tooltip" data-bs-placement="top" title="GraphictoriaLauncherBeta.exe">
|
||||
<div class="bg-light d-flex p-3">
|
||||
<i class="m-auto fs-1 fa-regular fa-browser"></i>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<p class="text-truncate">GraphictoriaLauncherBeta.exe</p>
|
||||
<button class="btn btn-sm btn-danger mt-1 w-100">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graphictoria-item-card graphictoria-game-card">
|
||||
<div class="card m-2" data-bs-toggle="tooltip" data-bs-placement="top" title="Libraries.zip">
|
||||
<div class="bg-light d-flex p-3">
|
||||
<i class="m-auto fs-1 fa-regular fa-file-zipper"></i>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<p class="text-truncate">Libraries.zip</p>
|
||||
<button class="btn btn-sm btn-danger mt-1 w-100">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<h5>Needed Files</h5>
|
||||
<div class="small">
|
||||
<p class="text-success">Libraries.zip</p>
|
||||
<p class="text-danger">GraphictoriaApp.zip</p>
|
||||
</div>
|
||||
<hr/>
|
||||
<h5>Optional Files</h5>
|
||||
<div class="small">
|
||||
<p class="text-warning">GraphictoriaLauncherBeta 2.exe</p>
|
||||
<p class="text-success">GraphictoriaLauncherBeta.exe</p>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="mb-0 mt-3">Optional Configuration</h5>
|
||||
<p class="text-muted mb-3">Only change if you've updated the security settings on the client/rcc. Shutting down game servers will delay deployment by 10 minutes.</p>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="gt-shut-down-servers">
|
||||
<label class="form-check-label" for="gt-shut-down-servers">Shut down game servers.</label>
|
||||
</div>
|
||||
<label for="gt-rcc-security-key" class="form-label mt-2">Update RCC Security Key</label>
|
||||
<input type="text" id="gt-rcc-security-key" class="form-control" placeholder="New RCC Security Key"/>
|
||||
<label for="gt-rcc-security-key" class="form-label mt-2">Update Version Compatibility Salt</label>
|
||||
<input type="text" id="gt-rcc-security-key" class="form-control" placeholder="New Version Compatibility Salt"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted">No deployments are selected.</p>
|
||||
<hr />
|
||||
<button class="btn btn-primary">Deploy / Revert Deployment</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endpush
|
||||