mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-13 04:30:41 -05:00
Merge branch 'develop'
This commit is contained in:
commit
1e8a55fdcd
@ -21,12 +21,17 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||||
|
'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'],
|
||||||
])->validateWithBag('updateProfileInformation');
|
])->validateWithBag('updateProfileInformation');
|
||||||
|
|
||||||
if (isset($input['photo'])) {
|
if (isset($input['photo'])) {
|
||||||
$user->updateProfilePhoto($input['photo']);
|
$user->updateProfilePhoto($input['photo']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($input['cover'])) {
|
||||||
|
$user->updateCoverPhoto($input['cover']);
|
||||||
|
}
|
||||||
|
|
||||||
if ($input['email'] !== $user->email &&
|
if ($input['email'] !== $user->email &&
|
||||||
$user instanceof MustVerifyEmail) {
|
$user instanceof MustVerifyEmail) {
|
||||||
$this->updateVerifiedUser($user, $input);
|
$this->updateVerifiedUser($user, $input);
|
||||||
|
10
app/Exceptions/CircularDependencyException.php
Normal file
10
app/Exceptions/CircularDependencyException.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class CircularDependencyException extends Exception
|
||||||
|
{
|
||||||
|
protected $message = 'Circular dependency detected.';
|
||||||
|
}
|
36
app/Http/Controllers/Api/V0/ApiController.php
Normal file
36
app/Http/Controllers/Api/V0/ApiController.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V0;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ApiController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the given relationship should be included in the request. If more than one relationship is provided,
|
||||||
|
* only one needs to be present in the request for this method to return true.
|
||||||
|
*/
|
||||||
|
public static function shouldInclude(string|array $relationships): bool
|
||||||
|
{
|
||||||
|
$param = request()->get('include');
|
||||||
|
|
||||||
|
if (! $param) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$includeValues = explode(',', Str::lower($param));
|
||||||
|
|
||||||
|
if (is_array($relationships)) {
|
||||||
|
foreach ($relationships as $relationship) {
|
||||||
|
if (in_array(Str::lower($relationship), $includeValues)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array(Str::lower($relationships), $includeValues);
|
||||||
|
}
|
||||||
|
}
|
@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\V0;
|
namespace App\Http\Controllers\Api\V0;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Filters\V1\ModFilter;
|
||||||
use App\Http\Requests\Api\V0\StoreModRequest;
|
use App\Http\Requests\Api\V0\StoreModRequest;
|
||||||
use App\Http\Requests\Api\V0\UpdateModRequest;
|
use App\Http\Requests\Api\V0\UpdateModRequest;
|
||||||
use App\Http\Resources\Api\V0\ModResource;
|
use App\Http\Resources\Api\V0\ModResource;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
|
|
||||||
class ModController extends Controller
|
class ModController extends ApiController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index(ModFilter $filters)
|
||||||
{
|
{
|
||||||
return ModResource::collection(Mod::paginate());
|
return ModResource::collection(Mod::filter($filters)->paginate());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\V0;
|
namespace App\Http\Controllers\Api\V0;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Filters\V1\UserFilter;
|
||||||
use App\Http\Requests\Api\V0\StoreUserRequest;
|
use App\Http\Requests\Api\V0\StoreUserRequest;
|
||||||
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
||||||
use App\Http\Resources\Api\V0\UserResource;
|
use App\Http\Resources\Api\V0\UserResource;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
||||||
class UsersController extends Controller
|
class UsersController extends ApiController
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index(UserFilter $filters)
|
||||||
{
|
{
|
||||||
return UserResource::collection(User::paginate());
|
return UserResource::collection(User::filter($filters)->paginate());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,19 +27,27 @@ class ModController extends Controller
|
|||||||
|
|
||||||
public function show(int $modId, string $slug)
|
public function show(int $modId, string $slug)
|
||||||
{
|
{
|
||||||
$mod = Mod::select()
|
$mod = Mod::withTotalDownloads()
|
||||||
->withTotalDownloads()
|
->with([
|
||||||
->with(['latestSptVersion', 'users:id,name'])
|
'versions',
|
||||||
->with('license:id,name,link')
|
'versions.sptVersion',
|
||||||
->find($modId);
|
'versions.dependencies',
|
||||||
|
'versions.dependencies.resolvedVersion',
|
||||||
|
'versions.dependencies.resolvedVersion.mod',
|
||||||
|
'users:id,name',
|
||||||
|
'license:id,name,link',
|
||||||
|
])
|
||||||
|
->findOrFail($modId);
|
||||||
|
|
||||||
if (! $mod || $mod->slug !== $slug) {
|
if ($mod->slug !== $slug) {
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->authorize('view', $mod);
|
$this->authorize('view', $mod);
|
||||||
|
|
||||||
return view('mod.show', compact('mod'));
|
$latestVersion = $mod->versions->sortByDesc('version')->first();
|
||||||
|
|
||||||
|
return view('mod.show', compact(['mod', 'latestVersion']));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(ModRequest $request, Mod $mod)
|
public function update(ModRequest $request, Mod $mod)
|
||||||
|
25
app/Http/Controllers/UserController.php
Normal file
25
app/Http/Controllers/UserController.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function show(Request $request, User $user, string $username)
|
||||||
|
{
|
||||||
|
if ($user->slug() !== $username) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->user()?->cannot('view', $user)) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('user.show', compact('user'));
|
||||||
|
}
|
||||||
|
}
|
143
app/Http/Filters/V1/ModFilter.php
Normal file
143
app/Http/Filters/V1/ModFilter.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Filters\V1;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ModFilter extends QueryFilter
|
||||||
|
{
|
||||||
|
protected array $sortable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'teaser',
|
||||||
|
'source_code_link',
|
||||||
|
'featured',
|
||||||
|
'contains_ads',
|
||||||
|
'contains_ai_content',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'published_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: Many of these are repeated across UserFilter and ModFilter. Consider refactoring into a shared trait.
|
||||||
|
// Also, consider using common filter types and making the field names dynamic.
|
||||||
|
|
||||||
|
public function id(string $value): Builder
|
||||||
|
{
|
||||||
|
$ids = array_map('trim', explode(',', $value));
|
||||||
|
|
||||||
|
return $this->builder->whereIn('id', $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hub_id(string $value): Builder
|
||||||
|
{
|
||||||
|
$ids = array_map('trim', explode(',', $value));
|
||||||
|
|
||||||
|
return $this->builder->whereIn('hub_id', $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||||
|
$like = Str::replace('*', '%', $value);
|
||||||
|
|
||||||
|
return $this->builder->where('name', 'like', $like);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function slug(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||||
|
$like = Str::replace('*', '%', $value);
|
||||||
|
|
||||||
|
return $this->builder->where('slug', 'like', $like);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function teaser(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||||
|
$like = Str::replace('*', '%', $value);
|
||||||
|
|
||||||
|
return $this->builder->where('teaser', 'like', $like);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function source_code_link(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||||
|
$like = Str::replace('*', '%', $value);
|
||||||
|
|
||||||
|
return $this->builder->where('source_code_link', 'like', $like);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function created_at(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||||
|
$dates = array_map('trim', explode(',', $value));
|
||||||
|
if (count($dates) > 1) {
|
||||||
|
return $this->builder->whereBetween('created_at', $dates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->whereDate('created_at', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated_at(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||||
|
$dates = array_map('trim', explode(',', $value));
|
||||||
|
if (count($dates) > 1) {
|
||||||
|
return $this->builder->whereBetween('updated_at', $dates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->whereDate('updated_at', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function published_at(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||||
|
$dates = array_map('trim', explode(',', $value));
|
||||||
|
if (count($dates) > 1) {
|
||||||
|
return $this->builder->whereBetween('published_at', $dates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->whereDate('published_at', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function featured(string $value): Builder
|
||||||
|
{
|
||||||
|
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||||
|
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
|
// This column is not nullable.
|
||||||
|
if ($value === null) {
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->where('featured', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contains_ads(string $value): Builder
|
||||||
|
{
|
||||||
|
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||||
|
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
|
// This column is not nullable.
|
||||||
|
if ($value === null) {
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->where('contains_ads', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function contains_ai_content(string $value): Builder
|
||||||
|
{
|
||||||
|
// We need to convert the string user input to a boolean, or null if it's not a valid "truthy/falsy" value.
|
||||||
|
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
|
||||||
|
// This column is not nullable.
|
||||||
|
if ($value === null) {
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->where('contains_ai_content', $value);
|
||||||
|
}
|
||||||
|
}
|
61
app/Http/Filters/V1/QueryFilter.php
Normal file
61
app/Http/Filters/V1/QueryFilter.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Filters\V1;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
abstract class QueryFilter
|
||||||
|
{
|
||||||
|
protected Builder $builder;
|
||||||
|
|
||||||
|
protected Request $request;
|
||||||
|
|
||||||
|
protected array $sortable = [];
|
||||||
|
|
||||||
|
public function __construct(Request $request)
|
||||||
|
{
|
||||||
|
$this->request = $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(Builder $builder): Builder
|
||||||
|
{
|
||||||
|
$this->builder = $builder;
|
||||||
|
|
||||||
|
foreach ($this->request->all() as $attribute => $value) {
|
||||||
|
if (method_exists($this, $attribute)) {
|
||||||
|
$this->$attribute($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filter(array $filters): Builder
|
||||||
|
{
|
||||||
|
foreach ($filters as $attribute => $value) {
|
||||||
|
if (method_exists($this, $attribute)) {
|
||||||
|
$this->$attribute($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sort(string $values): Builder
|
||||||
|
{
|
||||||
|
$sortables = array_map('trim', explode(',', $values));
|
||||||
|
|
||||||
|
foreach ($sortables as $sortable) {
|
||||||
|
$direction = Str::startsWith($sortable, '-') ? 'desc' : 'asc';
|
||||||
|
$column = Str::of($sortable)->remove('-')->value();
|
||||||
|
|
||||||
|
if (in_array($column, $this->sortable)) {
|
||||||
|
$this->builder->orderBy($column, $direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder;
|
||||||
|
}
|
||||||
|
}
|
55
app/Http/Filters/V1/UserFilter.php
Normal file
55
app/Http/Filters/V1/UserFilter.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Filters\V1;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class UserFilter extends QueryFilter
|
||||||
|
{
|
||||||
|
protected array $sortable = [
|
||||||
|
'name',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO: Many of these are repeated across UserFilter and ModFilter. Consider refactoring into a shared trait.
|
||||||
|
// Also, consider using common filter types and making the field names dynamic.
|
||||||
|
|
||||||
|
public function id(string $value): Builder
|
||||||
|
{
|
||||||
|
$ids = array_map('trim', explode(',', $value));
|
||||||
|
|
||||||
|
return $this->builder->whereIn('id', $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API handles the wildcard character as an asterisk (*), but the database uses the percentage sign (%).
|
||||||
|
$like = Str::replace('*', '%', $value);
|
||||||
|
|
||||||
|
return $this->builder->where('name', 'like', $like);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function created_at(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||||
|
$dates = array_map('trim', explode(',', $value));
|
||||||
|
if (count($dates) > 1) {
|
||||||
|
return $this->builder->whereBetween('created_at', $dates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->whereDate('created_at', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated_at(string $value): Builder
|
||||||
|
{
|
||||||
|
// The API allows for a range of dates to be passed as a comma-separated list.
|
||||||
|
$dates = array_map('trim', explode(',', $value));
|
||||||
|
if (count($dates) > 1) {
|
||||||
|
return $this->builder->whereBetween('updated_at', $dates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->builder->whereDate('updated_at', $value);
|
||||||
|
}
|
||||||
|
}
|
25
app/Http/Resources/Api/V0/LicenseResource.php
Normal file
25
app/Http/Resources/Api/V0/LicenseResource.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api\V0;
|
||||||
|
|
||||||
|
use App\Models\License;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin License */
|
||||||
|
class LicenseResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'license',
|
||||||
|
'id' => $this->id,
|
||||||
|
'attributes' => [
|
||||||
|
'name' => $this->name,
|
||||||
|
'link' => $this->link,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources\Api\V0;
|
namespace App\Http\Resources\Api\V0;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V0\ApiController;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
@ -18,6 +19,7 @@ class ModResource extends JsonResource
|
|||||||
'type' => 'mod',
|
'type' => 'mod',
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'attributes' => [
|
'attributes' => [
|
||||||
|
'hub_id' => $this->hub_id,
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'slug' => $this->slug,
|
'slug' => $this->slug,
|
||||||
'teaser' => $this->teaser,
|
'teaser' => $this->teaser,
|
||||||
@ -32,34 +34,53 @@ class ModResource extends JsonResource
|
|||||||
'contains_ads' => $this->contains_ads,
|
'contains_ads' => $this->contains_ads,
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
|
'published_at' => $this->published_at,
|
||||||
],
|
],
|
||||||
'relationships' => [
|
'relationships' => [
|
||||||
'users' => [
|
'users' => $this->users->map(fn ($user) => [
|
||||||
'data' => $this->users->map(fn ($user) => [
|
'data' => [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
])->toArray(),
|
],
|
||||||
|
'links' => [
|
||||||
// TODO: Provide 'links.self' to user profile
|
'self' => $user->profileUrl(),
|
||||||
//'links' => ['self' => '#'],
|
],
|
||||||
],
|
])->toArray(),
|
||||||
'license' => [
|
'versions' => $this->versions->map(fn ($version) => [
|
||||||
'data' => [
|
'data' => [
|
||||||
'type' => 'license',
|
'type' => 'version',
|
||||||
'id' => $this->license_id,
|
'id' => $version->id,
|
||||||
|
],
|
||||||
|
|
||||||
|
// TODO: The download link to the version can be placed here, but I'd like to track the number of
|
||||||
|
// downloads that are made, so we'll need a new route/feature for that. #35
|
||||||
|
'links' => [
|
||||||
|
'self' => $version->link,
|
||||||
|
],
|
||||||
|
|
||||||
|
])->toArray(),
|
||||||
|
'license' => [
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'type' => 'license',
|
||||||
|
'id' => $this->license_id,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'included' => $this->users->map(fn ($user) => new UserResource($user)),
|
'includes' => $this->when(
|
||||||
|
ApiController::shouldInclude(['users', 'license', 'versions']),
|
||||||
// TODO: Provide 'included' data for attached 'license':
|
fn () => collect([
|
||||||
//new LicenseResource($this->license)
|
'users' => $this->users->map(fn ($user) => new UserResource($user)),
|
||||||
|
'license' => new LicenseResource($this->license),
|
||||||
|
'versions' => $this->versions->map(fn ($version) => new ModVersionResource($version)),
|
||||||
|
])
|
||||||
|
->filter(fn ($value, $key) => ApiController::shouldInclude($key))
|
||||||
|
->flatten(1)
|
||||||
|
->values()
|
||||||
|
),
|
||||||
'links' => [
|
'links' => [
|
||||||
'self' => route('mod.show', [
|
'self' => $this->detailUrl(),
|
||||||
'mod' => $this->id,
|
|
||||||
'slug' => $this->slug,
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
54
app/Http/Resources/Api/V0/ModVersionResource.php
Normal file
54
app/Http/Resources/Api/V0/ModVersionResource.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api\V0;
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin ModVersion */
|
||||||
|
class ModVersionResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'mod_version',
|
||||||
|
'id' => $this->id,
|
||||||
|
'attributes' => [
|
||||||
|
'hub_id' => $this->hub_id,
|
||||||
|
'mod_id' => $this->mod_id,
|
||||||
|
'version' => $this->version,
|
||||||
|
|
||||||
|
// TODO: This should only be visible on the mod version show route(?) which doesn't exist.
|
||||||
|
//'description' => $this->when(
|
||||||
|
// $request->routeIs('api.v0.modversion.show'),
|
||||||
|
// $this->description
|
||||||
|
//),
|
||||||
|
|
||||||
|
// TODO: The download link to the version can be placed here, but I'd like to track the number of
|
||||||
|
// downloads that are made, so we'll need a new route/feature for that. #35
|
||||||
|
'link' => $this->link,
|
||||||
|
|
||||||
|
'spt_version_id' => $this->spt_version_id,
|
||||||
|
'virus_total_link' => $this->virus_total_link,
|
||||||
|
'downloads' => $this->downloads,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
'published_at' => $this->published_at,
|
||||||
|
],
|
||||||
|
'relationships' => [
|
||||||
|
'spt_version' => [
|
||||||
|
[
|
||||||
|
'data' => [
|
||||||
|
'type' => 'spt_version',
|
||||||
|
'id' => $this->spt_version_id,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Resources\Api\V0;
|
namespace App\Http\Resources\Api\V0;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V0\ApiController;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
@ -9,9 +10,6 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||||||
/** @mixin User */
|
/** @mixin User */
|
||||||
class UserResource extends JsonResource
|
class UserResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Transform the resource into an array.
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -21,6 +19,7 @@ class UserResource extends JsonResource
|
|||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'user_role_id' => $this->user_role_id,
|
'user_role_id' => $this->user_role_id,
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
],
|
],
|
||||||
'relationships' => [
|
'relationships' => [
|
||||||
'user_role' => [
|
'user_role' => [
|
||||||
@ -30,11 +29,13 @@ class UserResource extends JsonResource
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
// TODO: Provide 'included' data for attached 'user_role'
|
'includes' => $this->when(
|
||||||
//'included' => [new UserRoleResource($this->role)],
|
ApiController::shouldInclude('user_role'),
|
||||||
|
new UserRoleResource($this->role)
|
||||||
// TODO: Provide 'links.self' to user profile:
|
),
|
||||||
//'links' => ['self' => '#'],
|
'links' => [
|
||||||
|
'self' => $this->profileUrl(),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
app/Http/Resources/Api/V0/UserRoleResource.php
Normal file
25
app/Http/Resources/Api/V0/UserRoleResource.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Api\V0;
|
||||||
|
|
||||||
|
use App\Models\UserRole;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin UserRole */
|
||||||
|
class UserRoleResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'type' => 'user_role',
|
||||||
|
'id' => $this->id,
|
||||||
|
'attributes' => [
|
||||||
|
'name' => $this->name,
|
||||||
|
'short_name' => $this->short_name,
|
||||||
|
'description' => $this->description,
|
||||||
|
'color_class' => $this->color_class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ class ModResource extends JsonResource
|
|||||||
'license' => new LicenseResource($this->whenLoaded('license')),
|
'license' => new LicenseResource($this->whenLoaded('license')),
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
|
'published_at' => $this->published_at,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ class ModVersionResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
|
'published_at' => $this->published_at,
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'version' => $this->version,
|
'version' => $this->version,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
|
@ -36,6 +36,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
{
|
{
|
||||||
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
|
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
|
||||||
// tables to store the data to save on memory; we don't want this to be a hog.
|
// tables to store the data to save on memory; we don't want this to be a hog.
|
||||||
|
$this->bringUserAvatarLocal();
|
||||||
$this->bringFileAuthorsLocal();
|
$this->bringFileAuthorsLocal();
|
||||||
$this->bringFileOptionsLocal();
|
$this->bringFileOptionsLocal();
|
||||||
$this->bringFileContentLocal();
|
$this->bringFileContentLocal();
|
||||||
@ -54,6 +55,36 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
|
|
||||||
// Re-sync search.
|
// Re-sync search.
|
||||||
Artisan::call('app:search-sync');
|
Artisan::call('app:search-sync');
|
||||||
|
|
||||||
|
Artisan::call('cache:clear');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring the user avatar table from the Hub database to the local database temporary table.
|
||||||
|
*/
|
||||||
|
protected function bringUserAvatarLocal(): void
|
||||||
|
{
|
||||||
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||||
|
DB::statement('CREATE TEMPORARY TABLE temp_user_avatar (
|
||||||
|
avatarID INT,
|
||||||
|
avatarExtension VARCHAR(255),
|
||||||
|
userID INT,
|
||||||
|
fileHash VARCHAR(255)
|
||||||
|
)');
|
||||||
|
|
||||||
|
DB::connection('mysql_hub')
|
||||||
|
->table('wcf1_user_avatar')
|
||||||
|
->orderBy('avatarID')
|
||||||
|
->chunk(200, function ($avatars) {
|
||||||
|
foreach ($avatars as $avatar) {
|
||||||
|
DB::table('temp_user_avatar')->insert([
|
||||||
|
'avatarID' => (int) $avatar->avatarID,
|
||||||
|
'avatarExtension' => $avatar->avatarExtension,
|
||||||
|
'userID' => (int) $avatar->userID,
|
||||||
|
'fileHash' => $avatar->fileHash,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,10 +93,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileAuthorsLocal(): void
|
protected function bringFileAuthorsLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_author (
|
DB::statement('CREATE TEMPORARY TABLE temp_file_author (fileID INT, userID INT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
fileID INT,
|
|
||||||
userID INT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_author')
|
->table('filebase1_file_author')
|
||||||
@ -86,11 +114,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileOptionsLocal(): void
|
protected function bringFileOptionsLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
|
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (fileID INT, optionID INT, optionValue VARCHAR(255)) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
fileID INT,
|
|
||||||
optionID INT,
|
|
||||||
optionValue VARCHAR(255)
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_option_value')
|
->table('filebase1_file_option_value')
|
||||||
@ -112,12 +136,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileContentLocal(): void
|
protected function bringFileContentLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_content (
|
DB::statement('CREATE TEMPORARY TABLE temp_file_content (fileID INT, subject VARCHAR(255), teaser VARCHAR(255), message LONGTEXT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
fileID INT,
|
|
||||||
subject VARCHAR(255),
|
|
||||||
teaser VARCHAR(255),
|
|
||||||
message LONGTEXT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_content')
|
->table('filebase1_file_content')
|
||||||
@ -140,10 +159,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileVersionLabelsLocal(): void
|
protected function bringFileVersionLabelsLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
|
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (labelID INT, objectID INT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
labelID INT,
|
|
||||||
objectID INT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('wcf1_label_object')
|
->table('wcf1_label_object')
|
||||||
@ -165,10 +181,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
protected function bringFileVersionContentLocal(): void
|
protected function bringFileVersionContentLocal(): void
|
||||||
{
|
{
|
||||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
||||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
|
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (versionID INT, description TEXT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||||
versionID INT,
|
|
||||||
description TEXT
|
|
||||||
)');
|
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('filebase1_file_version_content')
|
->table('filebase1_file_version_content')
|
||||||
@ -188,15 +201,33 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
*/
|
*/
|
||||||
protected function importUsers(): void
|
protected function importUsers(): void
|
||||||
{
|
{
|
||||||
|
// Initialize a cURL handler for downloading mod thumbnails.
|
||||||
|
$curl = curl_init();
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
|
||||||
DB::connection('mysql_hub')
|
DB::connection('mysql_hub')
|
||||||
->table('wcf1_user as u')
|
->table('wcf1_user as u')
|
||||||
->select('u.userID', 'u.username', 'u.email', 'u.password', 'u.registrationDate', 'u.banned', 'u.banReason', 'u.banExpires', 'u.rankID', 'r.rankTitle')
|
->select(
|
||||||
|
'u.userID',
|
||||||
|
'u.username',
|
||||||
|
'u.email',
|
||||||
|
'u.password',
|
||||||
|
'u.registrationDate',
|
||||||
|
'u.banned',
|
||||||
|
'u.banReason',
|
||||||
|
'u.banExpires',
|
||||||
|
'u.coverPhotoHash',
|
||||||
|
'u.coverPhotoExtension',
|
||||||
|
'u.rankID',
|
||||||
|
'r.rankTitle',
|
||||||
|
)
|
||||||
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
|
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
|
||||||
->chunkById(250, function (Collection $users) {
|
->chunkById(250, function (Collection $users) use ($curl) {
|
||||||
$userData = $bannedUsers = $userRanks = [];
|
$userData = $bannedUsers = $userRanks = [];
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$userData[] = $this->collectUserData($user);
|
$userData[] = $this->collectUserData($curl, $user);
|
||||||
|
|
||||||
$bannedUserData = $this->collectBannedUserData($user);
|
$bannedUserData = $this->collectBannedUserData($user);
|
||||||
if ($bannedUserData) {
|
if ($bannedUserData) {
|
||||||
@ -213,15 +244,20 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
$this->handleBannedUsers($bannedUsers);
|
$this->handleBannedUsers($bannedUsers);
|
||||||
$this->handleUserRoles($userRanks);
|
$this->handleUserRoles($userRanks);
|
||||||
}, 'userID');
|
}, 'userID');
|
||||||
|
|
||||||
|
// Close the cURL handler.
|
||||||
|
curl_close($curl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function collectUserData($user): array
|
protected function collectUserData(CurlHandle $curl, object $user): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'hub_id' => (int) $user->userID,
|
'hub_id' => (int) $user->userID,
|
||||||
'name' => $user->username,
|
'name' => $user->username,
|
||||||
'email' => mb_convert_case($user->email, MB_CASE_LOWER, 'UTF-8'),
|
'email' => Str::lower($user->email),
|
||||||
'password' => $this->cleanPasswordHash($user->password),
|
'password' => $this->cleanPasswordHash($user->password),
|
||||||
|
'profile_photo_path' => $this->fetchUserAvatar($curl, $user),
|
||||||
|
'cover_photo_path' => $this->fetchUserCoverPhoto($curl, $user),
|
||||||
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
||||||
'updated_at' => now('UTC')->toDateTimeString(),
|
'updated_at' => now('UTC')->toDateTimeString(),
|
||||||
];
|
];
|
||||||
@ -240,6 +276,75 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
return str_starts_with($clean, '$2') ? $clean : '';
|
return str_starts_with($clean, '$2') ? $clean : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the user avatar from the Hub and store it anew.
|
||||||
|
*/
|
||||||
|
protected function fetchUserAvatar(CurlHandle $curl, object $user): string
|
||||||
|
{
|
||||||
|
// Fetch the user's avatar data from the temporary table.
|
||||||
|
$avatar = DB::table('temp_user_avatar')->where('userID', $user->userID)->first();
|
||||||
|
|
||||||
|
if (! $avatar) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashShort = substr($avatar->fileHash, 0, 2);
|
||||||
|
$fileName = $avatar->fileHash.'.'.$avatar->avatarExtension;
|
||||||
|
$hubUrl = 'https://hub.sp-tarkov.com/images/avatars/'.$hashShort.'/'.$avatar->avatarID.'-'.$fileName;
|
||||||
|
$relativePath = 'user-avatars/'.$fileName;
|
||||||
|
|
||||||
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and store an image from the Hub.
|
||||||
|
*/
|
||||||
|
protected function fetchAndStoreImage(CurlHandle $curl, string $hubUrl, string $relativePath): string
|
||||||
|
{
|
||||||
|
// Determine the disk to use based on the environment.
|
||||||
|
$disk = match (config('app.env')) {
|
||||||
|
'production' => 'r2', // Cloudflare R2 Storage
|
||||||
|
default => 'public', // Local
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check to make sure the image doesn't already exist.
|
||||||
|
if (Storage::disk($disk)->exists($relativePath)) {
|
||||||
|
return $relativePath; // Already exists, return the path.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the image using the cURL handler.
|
||||||
|
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||||
|
$image = curl_exec($curl);
|
||||||
|
|
||||||
|
if ($image === false) {
|
||||||
|
Log::error('There was an error attempting to download the image. cURL error: '.curl_error($curl));
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the image on the disk.
|
||||||
|
Storage::disk($disk)->put($relativePath, $image);
|
||||||
|
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the user avatar from the Hub and store it anew.
|
||||||
|
*/
|
||||||
|
protected function fetchUserCoverPhoto(CurlHandle $curl, object $user): string
|
||||||
|
{
|
||||||
|
if (empty($user->coverPhotoHash) || empty($user->coverPhotoExtension)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashShort = substr($user->coverPhotoHash, 0, 2);
|
||||||
|
$fileName = $user->coverPhotoHash.'.'.$user->coverPhotoExtension;
|
||||||
|
$hubUrl = 'https://hub.sp-tarkov.com/images/coverPhotos/'.$hashShort.'/'.$user->userID.'-'.$fileName;
|
||||||
|
$relativePath = 'user-covers/'.$fileName;
|
||||||
|
|
||||||
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean the registration date from the Hub database.
|
* Clean the registration date from the Hub database.
|
||||||
*/
|
*/
|
||||||
@ -311,9 +416,6 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Build an array of user rank data ready to be inserted into the local database.
|
|
||||||
*/
|
|
||||||
protected function collectUserRankData($user): ?array
|
protected function collectUserRankData($user): ?array
|
||||||
{
|
{
|
||||||
if ($user->rankID && $user->rankTitle) {
|
if ($user->rankID && $user->rankTitle) {
|
||||||
@ -531,7 +633,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
'users' => $modAuthors,
|
'users' => $modAuthors,
|
||||||
'name' => $modContent?->subject ?? '',
|
'name' => $modContent?->subject ?? '',
|
||||||
'slug' => Str::slug($modContent?->subject ?? ''),
|
'slug' => Str::slug($modContent?->subject ?? ''),
|
||||||
'teaser' => Str::limit($modContent?->teaser ?? ''),
|
'teaser' => Str::limit($modContent?->teaser ?? '', 255),
|
||||||
'description' => $this->cleanHubContent($modContent?->message ?? ''),
|
'description' => $this->cleanHubContent($modContent?->message ?? ''),
|
||||||
'thumbnail' => $this->fetchModThumbnail($curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
|
'thumbnail' => $this->fetchModThumbnail($curl, $mod->fileID, $mod->iconHash, $mod->iconExtension),
|
||||||
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
|
'license_id' => License::whereHubId($mod->licenseID)->value('id'),
|
||||||
@ -540,6 +642,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
'contains_ai_content' => (bool) $optionContainsAi?->contains_ai,
|
'contains_ai_content' => (bool) $optionContainsAi?->contains_ai,
|
||||||
'contains_ads' => (bool) $optionContainsAds?->contains_ads,
|
'contains_ads' => (bool) $optionContainsAds?->contains_ads,
|
||||||
'disabled' => (bool) $mod->isDisabled,
|
'disabled' => (bool) $mod->isDisabled,
|
||||||
|
'published_at' => Carbon::parse($mod->time, 'UTC'),
|
||||||
'created_at' => Carbon::parse($mod->time, 'UTC'),
|
'created_at' => Carbon::parse($mod->time, 'UTC'),
|
||||||
'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'),
|
'updated_at' => Carbon::parse($mod->lastChangeTime, 'UTC'),
|
||||||
];
|
];
|
||||||
@ -549,7 +652,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
// Remove the user_id from the mod data before upserting.
|
// Remove the user_id from the mod data before upserting.
|
||||||
$insertModData = array_map(fn ($mod) => Arr::except($mod, 'users'), $modData);
|
$insertModData = array_map(fn ($mod) => Arr::except($mod, 'users'), $modData);
|
||||||
|
|
||||||
Mod::upsert($insertModData, ['hub_id'], [
|
Mod::withoutGlobalScopes()->upsert($insertModData, ['hub_id'], [
|
||||||
'name',
|
'name',
|
||||||
'slug',
|
'slug',
|
||||||
'teaser',
|
'teaser',
|
||||||
@ -561,6 +664,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
'contains_ai_content',
|
'contains_ai_content',
|
||||||
'contains_ads',
|
'contains_ads',
|
||||||
'disabled',
|
'disabled',
|
||||||
|
'published_at',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]);
|
]);
|
||||||
@ -582,7 +686,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
{
|
{
|
||||||
// Alright, hear me out... Shut up.
|
// Alright, hear me out... Shut up.
|
||||||
|
|
||||||
$converter = new HtmlConverter();
|
$converter = new HtmlConverter;
|
||||||
$clean = Purify::clean($dirty);
|
$clean = Purify::clean($dirty);
|
||||||
|
|
||||||
return $converter->convert($clean);
|
return $converter->convert($clean);
|
||||||
@ -604,31 +708,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
$hubUrl = 'https://hub.sp-tarkov.com/files/images/file/'.$hashShort.'/'.$fileName;
|
$hubUrl = 'https://hub.sp-tarkov.com/files/images/file/'.$hashShort.'/'.$fileName;
|
||||||
$relativePath = 'mods/'.$fileName;
|
$relativePath = 'mods/'.$fileName;
|
||||||
|
|
||||||
// Determine the disk to use based on the environment.
|
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||||
$disk = match (config('app.env')) {
|
|
||||||
'production' => 'r2', // Cloudflare R2 Storage
|
|
||||||
default => 'public', // Local
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check to make sure the image doesn't already exist.
|
|
||||||
if (Storage::disk($disk)->exists($relativePath)) {
|
|
||||||
return $relativePath; // Already exists, return the path.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download the image using the cURL handler.
|
|
||||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
|
||||||
$image = curl_exec($curl);
|
|
||||||
|
|
||||||
if ($image === false) {
|
|
||||||
Log::error('There was an error attempting to download a mod thumbnail. cURL error: '.curl_error($curl));
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the image on the disk.
|
|
||||||
Storage::disk($disk)->put($relativePath, $image);
|
|
||||||
|
|
||||||
return $relativePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -678,13 +758,14 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
||||||
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
||||||
'disabled' => (bool) $version->isDisabled,
|
'disabled' => (bool) $version->isDisabled,
|
||||||
|
'published_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! empty($insertData)) {
|
if (! empty($insertData)) {
|
||||||
ModVersion::upsert($insertData, ['hub_id'], [
|
ModVersion::withoutGlobalScopes()->upsert($insertData, ['hub_id'], [
|
||||||
'mod_id',
|
'mod_id',
|
||||||
'version',
|
'version',
|
||||||
'description',
|
'description',
|
||||||
@ -692,6 +773,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
'spt_version_id',
|
'spt_version_id',
|
||||||
'virus_total_link',
|
'virus_total_link',
|
||||||
'downloads',
|
'downloads',
|
||||||
|
'published_at',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
]);
|
]);
|
||||||
@ -705,6 +787,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
|||||||
public function failed(Exception $exception): void
|
public function failed(Exception $exception): void
|
||||||
{
|
{
|
||||||
// Explicitly drop the temporary tables.
|
// Explicitly drop the temporary tables.
|
||||||
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||||
|
76
app/Livewire/Profile/UpdateProfileForm.php
Normal file
76
app/Livewire/Profile/UpdateProfileForm.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Profile;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||||
|
use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm;
|
||||||
|
use Livewire\Features\SupportRedirects\Redirector;
|
||||||
|
|
||||||
|
class UpdateProfileForm extends UpdateProfileInformationForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The new cover photo for the user.
|
||||||
|
*
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
public $cover;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the photo is temporarily uploaded.
|
||||||
|
*/
|
||||||
|
public function updatedPhoto(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'photo' => 'image|mimes:jpg,jpeg,png|max:1024', // 1MB Max
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the cover is temporarily uploaded.
|
||||||
|
*/
|
||||||
|
public function updatedCover(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'cover' => 'image|mimes:jpg,jpeg,png|max:2048', // 2MB Max
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function updateProfileInformation(UpdatesUserProfileInformation $updater): RedirectResponse|Redirector|null
|
||||||
|
{
|
||||||
|
$this->resetErrorBag();
|
||||||
|
|
||||||
|
$updater->update(
|
||||||
|
Auth::user(),
|
||||||
|
$this->photo || $this->cover
|
||||||
|
? array_merge($this->state, array_filter([
|
||||||
|
'photo' => $this->photo,
|
||||||
|
'cover' => $this->cover,
|
||||||
|
])) : $this->state
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($this->photo) || isset($this->cover)) {
|
||||||
|
return redirect()->route('profile.show');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('saved');
|
||||||
|
|
||||||
|
$this->dispatch('refresh-navigation-menu');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete user's profile photo.
|
||||||
|
*/
|
||||||
|
public function deleteCoverPhoto(): void
|
||||||
|
{
|
||||||
|
Auth::user()->deleteCoverPhoto();
|
||||||
|
|
||||||
|
$this->dispatch('refresh-navigation-menu');
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,9 @@ class License extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a license and mod.
|
||||||
|
*/
|
||||||
public function mods(): HasMany
|
public function mods(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Mod::class);
|
return $this->hasMany(Mod::class);
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Http\Filters\V1\QueryFilter;
|
||||||
use App\Models\Scopes\DisabledScope;
|
use App\Models\Scopes\DisabledScope;
|
||||||
|
use App\Models\Scopes\PublishedScope;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@ -16,34 +19,47 @@ use Illuminate\Support\Str;
|
|||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $name
|
||||||
* @property string $slug
|
* @property string $slug
|
||||||
*/
|
*/
|
||||||
class Mod extends Model
|
class Mod extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, Searchable, SoftDeletes;
|
use HasFactory, Searchable, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post boot method to configure the model.
|
||||||
|
*/
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
// Apply the global scope to exclude disabled mods.
|
// Apply the global scope to exclude disabled mods.
|
||||||
static::addGlobalScope(new DisabledScope);
|
static::addGlobalScope(new DisabledScope);
|
||||||
|
// Apply the global scope to exclude non-published mods.
|
||||||
|
static::addGlobalScope(new PublishedScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The users that belong to the mod.
|
* The relationship between a mod and its users.
|
||||||
*/
|
*/
|
||||||
public function users(): BelongsToMany
|
public function users(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class);
|
return $this->belongsToMany(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod and its license.
|
||||||
|
*/
|
||||||
public function license(): BelongsTo
|
public function license(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(License::class);
|
return $this->belongsTo(License::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod and its versions.
|
||||||
|
*/
|
||||||
public function versions(): HasMany
|
public function versions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ModVersion::class);
|
return $this->hasMany(ModVersion::class)->orderByDesc('version');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,17 +73,21 @@ class Mod extends Model
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod and its last updated version.
|
||||||
|
*/
|
||||||
public function lastUpdatedVersion(): HasOne
|
public function lastUpdatedVersion(): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(ModVersion::class)->orderByDesc('updated_at')->with('sptVersion');
|
return $this->hasOne(ModVersion::class)
|
||||||
|
->orderByDesc('updated_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the indexable data array for the model.
|
* The data that is searchable by Scout.
|
||||||
*/
|
*/
|
||||||
public function toSearchableArray(): array
|
public function toSearchableArray(): array
|
||||||
{
|
{
|
||||||
$latestSptVersion = $this->latestSptVersion()->first();
|
$latestVersion = $this->latestVersion()->with('sptVersion')->first();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $this->id,
|
'id' => (int) $this->id,
|
||||||
@ -78,27 +98,25 @@ class Mod extends Model
|
|||||||
'featured' => $this->featured,
|
'featured' => $this->featured,
|
||||||
'created_at' => strtotime($this->created_at),
|
'created_at' => strtotime($this->created_at),
|
||||||
'updated_at' => strtotime($this->updated_at),
|
'updated_at' => strtotime($this->updated_at),
|
||||||
'latestSptVersion' => $latestSptVersion?->sptVersion->version,
|
'published_at' => strtotime($this->published_at),
|
||||||
'latestSptVersionColorClass' => $latestSptVersion?->sptVersion->color_class,
|
'latestVersion' => $latestVersion?->sptVersion->version,
|
||||||
|
'latestVersionColorClass' => $latestVersion?->sptVersion->color_class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function latestSptVersion(): HasOne
|
/**
|
||||||
|
* The relationship to the latest mod version, dictated by the mod version number.
|
||||||
|
*/
|
||||||
|
public function latestVersion(): HasOne
|
||||||
{
|
{
|
||||||
return $this->hasOne(ModVersion::class)
|
return $this->hasOne(ModVersion::class)
|
||||||
->orderByDesc(
|
|
||||||
SptVersion::select('version')
|
|
||||||
->whereColumn('mod_versions.spt_version_id', 'spt_versions.id')
|
|
||||||
->orderByDesc('version')
|
|
||||||
->take(1),
|
|
||||||
)
|
|
||||||
->with('sptVersion')
|
|
||||||
->orderByDesc('version')
|
->orderByDesc('version')
|
||||||
|
->orderByDesc('updated_at')
|
||||||
->take(1);
|
->take(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the model should be searchable.
|
* Determine if the model instance should be searchable.
|
||||||
*/
|
*/
|
||||||
public function shouldBeSearchable(): bool
|
public function shouldBeSearchable(): bool
|
||||||
{
|
{
|
||||||
@ -106,7 +124,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URL to the thumbnail.
|
* Build the URL to the mod's thumbnail.
|
||||||
*/
|
*/
|
||||||
public function thumbnailUrl(): Attribute
|
public function thumbnailUrl(): Attribute
|
||||||
{
|
{
|
||||||
@ -118,7 +136,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the disk where the thumbnail is stored.
|
* Get the disk where the thumbnail is stored based on the current environment.
|
||||||
*/
|
*/
|
||||||
protected function thumbnailDisk(): string
|
protected function thumbnailDisk(): string
|
||||||
{
|
{
|
||||||
@ -128,6 +146,25 @@ class Mod extends Model
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query by applying QueryFilter filters.
|
||||||
|
*/
|
||||||
|
public function scopeFilter(Builder $builder, QueryFilter $filters): Builder
|
||||||
|
{
|
||||||
|
return $filters->apply($builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the URL to the mod's detail page.
|
||||||
|
*/
|
||||||
|
public function detailUrl(): string
|
||||||
|
{
|
||||||
|
return route('mod.show', [$this->id, $this->slug]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that should be cast to native types.
|
||||||
|
*/
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -139,7 +176,7 @@ class Mod extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the slug is always lower case when retrieved and slugified when saved.
|
* Mutate the slug attribute to always be lower case on get and slugified on set.
|
||||||
*/
|
*/
|
||||||
protected function slug(): Attribute
|
protected function slug(): Attribute
|
||||||
{
|
{
|
||||||
|
43
app/Models/ModDependency.php
Normal file
43
app/Models/ModDependency.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $mod_version_id
|
||||||
|
* @property int $dependency_mod_id
|
||||||
|
* @property string $version_constraint
|
||||||
|
* @property int|null $resolved_version_id
|
||||||
|
*/
|
||||||
|
class ModDependency extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod dependency and mod version.
|
||||||
|
*/
|
||||||
|
public function modVersion(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ModVersion::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod dependency and mod.
|
||||||
|
*/
|
||||||
|
public function dependencyMod(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Mod::class, 'dependency_mod_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod dependency and resolved mod version.
|
||||||
|
*/
|
||||||
|
public function resolvedVersion(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ModVersion::class, 'resolved_version_id');
|
||||||
|
}
|
||||||
|
}
|
@ -3,25 +3,50 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Scopes\DisabledScope;
|
use App\Models\Scopes\DisabledScope;
|
||||||
|
use App\Models\Scopes\PublishedScope;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $mod_id
|
||||||
|
* @property string $version
|
||||||
|
*/
|
||||||
class ModVersion extends Model
|
class ModVersion extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post boot method to configure the model.
|
||||||
|
*/
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
static::addGlobalScope(new DisabledScope);
|
static::addGlobalScope(new DisabledScope);
|
||||||
|
static::addGlobalScope(new PublishedScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod version and mod.
|
||||||
|
*/
|
||||||
public function mod(): BelongsTo
|
public function mod(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Mod::class);
|
return $this->belongsTo(Mod::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod version and its dependencies.
|
||||||
|
*/
|
||||||
|
public function dependencies(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ModDependency::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a mod version and SPT version.
|
||||||
|
*/
|
||||||
public function sptVersion(): BelongsTo
|
public function sptVersion(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(SptVersion::class);
|
return $this->belongsTo(SptVersion::class);
|
||||||
|
19
app/Models/Scopes/PublishedScope.php
Normal file
19
app/Models/Scopes/PublishedScope.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Scopes;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Scope;
|
||||||
|
|
||||||
|
class PublishedScope implements Scope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Apply the scope to a given Eloquent query builder.
|
||||||
|
*/
|
||||||
|
public function apply(Builder $builder, Model $model): void
|
||||||
|
{
|
||||||
|
$builder->whereNotNull($model->getTable().'.published_at')
|
||||||
|
->where($model->getTable().'.published_at', '<=', now());
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,9 @@ class SptVersion extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes;
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between an SPT version and mod version.
|
||||||
|
*/
|
||||||
public function modVersions(): HasMany
|
public function modVersions(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(ModVersion::class);
|
return $this->hasMany(ModVersion::class);
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Http\Filters\V1\QueryFilter;
|
||||||
use App\Notifications\ResetPassword;
|
use App\Notifications\ResetPassword;
|
||||||
use App\Notifications\VerifyEmail;
|
use App\Notifications\VerifyEmail;
|
||||||
|
use App\Traits\HasCoverPhoto;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
@ -21,6 +24,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
{
|
{
|
||||||
use Bannable;
|
use Bannable;
|
||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
|
use HasCoverPhoto;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HasProfilePhoto;
|
use HasProfilePhoto;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
@ -38,11 +42,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
'profile_photo_url',
|
'profile_photo_url',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user and their mods.
|
||||||
|
*/
|
||||||
public function mods(): BelongsToMany
|
public function mods(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Mod::class);
|
return $this->belongsToMany(Mod::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data that is searchable by Scout.
|
||||||
|
*/
|
||||||
public function toSearchableArray(): array
|
public function toSearchableArray(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -51,28 +61,25 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the model instance should be searchable.
|
||||||
|
*/
|
||||||
public function shouldBeSearchable(): bool
|
public function shouldBeSearchable(): bool
|
||||||
{
|
{
|
||||||
return ! is_null($this->email_verified_at);
|
return ! is_null($this->email_verified_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function assignRole(UserRole $role): bool
|
/**
|
||||||
{
|
* Check if the user has the role of a moderator.
|
||||||
$this->role()->associate($role);
|
*/
|
||||||
|
|
||||||
return $this->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function role(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(UserRole::class, 'user_role_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isMod(): bool
|
public function isMod(): bool
|
||||||
{
|
{
|
||||||
return Str::lower($this->role?->name) === 'moderator';
|
return Str::lower($this->role?->name) === 'moderator';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has the role of an administrator.
|
||||||
|
*/
|
||||||
public function isAdmin(): bool
|
public function isAdmin(): bool
|
||||||
{
|
{
|
||||||
return Str::lower($this->role?->name) === 'administrator';
|
return Str::lower($this->role?->name) === 'administrator';
|
||||||
@ -94,12 +101,49 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
$this->notify(new ResetPassword($token));
|
$this->notify(new ResetPassword($token));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function casts(): array
|
/**
|
||||||
|
* Get the relative URL to the user's profile page.
|
||||||
|
*/
|
||||||
|
public function profileUrl(): string
|
||||||
{
|
{
|
||||||
return [
|
return route('user.show', [
|
||||||
'email_verified_at' => 'datetime',
|
'user' => $this->id,
|
||||||
'password' => 'hashed',
|
'username' => $this->slug(),
|
||||||
];
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the slug of the user's name.
|
||||||
|
*/
|
||||||
|
public function slug(): string
|
||||||
|
{
|
||||||
|
return Str::lower(Str::slug($this->name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a role to the user.
|
||||||
|
*/
|
||||||
|
public function assignRole(UserRole $role): bool
|
||||||
|
{
|
||||||
|
$this->role()->associate($role);
|
||||||
|
|
||||||
|
return $this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user and their role.
|
||||||
|
*/
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserRole::class, 'user_role_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a query by applying QueryFilter filters.
|
||||||
|
*/
|
||||||
|
public function scopeFilter(Builder $builder, QueryFilter $filters): Builder
|
||||||
|
{
|
||||||
|
return $filters->apply($builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,9 +151,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
*/
|
*/
|
||||||
protected function profilePhotoDisk(): string
|
protected function profilePhotoDisk(): string
|
||||||
{
|
{
|
||||||
return match (config('app.env')) {
|
return config('filesystems.asset_upload', 'public');
|
||||||
'production' => 'r2', // Cloudflare R2 Storage
|
}
|
||||||
default => 'public', // Local
|
|
||||||
};
|
/**
|
||||||
|
* The attributes that should be cast to native types.
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,9 @@ class UserRole extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The relationship between a user role and users.
|
||||||
|
*/
|
||||||
public function users(): HasMany
|
public function users(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(User::class);
|
return $this->hasMany(User::class);
|
||||||
|
33
app/Observers/ModDependencyObserver.php
Normal file
33
app/Observers/ModDependencyObserver.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Services\ModVersionService;
|
||||||
|
|
||||||
|
class ModDependencyObserver
|
||||||
|
{
|
||||||
|
protected ModVersionService $modVersionService;
|
||||||
|
|
||||||
|
public function __construct(ModVersionService $modVersionService)
|
||||||
|
{
|
||||||
|
$this->modVersionService = $modVersionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saved(ModDependency $modDependency): void
|
||||||
|
{
|
||||||
|
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
||||||
|
if ($modVersion) {
|
||||||
|
$this->modVersionService->resolveDependencies($modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(ModDependency $modDependency): void
|
||||||
|
{
|
||||||
|
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
||||||
|
if ($modVersion) {
|
||||||
|
$this->modVersionService->resolveDependencies($modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
app/Observers/ModVersionObserver.php
Normal file
33
app/Observers/ModVersionObserver.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use App\Services\ModVersionService;
|
||||||
|
|
||||||
|
class ModVersionObserver
|
||||||
|
{
|
||||||
|
protected ModVersionService $modVersionService;
|
||||||
|
|
||||||
|
public function __construct(ModVersionService $modVersionService)
|
||||||
|
{
|
||||||
|
$this->modVersionService = $modVersionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saved(ModVersion $modVersion): void
|
||||||
|
{
|
||||||
|
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
||||||
|
foreach ($dependencies as $dependency) {
|
||||||
|
$this->modVersionService->resolveDependencies($dependency->modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(ModVersion $modVersion): void
|
||||||
|
{
|
||||||
|
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
||||||
|
foreach ($dependencies as $dependency) {
|
||||||
|
$this->modVersionService->resolveDependencies($dependency->modVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ use App\Models\User;
|
|||||||
class ModPolicy
|
class ModPolicy
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can view any models.
|
* Determine whether the user can view multiple models.
|
||||||
*/
|
*/
|
||||||
public function viewAny(User $user): bool
|
public function viewAny(User $user): bool
|
||||||
{
|
{
|
||||||
@ -16,7 +16,7 @@ class ModPolicy
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can view the model.
|
* Determine whether the user can view a specific model.
|
||||||
*/
|
*/
|
||||||
public function view(?User $user, Mod $mod): bool
|
public function view(?User $user, Mod $mod): bool
|
||||||
{
|
{
|
||||||
|
47
app/Policies/UserPolicy.php
Normal file
47
app/Policies/UserPolicy.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class UserPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $userCurrent, User $userResource): bool
|
||||||
|
{
|
||||||
|
// TODO: check to see if the userResource has blocked the userCurrent.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restore(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forceDelete(User $user, User $model): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -2,9 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Observers\ModDependencyObserver;
|
||||||
|
use App\Observers\ModVersionObserver;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\Number;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@ -25,9 +30,23 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
// Allow mass assignment for all models. Be careful!
|
// Allow mass assignment for all models. Be careful!
|
||||||
Model::unguard();
|
Model::unguard();
|
||||||
|
|
||||||
|
// Register observers.
|
||||||
|
ModVersion::observe(ModVersionObserver::class);
|
||||||
|
ModDependency::observe(ModDependencyObserver::class);
|
||||||
|
|
||||||
// This gate determines who can access the Pulse dashboard.
|
// This gate determines who can access the Pulse dashboard.
|
||||||
Gate::define('viewPulse', function (User $user) {
|
Gate::define('viewPulse', function (User $user) {
|
||||||
return $user->isAdmin();
|
return $user->isAdmin();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register a number macro to format download numbers.
|
||||||
|
Number::macro('downloads', function (int|float $number) {
|
||||||
|
return Number::forHumans(
|
||||||
|
$number,
|
||||||
|
$number > 1000000 ? 2 : ($number > 1000 ? 1 : 0),
|
||||||
|
maxPrecision: null,
|
||||||
|
abbreviate: true
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
99
app/Services/ModVersionService.php
Normal file
99
app/Services/ModVersionService.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Exceptions\CircularDependencyException;
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Composer\Semver\Semver;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
class ModVersionService
|
||||||
|
{
|
||||||
|
protected array $visited = [];
|
||||||
|
|
||||||
|
protected array $stack = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve dependencies for the given mod version.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
public function resolveDependencies(ModVersion $modVersion): array
|
||||||
|
{
|
||||||
|
$resolvedVersions = [];
|
||||||
|
$this->visited = [];
|
||||||
|
$this->stack = [];
|
||||||
|
|
||||||
|
$this->processDependencies($modVersion, $resolvedVersions);
|
||||||
|
|
||||||
|
return $resolvedVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a depth-first search to resolve dependencies for the given mod version.
|
||||||
|
*
|
||||||
|
* @throws CircularDependencyException
|
||||||
|
*/
|
||||||
|
protected function processDependencies(ModVersion $modVersion, array &$resolvedVersions): void
|
||||||
|
{
|
||||||
|
if (in_array($modVersion->id, $this->stack)) {
|
||||||
|
throw new CircularDependencyException("Circular dependency detected in ModVersion ID: {$modVersion->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($modVersion->id, $this->visited)) {
|
||||||
|
return; // Skip already processed versions
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->visited[] = $modVersion->id;
|
||||||
|
$this->stack[] = $modVersion->id;
|
||||||
|
|
||||||
|
/** @var Collection|ModDependency[] $dependencies */
|
||||||
|
$dependencies = $this->getDependencies($modVersion);
|
||||||
|
|
||||||
|
foreach ($dependencies as $dependency) {
|
||||||
|
$resolvedVersionId = $this->resolveVersionIdForDependency($dependency);
|
||||||
|
|
||||||
|
if ($dependency->resolved_version_id !== $resolvedVersionId) {
|
||||||
|
$dependency->updateQuietly(['resolved_version_id' => $resolvedVersionId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedVersions[$dependency->id] = $resolvedVersionId ? ModVersion::find($resolvedVersionId) : null;
|
||||||
|
|
||||||
|
if ($resolvedVersionId) {
|
||||||
|
$nextModVersion = ModVersion::find($resolvedVersionId);
|
||||||
|
if ($nextModVersion) {
|
||||||
|
$this->processDependencies($nextModVersion, $resolvedVersions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
array_pop($this->stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the dependencies for the given mod version.
|
||||||
|
*/
|
||||||
|
protected function getDependencies(ModVersion $modVersion): Collection
|
||||||
|
{
|
||||||
|
return $modVersion->dependencies()->with(['dependencyMod.versions'])->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the latest version ID that satisfies the version constraint on given dependency.
|
||||||
|
*/
|
||||||
|
protected function resolveVersionIdForDependency(ModDependency $dependency): ?int
|
||||||
|
{
|
||||||
|
$mod = $dependency->dependencyMod;
|
||||||
|
|
||||||
|
if (! $mod || $mod->versions->isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$availableVersions = $mod->versions->pluck('id', 'version')->toArray();
|
||||||
|
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $dependency->version_constraint);
|
||||||
|
|
||||||
|
// Versions are sorted in descending order by default. Take the first key (the latest version) using `reset()`.
|
||||||
|
return $satisfyingVersions ? $availableVersions[reset($satisfyingVersions)] : null;
|
||||||
|
}
|
||||||
|
}
|
72
app/Traits/HasCoverPhoto.php
Normal file
72
app/Traits/HasCoverPhoto.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
trait HasCoverPhoto
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Update the user's cover photo.
|
||||||
|
*/
|
||||||
|
public function updateCoverPhoto(UploadedFile $cover, $storagePath = 'cover-photos'): void
|
||||||
|
{
|
||||||
|
tap($this->cover_photo_path, function ($previous) use ($cover, $storagePath) {
|
||||||
|
$this->forceFill([
|
||||||
|
'cover_photo_path' => $cover->storePublicly(
|
||||||
|
$storagePath, ['disk' => $this->coverPhotoDisk()]
|
||||||
|
),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
if ($previous) {
|
||||||
|
Storage::disk($this->coverPhotoDisk())->delete($previous);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the disk that cover photos should be stored on.
|
||||||
|
*/
|
||||||
|
protected function coverPhotoDisk(): string
|
||||||
|
{
|
||||||
|
return config('filesystems.asset_upload', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's cover photo.
|
||||||
|
*/
|
||||||
|
public function deleteCoverPhoto(): void
|
||||||
|
{
|
||||||
|
if (is_null($this->cover_photo_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::disk($this->coverPhotoDisk())->delete($this->cover_photo_path);
|
||||||
|
|
||||||
|
$this->forceFill([
|
||||||
|
'cover_photo_path' => null,
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL to the user's cover photo.
|
||||||
|
*/
|
||||||
|
public function coverPhotoUrl(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::get(function (): string {
|
||||||
|
return $this->cover_photo_path
|
||||||
|
? Storage::disk($this->coverPhotoDisk())->url($this->cover_photo_path)
|
||||||
|
: $this->defaultCoverPhotoUrl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default profile photo URL if no profile photo has been uploaded.
|
||||||
|
*/
|
||||||
|
protected function defaultCoverPhotoUrl(): string
|
||||||
|
{
|
||||||
|
return 'https://picsum.photos/seed/'.urlencode($this->name).'/720/100?blur=2';
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,8 @@ class ModListSection extends Component
|
|||||||
return Cache::remember('homepage-featured-mods', now()->addMinutes(5), function () {
|
return Cache::remember('homepage-featured-mods', now()->addMinutes(5), function () {
|
||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['latestSptVersion', 'users:id,name'])
|
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||||
|
->whereHas('latestVersion')
|
||||||
->where('featured', true)
|
->where('featured', true)
|
||||||
->latest()
|
->latest()
|
||||||
->limit(6)
|
->limit(6)
|
||||||
@ -42,7 +43,8 @@ class ModListSection extends Component
|
|||||||
return Cache::remember('homepage-latest-mods', now()->addMinutes(5), function () {
|
return Cache::remember('homepage-latest-mods', now()->addMinutes(5), function () {
|
||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['latestSptVersion', 'users:id,name'])
|
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||||
|
->whereHas('latestVersion')
|
||||||
->latest()
|
->latest()
|
||||||
->limit(6)
|
->limit(6)
|
||||||
->get();
|
->get();
|
||||||
@ -54,7 +56,8 @@ class ModListSection extends Component
|
|||||||
return Cache::remember('homepage-updated-mods', now()->addMinutes(5), function () {
|
return Cache::remember('homepage-updated-mods', now()->addMinutes(5), function () {
|
||||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||||
->withTotalDownloads()
|
->withTotalDownloads()
|
||||||
->with(['lastUpdatedVersion', 'users:id,name'])
|
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion', 'users:id,name'])
|
||||||
|
->whereHas('lastUpdatedVersion')
|
||||||
->orderByDesc(
|
->orderByDesc(
|
||||||
ModVersion::select('updated_at')
|
ModVersion::select('updated_at')
|
||||||
->whereColumn('mod_id', 'mods.id')
|
->whereColumn('mod_id', 'mods.id')
|
||||||
@ -79,12 +82,12 @@ class ModListSection extends Component
|
|||||||
[
|
[
|
||||||
'title' => 'Featured Mods',
|
'title' => 'Featured Mods',
|
||||||
'mods' => $this->modsFeatured,
|
'mods' => $this->modsFeatured,
|
||||||
'versionScope' => 'latestSptVersion',
|
'versionScope' => 'latestVersion',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => 'Newest Mods',
|
'title' => 'Newest Mods',
|
||||||
'mods' => $this->modsLatest,
|
'mods' => $this->modsLatest,
|
||||||
'versionScope' => 'latestSptVersion',
|
'versionScope' => 'latestVersion',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'title' => 'Recently Updated Mods',
|
'title' => 'Recently Updated Mods',
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"aws/aws-sdk-php": "^3.314",
|
"aws/aws-sdk-php": "^3.314",
|
||||||
|
"composer/semver": "^3.4",
|
||||||
"filament/filament": "^3.2",
|
"filament/filament": "^3.2",
|
||||||
"http-interop/http-factory-guzzle": "^1.2",
|
"http-interop/http-factory-guzzle": "^1.2",
|
||||||
"laravel/framework": "^11.11",
|
"laravel/framework": "^11.11",
|
||||||
@ -52,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"phpstan": [
|
"phpstan": [
|
||||||
"./vendor/bin/phpstan analyse -c phpstan.neon --debug --memory-limit=2G"
|
"./vendor/bin/phpstan analyse --configuration phpstan.neon --error-format=table --memory-limit=2G"
|
||||||
],
|
],
|
||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
888
composer.lock
generated
888
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,18 @@ return [
|
|||||||
|
|
||||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Asset Upload Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that assets should be
|
||||||
|
| uploaded to. Typically, this will be either the "public" or "r2" disk.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'asset_upload' => env('ASSET_UPLOAD_DISK', 'public'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Filesystem Disks
|
| Filesystem Disks
|
||||||
|
@ -207,7 +207,7 @@ return [
|
|||||||
'maxJobs' => 0,
|
'maxJobs' => 0,
|
||||||
'memory' => 256,
|
'memory' => 256,
|
||||||
'tries' => 1,
|
'tries' => 1,
|
||||||
'timeout' => 900, // 15 Minutes
|
'timeout' => 1500, // 25 Minutes
|
||||||
'nice' => 0,
|
'nice' => 0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -64,17 +64,17 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'temporary_file_upload' => [
|
'temporary_file_upload' => [
|
||||||
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
'disk' => null,
|
||||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
'rules' => ['file', 'max:12288'],
|
||||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
'directory' => null,
|
||||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
'middleware' => 'throttle:5,1',
|
||||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
'preview_mimes' => [
|
||||||
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||||
],
|
],
|
||||||
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
'max_upload_time' => 5,
|
||||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
'cleanup' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
25
database/factories/ModDependencyFactory.php
Normal file
25
database/factories/ModDependencyFactory.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Mod;
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class ModDependencyFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ModDependency::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'mod_version_id' => ModVersion::factory(),
|
||||||
|
'dependency_mod_id' => Mod::factory(),
|
||||||
|
'version_constraint' => '^'.$this->faker->numerify('#.#.#'),
|
||||||
|
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
|
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ namespace Database\Factories;
|
|||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Random\RandomException;
|
use Random\RandomException;
|
||||||
|
|
||||||
@ -30,12 +31,22 @@ class ModFactory extends Factory
|
|||||||
'featured' => fake()->boolean(),
|
'featured' => fake()->boolean(),
|
||||||
'contains_ai_content' => fake()->boolean(),
|
'contains_ai_content' => fake()->boolean(),
|
||||||
'contains_ads' => fake()->boolean(),
|
'contains_ads' => fake()->boolean(),
|
||||||
'disabled' => fake()->boolean(),
|
'published_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
'created_at' => now(),
|
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
'updated_at' => now(),
|
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the mod should be disabled.
|
||||||
|
*/
|
||||||
|
public function disabled(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'disabled' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate that the mod should be soft-deleted.
|
* Indicate that the mod should be soft-deleted.
|
||||||
*/
|
*/
|
||||||
|
@ -16,15 +16,25 @@ class ModVersionFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'mod_id' => Mod::factory(),
|
'mod_id' => Mod::factory(),
|
||||||
'version' => fake()->numerify('1.#.#'),
|
'version' => fake()->numerify('#.#.#'),
|
||||||
'description' => fake()->text(),
|
'description' => fake()->text(),
|
||||||
'link' => fake()->url(),
|
'link' => fake()->url(),
|
||||||
'spt_version_id' => SptVersion::factory(),
|
'spt_version_id' => SptVersion::factory(),
|
||||||
'virus_total_link' => fake()->url(),
|
'virus_total_link' => fake()->url(),
|
||||||
'downloads' => fake()->randomNumber(),
|
'downloads' => fake()->randomNumber(),
|
||||||
'disabled' => fake()->boolean(),
|
'published_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
'updated_at' => Carbon::now(),
|
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the mod version should be disabled.
|
||||||
|
*/
|
||||||
|
public function disabled(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'disabled' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class SptVersionFactory extends Factory
|
|||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'version' => $this->faker->numerify('1.#.#'),
|
'version' => $this->faker->numerify('SPT 1.#.#'),
|
||||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
'updated_at' => Carbon::now(),
|
'updated_at' => Carbon::now(),
|
||||||
|
@ -29,7 +29,8 @@ return new class extends Migration
|
|||||||
->nullOnDelete()
|
->nullOnDelete()
|
||||||
->cascadeOnUpdate();
|
->cascadeOnUpdate();
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->string('profile_photo_path', 2048)->nullable();
|
$table->string('profile_photo_path', 2048)->nullable()->default(null);
|
||||||
|
$table->string('cover_photo_path', 2048)->nullable()->default(null);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ return new class extends Migration
|
|||||||
$table->boolean('contains_ads')->default(false);
|
$table->boolean('contains_ads')->default(false);
|
||||||
$table->boolean('disabled')->default(false);
|
$table->boolean('disabled')->default(false);
|
||||||
$table->softDeletes();
|
$table->softDeletes();
|
||||||
|
$table->timestamp('published_at')->nullable()->default(null);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
|
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
|
||||||
|
@ -33,6 +33,7 @@ return new class extends Migration
|
|||||||
$table->unsignedBigInteger('downloads');
|
$table->unsignedBigInteger('downloads');
|
||||||
$table->boolean('disabled')->default(false);
|
$table->boolean('disabled')->default(false);
|
||||||
$table->softDeletes();
|
$table->softDeletes();
|
||||||
|
$table->timestamp('published_at')->nullable()->default(null);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
|
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('mod_dependencies', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('mod_version_id')
|
||||||
|
->constrained('mod_versions')
|
||||||
|
->cascadeOnDelete()
|
||||||
|
->cascadeOnUpdate();
|
||||||
|
$table->foreignId('dependency_mod_id')
|
||||||
|
->constrained('mods')
|
||||||
|
->cascadeOnDelete()
|
||||||
|
->cascadeOnUpdate();
|
||||||
|
$table->string('version_constraint'); // e.g., ^1.0.1
|
||||||
|
$table->foreignId('resolved_version_id')
|
||||||
|
->nullable()
|
||||||
|
->constrained('mod_versions')
|
||||||
|
->nullOnDelete()
|
||||||
|
->cascadeOnUpdate();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['mod_version_id', 'dependency_mod_id', 'version_constraint'], 'mod_dependencies_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('mod_dependencies');
|
||||||
|
}
|
||||||
|
};
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Exceptions\CircularDependencyException;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\Mod;
|
use App\Models\Mod;
|
||||||
|
use App\Models\ModDependency;
|
||||||
use App\Models\ModVersion;
|
use App\Models\ModVersion;
|
||||||
use App\Models\SptVersion;
|
use App\Models\SptVersion;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@ -46,6 +48,31 @@ class DatabaseSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add 1000 mod versions, assigning them to the mods we just created.
|
// Add 1000 mod versions, assigning them to the mods we just created.
|
||||||
ModVersion::factory(1000)->recycle([$mods, $spt_versions])->create();
|
$modVersions = ModVersion::factory(1000)->recycle([$mods, $spt_versions])->create();
|
||||||
|
|
||||||
|
// Add ModDependencies to a subset of ModVersions.
|
||||||
|
foreach ($modVersions as $modVersion) {
|
||||||
|
$hasDependencies = rand(0, 100) < 30; // 30% chance to have dependencies
|
||||||
|
if ($hasDependencies) {
|
||||||
|
$numDependencies = rand(1, 3); // 1 to 3 dependencies
|
||||||
|
$dependencyMods = $mods->random($numDependencies);
|
||||||
|
foreach ($dependencyMods as $dependencyMod) {
|
||||||
|
try {
|
||||||
|
ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create([
|
||||||
|
'version_constraint' => $this->generateVersionConstraint(),
|
||||||
|
]);
|
||||||
|
} catch (CircularDependencyException $e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateVersionConstraint(): string
|
||||||
|
{
|
||||||
|
$versionConstraints = ['*', '^1.0.0', '>=2.0.0', '~1.1.0', '>=1.2.0 <2.0.0'];
|
||||||
|
|
||||||
|
return $versionConstraints[array_rand($versionConstraints)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
216
package-lock.json
generated
216
package-lock.json
generated
@ -556,9 +556,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz",
|
||||||
"integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
|
"integrity": "sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -570,9 +570,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.2.tgz",
|
||||||
"integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
|
"integrity": "sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -584,9 +584,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.2.tgz",
|
||||||
"integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
|
"integrity": "sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -598,9 +598,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.2.tgz",
|
||||||
"integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
|
"integrity": "sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -612,9 +612,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.2.tgz",
|
||||||
"integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
|
"integrity": "sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -626,9 +626,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.2.tgz",
|
||||||
"integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
|
"integrity": "sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@ -640,9 +640,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.2.tgz",
|
||||||
"integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
|
"integrity": "sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -654,9 +654,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.2.tgz",
|
||||||
"integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
|
"integrity": "sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -668,9 +668,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.2.tgz",
|
||||||
"integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
|
"integrity": "sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@ -682,9 +682,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.2.tgz",
|
||||||
"integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
|
"integrity": "sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@ -696,9 +696,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.2.tgz",
|
||||||
"integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
|
"integrity": "sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@ -710,9 +710,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.2.tgz",
|
||||||
"integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
|
"integrity": "sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -724,9 +724,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.2.tgz",
|
||||||
"integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
|
"integrity": "sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -738,9 +738,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.2.tgz",
|
||||||
"integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
|
"integrity": "sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@ -752,9 +752,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.2.tgz",
|
||||||
"integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
|
"integrity": "sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@ -766,9 +766,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.2.tgz",
|
||||||
"integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
|
"integrity": "sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@ -915,9 +915,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz",
|
||||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
"integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -970,9 +970,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.23.2",
|
"version": "4.23.3",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
|
||||||
"integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
|
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -990,9 +990,9 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001640",
|
"caniuse-lite": "^1.0.30001646",
|
||||||
"electron-to-chromium": "^1.4.820",
|
"electron-to-chromium": "^1.5.4",
|
||||||
"node-releases": "^2.0.14",
|
"node-releases": "^2.0.18",
|
||||||
"update-browserslist-db": "^1.1.0"
|
"update-browserslist-db": "^1.1.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -1013,9 +1013,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001642",
|
"version": "1.0.30001646",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz",
|
||||||
"integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==",
|
"integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -1161,9 +1161,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.829",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz",
|
||||||
"integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==",
|
"integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@ -1696,9 +1696,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.17",
|
"version": "2.0.18",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
|
||||||
"integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==",
|
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@ -1824,9 +1824,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.39",
|
"version": "8.4.40",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
|
||||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -1940,21 +1940,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-nested": {
|
"node_modules/postcss-nested": {
|
||||||
"version": "6.0.1",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||||
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
|
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-selector-parser": "^6.0.11"
|
"postcss-selector-parser": "^6.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0"
|
"node": ">=12.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/postcss/"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"postcss": "^8.2.14"
|
"postcss": "^8.2.14"
|
||||||
}
|
}
|
||||||
@ -2166,9 +2172,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.18.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.2.tgz",
|
||||||
"integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
|
"integrity": "sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2182,22 +2188,22 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.18.1",
|
"@rollup/rollup-android-arm-eabi": "4.19.2",
|
||||||
"@rollup/rollup-android-arm64": "4.18.1",
|
"@rollup/rollup-android-arm64": "4.19.2",
|
||||||
"@rollup/rollup-darwin-arm64": "4.18.1",
|
"@rollup/rollup-darwin-arm64": "4.19.2",
|
||||||
"@rollup/rollup-darwin-x64": "4.18.1",
|
"@rollup/rollup-darwin-x64": "4.19.2",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.19.2",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.18.1",
|
"@rollup/rollup-linux-arm-musleabihf": "4.19.2",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.18.1",
|
"@rollup/rollup-linux-arm64-gnu": "4.19.2",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.18.1",
|
"@rollup/rollup-linux-arm64-musl": "4.19.2",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.19.2",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.18.1",
|
"@rollup/rollup-linux-riscv64-gnu": "4.19.2",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.18.1",
|
"@rollup/rollup-linux-s390x-gnu": "4.19.2",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.18.1",
|
"@rollup/rollup-linux-x64-gnu": "4.19.2",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.18.1",
|
"@rollup/rollup-linux-x64-musl": "4.19.2",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.18.1",
|
"@rollup/rollup-win32-arm64-msvc": "4.19.2",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.18.1",
|
"@rollup/rollup-win32-ia32-msvc": "4.19.2",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.18.1",
|
"@rollup/rollup-win32-x64-msvc": "4.19.2",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2418,9 +2424,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.6",
|
"version": "3.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
||||||
"integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==",
|
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2564,9 +2570,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
|
||||||
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
|
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2745,9 +2751,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.4.5",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
||||||
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
|
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -1 +1 @@
|
|||||||
function n(){return{collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){let e=this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[];for(let t of e)t.removeEventListener("click",this.handleCheckboxClick),t.addEventListener("click",s=>this.handleCheckboxClick(s,t))},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default};
|
function n(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){if(this.isLoading=!0,this.areRecordsSelected(this.getRecordsInGroupOnPage(e))){this.deselectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e));return}this.selectRecords(await this.$wire.getGroupedSelectableTableRecordKeys(e)),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let o=s.indexOf(this.lastChecked),r=s.indexOf(t),l=[o,r].sort((i,d)=>i-d),c=[];for(let i=l[0];i<=l[1];i++)s[i].checked=t.checked,c.push(s[i].value);t.checked?this.selectRecords(c):this.deselectRecords(c)}this.lastChecked=t}}}export{n as default};
|
||||||
|
File diff suppressed because one or more lines are too long
@ -4,6 +4,18 @@
|
|||||||
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
|
<div class="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg italic font-extrabold leading-6 text-white">The Forge</p>
|
<p class="text-lg italic font-extrabold leading-6 text-white">The Forge</p>
|
||||||
|
<p class="mt-6 flex space-x-4">
|
||||||
|
<a href="https://discord.com/invite/Xn9msqQZan" title="{{ __('Join Our Discord!') }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" class="w-6 h-6">
|
||||||
|
<path fill="#fff" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.reddit.com/r/SPTarkov/" title="{{ __('Join Our Subreddit!') }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-6 h-6">
|
||||||
|
<path fill="#fff" d="M14.238 15.348c.085.084.085.221 0 .306-.465.462-1.194.687-2.231.687l-.008-.002-.008.002c-1.036 0-1.766-.225-2.231-.688-.085-.084-.085-.221 0-.305.084-.084.222-.084.307 0 .379.377 1.008.561 1.924.561l.008.002.008-.002c.915 0 1.544-.184 1.924-.561.085-.084.223-.084.307 0zm-3.44-2.418c0-.507-.414-.919-.922-.919-.509 0-.923.412-.923.919 0 .506.414.918.923.918.508.001.922-.411.922-.918zm13.202-.93c0 6.627-5.373 12-12 12s-12-5.373-12-12 5.373-12 12-12 12 5.373 12 12zm-5-.129c0-.851-.695-1.543-1.55-1.543-.417 0-.795.167-1.074.435-1.056-.695-2.485-1.137-4.066-1.194l.865-2.724 2.343.549-.003.034c0 .696.569 1.262 1.268 1.262.699 0 1.267-.566 1.267-1.262s-.568-1.262-1.267-1.262c-.537 0-.994.335-1.179.804l-2.525-.592c-.11-.027-.223.037-.257.145l-.965 3.038c-1.656.02-3.155.466-4.258 1.181-.277-.255-.644-.415-1.05-.415-.854.001-1.549.693-1.549 1.544 0 .566.311 1.056.768 1.325-.03.164-.05.331-.05.5 0 2.281 2.805 4.137 6.253 4.137s6.253-1.856 6.253-4.137c0-.16-.017-.317-.044-.472.486-.261.82-.766.82-1.353zm-4.872.141c-.509 0-.922.412-.922.919 0 .506.414.918.922.918s.922-.412.922-.918c0-.507-.413-.919-.922-.919z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-16 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 xl:col-span-2 xl:mt-0">
|
<div class="mt-16 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 xl:col-span-2 xl:mt-0">
|
||||||
<div class="sm:order-first">
|
<div class="sm:order-first">
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<img src="{{ Storage::url($result['thumbnail']) }}" alt="{{ $result['name'] }}" class="h-6 w-6 self-center">
|
<img src="{{ Storage::url($result['thumbnail']) }}" alt="{{ $result['name'] }}" class="h-6 w-6 self-center">
|
||||||
@endif
|
@endif
|
||||||
<p class="flex-grow">{{ $result['name'] }}</p>
|
<p class="flex-grow">{{ $result['name'] }}</p>
|
||||||
<p class="ml-auto self-center badge-version {{ $result['latestSptVersionColorClass'] }} }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
<p class="ml-auto self-center badge-version {{ $result['latestVersionColorClass'] }} }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||||
{{ $result['latestSptVersion'] }}
|
{{ $result['latestVersion'] }}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<p {{ $attributes->class(['text-slate-700 dark:text-gray-300 text-sm']) }}>
|
<p {{ $attributes->class(['text-slate-700 dark:text-gray-300 text-sm']) }}>
|
||||||
<span>{{ Number::format($mod->total_downloads) }} downloads</span>
|
<span title="{{ __('Exactly') }} {{ $mod->total_downloads }}">{{ Number::downloads($mod->total_downloads) }} downloads</span>
|
||||||
@if(!is_null($mod->created_at))
|
@if(!is_null($mod->created_at))
|
||||||
<span>
|
<span>
|
||||||
— Created
|
— Created
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
@foreach ($mods as $mod)
|
@foreach ($mods as $mod)
|
||||||
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}" class="mod-list-component">
|
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}" class="mod-list-component mx-auto w-full max-w-md md:max-w-2xl">
|
||||||
<div class="flex flex-col group h-full w-full max-w-md mx-auto bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl overflow-hidden md:max-w-2xl hover:shadow-lg hover:bg-gray-50 dark:hover:bg-black hover:shadow-gray-400 dark:hover:shadow-black transition-all duration-200">
|
<div class="flex flex-col group h-full w-full bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl overflow-hidden hover:shadow-lg hover:bg-gray-50 dark:hover:bg-black hover:shadow-gray-400 dark:hover:shadow-black transition-all duration-200">
|
||||||
<div class="h-auto md:h-full md:flex">
|
<div class="h-auto md:h-full md:flex">
|
||||||
<div class="h-auto md:h-full md:shrink-0 overflow-hidden">
|
<div class="h-auto md:h-full md:shrink-0 overflow-hidden">
|
||||||
@if(empty($mod->thumbnail))
|
@if (empty($mod->thumbnail))
|
||||||
<img src="https://placehold.co/450x450/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
<img src="https://placehold.co/450x450/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||||
<img src="https://placehold.co/450x450/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
<img src="https://placehold.co/450x450/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||||
@else
|
@else
|
||||||
@ -14,15 +14,17 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full justify-between p-5">
|
<div class="flex flex-col w-full justify-between p-5">
|
||||||
<div>
|
<div class="pb-3">
|
||||||
<div class="flex justify-between items-center space-x-3">
|
<div class="flex justify-between items-center space-x-3">
|
||||||
<h3 class="block mt-1 text-lg leading-tight font-medium text-black dark:text-white group-hover:underline">{{ $mod->name }}</h3>
|
<h3 class="block mt-1 text-lg leading-tight font-medium text-black dark:text-white group-hover:underline">{{ $mod->name }}</h3>
|
||||||
<span class="badge-version {{ $mod->{$versionScope}->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
<span class="badge-version {{ $mod->{$versionScope}->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||||
{{ $mod->{$versionScope}->sptVersion->version }}
|
{{ $mod->{$versionScope}->sptVersion->version }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">By {{ $mod->users->pluck('name')->implode(', ') }}</p>
|
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||||
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ $mod->teaser }}</p>
|
By {{ $mod->users->pluck('name')->implode(', ') }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ Str::limit($mod->teaser, 100) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,12 +6,14 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 max-w-7xl mx-auto pb-6 px-4 gap-6 sm:px-6 lg:px-8">
|
<div class="grid grid-cols-1 lg:grid-cols-3 max-w-7xl mx-auto py-6 px-4 gap-6 sm:px-6 lg:px-8">
|
||||||
<div class="lg:col-span-2 flex flex-col gap-6">
|
<div class="lg:col-span-2 flex flex-col gap-6">
|
||||||
|
|
||||||
|
{{-- Main Mod Details Card --}}
|
||||||
<div class="p-4 sm:p-6 text-center sm:text-left bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
<div class="p-4 sm:p-6 text-center sm:text-left bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||||
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||||
<div class="grow-0 shrink-0 flex justify-center items-center">
|
<div class="grow-0 shrink-0 flex justify-center items-center">
|
||||||
@if(empty($mod->thumbnail))
|
@if (empty($mod->thumbnail))
|
||||||
<img src="https://placehold.co/144x144/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden w-36 rounded-lg">
|
<img src="https://placehold.co/144x144/EEE/31343C?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="block dark:hidden w-36 rounded-lg">
|
||||||
<img src="https://placehold.co/144x144/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block w-36 rounded-lg">
|
<img src="https://placehold.co/144x144/31343C/EEE?font=source-sans-pro&text={{ $mod->name }}" alt="{{ $mod->name }}" class="hidden dark:block w-36 rounded-lg">
|
||||||
@else
|
@else
|
||||||
@ -19,61 +21,140 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="grow flex flex-col justify-center items-center sm:items-start text-gray-800 dark:text-gray-200">
|
<div class="grow flex flex-col justify-center items-center sm:items-start text-gray-800 dark:text-gray-200">
|
||||||
<h2 class="pb-1 sm:p-0 text-3xl font-bold text-gray-900 dark:text-white">
|
<div class="flex justify-between items-center space-x-3">
|
||||||
{{ $mod->name }}
|
<h2 class="pb-1 sm:pb-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
<span class="font-light text-nowrap text-gray-700 dark:text-gray-400">
|
{{ $mod->name }}
|
||||||
{{ $mod->latestSptVersion->version }}
|
<span class="font-light text-nowrap text-gray-700 dark:text-gray-400">
|
||||||
|
{{ $latestVersion->version }}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{{ __('Created by') }}
|
||||||
|
@foreach ($mod->users as $user)
|
||||||
|
<a href="{{ $user->profileUrl() }}" class="text-slate-600 dark:text-gray-200 hover:underline">{{ $user->name }}</a>{{ $loop->last ? '' : ',' }}
|
||||||
|
@endforeach
|
||||||
|
</p>
|
||||||
|
<p title="{{ __('Exactly') }} {{ $mod->total_downloads }}">{{ Number::downloads($mod->total_downloads) }} {{ __('Downloads') }}</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
<span class="badge-version {{ $latestVersion->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||||
|
{{ $latestVersion->sptVersion->version }} {{ __('Compatible') }}
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</p>
|
||||||
<p>{{ __('Created by') }} {{ $mod->users->pluck('name')->implode(', ') }}</p>
|
|
||||||
<p>{{ $mod->latestSptVersion->sptVersion->version }} {{ __('Compatible') }}</p>
|
|
||||||
<p>{{ $mod->total_downloads }} {{ __('Downloads') }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Mod teaser --}}
|
||||||
|
@if ($mod->teaser)
|
||||||
|
<p class="mt-6 pt-3 border-t-2 border-gray-200 dark:border-gray-800 text-gray-800 dark:text-gray-200">{{ $mod->teaser }}</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{{-- Mobile Download Button --}}
|
||||||
<div class="sm:hidden">
|
<a href="{{ $latestVersion->link }}" class="block lg:hidden">
|
||||||
<label for="tabs" class="sr-only">Select a tab</label>
|
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $latestVersion->version }})</button>
|
||||||
{{-- Use an "onChange" listener to redirect the user to the selected tab URL. --}}
|
</a>
|
||||||
<select id="tabs" name="tabs" class="block w-full rounded-md dark:text-white bg-gray-100 dark:bg-gray-950 border-gray-300 dark:border-gray-700 focus:border-grey-500 dark:focus:border-grey-600 focus:ring-grey-500 dark:focus:ring-grey-600">
|
|
||||||
<option selected>Description</option>
|
|
||||||
<option>Versions</option>
|
|
||||||
<option>Comments</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="hidden sm:block">
|
|
||||||
<nav class="isolate flex divide-x divide-gray-200 dark:divide-gray-800 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl" aria-label="Tabs">
|
|
||||||
<a href="#description" class="tab rounded-l-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10" aria-current="page">
|
|
||||||
<span>Description</span>
|
|
||||||
<span aria-hidden="true" class="bg-gray-500 absolute inset-x-0 bottom-0 h-0.5"></span> </a>
|
|
||||||
<a href="#versions" class="tab group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
|
||||||
<span>Versions</span>
|
|
||||||
<span aria-hidden="true" class="bg-transparent absolute inset-x-0 bottom-0 h-0.5"></span>
|
|
||||||
</a>
|
|
||||||
<a href="#comments" class="tab rounded-r-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
|
||||||
<span>Comments</span>
|
|
||||||
<span aria-hidden="true" class="bg-transparent absolute inset-x-0 bottom-0 h-0.5"></span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="description" class="user-markdown p-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
{{-- Tabs --}}
|
||||||
{{-- The description below is safe to write directly because it has been run though HTMLPurifier during the import process. --}}
|
<div x-data="{ selectedTab: window.location.hash ? window.location.hash.substring(1) : 'description' }" x-init="$watch('selectedTab', (tab) => {window.location.hash = tab})" class="lg:col-span-2 flex flex-col gap-6">
|
||||||
<p>{!! Str::markdown($mod->description) !!}</p>
|
<div>
|
||||||
|
{{-- Mobile Dropdown --}}
|
||||||
|
<div class="sm:hidden">
|
||||||
|
<label for="tabs" class="sr-only">{{ __('Select a tab') }}</label>
|
||||||
|
<select id="tabs" name="tabs" x-model="selectedTab" class="block w-full rounded-md dark:text-white bg-gray-100 dark:bg-gray-950 border-gray-300 dark:border-gray-700 focus:border-grey-500 dark:focus:border-grey-600 focus:ring-grey-500 dark:focus:ring-grey-600">
|
||||||
|
<option value="description">{{ __('Description') }}</option>
|
||||||
|
<option value="versions">{{ __('Versions') }}</option>
|
||||||
|
<option value="comments">{{ __('Comments') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Desktop Tabs --}}
|
||||||
|
<div class="hidden sm:block">
|
||||||
|
<nav class="isolate flex divide-x divide-gray-200 dark:divide-gray-800 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl" aria-label="Tabs">
|
||||||
|
<button @click="selectedTab = 'description'" class="tab rounded-l-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10" aria-current="page">
|
||||||
|
<span>{{ __('Description') }}</span>
|
||||||
|
<span aria-hidden="true" :class="selectedTab === 'description' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'"></span>
|
||||||
|
</button>
|
||||||
|
<button @click="selectedTab = 'versions'" class="tab group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
||||||
|
<span>{{ __('Versions') }}</span>
|
||||||
|
<span aria-hidden="true" :class="selectedTab === 'versions' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'"></span>
|
||||||
|
</button>
|
||||||
|
<button @click="selectedTab = 'comments'" class="tab rounded-r-xl group relative min-w-0 flex-1 overflow-hidden py-4 px-4 text-center text-sm font-medium text-gray-900 dark:text-white bg-white dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-black dark:hover:text-white focus:z-10">
|
||||||
|
<span>{{ __('Comments') }}</span>
|
||||||
|
<span aria-hidden="true" :class="selectedTab === 'comments' ? 'bg-gray-500 absolute inset-x-0 bottom-0 h-0.5' : 'bottom-0 h-0.5'"></span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Mod Description --}}
|
||||||
|
<div x-show="selectedTab === 'description'" class="user-markdown p-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||||
|
{{-- The description below is safe to write directly because it has been run though HTMLPurifier. --}}
|
||||||
|
{!! Str::markdown($mod->description) !!}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Mod Versions --}}
|
||||||
|
<div x-show="selectedTab === 'versions'">
|
||||||
|
@foreach ($mod->versions as $version)
|
||||||
|
<div class="p-4 mb-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||||
|
<div class="pb-6 border-b-2 border-gray-200 dark:border-gray-800">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a class="text-2xl font-extrabold" href="{{ $version->link }}">
|
||||||
|
{{ __('Version') }} {{ $version->version }}
|
||||||
|
</a>
|
||||||
|
<p class="text-gray-700 dark:text-gray-300" title="{{ __('Exactly') }} {{ $version->downloads }}">{{ Number::downloads($version->downloads) }} {{ __('Downloads') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="badge-version {{ $version->sptVersion->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||||
|
{{ $version->sptVersion->version }}
|
||||||
|
</span>
|
||||||
|
<a href="{{ $version->virus_total_link }}">{{__('Virus Total Results')}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-gray-600 dark:text-gray-400">
|
||||||
|
<span>{{ __('Created') }} {{ $version->created_at->format("M d, h:m a") }}</span>
|
||||||
|
<span>{{ __('Updated') }} {{ $version->updated_at->format("M d, h:m a") }}</span>
|
||||||
|
</div>
|
||||||
|
@if ($version->dependencies->isNotEmpty() && $version->dependencies->contains(fn($dependency) => $dependency->resolvedVersion?->mod))
|
||||||
|
<div class="text-gray-600 dark:text-gray-400">
|
||||||
|
{{ __('Dependencies:') }}
|
||||||
|
@foreach ($version->dependencies as $dependency)
|
||||||
|
@if ($dependency->resolvedVersion?->mod)
|
||||||
|
<a href="{{ $dependency->resolvedVersion->mod->detailUrl() }}">
|
||||||
|
{{ $dependency->resolvedVersion->mod->name }} ({{ $dependency->resolvedVersion->version }})
|
||||||
|
</a>@if (!$loop->last), @endif
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="p-3 user-markdown">
|
||||||
|
{{-- The description below is safe to write directly because it has been run though HTMLPurifier during the import process. --}}
|
||||||
|
{!! Str::markdown($version->description) !!}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Comments --}}
|
||||||
|
<div x-show="selectedTab === 'comments'" class="user-markdown p-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||||
|
<p>{{ __('The comments go here.') }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Right Column --}}
|
||||||
<div class="col-span-1 flex flex-col gap-6">
|
<div class="col-span-1 flex flex-col gap-6">
|
||||||
<a href="{{ $mod->latestSptVersion->link }}" class="block">
|
|
||||||
<button type="button" class="w-full">{{ __('Download Latest Version') }} ({{ $mod->latestSptVersion->version }})</button>
|
{{-- Desktop Download Button --}}
|
||||||
|
<a href="{{ $latestVersion->link }}" class="hidden lg:block">
|
||||||
|
<button class="text-lg font-extrabold hover:bg-cyan-400 dark:hover:bg-cyan-600 shadow-md dark:shadow-gray-950 drop-shadow-2xl bg-cyan-500 dark:bg-cyan-700 rounded-xl w-full h-20">{{ __('Download Latest Version') }} ({{ $latestVersion->version }})</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{{-- Additional Mod Details --}}
|
||||||
<div class="p-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
<div class="p-4 sm:p-6 bg-white dark:bg-gray-950 rounded-xl shadow-md dark:shadow-gray-950 drop-shadow-2xl">
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ __('Details') }}</h2>
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ __('Details') }}</h2>
|
||||||
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-800 text-gray-900 dark:text-gray-100">
|
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-800 text-gray-900 dark:text-gray-100">
|
||||||
@if($mod->license)
|
@if ($mod->license)
|
||||||
<li class="px-4 py-4 sm:px-0">
|
<li class="px-4 py-4 sm:px-0">
|
||||||
<h3>{{ __('License') }}</h3>
|
<h3>{{ __('License') }}</h3>
|
||||||
<p class="truncate">
|
<p class="truncate">
|
||||||
@ -83,7 +164,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
@if($mod->source_code_link)
|
@if ($mod->source_code_link)
|
||||||
<li class="px-4 py-4 sm:px-0">
|
<li class="px-4 py-4 sm:px-0">
|
||||||
<h3>{{ __('Source Code') }}</h3>
|
<h3>{{ __('Source Code') }}</h3>
|
||||||
<p class="truncate">
|
<p class="truncate">
|
||||||
@ -93,17 +174,29 @@
|
|||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
@if($mod->latestSptVersion->virus_total_link)
|
@if ($latestVersion->virus_total_link)
|
||||||
<li class="px-4 py-4 sm:px-0">
|
<li class="px-4 py-4 sm:px-0">
|
||||||
<h3>{{ __('Latest VirusTotal Result') }}</h3>
|
<h3>{{ __('Latest Version VirusTotal Result') }}</h3>
|
||||||
<p class="truncate">
|
<p class="truncate">
|
||||||
<a href="{{ $mod->latestSptVersion->virus_total_link }}" title="{{ $mod->latestSptVersion->virus_total_link }}" target="_blank">
|
<a href="{{ $latestVersion->virus_total_link }}" title="{{ $latestVersion->virus_total_link }}" target="_blank">
|
||||||
{{ $mod->latestSptVersion->virus_total_link }}
|
{{ $latestVersion->virus_total_link }}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
@if($mod->contains_ads)
|
@if ($latestVersion->dependencies->isNotEmpty() && $latestVersion->dependencies->contains(fn($dependency) => $dependency->resolvedVersion?->mod))
|
||||||
|
<li class="px-4 py-4 sm:px-0">
|
||||||
|
<h3>{{ __('Latest Version Dependencies') }}</h3>
|
||||||
|
<p class="truncate">
|
||||||
|
@foreach ($latestVersion->dependencies as $dependency)
|
||||||
|
<a href="{{ $dependency->resolvedVersion->mod->detailUrl() }}">
|
||||||
|
{{ $dependency->resolvedVersion->mod->name }} ({{ $dependency->resolvedVersion->version }})
|
||||||
|
</a><br />
|
||||||
|
@endforeach
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
@if ($mod->contains_ads)
|
||||||
<li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
|
<li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
|
||||||
<svg class="grow-0 w-[16px] h-[16px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="grow-0 w-[16px] h-[16px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"/>
|
||||||
@ -113,7 +206,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
@if($mod->contains_ai_content)
|
@if ($mod->contains_ai_content)
|
||||||
<li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
|
<li class="px-4 py-4 sm:px-0 flex flex-row gap-2 items-center">
|
||||||
<svg class="grow-0 w-[16px] h-[16px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
<svg class="grow-0 w-[16px] h-[16px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Zm3.844-8.791a.75.75 0 0 0-1.188-.918l-3.7 4.79-1.649-1.833a.75.75 0 1 0-1.114 1.004l2.25 2.5a.75.75 0 0 0 1.15-.043l4.25-5.5Z" clip-rule="evenodd"/>
|
||||||
|
@ -63,18 +63,24 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{{ __('Dashboard') }}
|
{{ __('Dashboard') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<a href="{{ auth()->user()->profileUrl() }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||||
<div class="flex flex-col py-1.5">
|
|
||||||
<a href="{{ route('profile.show') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||||
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
{{ __('Profile') }}
|
{{ __('Profile') }}
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col py-1.5">
|
||||||
|
<a href="{{ route('profile.show') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 00-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 00-2.282.819l-.922 1.597a1.875 1.875 0 00.432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 000 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 00-.432 2.385l.922 1.597a1.875 1.875 0 002.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 002.28-.819l.923-1.597a1.875 1.875 0 00-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 000-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 00-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 00-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 00-1.85-1.567h-1.843zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
{{ __('Edit Profile') }}
|
||||||
|
</a>
|
||||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||||
<a href="{{ route('api-tokens.index') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
<a href="{{ route('api-tokens.index') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path fill-rule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 00-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 00-2.282.819l-.922 1.597a1.875 1.875 0 00.432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 000 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 00-.432 2.385l.922 1.597a1.875 1.875 0 002.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 002.28-.819l.923-1.597a1.875 1.875 0 00-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 000-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 00-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 00-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 00-1.85-1.567h-1.843zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" clip-rule="evenodd"/>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23-.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ __('API Tokens') }}
|
{{ __('API Tokens') }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||||
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
||||||
@livewire('profile.update-profile-information-form')
|
@livewire('profile.update-profile-form')
|
||||||
|
|
||||||
<x-section-border />
|
<x-section-border />
|
||||||
@endif
|
@endif
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<x-slot name="form">
|
<x-slot name="form">
|
||||||
<!-- Profile Photo -->
|
<!-- Profile Picture -->
|
||||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||||
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
||||||
<!-- Profile Photo File Input -->
|
<!-- Profile Photo File Input -->
|
||||||
@ -24,7 +24,7 @@
|
|||||||
reader.readAsDataURL($refs.photo.files[0]);
|
reader.readAsDataURL($refs.photo.files[0]);
|
||||||
" />
|
" />
|
||||||
|
|
||||||
<x-label for="photo" value="{{ __('Photo') }}" />
|
<x-label for="photo" value="{{ __('Profile Picture') }}" />
|
||||||
|
|
||||||
<!-- Current Profile Photo -->
|
<!-- Current Profile Photo -->
|
||||||
<div class="mt-2" x-show="! photoPreview">
|
<div class="mt-2" x-show="! photoPreview">
|
||||||
@ -52,6 +52,48 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
<!-- Cover Picture -->
|
||||||
|
<div x-data="{coverName: null, coverPreview: null}" class="col-span-6 sm:col-span-4">
|
||||||
|
<!-- Cover Picture File Input -->
|
||||||
|
<input type="file" id="cover" class="hidden"
|
||||||
|
wire:model.live="cover"
|
||||||
|
x-ref="cover"
|
||||||
|
x-on:change="
|
||||||
|
coverName = $refs.cover.files[0].name;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
coverPreview = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL($refs.cover.files[0]);
|
||||||
|
" />
|
||||||
|
|
||||||
|
<x-label for="cover" value="{{ __('Cover Picture') }}" />
|
||||||
|
|
||||||
|
<!-- Current Cover Photo -->
|
||||||
|
<div class="mt-2" x-show="! coverPreview">
|
||||||
|
<img src="{{ $this->user->cover_photo_url }}" alt="{{ $this->user->name }}" class="rounded-sm h-20 w-60 object-cover">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Cover Photo Preview -->
|
||||||
|
<div class="mt-2" x-show="coverPreview" style="display: none;">
|
||||||
|
<span class="block h-20 w-60 bg-cover bg-no-repeat bg-center"
|
||||||
|
x-bind:style="'background-image: url(\'' + coverPreview + '\');'">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-secondary-button class="mt-2 me-2" type="button" x-on:click.prevent="$refs.cover.click()">
|
||||||
|
{{ __('Select A New Cover Photo') }}
|
||||||
|
</x-secondary-button>
|
||||||
|
|
||||||
|
@if ($this->user->cover_photo_path)
|
||||||
|
<x-secondary-button type="button" class="mt-2" wire:click="deleteCoverPhoto">
|
||||||
|
{{ __('Remove Cover Photo') }}
|
||||||
|
</x-secondary-button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<x-input-error for="cover" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Name -->
|
<!-- Name -->
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<x-label for="name" value="{{ __('Name') }}" />
|
<x-label for="name" value="{{ __('Name') }}" />
|
||||||
@ -88,7 +130,7 @@
|
|||||||
{{ __('Saved.') }}
|
{{ __('Saved.') }}
|
||||||
</x-action-message>
|
</x-action-message>
|
||||||
|
|
||||||
<x-button wire:loading.attr="disabled" wire:target="photo">
|
<x-button wire:loading.attr="disabled" wire:target="photo,cover">
|
||||||
{{ __('Save') }}
|
{{ __('Save') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
35
resources/views/user/show.blade.php
Normal file
35
resources/views/user/show.blade.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
|
||||||
|
<div class="sm:-mt-12 dark:bg-gray-800 dark:text-gray-100">
|
||||||
|
<div>
|
||||||
|
<img class="h-32 w-full object-cover lg:h-48" src="{{ $user->cover_photo_url }}" alt="{{ $user->name }}">
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="-mt-12 sm:-mt-16 sm:flex sm:items-end sm:space-x-5">
|
||||||
|
<div class="flex">
|
||||||
|
<img class="h-24 w-24 rounded-full ring-4 ring-white dark:ring-gray-800 sm:h-32 sm:w-32" src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 sm:flex sm:min-w-0 sm:flex-1 sm:items-center sm:justify-end sm:space-x-6 sm:pb-1">
|
||||||
|
<div class="mt-6 min-w-0 flex-1 sm:hidden md:block">
|
||||||
|
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||||
|
</div>
|
||||||
|
{{--
|
||||||
|
<div class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||||
|
<button type="button" class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M3 4a2 2 0 00-2 2v1.161l8.441 4.221a1.25 1.25 0 001.118 0L19 7.162V6a2 2 0 00-2-2H3z" />
|
||||||
|
<path d="M19 8.839l-7.77 3.885a2.75 2.75 0 01-2.46 0L1 8.839V14a2 2 0 002 2h14a2 2 0 002-2V8.839z" />
|
||||||
|
</svg>
|
||||||
|
<span>Message</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
--}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 hidden min-w-0 flex-1 sm:block md:hidden">
|
||||||
|
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</x-app-layout>
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\ModController;
|
use App\Http\Controllers\ModController;
|
||||||
|
use App\Http\Controllers\UserController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware(['auth.banned'])->group(function () {
|
Route::middleware(['auth.banned'])->group(function () {
|
||||||
@ -11,7 +12,11 @@ Route::middleware(['auth.banned'])->group(function () {
|
|||||||
|
|
||||||
Route::controller(ModController::class)->group(function () {
|
Route::controller(ModController::class)->group(function () {
|
||||||
Route::get('/mods', 'index')->name('mods');
|
Route::get('/mods', 'index')->name('mods');
|
||||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
|
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::controller(UserController::class)->group(function () {
|
||||||
|
Route::get('/user/{user}/{username}', 'show')->where(['user' => '[0-9]+'])->name('user.show');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
||||||
|
238
tests/Feature/ModDependencyTest.php
Normal file
238
tests/Feature/ModDependencyTest.php
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Exceptions\CircularDependencyException;
|
||||||
|
use App\Models\Mod;
|
||||||
|
use App\Models\ModDependency;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('resolves mod version dependency when mod version is created', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Create versions for Mod A that depends on Mod B
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves mod version dependency when mod version is updated', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
$modBv3 = ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Create versions for Mod A that depends on Mod B
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
|
// Update the mod B version
|
||||||
|
$modBv3->update(['version' => '1.1.2']);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves mod version dependency when mod version is deleted', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
$modBv3 = ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Create versions for Mod A that depends on Mod B
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
|
// Update the mod B version
|
||||||
|
$modBv3->delete();
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves mod version dependency after semantic version constraint is updated', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.1']);
|
||||||
|
|
||||||
|
// Create versions for Mod A that depends on Mod B
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
|
// Update the dependency version constraint
|
||||||
|
$modDependency->update(['version_constraint' => '^2.0.0']);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('2.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves mod version dependency with exact semantic version constraint', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Create versions for Mod A that depends on Mod B
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '1.1.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves mod version dependency with complex semantic version constraint', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.2.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.2.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Create versions for Mod A that depends on Mod B
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '>=1.0.0 <2.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.2.1');
|
||||||
|
|
||||||
|
$modDependency->update(['version_constraint' => '1.0.0 || >=1.1.0 <1.2.0']);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves null when no mod versions are available', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create version for Mod A that has no resolvable dependency
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolved_version_id)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves null when no mod versions match against semantic version constraint', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
|
||||||
|
// Create versions for Mod B
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Create version for Mod A that has no resolvable dependency
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
$modDependency = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '~1.2.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependency->refresh();
|
||||||
|
expect($modDependency->resolved_version_id)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves multiple dependencies', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
$modC = Mod::factory()->create(['name' => 'Mod C']);
|
||||||
|
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modB->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '1.0.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '1.1.0']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '1.1.1']);
|
||||||
|
ModVersion::factory()->create(['mod_id' => $modC->id, 'version' => '2.0.0']);
|
||||||
|
|
||||||
|
// Creating a version for Mod A that depends on Mod B and Mod C
|
||||||
|
$modAv1 = ModVersion::factory()->create(['mod_id' => $modA->id, 'version' => '1.0.0']);
|
||||||
|
|
||||||
|
$modDependencyB = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
$modDependencyC = ModDependency::factory()->recycle([$modAv1, $modC])->create([
|
||||||
|
'version_constraint' => '^1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$modDependencyB->refresh();
|
||||||
|
expect($modDependencyB->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
|
||||||
|
$modDependencyC->refresh();
|
||||||
|
expect($modDependencyC->resolvedVersion->version)->toBe('1.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws exception when there is a circular version dependency', function () {
|
||||||
|
$modA = Mod::factory()->create(['name' => 'Mod A']);
|
||||||
|
$modAv1 = ModVersion::factory()->recycle($modA)->create(['version' => '1.0.0']);
|
||||||
|
|
||||||
|
$modB = Mod::factory()->create(['name' => 'Mod B']);
|
||||||
|
$modBv1 = ModVersion::factory()->recycle($modB)->create(['version' => '1.0.0']);
|
||||||
|
|
||||||
|
$modDependencyAtoB = ModDependency::factory()->recycle([$modAv1, $modB])->create([
|
||||||
|
'version_constraint' => '1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create circular dependencies
|
||||||
|
$modDependencyBtoA = ModDependency::factory()->recycle([$modBv1, $modA])->create([
|
||||||
|
'version_constraint' => '1.0.0',
|
||||||
|
]);
|
||||||
|
})->throws(CircularDependencyException::class);
|
42
tests/Feature/ModTest.php
Normal file
42
tests/Feature/ModTest.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Mod;
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('shows the latest version on the mod detail page', function () {
|
||||||
|
// Create a mod instance
|
||||||
|
$mod = Mod::factory()->create();
|
||||||
|
|
||||||
|
// Create 5 mod versions with specified versions
|
||||||
|
$versions = [
|
||||||
|
'1.0.0',
|
||||||
|
'1.1.0',
|
||||||
|
'1.2.0',
|
||||||
|
'2.0.0',
|
||||||
|
'2.1.0',
|
||||||
|
];
|
||||||
|
|
||||||
|
// get the highest version in the array
|
||||||
|
$latestVersion = max($versions);
|
||||||
|
|
||||||
|
foreach ($versions as $version) {
|
||||||
|
ModVersion::factory()->create([
|
||||||
|
'mod_id' => $mod->id,
|
||||||
|
'version' => $version,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a request to the mod's detail URL
|
||||||
|
$response = $this->get($mod->detailUrl());
|
||||||
|
|
||||||
|
$this->assertEquals('2.1.0', $latestVersion);
|
||||||
|
|
||||||
|
// Assert the latest version is next to the mod's name
|
||||||
|
$response->assertSeeInOrder(explode(' ', "$mod->name $latestVersion"));
|
||||||
|
|
||||||
|
// Assert the latest version is in the latest download button
|
||||||
|
$response->assertSeeText(__('Download Latest Version')." ($latestVersion)");
|
||||||
|
});
|
39
tests/Feature/ModVersionTest.php
Normal file
39
tests/Feature/ModVersionTest.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ModVersion;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('includes only published mod versions', function () {
|
||||||
|
$publishedMod = ModVersion::factory()->create([
|
||||||
|
'published_at' => Carbon::now()->subDay(),
|
||||||
|
]);
|
||||||
|
$unpublishedMod = ModVersion::factory()->create([
|
||||||
|
'published_at' => Carbon::now()->addDay(),
|
||||||
|
]);
|
||||||
|
$noPublishedDateMod = ModVersion::factory()->create([
|
||||||
|
'published_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$all = ModVersion::withoutGlobalScopes()->get();
|
||||||
|
expect($all)->toHaveCount(3);
|
||||||
|
|
||||||
|
$mods = ModVersion::all();
|
||||||
|
|
||||||
|
expect($mods)->toHaveCount(1)
|
||||||
|
->and($mods->contains($publishedMod))->toBeTrue()
|
||||||
|
->and($mods->contains($unpublishedMod))->toBeFalse()
|
||||||
|
->and($mods->contains($noPublishedDateMod))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles null published_at as not published', function () {
|
||||||
|
$modWithNoPublishDate = ModVersion::factory()->create([
|
||||||
|
'published_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mods = ModVersion::all();
|
||||||
|
|
||||||
|
expect($mods->contains($modWithNoPublishDate))->toBeFalse();
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user