mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-12 20:20:41 -05:00
Merge branch 'develop'
This commit is contained in:
commit
424a329688
29
.env.full
29
.env.full
@ -31,18 +31,6 @@ DB_PASSWORD=password
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_0900_ai_ci
|
||||
|
||||
# This is only needed if you are running the `artisan app:import-hub` command.
|
||||
# For normal development you should just seed the database with fake data by
|
||||
# running the command: `php artisan migrate:fresh --seed`
|
||||
DB_HUB_CONNECTION=mysql
|
||||
DB_HUB_HOST=
|
||||
DB_HUB_PORT=
|
||||
DB_HUB_DATABASE=
|
||||
DB_HUB_USERNAME=
|
||||
DB_HUB_PASSWORD=
|
||||
DB_HUB_CHARSET=utf8mb4
|
||||
DB_HUB_COLLATION=utf8mb4_0900_ai_ci
|
||||
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_CONNECTION=default
|
||||
SESSION_LIFETIME=120
|
||||
@ -82,7 +70,22 @@ MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="no-reply@sp-tarkov.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
OCTANE_SERVER=swoole
|
||||
OCTANE_SERVER=frankenphp
|
||||
OCTANE_HTTPS=false
|
||||
|
||||
SAIL_XDEBUG_MODE=develop,debug,coverage
|
||||
|
||||
# Everything below is only needed if you are running the `artisan app:import-hub` command.
|
||||
# For normal development you should just seed the database with fake data by
|
||||
# running the command: `php artisan migrate:fresh --seed`
|
||||
DB_HUB_CONNECTION=mysql
|
||||
DB_HUB_HOST=
|
||||
DB_HUB_PORT=
|
||||
DB_HUB_DATABASE=
|
||||
DB_HUB_USERNAME=
|
||||
DB_HUB_PASSWORD=
|
||||
DB_HUB_CHARSET=utf8mb4
|
||||
DB_HUB_COLLATION=utf8mb4_0900_ai_ci
|
||||
|
||||
GITEA_DOMAIN=
|
||||
GITEA_TOKEN=
|
||||
|
27
.gitignore
vendored
27
.gitignore
vendored
@ -3,19 +3,20 @@
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/caddy
|
||||
/config/psysh
|
||||
/data
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.fleet
|
||||
.idea
|
||||
.phpunit.cache
|
||||
.vscode
|
||||
caddy
|
||||
config/psysh
|
||||
config/caddy
|
||||
data
|
||||
node_modules
|
||||
public/build
|
||||
public/hot
|
||||
public/storage
|
||||
storage/*.key
|
||||
vendor
|
||||
auth.json
|
||||
frankenphp
|
||||
frankenphp-worker.php
|
||||
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ImportHubData;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ImportHub extends Command
|
||||
{
|
||||
protected $signature = 'app:import-hub';
|
||||
|
||||
protected $description = 'Connects to the Hub database and imports the data into the Laravel database.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// Add the ImportHubData job to the queue.
|
||||
ImportHubData::dispatch()->onQueue('long');
|
||||
|
||||
$this->info('The import job has been added to the queue.');
|
||||
}
|
||||
}
|
20
app/Console/Commands/ImportHubCommand.php
Normal file
20
app/Console/Commands/ImportHubCommand.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ImportHubDataJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ImportHubCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:import-hub';
|
||||
|
||||
protected $description = 'Connects to the Hub database and imports the data into the Laravel database';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
ImportHubDataJob::dispatch()->onQueue('long');
|
||||
|
||||
$this->info('ImportHubDataJob has been added to the queue');
|
||||
}
|
||||
}
|
22
app/Console/Commands/ResolveVersionsCommand.php
Normal file
22
app/Console/Commands/ResolveVersionsCommand.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\ResolveDependenciesJob;
|
||||
use App\Jobs\ResolveSptVersionsJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ResolveVersionsCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:resolve-versions';
|
||||
|
||||
protected $description = 'Resolve SPT and dependency versions for all mods';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
ResolveSptVersionsJob::dispatch()->onQueue('default');
|
||||
ResolveDependenciesJob::dispatch()->onQueue('default');
|
||||
|
||||
$this->info('ResolveSptVersionsJob and ResolveDependenciesJob have been added to the queue');
|
||||
}
|
||||
}
|
@ -5,11 +5,11 @@ namespace App\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class SearchSync extends Command
|
||||
class SearchSyncCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:search-sync';
|
||||
|
||||
protected $description = 'Syncs all search settings and indexes with the database data.';
|
||||
protected $description = 'Syncs all search settings and indexes with the database data';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
@ -18,6 +18,6 @@ class SearchSync extends Command
|
||||
Artisan::call('scout:import', ['model' => '\App\Models\Mod']);
|
||||
Artisan::call('scout:import', ['model' => '\App\Models\User']);
|
||||
|
||||
$this->info('The search synchronisation jobs have been added to the queue.');
|
||||
$this->info('The search synchronisation jobs have been added to the queue');
|
||||
}
|
||||
}
|
20
app/Console/Commands/SptVersionModCountsCommand.php
Normal file
20
app/Console/Commands/SptVersionModCountsCommand.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\SptVersionModCountsJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SptVersionModCountsCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:count-mods';
|
||||
|
||||
protected $description = 'Recalculate the mod counts for each SPT version';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
SptVersionModCountsJob::dispatch()->onQueue('default');
|
||||
|
||||
$this->info('SptVersionModCountsJob has been added to the queue');
|
||||
}
|
||||
}
|
20
app/Console/Commands/UpdateModDownloadsCommand.php
Normal file
20
app/Console/Commands/UpdateModDownloadsCommand.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\UpdateModDownloadsJob;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateModDownloadsCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:update-downloads';
|
||||
|
||||
protected $description = 'Recalculate total downloads for all mods';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
UpdateModDownloadsJob::dispatch()->onQueue('default');
|
||||
|
||||
$this->info('UpdateModDownloadsJob added to the queue');
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class UploadAssets extends Command
|
||||
class UploadAssetsCommand extends Command
|
||||
{
|
||||
protected $signature = 'app:upload-assets';
|
||||
|
@ -4,7 +4,4 @@ namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CircularDependencyException extends Exception
|
||||
{
|
||||
protected $message = 'Circular dependency detected.';
|
||||
}
|
||||
class CircularDependencyException extends Exception {}
|
||||
|
7
app/Exceptions/InvalidVersionNumberException.php
Normal file
7
app/Exceptions/InvalidVersionNumberException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidVersionNumberException extends Exception {}
|
@ -15,7 +15,7 @@ class ModController extends Controller
|
||||
{
|
||||
$this->authorize('viewAny', Mod::class);
|
||||
|
||||
return ModResource::collection(Mod::all());
|
||||
return view('mod.index');
|
||||
}
|
||||
|
||||
public function store(ModRequest $request)
|
||||
@ -27,16 +27,15 @@ class ModController extends Controller
|
||||
|
||||
public function show(int $modId, string $slug)
|
||||
{
|
||||
$mod = Mod::withTotalDownloads()
|
||||
->with([
|
||||
$mod = Mod::with([
|
||||
'versions',
|
||||
'versions.sptVersion',
|
||||
'versions.dependencies',
|
||||
'versions.dependencies.resolvedVersion',
|
||||
'versions.dependencies.resolvedVersion.mod',
|
||||
'versions.latestSptVersion:id,version,color_class',
|
||||
'versions.latestResolvedDependencies',
|
||||
'versions.latestResolvedDependencies.mod:id,name,slug',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereHas('latestVersion')
|
||||
->findOrFail($modId);
|
||||
|
||||
if ($mod->slug !== $slug) {
|
||||
@ -45,9 +44,7 @@ class ModController extends Controller
|
||||
|
||||
$this->authorize('view', $mod);
|
||||
|
||||
$latestVersion = $mod->versions->sortByDesc('version')->first();
|
||||
|
||||
return view('mod.show', compact(['mod', 'latestVersion']));
|
||||
return view('mod.show', compact(['mod']));
|
||||
}
|
||||
|
||||
public function update(ModRequest $request, Mod $mod)
|
||||
|
142
app/Http/Filters/ModFilter.php
Normal file
142
app/Http/Filters/ModFilter.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Filters;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ModFilter
|
||||
{
|
||||
/**
|
||||
* The query builder instance for the mod model.
|
||||
*/
|
||||
protected Builder $builder;
|
||||
|
||||
/**
|
||||
* The filter that should be applied to the query.
|
||||
*/
|
||||
protected array $filters;
|
||||
|
||||
public function __construct(array $filters)
|
||||
{
|
||||
$this->builder = $this->baseQuery();
|
||||
$this->filters = $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for the mod listing.
|
||||
*/
|
||||
private function baseQuery(): Builder
|
||||
{
|
||||
return Mod::select([
|
||||
'mods.id',
|
||||
'mods.name',
|
||||
'mods.slug',
|
||||
'mods.teaser',
|
||||
'mods.thumbnail',
|
||||
'mods.featured',
|
||||
'mods.downloads',
|
||||
'mods.created_at',
|
||||
])->with([
|
||||
'users:id,name',
|
||||
'latestVersion' => function ($query) {
|
||||
$query->with('latestSptVersion:id,version,color_class');
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filters to the query.
|
||||
*/
|
||||
public function apply(): Builder
|
||||
{
|
||||
foreach ($this->filters as $method => $value) {
|
||||
if (method_exists($this, $method) && ! empty($value)) {
|
||||
$this->$method($value);
|
||||
}
|
||||
}
|
||||
|
||||
//dd($this->builder->toRawSql());
|
||||
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Order the query by the given type.
|
||||
*/
|
||||
private function order(string $type): Builder
|
||||
{
|
||||
// We order the "recently updated" mods by the ModVersion's updated_at value.
|
||||
if ($type === 'updated') {
|
||||
return $this->builder
|
||||
->joinSub(
|
||||
ModVersion::select('mod_id', DB::raw('MAX(updated_at) as latest_updated_at'))->groupBy('mod_id'),
|
||||
'latest_versions',
|
||||
'mods.id',
|
||||
'=',
|
||||
'latest_versions.mod_id'
|
||||
)
|
||||
->orderByDesc('latest_versions.latest_updated_at');
|
||||
}
|
||||
|
||||
// By default, we simply order by the column on the mods table/query.
|
||||
$column = match ($type) {
|
||||
'downloaded' => 'downloads',
|
||||
default => 'created_at',
|
||||
};
|
||||
|
||||
return $this->builder->orderByDesc($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the results by the given search term.
|
||||
*/
|
||||
private function query(string $term): Builder
|
||||
{
|
||||
return $this->builder->whereLike('name', "%$term%");
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the results by the featured status.
|
||||
*/
|
||||
private function featured(string $option): Builder
|
||||
{
|
||||
return match ($option) {
|
||||
'exclude' => $this->builder->where('featured', false),
|
||||
'only' => $this->builder->where('featured', true),
|
||||
default => $this->builder,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the results to specific SPT versions.
|
||||
*/
|
||||
private function sptVersions(array $versions): Builder
|
||||
{
|
||||
// Parse the versions into major, minor, and patch arrays
|
||||
$parsedVersions = array_map(fn ($version) => [
|
||||
'major' => (int) explode('.', $version)[0],
|
||||
'minor' => (int) (explode('.', $version)[1] ?? 0),
|
||||
'patch' => (int) (explode('.', $version)[2] ?? 0),
|
||||
], $versions);
|
||||
|
||||
[$majorVersions, $minorVersions, $patchVersions] = array_map('array_unique', [
|
||||
array_column($parsedVersions, 'major'),
|
||||
array_column($parsedVersions, 'minor'),
|
||||
array_column($parsedVersions, 'patch'),
|
||||
]);
|
||||
|
||||
return $this->builder
|
||||
->join('mod_versions as mv', 'mods.id', '=', 'mv.mod_id')
|
||||
->join('mod_version_spt_version as mvsv', 'mv.id', '=', 'mvsv.mod_version_id')
|
||||
->join('spt_versions as sv', 'mvsv.spt_version_id', '=', 'sv.id')
|
||||
->whereIn('sv.version_major', $majorVersions)
|
||||
->whereIn('sv.version_minor', $minorVersions)
|
||||
->whereIn('sv.version_patch', $patchVersions)
|
||||
->where('sv.version', '!=', '0.0.0')
|
||||
->groupBy('mods.id')
|
||||
->distinct();
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ use Illuminate\Support\Str;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
|
||||
class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
class ImportHubDataJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
@ -42,6 +42,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
$this->bringFileContentLocal();
|
||||
$this->bringFileVersionLabelsLocal();
|
||||
$this->bringFileVersionContentLocal();
|
||||
$this->bringSptVersionTagsLocal();
|
||||
|
||||
// Begin to import the data into the permanent local database tables.
|
||||
$this->importUsers();
|
||||
@ -53,9 +54,10 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
// Ensure that we've disconnected from the Hub database, clearing temporary tables.
|
||||
DB::connection('mysql_hub')->disconnect();
|
||||
|
||||
// Re-sync search.
|
||||
Artisan::call('app:search-sync');
|
||||
|
||||
Artisan::call('app:resolve-versions');
|
||||
Artisan::call('app:count-mods');
|
||||
Artisan::call('app:update-downloads');
|
||||
Artisan::call('cache:clear');
|
||||
}
|
||||
|
||||
@ -70,19 +72,24 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
avatarExtension VARCHAR(255),
|
||||
userID INT,
|
||||
fileHash VARCHAR(255)
|
||||
)');
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_user_avatar')
|
||||
->orderBy('avatarID')
|
||||
->chunk(200, function ($avatars) {
|
||||
$insertData = [];
|
||||
foreach ($avatars as $avatar) {
|
||||
DB::table('temp_user_avatar')->insert([
|
||||
$insertData[] = [
|
||||
'avatarID' => (int) $avatar->avatarID,
|
||||
'avatarExtension' => $avatar->avatarExtension,
|
||||
'userID' => (int) $avatar->userID,
|
||||
'fileHash' => $avatar->fileHash,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_user_avatar')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -93,17 +100,25 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
protected function bringFileAuthorsLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_author (fileID INT, userID INT) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_author (
|
||||
fileID INT,
|
||||
userID INT
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_author')
|
||||
->orderBy('fileID')
|
||||
->chunk(200, function ($relationships) {
|
||||
$insertData = [];
|
||||
foreach ($relationships as $relationship) {
|
||||
DB::table('temp_file_author')->insert([
|
||||
$insertData[] = [
|
||||
'fileID' => (int) $relationship->fileID,
|
||||
'userID' => (int) $relationship->userID,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_file_author')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -114,18 +129,27 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
protected function bringFileOptionsLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS 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');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_option_values (
|
||||
fileID INT,
|
||||
optionID INT,
|
||||
optionValue VARCHAR(255)
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_option_value')
|
||||
->orderBy('fileID')
|
||||
->chunk(200, function ($options) {
|
||||
$insertData = [];
|
||||
foreach ($options as $option) {
|
||||
DB::table('temp_file_option_values')->insert([
|
||||
$insertData[] = [
|
||||
'fileID' => (int) $option->fileID,
|
||||
'optionID' => (int) $option->optionID,
|
||||
'optionValue' => $option->optionValue,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_file_option_values')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -136,19 +160,29 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
protected function bringFileContentLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS 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');
|
||||
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');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_content')
|
||||
->orderBy('fileID')
|
||||
->chunk(200, function ($contents) {
|
||||
$insertData = [];
|
||||
foreach ($contents as $content) {
|
||||
DB::table('temp_file_content')->insert([
|
||||
$insertData[] = [
|
||||
'fileID' => (int) $content->fileID,
|
||||
'subject' => $content->subject,
|
||||
'teaser' => $content->teaser,
|
||||
'message' => $content->message,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_file_content')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -159,18 +193,26 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
protected function bringFileVersionLabelsLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS 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');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_labels (
|
||||
labelID INT,
|
||||
objectID INT
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_label_object')
|
||||
->where('objectTypeID', 387)
|
||||
->orderBy('labelID')
|
||||
->chunk(200, function ($options) {
|
||||
$insertData = [];
|
||||
foreach ($options as $option) {
|
||||
DB::table('temp_file_version_labels')->insert([
|
||||
$insertData[] = [
|
||||
'labelID' => (int) $option->labelID,
|
||||
'objectID' => (int) $option->objectID,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_file_version_labels')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -181,17 +223,54 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
protected function bringFileVersionContentLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS 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');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_file_version_content (
|
||||
versionID INT,
|
||||
description TEXT
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('filebase1_file_version_content')
|
||||
->orderBy('versionID')
|
||||
->chunk(200, function ($options) {
|
||||
$insertData = [];
|
||||
foreach ($options as $option) {
|
||||
DB::table('temp_file_version_content')->insert([
|
||||
$insertData[] = [
|
||||
'versionID' => (int) $option->versionID,
|
||||
'description' => $option->description,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_file_version_content')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function bringSptVersionTagsLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_spt_version_tags');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_spt_version_tags (
|
||||
hub_id INT,
|
||||
version VARCHAR(255),
|
||||
color_class VARCHAR(255)
|
||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_label')
|
||||
->where('groupID', 1)
|
||||
->orderBy('labelID')
|
||||
->chunk(100, function (Collection $versions) {
|
||||
$insertData = [];
|
||||
foreach ($versions as $version) {
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $version->labelID,
|
||||
'version' => $version->label,
|
||||
'color_class' => $version->cssClassName,
|
||||
];
|
||||
}
|
||||
|
||||
if ($insertData) {
|
||||
DB::table('temp_spt_version_tags')->insert($insertData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -534,39 +613,139 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
*/
|
||||
protected function importSptVersions(): void
|
||||
{
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_label')
|
||||
->where('groupID', 1)
|
||||
->chunkById(100, function (Collection $versions) {
|
||||
$domain = config('services.gitea.domain');
|
||||
$token = config('services.gitea.token');
|
||||
|
||||
if (empty($domain) || empty($token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = "{$domain}/api/v1/repos/SPT/Stable-releases/releases?draft=false&pre-release=false&token={$token}";
|
||||
|
||||
$response = json_decode(file_get_contents($url), true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('JSON Decode Error: '.json_last_error_msg());
|
||||
}
|
||||
|
||||
if (empty($response)) {
|
||||
throw new Exception('No version data found in the API response.');
|
||||
}
|
||||
|
||||
$latestVersion = $this->getLatestVersion($response);
|
||||
|
||||
$insertData = [];
|
||||
foreach ($versions as $version) {
|
||||
foreach ($response as $version) {
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $version->labelID,
|
||||
'version' => $version->label,
|
||||
'color_class' => $this->translateColour($version->cssClassName),
|
||||
'version' => $version['tag_name'],
|
||||
'link' => $version['html_url'],
|
||||
'color_class' => $this->detectVersionColor($version['tag_name'], $latestVersion),
|
||||
'created_at' => Carbon::parse($version['created_at'], 'UTC'),
|
||||
'updated_at' => Carbon::parse($version['created_at'], 'UTC'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($insertData)) {
|
||||
DB::table('spt_versions')->upsert($insertData, ['hub_id'], ['version', 'color_class']);
|
||||
// Add a fake 0.0.0 version for outdated mods.
|
||||
$insertData[] = [
|
||||
'version' => '0.0.0',
|
||||
'link' => '',
|
||||
'color_class' => 'black',
|
||||
'created_at' => Carbon::now('UTC'),
|
||||
'updated_at' => Carbon::now('UTC'),
|
||||
];
|
||||
|
||||
// Upsert won't work here. Do it manually. :(
|
||||
foreach ($insertData as $data) {
|
||||
$existingVersion = SptVersion::where('version', $data['version'])->first();
|
||||
if ($existingVersion) {
|
||||
$existingVersion->update([
|
||||
'link' => $data['link'],
|
||||
'color_class' => $data['color_class'],
|
||||
'created_at' => $data['created_at'],
|
||||
'updated_at' => $data['updated_at'],
|
||||
]);
|
||||
} else {
|
||||
SptVersion::create($data);
|
||||
}
|
||||
}
|
||||
}, 'labelID');
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the colour class from the Hub database to the local database.
|
||||
* Get the latest current version from the response data.
|
||||
*/
|
||||
protected function translateColour(string $colour = ''): string
|
||||
protected function getLatestVersion(array $versions): string
|
||||
{
|
||||
return match ($colour) {
|
||||
'green' => 'green',
|
||||
'slightly-outdated' => 'lime',
|
||||
'yellow' => 'yellow',
|
||||
'red' => 'red',
|
||||
$semanticVersions = array_map(
|
||||
fn ($version) => $this->extractSemanticVersion($version['tag_name']),
|
||||
$versions
|
||||
);
|
||||
|
||||
usort($semanticVersions, 'version_compare');
|
||||
|
||||
return end($semanticVersions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the last semantic version from a string.
|
||||
* If the version has no patch number, return it as `~<major>.<minor>.0`.
|
||||
*/
|
||||
protected function extractSemanticVersion(string $versionString, bool $appendPatch = false): ?string
|
||||
{
|
||||
// Match both two-part and three-part semantic versions
|
||||
preg_match_all('/\b\d+\.\d+(?:\.\d+)?\b/', $versionString, $matches);
|
||||
|
||||
// Get the last version found, if any
|
||||
$version = end($matches[0]) ?: null;
|
||||
|
||||
if (! $appendPatch) {
|
||||
return $version;
|
||||
}
|
||||
|
||||
// If version is two-part (e.g., "3.9"), add ".0" and prefix with "~"
|
||||
if ($version && preg_match('/^\d+\.\d+$/', $version)) {
|
||||
$version = '~'.$version.'.0';
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the version string into a color class.
|
||||
*/
|
||||
protected function detectVersionColor(string $versionString, string $currentVersion): string
|
||||
{
|
||||
$version = $this->extractSemanticVersion($versionString);
|
||||
if (! $version) {
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
if ($version === '0.0.0') {
|
||||
return 'black';
|
||||
}
|
||||
|
||||
[$currentMajor, $currentMinor] = explode('.', $currentVersion);
|
||||
[$major, $minor] = explode('.', $version);
|
||||
|
||||
$currentMajor = (int) $currentMajor;
|
||||
$currentMinor = (int) $currentMinor;
|
||||
$major = (int) $major;
|
||||
$minor = (int) $minor;
|
||||
|
||||
if ($major == $currentMajor) {
|
||||
$difference = $currentMinor - $minor;
|
||||
|
||||
return match ($difference) {
|
||||
0 => 'green',
|
||||
1 => 'lime',
|
||||
2 => 'yellow',
|
||||
3 => 'red',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the mods from the Hub database to the local database.
|
||||
*/
|
||||
@ -748,17 +927,21 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch the version string using the labelID from the hub.
|
||||
$sptVersionTemp = DB::table('temp_spt_version_tags')->where('hub_id', $versionLabel->labelID)->value('version');
|
||||
$sptVersionConstraint = $this->extractSemanticVersion($sptVersionTemp, appendPatch: true) ?? '0.0.0';
|
||||
|
||||
$insertData[] = [
|
||||
'hub_id' => (int) $version->versionID,
|
||||
'mod_id' => $modId,
|
||||
'version' => $version->versionNumber,
|
||||
'version' => $this->extractSemanticVersion($version->versionNumber) ?? '0.0.0',
|
||||
'description' => $this->cleanHubContent($versionContent->description ?? ''),
|
||||
'link' => $version->downloadURL,
|
||||
'spt_version_id' => SptVersion::whereHubId($versionLabel->labelID)->value('id'),
|
||||
'spt_version_constraint' => $sptVersionConstraint,
|
||||
'virus_total_link' => $optionVirusTotal?->virus_total_link ?? '',
|
||||
'downloads' => max((int) $version->downloads, 0), // At least 0.
|
||||
'disabled' => (bool) $version->isDisabled,
|
||||
'published_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
'published_at' => $sptVersionConstraint === '0.0.0' ? null : Carbon::parse($version->uploadTime, 'UTC'),
|
||||
'created_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
'updated_at' => Carbon::parse($version->uploadTime, 'UTC'),
|
||||
];
|
||||
@ -770,9 +953,10 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
'version',
|
||||
'description',
|
||||
'link',
|
||||
'spt_version_id',
|
||||
'spt_version_constraint',
|
||||
'virus_total_link',
|
||||
'downloads',
|
||||
'disabled',
|
||||
'published_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
@ -793,6 +977,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_labels');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_version_content');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_spt_version_tags');
|
||||
|
||||
// Close the connections. This should drop the temporary tables as well, but I like to be explicit.
|
||||
DB::connection('mysql_hub')->disconnect();
|
29
app/Jobs/ResolveDependenciesJob.php
Normal file
29
app/Jobs/ResolveDependenciesJob.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\DependencyVersionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ResolveDependenciesJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Resolve the SPT versions for each of the mod versions.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$dependencyVersionService = new DependencyVersionService;
|
||||
|
||||
foreach (ModVersion::all() as $modVersion) {
|
||||
$dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
}
|
||||
}
|
29
app/Jobs/ResolveSptVersionsJob.php
Normal file
29
app/Jobs/ResolveSptVersionsJob.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\SptVersionService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ResolveSptVersionsJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Resolve the SPT versions for each of the mod versions.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$sptVersionService = new SptVersionService;
|
||||
|
||||
foreach (ModVersion::all() as $modVersion) {
|
||||
$sptVersionService->resolve($modVersion);
|
||||
}
|
||||
}
|
||||
}
|
26
app/Jobs/SptVersionModCountsJob.php
Normal file
26
app/Jobs/SptVersionModCountsJob.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class SptVersionModCountsJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Recalculate the mod counts for each SPT version.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
SptVersion::all()->each(function (SptVersion $sptVersion) {
|
||||
$sptVersion->updateModCount();
|
||||
});
|
||||
}
|
||||
}
|
27
app/Jobs/UpdateModDownloadsJob.php
Normal file
27
app/Jobs/UpdateModDownloadsJob.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UpdateModDownloadsJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Recalculate the total download counts for each mod.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
Mod::with('versions')->chunk(100, function ($mods) {
|
||||
foreach ($mods as $mod) {
|
||||
$mod->calculateDownloads();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
127
app/Livewire/Mod/Index.php
Normal file
127
app/Livewire/Mod/Index.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Mod;
|
||||
|
||||
use App\Http\Filters\ModFilter;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Session;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
/**
|
||||
* The search query value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public string $query = '';
|
||||
|
||||
/**
|
||||
* The sort order value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public string $order = 'created';
|
||||
|
||||
/**
|
||||
* The SPT versions filter value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public array $sptVersions = [];
|
||||
|
||||
/**
|
||||
* The featured filter value.
|
||||
*/
|
||||
#[Url]
|
||||
#[Session]
|
||||
public string $featured = 'include';
|
||||
|
||||
/**
|
||||
* The available SPT versions.
|
||||
*/
|
||||
public Collection $availableSptVersions;
|
||||
|
||||
/**
|
||||
* The component mount method, run only once when the component is mounted.
|
||||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->availableSptVersions = $this->availableSptVersions ?? Cache::remember('available-spt-versions', 60 * 60, function () {
|
||||
return SptVersion::getVersionsForLastThreeMinors();
|
||||
});
|
||||
|
||||
$this->sptVersions = $this->sptVersions ?? $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all patch versions of the latest minor SPT version.
|
||||
*/
|
||||
public function getLatestMinorVersions(): Collection
|
||||
{
|
||||
return $this->availableSptVersions->filter(function (SptVersion $sptVersion) {
|
||||
return $sptVersion->isLatestMinor();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The component mount method.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
// Fetch the mods using the filters saved to the component properties.
|
||||
$filters = [
|
||||
'query' => $this->query,
|
||||
'featured' => $this->featured,
|
||||
'order' => $this->order,
|
||||
'sptVersions' => $this->sptVersions,
|
||||
];
|
||||
$mods = (new ModFilter($filters))->apply()->paginate(16);
|
||||
|
||||
// Check if the current page is greater than the last page. Redirect if it is.
|
||||
if ($mods->currentPage() > $mods->lastPage()) {
|
||||
$this->redirectRoute('mods', ['page' => $mods->lastPage()]);
|
||||
}
|
||||
|
||||
return view('livewire.mod.index', compact('mods'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The method to reset the filters.
|
||||
*/
|
||||
public function resetFilters(): void
|
||||
{
|
||||
$this->query = '';
|
||||
$this->sptVersions = $this->getLatestMinorVersions()->pluck('version')->toArray();
|
||||
$this->featured = 'include';
|
||||
|
||||
// Clear local storage
|
||||
$this->dispatch('clear-filters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the count of active filters.
|
||||
*/
|
||||
#[Computed]
|
||||
public function filterCount(): int
|
||||
{
|
||||
$count = 0;
|
||||
if ($this->query !== '') {
|
||||
$count++;
|
||||
}
|
||||
if ($this->featured !== 'include') {
|
||||
$count++;
|
||||
}
|
||||
$count += count($this->sptVersions);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
@ -34,10 +34,20 @@ class Mod extends Model
|
||||
{
|
||||
// Apply the global scope to exclude disabled mods.
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
|
||||
// Apply the global scope to exclude non-published mods.
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the total number of downloads for the mod.
|
||||
*/
|
||||
public function calculateDownloads(): void
|
||||
{
|
||||
$this->downloads = $this->versions->sum('downloads');
|
||||
$this->saveQuietly();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its users.
|
||||
*/
|
||||
@ -59,18 +69,9 @@ class Mod extends Model
|
||||
*/
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class)->orderByDesc('version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to include the total number of downloads for a mod.
|
||||
*/
|
||||
public function scopeWithTotalDownloads($query)
|
||||
{
|
||||
return $query->addSelect([
|
||||
'total_downloads' => ModVersion::selectRaw('SUM(downloads) AS total_downloads')
|
||||
->whereColumn('mod_id', 'mods.id'),
|
||||
]);
|
||||
return $this->hasMany(ModVersion::class)
|
||||
->whereHas('latestSptVersion')
|
||||
->orderByDesc('version');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,6 +80,7 @@ class Mod extends Model
|
||||
public function lastUpdatedVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('latestSptVersion')
|
||||
->orderByDesc('updated_at');
|
||||
}
|
||||
|
||||
@ -87,10 +89,8 @@ class Mod extends Model
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$latestVersion = $this->latestVersion()->with('sptVersion')->first();
|
||||
|
||||
return [
|
||||
'id' => (int) $this->id,
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'description' => $this->description,
|
||||
@ -99,8 +99,8 @@ class Mod extends Model
|
||||
'created_at' => strtotime($this->created_at),
|
||||
'updated_at' => strtotime($this->updated_at),
|
||||
'published_at' => strtotime($this->published_at),
|
||||
'latestVersion' => $latestVersion?->sptVersion->version,
|
||||
'latestVersionColorClass' => $latestVersion?->sptVersion->color_class,
|
||||
'latestVersion' => $this->latestVersion()?->first()?->latestSptVersion()?->first()?->version_formatted,
|
||||
'latestVersionColorClass' => $this->latestVersion()?->first()?->latestSptVersion()?->first()?->color_class,
|
||||
];
|
||||
}
|
||||
|
||||
@ -110,6 +110,7 @@ class Mod extends Model
|
||||
public function latestVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->whereHas('sptVersions')
|
||||
->orderByDesc('version')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1);
|
||||
@ -120,7 +121,31 @@ class Mod extends Model
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return ! $this->disabled;
|
||||
// Ensure the mod is not disabled.
|
||||
if ($this->disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the mod has a publish date.
|
||||
if (is_null($this->published_at)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch the latest version instance.
|
||||
$latestVersion = $this->latestVersion()?->first();
|
||||
|
||||
// Ensure the mod has a latest version.
|
||||
if (is_null($latestVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the latest version has a latest SPT version.
|
||||
if ($latestVersion->latestSptVersion()->doesntExist()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All conditions are met; the mod should be searchable.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,12 +5,13 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $mod_version_id
|
||||
* @property int $dependency_mod_id
|
||||
* @property string $version_constraint
|
||||
* @property string $constraint
|
||||
* @property int|null $resolved_version_id
|
||||
*/
|
||||
class ModDependency extends Model
|
||||
@ -18,7 +19,7 @@ class ModDependency extends Model
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The relationship between a mod dependency and mod version.
|
||||
* The relationship between the mod dependency and the mod version.
|
||||
*/
|
||||
public function modVersion(): BelongsTo
|
||||
{
|
||||
@ -26,18 +27,18 @@ class ModDependency extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod dependency and mod.
|
||||
* The relationship between the mod dependency and the resolved dependency.
|
||||
*/
|
||||
public function dependencyMod(): BelongsTo
|
||||
public function resolvedDependencies(): HasMany
|
||||
{
|
||||
return $this->belongsTo(Mod::class, 'dependency_mod_id');
|
||||
return $this->hasMany(ModResolvedDependency::class, 'dependency_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod dependency and resolved mod version.
|
||||
* The relationship between the mod dependency and the dependent mod.
|
||||
*/
|
||||
public function resolvedVersion(): BelongsTo
|
||||
public function dependentMod(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'resolved_version_id');
|
||||
return $this->belongsTo(Mod::class, 'dependent_mod_id');
|
||||
}
|
||||
}
|
||||
|
33
app/Models/ModResolvedDependency.php
Normal file
33
app/Models/ModResolvedDependency.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ModResolvedDependency extends Model
|
||||
{
|
||||
/**
|
||||
* The relationship between the resolved dependency and the mod version.
|
||||
*/
|
||||
public function modVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'mod_version_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between the resolved dependency and the dependency.
|
||||
*/
|
||||
public function dependency(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModDependency::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between the resolved dependency and the resolved mod version.
|
||||
*/
|
||||
public function resolvedModVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ModVersion::class, 'resolved_mod_version_id');
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ use App\Models\Scopes\PublishedScope;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@ -45,10 +46,47 @@ class ModVersion extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and SPT version.
|
||||
* The relationship between a mod version and its resolved dependencies.
|
||||
*/
|
||||
public function sptVersion(): BelongsTo
|
||||
public function resolvedDependencies(): BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(SptVersion::class);
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_resolved_dependencies', 'mod_version_id', 'resolved_mod_version_id')
|
||||
->withPivot('dependency_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its each of it's resolved dependencies' latest versions.
|
||||
*/
|
||||
public function latestResolvedDependencies(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_resolved_dependencies', 'mod_version_id', 'resolved_mod_version_id')
|
||||
->withPivot('dependency_id')
|
||||
->join('mod_versions as latest_versions', function ($join) {
|
||||
$join->on('latest_versions.id', '=', 'mod_versions.id')
|
||||
->whereRaw('latest_versions.version = (SELECT MAX(mv.version) FROM mod_versions mv WHERE mv.mod_id = mod_versions.mod_id)');
|
||||
})
|
||||
->with('mod:id,name,slug')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and each of its SPT versions' latest version.
|
||||
* Hint: Be sure to call `->first()` on this to get the actual instance.
|
||||
*/
|
||||
public function latestSptVersion(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderBy('version', 'desc')
|
||||
->limit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and its SPT versions.
|
||||
*/
|
||||
public function sptVersions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(SptVersion::class, 'mod_version_spt_version')
|
||||
->orderByDesc('version');
|
||||
}
|
||||
}
|
||||
|
@ -2,20 +2,175 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\InvalidVersionNumberException;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SptVersion extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Get all versions for the last three minor versions.
|
||||
*/
|
||||
public static function getVersionsForLastThreeMinors(): Collection
|
||||
{
|
||||
$lastThreeMinorVersions = self::getLastThreeMinorVersions();
|
||||
|
||||
// Extract major and minor arrays.
|
||||
$majorVersions = array_column($lastThreeMinorVersions, 'major');
|
||||
$minorVersions = array_column($lastThreeMinorVersions, 'minor');
|
||||
|
||||
// Fetch all versions for the last three minor versions with mod count.
|
||||
return self::select(['spt_versions.id', 'spt_versions.version', 'spt_versions.color_class', 'spt_versions.mod_count'])
|
||||
->join('mod_version_spt_version', 'spt_versions.id', '=', 'mod_version_spt_version.spt_version_id')
|
||||
->join('mod_versions', 'mod_version_spt_version.mod_version_id', '=', 'mod_versions.id')
|
||||
->join('mods', 'mod_versions.mod_id', '=', 'mods.id')
|
||||
->where('spt_versions.version', '!=', '0.0.0')
|
||||
->whereIn('spt_versions.version_major', $majorVersions)
|
||||
->whereIn('spt_versions.version_minor', $minorVersions)
|
||||
->where('spt_versions.mod_count', '>', 0)
|
||||
->groupBy('spt_versions.id', 'spt_versions.version', 'spt_versions.color_class', 'spt_versions.mod_count')
|
||||
->orderBy('spt_versions.version_major', 'DESC')
|
||||
->orderBy('spt_versions.version_minor', 'DESC')
|
||||
->orderBy('spt_versions.version_patch', 'DESC')
|
||||
->orderBy('spt_versions.version_pre_release', 'ASC')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last three minor versions (major.minor format).
|
||||
*/
|
||||
public static function getLastThreeMinorVersions(): array
|
||||
{
|
||||
return self::selectRaw('CONCAT(version_major, ".", version_minor) AS minor_version, version_major, version_minor')
|
||||
->where('version', '!=', '0.0.0')
|
||||
->groupBy('version_major', 'version_minor')
|
||||
->orderByDesc('version_major')
|
||||
->orderByDesc('version_minor')
|
||||
->limit(3)
|
||||
->get()
|
||||
->map(function ($version) {
|
||||
return [
|
||||
'major' => (int) $version->version_major,
|
||||
'minor' => (int) $version->version_minor,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the model is booted.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Callback that runs before saving the model.
|
||||
static::saving(function ($model) {
|
||||
// Extract the version sections from the version string.
|
||||
if (! empty($model->version)) {
|
||||
// Default values in case there's an exception.
|
||||
$model->version_major = 0;
|
||||
$model->version_minor = 0;
|
||||
$model->version_patch = 0;
|
||||
$model->version_pre_release = '';
|
||||
|
||||
try {
|
||||
$versionSections = self::extractVersionSections($model->version);
|
||||
} catch (InvalidVersionNumberException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$model->version_major = $versionSections['major'];
|
||||
$model->version_minor = $versionSections['minor'];
|
||||
$model->version_patch = $versionSections['patch'];
|
||||
$model->version_pre_release = $versionSections['pre_release'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the version sections from the version string.
|
||||
*
|
||||
* @throws InvalidVersionNumberException
|
||||
*/
|
||||
public static function extractVersionSections(string $version): array
|
||||
{
|
||||
$matches = [];
|
||||
|
||||
// Perform the regex match to capture the version sections, including the possible preRelease section.
|
||||
preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([a-zA-Z0-9]+))?$/', $version, $matches);
|
||||
|
||||
if (! $matches) {
|
||||
throw new InvalidVersionNumberException('Invalid SPT version number: '.$version);
|
||||
}
|
||||
|
||||
return [
|
||||
'major' => $matches[1] ?? 0,
|
||||
'minor' => $matches[2] ?? 0,
|
||||
'patch' => $matches[3] ?? 0,
|
||||
'pre_release' => $matches[4] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the mod count for this SptVersion.
|
||||
*/
|
||||
public function updateModCount(): void
|
||||
{
|
||||
$modCount = $this->modVersions()
|
||||
->distinct('mod_id')
|
||||
->count('mod_id');
|
||||
|
||||
$this->mod_count = $modCount;
|
||||
$this->saveQuietly();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between an SPT version and mod version.
|
||||
*/
|
||||
public function modVersions(): HasMany
|
||||
public function modVersions(): BelongsToMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class);
|
||||
return $this->belongsToMany(ModVersion::class, 'mod_version_spt_version');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version with "SPT " prepended.
|
||||
*/
|
||||
public function getVersionFormattedAttribute(): string
|
||||
{
|
||||
return __('SPT ').$this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the version is part of the latest version's minor releases.
|
||||
* For example, if the latest version is 1.2.0, this method will return true for 1.2.0, 1.2.1, 1.2.2, etc.
|
||||
*/
|
||||
public function isLatestMinor(): bool
|
||||
{
|
||||
$latestVersion = self::getLatest();
|
||||
|
||||
if (! $latestVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->version_major == $latestVersion->version_major && $this->version_minor == $latestVersion->version_minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest SPT version.
|
||||
*
|
||||
* @cached latest_spt_version 300s
|
||||
*/
|
||||
public static function getLatest(): ?SptVersion
|
||||
{
|
||||
return Cache::remember('latest_spt_version', 300, function () {
|
||||
return SptVersion::select(['version', 'version_major', 'version_minor', 'version_patch', 'version_pre_release'])
|
||||
->orderByDesc('version')
|
||||
->first();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,31 +3,30 @@
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\ModVersionService;
|
||||
use App\Services\DependencyVersionService;
|
||||
|
||||
class ModDependencyObserver
|
||||
{
|
||||
protected ModVersionService $modVersionService;
|
||||
protected DependencyVersionService $dependencyVersionService;
|
||||
|
||||
public function __construct(ModVersionService $modVersionService)
|
||||
public function __construct(DependencyVersionService $dependencyVersionService)
|
||||
{
|
||||
$this->modVersionService = $modVersionService;
|
||||
$this->dependencyVersionService = $dependencyVersionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ModDependency "saved" event.
|
||||
*/
|
||||
public function saved(ModDependency $modDependency): void
|
||||
{
|
||||
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
||||
if ($modVersion) {
|
||||
$this->modVersionService->resolveDependencies($modVersion);
|
||||
}
|
||||
$this->dependencyVersionService->resolve($modDependency->modVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ModDependency "deleted" event.
|
||||
*/
|
||||
public function deleted(ModDependency $modDependency): void
|
||||
{
|
||||
$modVersion = ModVersion::find($modDependency->mod_version_id);
|
||||
if ($modVersion) {
|
||||
$this->modVersionService->resolveDependencies($modVersion);
|
||||
}
|
||||
$this->dependencyVersionService->resolve($modDependency->modVersion);
|
||||
}
|
||||
}
|
||||
|
49
app/Observers/ModObserver.php
Normal file
49
app/Observers/ModObserver.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Services\DependencyVersionService;
|
||||
|
||||
class ModObserver
|
||||
{
|
||||
protected DependencyVersionService $dependencyVersionService;
|
||||
|
||||
public function __construct(
|
||||
DependencyVersionService $dependencyVersionService,
|
||||
) {
|
||||
$this->dependencyVersionService = $dependencyVersionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Mod "saved" event.
|
||||
*/
|
||||
public function saved(Mod $mod): void
|
||||
{
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
}
|
||||
|
||||
$this->updateRelatedSptVersions($mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties on related SptVersions.
|
||||
*/
|
||||
protected function updateRelatedSptVersions(Mod $mod): void
|
||||
{
|
||||
$sptVersions = $mod->versions->flatMap->sptVersions->unique();
|
||||
|
||||
foreach ($sptVersions as $sptVersion) {
|
||||
$sptVersion->updateModCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Mod "deleted" event.
|
||||
*/
|
||||
public function deleted(Mod $mod): void
|
||||
{
|
||||
$this->updateRelatedSptVersions($mod);
|
||||
}
|
||||
}
|
@ -2,32 +2,66 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\ModVersionService;
|
||||
use App\Services\DependencyVersionService;
|
||||
use App\Services\SptVersionService;
|
||||
|
||||
class ModVersionObserver
|
||||
{
|
||||
protected ModVersionService $modVersionService;
|
||||
protected DependencyVersionService $dependencyVersionService;
|
||||
|
||||
public function __construct(ModVersionService $modVersionService)
|
||||
{
|
||||
$this->modVersionService = $modVersionService;
|
||||
protected SptVersionService $sptVersionService;
|
||||
|
||||
public function __construct(
|
||||
DependencyVersionService $dependencyVersionService,
|
||||
SptVersionService $sptVersionService,
|
||||
) {
|
||||
$this->dependencyVersionService = $dependencyVersionService;
|
||||
$this->sptVersionService = $sptVersionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ModVersion "saved" event.
|
||||
*/
|
||||
public function saved(ModVersion $modVersion): void
|
||||
{
|
||||
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
||||
foreach ($dependencies as $dependency) {
|
||||
$this->modVersionService->resolveDependencies($dependency->modVersion);
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
|
||||
$this->sptVersionService->resolve($modVersion);
|
||||
|
||||
$this->updateRelatedSptVersions($modVersion); // After resolving SPT versions.
|
||||
$this->updateRelatedMod($modVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties on related SptVersions.
|
||||
*/
|
||||
protected function updateRelatedSptVersions(ModVersion $modVersion): void
|
||||
{
|
||||
$sptVersions = $modVersion->sptVersions; // These should already be resolved.
|
||||
|
||||
foreach ($sptVersions as $sptVersion) {
|
||||
$sptVersion->updateModCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties on the related Mod.
|
||||
*/
|
||||
protected function updateRelatedMod(ModVersion $modVersion): void
|
||||
{
|
||||
$mod = $modVersion->mod;
|
||||
$mod->calculateDownloads();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ModVersion "deleted" event.
|
||||
*/
|
||||
public function deleted(ModVersion $modVersion): void
|
||||
{
|
||||
$dependencies = ModDependency::where('resolved_version_id', $modVersion->id)->get();
|
||||
foreach ($dependencies as $dependency) {
|
||||
$this->modVersionService->resolveDependencies($dependency->modVersion);
|
||||
}
|
||||
$this->dependencyVersionService->resolve($modVersion);
|
||||
|
||||
$this->updateRelatedSptVersions($modVersion); // After resolving SPT versions.
|
||||
$this->updateRelatedMod($modVersion);
|
||||
}
|
||||
}
|
||||
|
44
app/Observers/SptVersionObserver.php
Normal file
44
app/Observers/SptVersionObserver.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\SptVersionService;
|
||||
|
||||
class SptVersionObserver
|
||||
{
|
||||
protected SptVersionService $sptVersionService;
|
||||
|
||||
public function __construct(SptVersionService $sptVersionService)
|
||||
{
|
||||
$this->sptVersionService = $sptVersionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the SptVersion "saved" event.
|
||||
*/
|
||||
public function saved(): void
|
||||
{
|
||||
$this->resolveSptVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the SptVersion's dependencies.
|
||||
*/
|
||||
private function resolveSptVersion(): void
|
||||
{
|
||||
$modVersions = ModVersion::all();
|
||||
|
||||
foreach ($modVersions as $modVersion) {
|
||||
$this->sptVersionService->resolve($modVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the SptVersion "deleted" event.
|
||||
*/
|
||||
public function deleted(): void
|
||||
{
|
||||
$this->resolveSptVersion();
|
||||
}
|
||||
}
|
@ -10,9 +10,9 @@ class ModPolicy
|
||||
/**
|
||||
* Determine whether the user can view multiple models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
public function viewAny(?User $user): bool
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,11 +2,16 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use App\Models\User;
|
||||
use App\Observers\ModDependencyObserver;
|
||||
use App\Observers\ModObserver;
|
||||
use App\Observers\ModVersionObserver;
|
||||
use App\Observers\SptVersionObserver;
|
||||
use App\Services\LatestSptVersionService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Number;
|
||||
@ -19,7 +24,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->singleton(LatestSptVersionService::class, function ($app) {
|
||||
return new LatestSptVersionService;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,8 +38,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
Model::unguard();
|
||||
|
||||
// Register observers.
|
||||
Mod::observe(ModObserver::class);
|
||||
ModVersion::observe(ModVersionObserver::class);
|
||||
ModDependency::observe(ModDependencyObserver::class);
|
||||
SptVersion::observe(SptVersionObserver::class);
|
||||
|
||||
// This gate determines who can access the Pulse dashboard.
|
||||
Gate::define('viewPulse', function (User $user) {
|
||||
|
47
app/Services/DependencyVersionService.php
Normal file
47
app/Services/DependencyVersionService.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use Composer\Semver\Semver;
|
||||
|
||||
class DependencyVersionService
|
||||
{
|
||||
/**
|
||||
* Resolve the dependencies for a mod version.
|
||||
*/
|
||||
public function resolve(ModVersion $modVersion): void
|
||||
{
|
||||
$dependencies = $this->satisfyConstraint($modVersion);
|
||||
$modVersion->resolvedDependencies()->sync($dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Satisfies all dependency constraints of a ModVersion.
|
||||
*/
|
||||
private function satisfyConstraint(ModVersion $modVersion): array
|
||||
{
|
||||
// Eager load the dependencies and their mod versions.
|
||||
$modVersion->load('dependencies.dependentMod.versions');
|
||||
|
||||
// Iterate over each ModVersion dependency.
|
||||
$dependencies = [];
|
||||
foreach ($modVersion->dependencies as $dependency) {
|
||||
|
||||
// Get all dependent mod versions.
|
||||
$dependentModVersions = $dependency->dependentMod->versions()->get();
|
||||
|
||||
// Filter the dependent mod versions to find the ones that satisfy the dependency constraint.
|
||||
$matchedVersions = $dependentModVersions->filter(function ($version) use ($dependency) {
|
||||
return Semver::satisfies($version->version, $dependency->constraint);
|
||||
});
|
||||
|
||||
// Map the matched versions to the sync data.
|
||||
foreach ($matchedVersions as $matchedVersion) {
|
||||
$dependencies[$matchedVersion->id] = ['dependency_id' => $dependency->id];
|
||||
}
|
||||
}
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
38
app/Services/SptVersionService.php
Normal file
38
app/Services/SptVersionService.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Composer\Semver\Semver;
|
||||
|
||||
class SptVersionService
|
||||
{
|
||||
/**
|
||||
* Resolve dependencies for the given mod version.
|
||||
*/
|
||||
public function resolve(ModVersion $modVersion): void
|
||||
{
|
||||
$satisfyingVersionIds = $this->satisfyConstraint($modVersion);
|
||||
$modVersion->sptVersions()->sync($satisfyingVersionIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Satisfies the version constraint of a given ModVersion. Returns the ID of the satisfying SptVersion.
|
||||
*/
|
||||
private function satisfyConstraint(ModVersion $modVersion): array
|
||||
{
|
||||
$availableVersions = SptVersion::query()
|
||||
->orderBy('version', 'desc')
|
||||
->pluck('id', 'version')
|
||||
->toArray();
|
||||
|
||||
$satisfyingVersions = Semver::satisfiedBy(array_keys($availableVersions), $modVersion->spt_version_constraint);
|
||||
if (empty($satisfyingVersions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return the IDs of all satisfying versions
|
||||
return array_map(fn ($version) => $availableVersions[$version], $satisfyingVersions);
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class ModListSection extends Component
|
||||
@ -26,47 +26,52 @@ class ModListSection extends Component
|
||||
|
||||
private function fetchFeaturedMods(): Collection
|
||||
{
|
||||
return Cache::remember('homepage-featured-mods', now()->addMinutes(5), function () {
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||
->withTotalDownloads()
|
||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||
->whereHas('latestVersion')
|
||||
->where('featured', true)
|
||||
->latest()
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads'])
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->whereFeatured(true)
|
||||
->inRandomOrder()
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
private function fetchLatestMods(): Collection
|
||||
{
|
||||
return Cache::remember('homepage-latest-mods', now()->addMinutes(5), function () {
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at'])
|
||||
->withTotalDownloads()
|
||||
->with(['latestVersion', 'latestVersion.sptVersion', 'users:id,name'])
|
||||
->whereHas('latestVersion')
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'created_at', 'downloads'])
|
||||
->with([
|
||||
'latestVersion',
|
||||
'latestVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->latest()
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
private function fetchUpdatedMods(): Collection
|
||||
{
|
||||
return Cache::remember('homepage-updated-mods', now()->addMinutes(5), function () {
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured'])
|
||||
->withTotalDownloads()
|
||||
->with(['lastUpdatedVersion', 'lastUpdatedVersion.sptVersion', 'users:id,name'])
|
||||
->whereHas('lastUpdatedVersion')
|
||||
->orderByDesc(
|
||||
ModVersion::select('updated_at')
|
||||
->whereColumn('mod_id', 'mods.id')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1)
|
||||
return Mod::select(['id', 'name', 'slug', 'teaser', 'thumbnail', 'featured', 'downloads'])
|
||||
->with([
|
||||
'lastUpdatedVersion',
|
||||
'lastUpdatedVersion.latestSptVersion:id,version,color_class',
|
||||
'users:id,name',
|
||||
'license:id,name,link',
|
||||
])
|
||||
->joinSub(
|
||||
ModVersion::select('mod_id', DB::raw('MAX(updated_at) as latest_updated_at'))->groupBy('mod_id'),
|
||||
'latest_versions',
|
||||
'mods.id',
|
||||
'=',
|
||||
'latest_versions.mod_id'
|
||||
)
|
||||
->orderByDesc('latest_versions.latest_updated_at')
|
||||
->limit(6)
|
||||
->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
@ -80,17 +85,17 @@ class ModListSection extends Component
|
||||
{
|
||||
return [
|
||||
[
|
||||
'title' => 'Featured Mods',
|
||||
'title' => __('Featured Mods'),
|
||||
'mods' => $this->modsFeatured,
|
||||
'versionScope' => 'latestVersion',
|
||||
],
|
||||
[
|
||||
'title' => 'Newest Mods',
|
||||
'title' => __('Newest Mods'),
|
||||
'mods' => $this->modsLatest,
|
||||
'versionScope' => 'latestVersion',
|
||||
],
|
||||
[
|
||||
'title' => 'Recently Updated Mods',
|
||||
'title' => __('Recently Updated Mods'),
|
||||
'mods' => $this->modsUpdated,
|
||||
'versionScope' => 'lastUpdatedVersion',
|
||||
],
|
||||
|
842
composer.lock
generated
842
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
{"admin":{"listen":"localhost:2019"},"apps":{"frankenphp":{"workers":[{"file_name":"/var/www/html/public/frankenphp-worker.php"}]},"http":{"servers":{"srv0":{"automatic_https":{"disable_redirects":true},"listen":[":443"],"logs":{"logger_names":{"localhost":["log0"]}},"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"vars","root":"/var/www/html/public"},{"encodings":{"br":{},"gzip":{},"zstd":{}},"handler":"encode","prefer":["zstd","br","gzip"]}]},{"handle":[{"handler":"static_response","headers":{"Location":["{http.request.orig_uri.path}/"]},"status_code":308}],"match":[{"file":{"try_files":["{http.request.uri.path}/frankenphp-worker.php"]},"not":[{"path":["*/"]}]}]},{"handle":[{"handler":"rewrite","uri":"{http.matchers.file.relative}"}],"match":[{"file":{"split_path":[".php"],"try_files":["{http.request.uri.path}","{http.request.uri.path}/frankenphp-worker.php","frankenphp-worker.php"]}}]},{"handle":[{"handler":"php","resolve_root_symlink":true,"split_path":[".php"]}],"match":[{"path":["*.php"]}]},{"handle":[{"handler":"file_server"}]}]}]}]}],"match":[{"host":["localhost"]}],"terminal":true}]}}}},"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"encoder":{"fields":{"uri":{"actions":[{"parameter":"authorization","type":"replace","value":"REDACTED"}],"filter":"query"}},"format":"filter","wrap":{"format":"json"}},"include":["http.log.access.log0"],"level":"INFO"}}}}
|
@ -12,13 +12,10 @@ use Laravel\Octane\Events\WorkerErrorOccurred;
|
||||
use Laravel\Octane\Events\WorkerStarting;
|
||||
use Laravel\Octane\Events\WorkerStopping;
|
||||
use Laravel\Octane\Listeners\CloseMonologHandlers;
|
||||
use Laravel\Octane\Listeners\CollectGarbage;
|
||||
use Laravel\Octane\Listeners\DisconnectFromDatabases;
|
||||
use Laravel\Octane\Listeners\EnsureUploadedFilesAreValid;
|
||||
use Laravel\Octane\Listeners\EnsureUploadedFilesCanBeMoved;
|
||||
use Laravel\Octane\Listeners\FlushOnce;
|
||||
use Laravel\Octane\Listeners\FlushTemporaryContainerInstances;
|
||||
use Laravel\Octane\Listeners\FlushUploadedFiles;
|
||||
use Laravel\Octane\Listeners\ReportException;
|
||||
use Laravel\Octane\Listeners\StopWorkerIfNecessary;
|
||||
use Laravel\Octane\Octane;
|
||||
@ -38,7 +35,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'server' => env('OCTANE_SERVER', 'swoole'),
|
||||
'server' => env('OCTANE_SERVER', 'frankenphp'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -35,4 +35,9 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'gitea' => [
|
||||
'domain' => env('GITEA_DOMAIN', ''),
|
||||
'token' => env('GITEA_TOKEN', ''),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -16,8 +16,8 @@ class ModDependencyFactory extends Factory
|
||||
{
|
||||
return [
|
||||
'mod_version_id' => ModVersion::factory(),
|
||||
'dependency_mod_id' => Mod::factory(),
|
||||
'version_constraint' => '^'.$this->faker->numerify('#.#.#'),
|
||||
'dependent_mod_id' => Mod::factory(),
|
||||
'constraint' => '*',
|
||||
'created_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||
'updated_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||
];
|
||||
|
@ -19,7 +19,10 @@ class ModVersionFactory extends Factory
|
||||
'version' => fake()->numerify('#.#.#'),
|
||||
'description' => fake()->text(),
|
||||
'link' => fake()->url(),
|
||||
'spt_version_id' => SptVersion::factory(),
|
||||
|
||||
// Unless a custom constraint is provided, this will also generate the required SPT versions.
|
||||
'spt_version_constraint' => $this->faker->randomElement(['^1.0', '^2.0', '>=3.0', '<4.0']),
|
||||
|
||||
'virus_total_link' => fake()->url(),
|
||||
'downloads' => fake()->randomNumber(),
|
||||
'published_at' => Carbon::now()->subDays(rand(0, 365))->subHours(rand(0, 23)),
|
||||
@ -28,6 +31,46 @@ class ModVersionFactory extends Factory
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the model factory.
|
||||
*/
|
||||
public function configure(): ModVersionFactory
|
||||
{
|
||||
return $this->afterCreating(function (ModVersion $modVersion) {
|
||||
$this->ensureSptVersionsExist($modVersion); // Create SPT Versions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the required SPT versions exist and are associated with the mod version.
|
||||
*/
|
||||
protected function ensureSptVersionsExist(ModVersion $modVersion): void
|
||||
{
|
||||
$constraint = $modVersion->spt_version_constraint;
|
||||
|
||||
$requiredVersions = match ($constraint) {
|
||||
'^1.0' => ['1.0.0', '1.1.0', '1.2.0'],
|
||||
'^2.0' => ['2.0.0', '2.1.0'],
|
||||
'>=3.0' => ['3.0.0', '3.1.0', '3.2.0', '4.0.0'],
|
||||
'<4.0' => ['1.0.0', '2.0.0', '3.0.0'],
|
||||
default => [],
|
||||
};
|
||||
|
||||
// If the version is anything but the default, no SPT versions are created.
|
||||
if (! $requiredVersions) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($requiredVersions as $version) {
|
||||
SptVersion::firstOrCreate(['version' => $version], [
|
||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||
'link' => $this->faker->url,
|
||||
]);
|
||||
}
|
||||
|
||||
$modVersion->sptVersions()->sync(SptVersion::whereIn('version', $requiredVersions)->pluck('id')->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the mod version should be disabled.
|
||||
*/
|
||||
|
@ -13,8 +13,9 @@ class SptVersionFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'version' => $this->faker->numerify('SPT 1.#.#'),
|
||||
'version' => $this->faker->numerify('#.#.#'),
|
||||
'color_class' => $this->faker->randomElement(['red', 'green', 'emerald', 'lime', 'yellow', 'grey']),
|
||||
'link' => $this->faker->url,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
];
|
||||
|
@ -26,6 +26,7 @@ return new class extends Migration
|
||||
->constrained('licenses')
|
||||
->nullOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->unsignedBigInteger('downloads')->default(0);
|
||||
$table->string('source_code_link');
|
||||
$table->boolean('featured')->default(false);
|
||||
$table->boolean('contains_ai_content')->default(false);
|
||||
@ -35,7 +36,9 @@ return new class extends Migration
|
||||
$table->timestamp('published_at')->nullable()->default(null);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['deleted_at', 'disabled'], 'mods_show_index');
|
||||
$table->index(['slug']);
|
||||
$table->index(['featured']);
|
||||
$table->index(['deleted_at', 'disabled', 'published_at'], 'mods_filtering_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,18 @@ return new class extends Migration
|
||||
->default(null)
|
||||
->unique();
|
||||
$table->string('version');
|
||||
$table->unsignedInteger('version_major');
|
||||
$table->unsignedInteger('version_minor');
|
||||
$table->unsignedInteger('version_patch');
|
||||
$table->string('version_pre_release');
|
||||
$table->unsignedInteger('mod_count')->default(0);
|
||||
$table->string('link');
|
||||
$table->string('color_class');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['version', 'deleted_at'], 'spt_versions_filtering_index');
|
||||
$table->index(['version', 'deleted_at', 'id'], 'spt_versions_filtering_index');
|
||||
$table->index(['version_major', 'version_minor', 'version_patch', 'version_pre_release', 'deleted_at'], 'spt_versions_lookup_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@ -23,12 +22,7 @@ return new class extends Migration
|
||||
$table->string('version');
|
||||
$table->longText('description');
|
||||
$table->string('link');
|
||||
$table->foreignIdFor(SptVersion::class)
|
||||
->nullable()
|
||||
->default(null)
|
||||
->constrained('spt_versions')
|
||||
->nullOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->string('spt_version_constraint');
|
||||
$table->string('virus_total_link');
|
||||
$table->unsignedBigInteger('downloads');
|
||||
$table->boolean('disabled')->default(false);
|
||||
@ -36,7 +30,9 @@ return new class extends Migration
|
||||
$table->timestamp('published_at')->nullable()->default(null);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'version'], 'mod_versions_filtering_index');
|
||||
$table->index(['version']);
|
||||
$table->index(['mod_id', 'deleted_at', 'disabled', 'published_at'], 'mod_versions_filtering_index');
|
||||
$table->index(['id', 'deleted_at'], 'mod_versions_id_deleted_at_index');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
<?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_version_spt_version', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('spt_version_id')->constrained('spt_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_version_id', 'spt_version_id'], 'mod_version_spt_version_index');
|
||||
$table->index(['spt_version_id', 'mod_version_id'], 'spt_version_mod_version_index');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mod_version_spt_version');
|
||||
}
|
||||
};
|
@ -10,23 +10,12 @@ return new class extends Migration
|
||||
{
|
||||
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->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('dependent_mod_id')->constrained('mods')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->string('constraint');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['mod_version_id', 'dependency_mod_id', 'version_constraint'], 'mod_dependencies_unique');
|
||||
$table->index(['mod_version_id', 'dependent_mod_id']);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
<?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_resolved_dependencies', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('dependency_id')->constrained('mod_dependencies')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->foreignId('resolved_mod_version_id')->constrained('mod_versions')->cascadeOnDelete()->cascadeOnUpdate();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['mod_version_id', 'dependency_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mod_resolved_dependencies');
|
||||
}
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Exceptions\CircularDependencyException;
|
||||
use App\Models\License;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
@ -20,7 +19,7 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
// Create a few SPT versions.
|
||||
$spt_versions = SptVersion::factory(10)->create();
|
||||
$spt_versions = SptVersion::factory(30)->create();
|
||||
|
||||
// Create some code licenses.
|
||||
$licenses = License::factory(10)->create();
|
||||
@ -39,40 +38,26 @@ class DatabaseSeeder extends Seeder
|
||||
// Add 100 users.
|
||||
$users = User::factory(100)->create();
|
||||
|
||||
// Add 200 mods, assigning them to the users we just created.
|
||||
// Add 300 mods, assigning them to the users we just created.
|
||||
$allUsers = $users->merge([$administrator, $moderator]);
|
||||
$mods = Mod::factory(200)->recycle([$licenses])->create();
|
||||
$mods = Mod::factory(300)->recycle([$licenses])->create();
|
||||
foreach ($mods as $mod) {
|
||||
$userIds = $allUsers->random(rand(1, 3))->pluck('id')->toArray();
|
||||
$mod->users()->attach($userIds);
|
||||
}
|
||||
|
||||
// Add 1000 mod versions, assigning them to the mods we just created.
|
||||
$modVersions = ModVersion::factory(1000)->recycle([$mods, $spt_versions])->create();
|
||||
// Add 3000 mod versions, assigning them to the mods we just created.
|
||||
$modVersions = ModVersion::factory(3000)->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);
|
||||
$dependencyMods = $mods->random(rand(1, 3)); // 1 to 3 dependencies
|
||||
foreach ($dependencyMods as $dependencyMod) {
|
||||
try {
|
||||
ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create([
|
||||
'version_constraint' => $this->generateVersionConstraint(),
|
||||
]);
|
||||
} catch (CircularDependencyException $e) {
|
||||
continue;
|
||||
ModDependency::factory()->recycle([$modVersion, $dependencyMod])->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,9 @@ services:
|
||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||
IGNITION_LOCAL_SITES_PATH: '${PWD}'
|
||||
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=80 --watch"
|
||||
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --host=localhost --port=80 --admin-port=2019 --watch"
|
||||
XDG_CONFIG_HOME: /var/www/html/config
|
||||
XDG_DATA_HOME: /var/www/html/data
|
||||
volumes:
|
||||
- '.:/var/www/html'
|
||||
networks:
|
||||
|
252
package-lock.json
generated
252
package-lock.json
generated
@ -556,9 +556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz",
|
||||
"integrity": "sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz",
|
||||
"integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -570,9 +570,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.2.tgz",
|
||||
"integrity": "sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz",
|
||||
"integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -584,9 +584,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.2.tgz",
|
||||
"integrity": "sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz",
|
||||
"integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -598,9 +598,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.2.tgz",
|
||||
"integrity": "sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz",
|
||||
"integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -612,9 +612,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.2.tgz",
|
||||
"integrity": "sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz",
|
||||
"integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -626,9 +626,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.2.tgz",
|
||||
"integrity": "sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz",
|
||||
"integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -640,9 +640,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.2.tgz",
|
||||
"integrity": "sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -654,9 +654,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.2.tgz",
|
||||
"integrity": "sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz",
|
||||
"integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -668,9 +668,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.2.tgz",
|
||||
"integrity": "sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -682,9 +682,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.2.tgz",
|
||||
"integrity": "sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -696,9 +696,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.2.tgz",
|
||||
"integrity": "sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -710,9 +710,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.2.tgz",
|
||||
"integrity": "sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz",
|
||||
"integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -724,9 +724,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.2.tgz",
|
||||
"integrity": "sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz",
|
||||
"integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -738,9 +738,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.2.tgz",
|
||||
"integrity": "sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz",
|
||||
"integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -752,9 +752,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.2.tgz",
|
||||
"integrity": "sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz",
|
||||
"integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -766,9 +766,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.2.tgz",
|
||||
"integrity": "sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz",
|
||||
"integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -780,22 +780,22 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
|
||||
"integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.8.tgz",
|
||||
"integrity": "sha512-DJs7B7NPD0JH7BVvdHWNviWmunlFhuEkz7FyFxE4japOWYMLl9b1D6+Z9mivJJPWr6AEbmlPqgiFRyLwFB1SgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mini-svg-data-uri": "^1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
|
||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz",
|
||||
"integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==",
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
|
||||
"integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -805,7 +805,7 @@
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
@ -877,9 +877,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
|
||||
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -897,11 +897,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.23.0",
|
||||
"caniuse-lite": "^1.0.30001599",
|
||||
"browserslist": "^4.23.3",
|
||||
"caniuse-lite": "^1.0.30001646",
|
||||
"fraction.js": "^4.3.7",
|
||||
"normalize-range": "^0.1.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
@ -915,9 +915,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz",
|
||||
"integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==",
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.6.tgz",
|
||||
"integrity": "sha512-Ekur6XDwhnJ5RgOCaxFnXyqlPALI3rVeukZMwOdfghW7/wGz784BYKiQq+QD8NPcr91KRo30KfHOchyijwWw7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1013,9 +1013,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001646",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz",
|
||||
"integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==",
|
||||
"version": "1.0.30001655",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz",
|
||||
"integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1161,9 +1161,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz",
|
||||
"integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==",
|
||||
"version": "1.5.13",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz",
|
||||
"integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@ -1214,9 +1214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -1294,9 +1294,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
|
||||
"integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@ -1425,9 +1425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
|
||||
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1592,9 +1592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1824,9 +1824,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.40",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
|
||||
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1966,9 +1966,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
|
||||
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2017,9 +2017,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz",
|
||||
"integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==",
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz",
|
||||
"integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@ -2037,6 +2037,7 @@
|
||||
"prettier-plugin-import-sort": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-multiline-arrays": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
@ -2074,6 +2075,9 @@
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-multiline-arrays": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
@ -2172,9 +2176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.2.tgz",
|
||||
"integrity": "sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==",
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz",
|
||||
"integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2188,22 +2192,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.19.2",
|
||||
"@rollup/rollup-android-arm64": "4.19.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.19.2",
|
||||
"@rollup/rollup-darwin-x64": "4.19.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.19.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.19.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.19.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.19.2",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.19.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.19.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.19.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.19.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.19.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.19.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.19.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.19.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.21.2",
|
||||
"@rollup/rollup-android-arm64": "4.21.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.21.2",
|
||||
"@rollup/rollup-darwin-x64": "4.21.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.21.2",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.21.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.21.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.21.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.21.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.21.2",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@ -2424,9 +2428,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
|
||||
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2475,9 +2479,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
|
||||
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2570,15 +2574,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
|
||||
"integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
||||
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.39",
|
||||
"rollup": "^4.13.0"
|
||||
"postcss": "^8.4.41",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@ -2597,6 +2601,7 @@
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
@ -2614,6 +2619,9 @@
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
|
12
phpstan.neon
12
phpstan.neon
@ -1,9 +1,13 @@
|
||||
includes:
|
||||
- ./vendor/larastan/larastan/extension.neon
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- app/
|
||||
- app
|
||||
- bootstrap
|
||||
- config
|
||||
- database
|
||||
- lang
|
||||
- routes
|
||||
excludePaths:
|
||||
analyseAndScan:
|
||||
- tests/
|
||||
level: 4
|
||||
- tests/**/*
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
function i({initialHeight:t}){return{height:t+"rem",init:function(){this.setInitialHeight(),this.setUpResizeObserver()},setInitialHeight:function(){this.height=t+"rem",!(this.$el.scrollHeight<=0)&&(this.$el.style.height=this.height)},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.height!==e&&(this.height=e,this.$el.style.height=this.height)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.height=this.$el.style.height}).observe(this.$el)}}}export{i as default};
|
||||
function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init:function(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight:function(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize:function(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver:function(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
|
||||
|
@ -6,7 +6,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
main a:not(.mod-list-component):not(.tab) {
|
||||
main a:not(.mod-list-component):not(.tab):not([role="menuitem"]) {
|
||||
@apply underline text-gray-800 hover:text-black dark:text-gray-200 dark:hover:text-white;
|
||||
}
|
||||
|
||||
@ -87,3 +87,24 @@ main a:not(.mod-list-component):not(.tab) {
|
||||
@apply my-2 ml-7 text-gray-800 dark:text-gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
--f: .5em;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
line-height: 1.5;
|
||||
padding-inline: 1lh;
|
||||
padding-bottom: var(--f);
|
||||
border-image: conic-gradient(#0008 0 0) 51%/var(--f);
|
||||
clip-path: polygon(100% calc(100% - var(--f)), 100% 100%, calc(100% - var(--f)) calc(100% - var(--f)), var(--f) calc(100% - var(--f)), 0 100%, 0 calc(100% - var(--f)), 999px calc(100% - var(--f) - 999px), calc(100% - 999px) calc(100% - var(--f) - 999px));
|
||||
transform: translate(calc((cos(45deg) - 1) * 100%), -100%) rotate(-45deg);
|
||||
transform-origin: 100% 100%;
|
||||
background-color: #0e7490;
|
||||
}
|
||||
|
6
resources/views/components/filter-checkbox.blade.php
Normal file
6
resources/views/components/filter-checkbox.blade.php
Normal file
@ -0,0 +1,6 @@
|
||||
@props(['id', 'name', 'value'])
|
||||
|
||||
<div class="flex items-center text-base sm:text-sm">
|
||||
<input id="{{ $id }}" wire:model.live="{{ $name }}" value="{{ $value }}" type="checkbox" class="cursor-pointer h-4 w-4 flex-shrink-0 rounded border-gray-300 text-gray-600 focus:ring-gray-500">
|
||||
<label for="{{ $id }}" class="cursor-pointer ml-3 min-w-0 inline-flex text-gray-600 dark:text-gray-300">{{ $slot }}</label>
|
||||
</div>
|
6
resources/views/components/filter-radio.blade.php
Normal file
6
resources/views/components/filter-radio.blade.php
Normal file
@ -0,0 +1,6 @@
|
||||
@props(['id', 'name', 'value'])
|
||||
|
||||
<div class="flex items-center text-base sm:text-sm">
|
||||
<input id="{{ $id }}" wire:model.live="{{ $name }}" value="{{ $value }}" type="radio" class="h-4 w-4 flex-shrink-0 rounded border-gray-300 text-gray-600 focus:ring-gray-500">
|
||||
<label for="{{ $id }}" class="cursor-pointer ml-3 min-w-0 inline-flex text-gray-600 dark:text-gray-300">{{ $slot }}</label>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
@props(['order', 'currentOrder'])
|
||||
|
||||
<a href="#{{ $order }}"
|
||||
@click.prevent="$wire.set('order', '{{ $order }}')"
|
||||
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 {{ $order === $currentOrder ? "font-bold text-slate-900 dark:text-white" : "" }}"
|
||||
role="menuitem" tabindex="-1">
|
||||
{{ $slot }}
|
||||
</a>
|
34
resources/views/components/mod-card.blade.php
Normal file
34
resources/views/components/mod-card.blade.php
Normal file
@ -0,0 +1,34 @@
|
||||
@props(['mod', 'versionScope' => 'latestVersion'])
|
||||
|
||||
<a href="/mod/{{ $mod->id }}/{{ $mod->slug }}" class="mod-list-component relative mx-auto w-full max-w-md md:max-w-2xl">
|
||||
@if ($mod->featured && !request()->routeIs('home'))
|
||||
<div class="ribbon z-10">{{ __('Featured!') }}</div>
|
||||
@endif
|
||||
<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:shrink-0 overflow-hidden">
|
||||
@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/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
|
||||
<img src="{{ $mod->thumbnailUrl }}" alt="{{ $mod->name }}" class="h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-between p-5">
|
||||
<div class="pb-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>
|
||||
<span class="badge-version {{ $mod->{$versionScope}->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->{$versionScope}->latestSptVersion->first()->version_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||
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>
|
||||
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
@ -1,6 +1,10 @@
|
||||
@props(['mods'])
|
||||
@props(['mods', 'versionScope', 'title'])
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 pt-16 sm:px-6 lg:px-8">
|
||||
{{--
|
||||
TODO: The button-link should be dynamic based on the versionScope. Eg. Featured `View All` button should take
|
||||
the user to the mods page with the `featured` query parameter set.
|
||||
--}}
|
||||
<x-page-content-title :title="$title" button-text="View All" button-link="/mods" />
|
||||
<x-mod-list :mods="$mods" :versionScope="$versionScope" />
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p {{ $attributes->class(['text-slate-700 dark:text-gray-300 text-sm']) }}>
|
||||
<span title="{{ __('Exactly') }} {{ $mod->total_downloads }}">{{ Number::downloads($mod->total_downloads) }} downloads</span>
|
||||
<span title="{{ __('Exactly') }} {{ $mod->downloads }}">{{ Number::downloads($mod->downloads) }} downloads</span>
|
||||
@if(!is_null($mod->created_at))
|
||||
<span>
|
||||
— Created
|
||||
|
@ -3,35 +3,7 @@
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
@foreach ($mods as $mod)
|
||||
@if ($mod->{$versionScope})
|
||||
<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 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:shrink-0 overflow-hidden">
|
||||
@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/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
|
||||
<img src="{{ $mod->thumbnailUrl }}" alt="{{ $mod->name }}" class="h-48 w-full object-cover md:h-full md:w-48 transform group-hover:scale-110 transition-all duration-200">
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col w-full justify-between p-5">
|
||||
<div class="pb-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>
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||
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>
|
||||
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<x-mod-card :mod="$mod" :versionScope="$versionScope"/>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
144
resources/views/livewire/mod/index.blade.php
Normal file
144
resources/views/livewire/mod/index.blade.php
Normal file
@ -0,0 +1,144 @@
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-8 sm:px-6 lg:px-8 bg-white dark:bg-gray-900 overflow-hidden shadow-xl dark:shadow-gray-900 rounded-none sm:rounded-lg">
|
||||
<h1 class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-200">{{ __('Mods') }}</h1>
|
||||
<p class="mt-4 text-base text-slate-500 dark:text-gray-300">{!! __('Explore an enhanced <abbr title="Single Player Tarkov">SPT</abbr> experience with the mods available below. Check out the featured mods for a tailored solo-survival game with maximum immersion.') !!}</p>
|
||||
|
||||
<section x-data="{ isFilterOpen: false }"
|
||||
@click.away="isFilterOpen = false"
|
||||
aria-labelledby="filter-heading"
|
||||
class="my-8 grid items-center border-t border-gray-300 dark:border-gray-700">
|
||||
<h2 id="filter-heading" class="sr-only">{{ __('Filters') }}</h2>
|
||||
<div class="relative col-start-1 row-start-1 py-4 border-b border-gray-300 dark:border-gray-700">
|
||||
<div class="mx-auto flex max-w-7xl space-x-6 divide-x divide-gray-300 dark:divide-gray-700 px-4 text-sm sm:px-6 lg:px-8">
|
||||
|
||||
<button type="button" @click="isFilterOpen = !isFilterOpen" class="group flex items-center font-medium text-gray-700 dark:text-gray-300" aria-controls="disclosure-1" aria-expanded="false">
|
||||
<svg class="mr-2 h-5 w-5 flex-none text-gray-400 group-hover:text-gray-500 dark:text-gray-600" aria-hidden="true" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M2.628 1.601C5.028 1.206 7.49 1 10 1s4.973.206 7.372.601a.75.75 0 01.628.74v2.288a2.25 2.25 0 01-.659 1.59l-4.682 4.683a2.25 2.25 0 00-.659 1.59v3.037c0 .684-.31 1.33-.844 1.757l-1.937 1.55A.75.75 0 018 18.25v-5.757a2.25 2.25 0 00-.659-1.591L2.659 6.22A2.25 2.25 0 012 4.629V2.34a.75.75 0 01.628-.74z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{ $this->filterCount }} {{ __('Filters') }}
|
||||
</button>
|
||||
|
||||
<search class="relative group pl-6">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-8 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="h-5 w-5 text-gray-400">
|
||||
<path fill-rule="evenodd" d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<input wire:model.live="query" class="w-full rounded-md border-0 bg-white dark:bg-gray-700 py-1.5 pl-10 pr-3 text-gray-900 dark:text-gray-300 ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 dark:placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-gray-600 dark:focus:bg-gray-200 dark:focus:text-black dark:focus:ring-0 sm:text-sm sm:leading-6" placeholder="{{ __('Search Mods') }}" />
|
||||
</search>
|
||||
|
||||
<button @click="$wire.call('resetFilters')" type="button" class="pl-6 text-gray-500 dark:text-gray-300">{{ __('Reset Filters') }}</button>
|
||||
|
||||
<div wire:loading.flex>
|
||||
<p class="pl-6 flex items-center font-medium text-gray-700 dark:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4 fill-cyan-600 dark:fill-cyan-600 motion-safe:animate-spin">
|
||||
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" />
|
||||
<path d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" />
|
||||
</svg>
|
||||
<span class="pl-1.5">{{ __('Loading...') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-cloak
|
||||
x-show="isFilterOpen"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform -translate-y-10"
|
||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 transform translate-y-0"
|
||||
x-transition:leave-end="opacity-0 transform -translate-y-10"
|
||||
id="disclosure-1"
|
||||
class="py-10 border-b border-gray-300 dark:border-gray-700">
|
||||
<div class="mx-auto grid max-w-7xl grid-cols-2 gap-x-4 px-4 text-sm sm:px-6 md:gap-x-6 lg:px-8">
|
||||
<div class="grid auto-rows-min grid-cols-1 gap-y-10 md:grid-cols-2 md:gap-x-6">
|
||||
@php
|
||||
$totalVersions = count($availableSptVersions);
|
||||
$half = ceil($totalVersions / 2);
|
||||
@endphp
|
||||
<fieldset>
|
||||
<legend class="block font-medium text-gray-900 dark:text-gray-100">{{ __('SPT Versions') }}</legend>
|
||||
<div class="space-y-6 pt-6 sm:space-y-4 sm:pt-4">
|
||||
@foreach ($availableSptVersions as $index => $version)
|
||||
@if ($index < $half)
|
||||
<x-filter-checkbox id="sptVersions-{{ $index }}" name="sptVersions" value="{{ $version->version }}">{{ $version->version }}</x-filter-checkbox>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="block font-medium text-gray-900 dark:text-gray-100"> </legend>
|
||||
<div class="space-y-6 pt-6 sm:space-y-4 sm:pt-4">
|
||||
@foreach ($availableSptVersions as $index => $version)
|
||||
@if ($index >= $half)
|
||||
<x-filter-checkbox id="sptVersions-{{ $index }}" name="sptVersions" value="{{ $version->version }}">{{ $version->version }}</x-filter-checkbox>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="grid auto-rows-min grid-cols-1 gap-y-10 md:grid-cols-2 md:gap-x-6">
|
||||
<fieldset>
|
||||
<legend class="block font-medium text-gray-900 dark:text-gray-100">{{ __('Featured') }}</legend>
|
||||
<div class="space-y-6 pt-6 sm:space-y-4 sm:pt-4">
|
||||
<x-filter-radio id="featured-0" name="featured" value="include">{{ __('Include') }}</x-filter-radio>
|
||||
<x-filter-radio id="featured-1" name="featured" value="exclude">{{ __('Exclude') }}</x-filter-radio>
|
||||
<x-filter-radio id="featured-2" name="featured" value="only">{{ __('Only') }}</x-filter-radio>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-start-1 row-start-1 py-4">
|
||||
<div class="mx-auto flex max-w-7xl justify-end px-4 sm:px-6 lg:px-8">
|
||||
<div class="relative inline-block" x-data="{ isSortOpen: false }" @click.away="isSortOpen = false">
|
||||
<div class="flex">
|
||||
<button type="button" @click="isSortOpen = !isSortOpen" class="group inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100" id="menu-button" :aria-expanded="isSortOpen.toString()" aria-haspopup="true">
|
||||
{{ __('Sort') }}
|
||||
<svg class="-mr-1 ml-1 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div x-cloak
|
||||
x-show="isSortOpen"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
class="absolute top-7 right-0 z-10 flex w-full min-w-[12rem] flex-col divide-y divide-slate-300 overflow-hidden rounded-xl border border-gray-300 bg-gray-100 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800"
|
||||
role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
|
||||
<div class="flex flex-col py-1.5">
|
||||
<x-filter-sort-menu-item order="created" :currentOrder="$order">{{ __('Newest') }}</x-filter-sort-menu-item>
|
||||
<x-filter-sort-menu-item order="updated" :currentOrder="$order">{{ __('Recently Updated') }}</x-filter-sort-menu-item>
|
||||
<x-filter-sort-menu-item order="downloaded" :currentOrder="$order">{{ __('Most Downloaded') }}</x-filter-sort-menu-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ $mods->onEachSide(1)->links() }}
|
||||
|
||||
{{-- Mod Listing --}}
|
||||
@if ($mods->isNotEmpty())
|
||||
<div class="my-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
@foreach ($mods as $mod)
|
||||
<x-mod-card :mod="$mod" />
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center text-gray-700 dark:text-gray-300">
|
||||
<p>{{ __('There were no mods found with those filters applied. ') }}</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mx-auto">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.182 16.318A4.486 4.486 0 0 0 12.016 15a4.486 4.486 0 0 0-3.198 1.318M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75Zm-.375 0h.008v.015h-.008V9.75Z" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{ $mods->onEachSide(1)->links() }}
|
||||
</div>
|
||||
</div>
|
3
resources/views/mod/index.blade.php
Normal file
3
resources/views/mod/index.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<x-app-layout>
|
||||
@livewire('mod.index')
|
||||
</x-app-layout>
|
@ -10,7 +10,10 @@
|
||||
<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="relative 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">
|
||||
@if ($mod->featured)
|
||||
<div class="ribbon z-10">{{ __('Featured!') }}</div>
|
||||
@endif
|
||||
<div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<div class="grow-0 shrink-0 flex justify-center items-center">
|
||||
@if (empty($mod->thumbnail))
|
||||
@ -25,7 +28,7 @@
|
||||
<h2 class="pb-1 sm:pb-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{{ $mod->name }}
|
||||
<span class="font-light text-nowrap text-gray-700 dark:text-gray-400">
|
||||
{{ $latestVersion->version }}
|
||||
{{ $mod->latestVersion->version }}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
@ -35,10 +38,10 @@
|
||||
<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 title="{{ __('Exactly') }} {{ $mod->downloads }}">{{ Number::downloads($mod->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 class="badge-version {{ $mod->latestVersion->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $mod->latestVersion->latestSptVersion->first()->version_formatted }} {{ __('Compatible') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@ -51,8 +54,8 @@
|
||||
</div>
|
||||
|
||||
{{-- Mobile Download Button --}}
|
||||
<a href="{{ $latestVersion->link }}" class="block lg:hidden">
|
||||
<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 href="{{ $mod->latestVersion->link }}" class="block lg:hidden">
|
||||
<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') }} ({{ $mod->latestVersion->version }})</button>
|
||||
</a>
|
||||
|
||||
{{-- Tabs --}}
|
||||
@ -105,8 +108,8 @@
|
||||
<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 class="badge-version {{ $version->latestSptVersion->first()->color_class }} inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-nowrap">
|
||||
{{ $version->latestSptVersion->first()->version_formatted }}
|
||||
</span>
|
||||
<a href="{{ $version->virus_total_link }}">{{__('Virus Total Results')}}</a>
|
||||
</div>
|
||||
@ -114,20 +117,18 @@
|
||||
<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))
|
||||
|
||||
{{-- Display latest resolved dependencies --}}
|
||||
@if ($version->latestResolvedDependencies->isNotEmpty())
|
||||
<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
|
||||
@foreach ($version->latestResolvedDependencies as $resolvedDependency)
|
||||
<a href="{{ $resolvedDependency->mod->detailUrl() }}">{{ $resolvedDependency->mod->name }} ({{ $resolvedDependency->version }})</a>@if (!$loop->last), @endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-3 user-markdown">
|
||||
<div class="py-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>
|
||||
@ -146,8 +147,8 @@
|
||||
<div class="col-span-1 flex flex-col gap-6">
|
||||
|
||||
{{-- 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 href="{{ $mod->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') }} ({{ $mod->latestVersion->version }})</button>
|
||||
</a>
|
||||
|
||||
{{-- Additional Mod Details --}}
|
||||
@ -174,21 +175,21 @@
|
||||
</p>
|
||||
</li>
|
||||
@endif
|
||||
@if ($latestVersion->virus_total_link)
|
||||
@if ($mod->latestVersion->virus_total_link)
|
||||
<li class="px-4 py-4 sm:px-0">
|
||||
<h3>{{ __('Latest Version VirusTotal Result') }}</h3>
|
||||
<p class="truncate">
|
||||
<a href="{{ $latestVersion->virus_total_link }}" title="{{ $latestVersion->virus_total_link }}" target="_blank">
|
||||
{{ $latestVersion->virus_total_link }}
|
||||
<a href="{{ $mod->latestVersion->virus_total_link }}" title="{{ $mod->latestVersion->virus_total_link }}" target="_blank">
|
||||
{{ $mod->latestVersion->virus_total_link }}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
@endif
|
||||
@if ($latestVersion->dependencies->isNotEmpty() && $latestVersion->dependencies->contains(fn($dependency) => $dependency->resolvedVersion?->mod))
|
||||
@if ($mod->latestVersion->dependencies->isNotEmpty() && $mod->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)
|
||||
@foreach ($mod->latestVersion->dependencies as $dependency)
|
||||
<a href="{{ $dependency->resolvedVersion->mod->detailUrl() }}">
|
||||
{{ $dependency->resolvedVersion->mod->name }} ({{ $dependency->resolvedVersion->version }})
|
||||
</a><br />
|
||||
|
@ -9,9 +9,7 @@
|
||||
</div>
|
||||
<div class="hidden lg:ml-6 lg:block">
|
||||
<div class="flex space-x-4">
|
||||
@auth
|
||||
<x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">{{ __('Dashboard') }}</x-nav-link>
|
||||
@endauth
|
||||
<x-nav-link href="{{ route('mods') }}" :active="request()->routeIs('mods')">{{ __('Mods') }}</x-nav-link>
|
||||
{{-- additional menu links here --}}
|
||||
</div>
|
||||
</div>
|
||||
@ -54,7 +52,15 @@
|
||||
<span class="sr-only">{{ __('Open user menu') }}</span>
|
||||
<img class="h-8 w-8 rounded-full" src="{{ auth()->user()->profile_photo_url }}" alt="{{ auth()->user()->name }}">
|
||||
</button>
|
||||
<div x-cloak x-show="profileDropdownOpen || openedWithKeyboard" x-transition x-trap="openedWithKeyboard" @click.outside="profileDropdownOpen = false, openedWithKeyboard = false" @keydown.down.prevent="$focus.wrap().next()" @keydown.up.prevent="$focus.wrap().previous()" class="absolute top-11 right-0 z-10 flex w-full min-w-[12rem] flex-col divide-y divide-slate-300 overflow-hidden rounded-xl border border-slate-300 bg-slate-100 dark:divide-slate-700 dark:border-slate-700 dark:bg-slate-800" role="menu">
|
||||
<div x-cloak
|
||||
x-show="profileDropdownOpen || openedWithKeyboard"
|
||||
x-transition
|
||||
x-trap="openedWithKeyboard"
|
||||
@click.outside="profileDropdownOpen = false, openedWithKeyboard = false"
|
||||
@keydown.down.prevent="$focus.wrap().next()"
|
||||
@keydown.up.prevent="$focus.wrap().previous()"
|
||||
class="absolute top-11 right-0 z-10 flex w-full min-w-[12rem] flex-col divide-y divide-slate-300 overflow-hidden rounded-xl border border-gray-300 bg-gray-100 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800"
|
||||
role="menu">
|
||||
<div class="flex flex-col py-1.5">
|
||||
<a href="{{ route('dashboard') }}" 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" fill="currentColor" class="w-4 h-4">
|
||||
|
@ -1,8 +1,14 @@
|
||||
<?php
|
||||
|
||||
use App\Console\Commands\ImportHub;
|
||||
use App\Console\Commands\ImportHubCommand;
|
||||
use App\Console\Commands\ResolveVersionsCommand;
|
||||
use App\Console\Commands\SptVersionModCountsCommand;
|
||||
use App\Console\Commands\UpdateModDownloadsCommand;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Schedule::command(ImportHub::class)->hourly();
|
||||
Schedule::command(ImportHubCommand::class)->hourly();
|
||||
Schedule::command(ResolveVersionsCommand::class)->hourlyAt(30);
|
||||
Schedule::command(SptVersionModCountsCommand::class)->hourlyAt(40);
|
||||
Schedule::command(UpdateModDownloadsCommand::class)->hourlyAt(45);
|
||||
|
||||
Schedule::command('horizon:snapshot')->everyFiveMinutes();
|
||||
|
312
tests/Feature/Mod/ModDependencyTest.php
Normal file
312
tests/Feature/Mod/ModDependencyTest.php
Normal file
@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModDependency;
|
||||
use App\Models\ModResolvedDependency;
|
||||
use App\Models\ModVersion;
|
||||
use App\Services\DependencyVersionService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('resolves mod version dependencies on create', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // Should resolve to dependentVersion1
|
||||
]);
|
||||
|
||||
// Check that the resolved dependency has been created
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->first())
|
||||
->not()->toBeNull()
|
||||
->resolved_mod_version_id->toBe($dependentVersion1->id);
|
||||
});
|
||||
|
||||
it('resolves multiple matching versions', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.1.0']);
|
||||
$dependentVersion3 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // Should resolve to dependentVersion1 and dependentVersion2
|
||||
]);
|
||||
|
||||
$resolvedDependencies = ModResolvedDependency::where('mod_version_id', $modVersion->id)->get();
|
||||
|
||||
expect($resolvedDependencies->count())->toBe(2)
|
||||
->and($resolvedDependencies->pluck('resolved_mod_version_id'))
|
||||
->toContain($dependentVersion1->id)
|
||||
->toContain($dependentVersion2->id);
|
||||
});
|
||||
|
||||
it('does not resolve dependencies when no versions match', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
ModVersion::factory()->recycle($dependentMod)->create(['version' => '3.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // No versions match
|
||||
]);
|
||||
|
||||
// Check that no resolved dependencies were created
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('updates resolved dependencies when constraint changes', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
|
||||
// Create a dependency with an initial constraint
|
||||
$dependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0', // Should resolve to dependentVersion1
|
||||
]);
|
||||
|
||||
$resolvedDependency = ModResolvedDependency::where('mod_version_id', $modVersion->id)->first();
|
||||
expect($resolvedDependency->resolved_mod_version_id)->toBe($dependentVersion1->id);
|
||||
|
||||
// Update the constraint
|
||||
$dependency->update(['constraint' => '^2.0']); // Should now resolve to dependentVersion2
|
||||
|
||||
$resolvedDependency = ModResolvedDependency::where('mod_version_id', $modVersion->id)->first();
|
||||
expect($resolvedDependency->resolved_mod_version_id)->toBe($dependentVersion2->id);
|
||||
});
|
||||
|
||||
it('removes resolved dependencies when dependency is removed', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
|
||||
// Create a dependency
|
||||
$dependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0',
|
||||
]);
|
||||
|
||||
$resolvedDependency = ModResolvedDependency::where('mod_version_id', $modVersion->id)->first();
|
||||
expect($resolvedDependency)->not()->toBeNull();
|
||||
|
||||
// Delete the dependency
|
||||
$dependency->delete();
|
||||
|
||||
// Check that the resolved dependency is removed
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles mod versions with no dependencies gracefully', function () {
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
// Check that the service was called and that no resolved dependencies were created.
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('resolves the correct versions with a complex semver constraint', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$dependentVersion1 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.2.0']);
|
||||
$dependentVersion3 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '1.5.0']);
|
||||
$dependentVersion4 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.0.0']);
|
||||
$dependentVersion5 = ModVersion::factory()->recycle($dependentMod)->create(['version' => '2.5.0']);
|
||||
|
||||
// Create a complex SemVer constraint
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '>1.0 <2.0 || >=2.5.0 <3.0', // Should resolve to dependentVersion2, dependentVersion3, and dependentVersion5
|
||||
]);
|
||||
|
||||
$resolvedDependencies = ModResolvedDependency::where('mod_version_id', $modVersion->id)->pluck('resolved_mod_version_id');
|
||||
|
||||
expect($resolvedDependencies)->toContain($dependentVersion2->id)
|
||||
->toContain($dependentVersion3->id)
|
||||
->toContain($dependentVersion5->id)
|
||||
->not->toContain($dependentVersion1->id)
|
||||
->not->toContain($dependentVersion4->id);
|
||||
});
|
||||
|
||||
it('resolves overlapping version constraints from multiple dependencies correctly', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
|
||||
$dependentMod1 = Mod::factory()->create();
|
||||
$dependentVersion1_1 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '1.0.0']);
|
||||
$dependentVersion1_2 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '1.5.0']);
|
||||
|
||||
$dependentMod2 = Mod::factory()->create();
|
||||
$dependentVersion2_1 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.0.0']);
|
||||
$dependentVersion2_2 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.5.0']);
|
||||
|
||||
// Create two dependencies with overlapping constraints
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod1])->create([
|
||||
'constraint' => '>=1.0 <2.0', // Matches both versions of dependentMod1
|
||||
]);
|
||||
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod2])->create([
|
||||
'constraint' => '>=1.5.0 <2.0.0', // Matches only the second version of dependentMod2
|
||||
]);
|
||||
|
||||
$resolvedDependencies = ModResolvedDependency::where('mod_version_id', $modVersion->id)->get();
|
||||
|
||||
expect($resolvedDependencies->pluck('resolved_mod_version_id'))
|
||||
->toContain($dependentVersion1_1->id)
|
||||
->toContain($dependentVersion1_2->id)
|
||||
->toContain($dependentVersion2_2->id)
|
||||
->not->toContain($dependentVersion2_1->id);
|
||||
});
|
||||
|
||||
it('handles the case where a dependent mod has no versions available', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
$dependentMod = Mod::factory()->create();
|
||||
|
||||
// Create a dependency where the dependent mod has no versions.
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '>=1.0.0',
|
||||
]);
|
||||
|
||||
// Verify that no versions were resolved.
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('handles a large number of versions efficiently', function () {
|
||||
$versionCount = 100;
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$dependentMod = Mod::factory()->create();
|
||||
for ($i = 0; $i < $versionCount; $i++) {
|
||||
ModVersion::factory()->recycle($dependentMod)->create(['version' => "1.0.$i"]);
|
||||
}
|
||||
|
||||
// Create a dependency with a broad constraint
|
||||
ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '>=1.0.0',
|
||||
]);
|
||||
|
||||
// Verify that all versions were resolved
|
||||
expect(ModResolvedDependency::where('mod_version_id', $modVersion->id)->count())->toBe($versionCount);
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a Mod is updated', function () {
|
||||
$mod = Mod::factory()->create();
|
||||
ModVersion::factory(2)->recycle($mod)->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$mod->update(['name' => 'New Mod Name']);
|
||||
|
||||
$mod->refresh();
|
||||
|
||||
expect($mod->versions)->toHaveCount(2);
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$serviceSpy->shouldReceive('resolve')->with($modVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a Mod is deleted', function () {
|
||||
$mod = Mod::factory()->create();
|
||||
ModVersion::factory(2)->recycle($mod)->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$mod->delete();
|
||||
|
||||
$mod->refresh();
|
||||
|
||||
expect($mod->versions)->toHaveCount(2);
|
||||
foreach ($mod->versions as $modVersion) {
|
||||
$serviceSpy->shouldReceive('resolve')->with($modVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModVersion is updated', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modVersion->update(['version' => '1.1.0']);
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModVersion is deleted', function () {
|
||||
$modVersion = ModVersion::factory()->create();
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modVersion->delete();
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModDependency is updated', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$modDependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0',
|
||||
]);
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modDependency->update(['constraint' => '^2.0']);
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('calls DependencyVersionService when a ModDependency is deleted', function () {
|
||||
$modVersion = ModVersion::factory()->create(['version' => '1.0.0']);
|
||||
$dependentMod = Mod::factory()->create();
|
||||
$modDependency = ModDependency::factory()->recycle([$modVersion, $dependentMod])->create([
|
||||
'constraint' => '^1.0',
|
||||
]);
|
||||
|
||||
$serviceSpy = $this->spy(DependencyVersionService::class);
|
||||
|
||||
$modDependency->delete();
|
||||
|
||||
$serviceSpy->shouldHaveReceived('resolve');
|
||||
});
|
||||
|
||||
it('displays the latest resolved dependencies on the mod detail page', function () {
|
||||
$dependentMod1 = Mod::factory()->create();
|
||||
$dependentMod1Version1 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '1.0.0']);
|
||||
$dependentMod1Version2 = ModVersion::factory()->recycle($dependentMod1)->create(['version' => '2.0.0']);
|
||||
|
||||
$dependentMod2 = Mod::factory()->create();
|
||||
$dependentMod2Version1 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.0.0']);
|
||||
$dependentMod2Version2 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.1.0']);
|
||||
$dependentMod2Version3 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.2.0']);
|
||||
$dependentMod2Version4 = ModVersion::factory()->recycle($dependentMod2)->create(['version' => '1.2.1']);
|
||||
|
||||
$mod = Mod::factory()->create();
|
||||
$mainModVersion = ModVersion::factory()->recycle($mod)->create();
|
||||
|
||||
ModDependency::factory()->recycle([$mainModVersion, $dependentMod1])->create(['constraint' => '>=1.0.0']);
|
||||
ModDependency::factory()->recycle([$mainModVersion, $dependentMod2])->create(['constraint' => '>=1.0.0']);
|
||||
|
||||
$mainModVersion->load('latestResolvedDependencies');
|
||||
|
||||
expect($mainModVersion->latestResolvedDependencies)->toHaveCount(2)
|
||||
->and($mainModVersion->latestResolvedDependencies->pluck('version'))
|
||||
->toContain($dependentMod1Version2->version) // Latest version of dependentMod1
|
||||
->toContain($dependentMod2Version4->version); // Latest version of dependentMod2
|
||||
|
||||
$response = $this->get(route('mod.show', ['mod' => $mod->id, 'slug' => $mod->slug]));
|
||||
|
||||
$response->assertSeeInOrder(explode(' ', __('Dependencies: ')."$dependentMod1->name ($dependentMod1Version2->version)"));
|
||||
$response->assertSeeInOrder(explode(' ', __('Dependencies: ')."$dependentMod2->name ($dependentMod2Version4->version)"));
|
||||
});
|
161
tests/Feature/Mod/ModFilterTest.php
Normal file
161
tests/Feature/Mod/ModFilterTest.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Filters\ModFilter;
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('filters mods by a single SPT version', function () {
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply the filter
|
||||
$filters = ['sptVersions' => [$sptVersion1->version]];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that only the correct mod is returned
|
||||
expect($filteredMods)->toHaveCount(1)
|
||||
->and($filteredMods->first()->id)->toBe($mod1->id);
|
||||
});
|
||||
|
||||
it('filters mods by multiple SPT versions', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
$sptVersion3 = SptVersion::factory()->create(['version' => '3.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate constraints
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
$mod3 = Mod::factory()->create();
|
||||
$modVersion3 = ModVersion::factory()->recycle($mod3)->create([
|
||||
'spt_version_constraint' => '3.0.0', // Constraint matching sptVersion3
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version)
|
||||
->and($modVersion3->sptVersions->pluck('version')->toArray())->toContain($sptVersion3->version);
|
||||
|
||||
// Apply the filter with multiple SPT versions
|
||||
$filters = ['sptVersions' => [$sptVersion1->version, $sptVersion3->version]];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that the correct mods are returned
|
||||
expect($filteredMods)->toHaveCount(2)
|
||||
->and($filteredMods->pluck('id')->toArray())->toContain($mod1->id, $mod3->id);
|
||||
});
|
||||
|
||||
it('returns no mods when no SPT versions match', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate constraints
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply the filter with a non-matching SPT version
|
||||
$filters = ['sptVersions' => ['3.0.0']]; // Version '3.0.0' does not exist in associations
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that no mods are returned
|
||||
expect($filteredMods)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('filters mods correctly with combined filters', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate names and featured status
|
||||
$mod1 = Mod::factory()->create(['name' => 'Awesome Mod', 'featured' => true]);
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create(['name' => 'Cool Mod', 'featured' => false]);
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply combined filters
|
||||
$filters = [
|
||||
'query' => 'Awesome',
|
||||
'featured' => 'only',
|
||||
'sptVersions' => [$sptVersion1->version],
|
||||
];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that only the correct mod is returned
|
||||
expect($filteredMods)->toHaveCount(1)
|
||||
->and($filteredMods->first()->id)->toBe($mod1->id);
|
||||
});
|
||||
|
||||
it('handles an empty SPT versions array correctly', function () {
|
||||
// Create the SPT versions
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
|
||||
// Create the mods and their versions with appropriate constraints
|
||||
$mod1 = Mod::factory()->create();
|
||||
$modVersion1 = ModVersion::factory()->recycle($mod1)->create([
|
||||
'spt_version_constraint' => '1.0.0', // Constraint matching sptVersion1
|
||||
]);
|
||||
|
||||
$mod2 = Mod::factory()->create();
|
||||
$modVersion2 = ModVersion::factory()->recycle($mod2)->create([
|
||||
'spt_version_constraint' => '2.0.0', // Constraint matching sptVersion2
|
||||
]);
|
||||
|
||||
// Confirm associations created by observers and services
|
||||
expect($modVersion1->sptVersions->pluck('version')->toArray())->toContain($sptVersion1->version)
|
||||
->and($modVersion2->sptVersions->pluck('version')->toArray())->toContain($sptVersion2->version);
|
||||
|
||||
// Apply the filter with an empty SPT versions array
|
||||
$filters = ['sptVersions' => []];
|
||||
$filteredMods = (new ModFilter($filters))->apply()->get();
|
||||
|
||||
// Assert that the behavior is as expected (return all mods, or none, depending on intended behavior)
|
||||
expect($filteredMods)->toHaveCount(2); // Modify this assertion to reflect your desired behavior
|
||||
});
|
52
tests/Feature/Mod/ModTest.php
Normal file
52
tests/Feature/Mod/ModTest.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Mod;
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('displays homepage mod cards with the latest supported spt version number', function () {
|
||||
$sptVersion1 = SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
$sptVersion2 = SptVersion::factory()->create(['version' => '2.0.0']);
|
||||
$sptVersion3 = SptVersion::factory()->create(['version' => '3.0.0']);
|
||||
|
||||
$mod1 = Mod::factory()->create();
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion1->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion2->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]);
|
||||
ModVersion::factory()->recycle($mod1)->create(['spt_version_constraint' => $sptVersion3->version]);
|
||||
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertSeeInOrder(explode(' ', "$mod1->name $sptVersion3->version_formatted"));
|
||||
});
|
||||
|
||||
it('displays the latest version on the mod detail page', function () {
|
||||
$versions = [
|
||||
'1.0.0',
|
||||
'1.1.0',
|
||||
'1.2.0',
|
||||
'2.0.0',
|
||||
'2.1.0',
|
||||
];
|
||||
$latestVersion = max($versions);
|
||||
|
||||
$mod = Mod::factory()->create();
|
||||
foreach ($versions as $version) {
|
||||
ModVersion::factory()->recycle($mod)->create(['version' => $version]);
|
||||
}
|
||||
|
||||
$response = $this->get($mod->detailUrl());
|
||||
|
||||
expect($latestVersion)->toBe('2.1.0');
|
||||
|
||||
// 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)");
|
||||
});
|
130
tests/Feature/Mod/ModVersionTest.php
Normal file
130
tests/Feature/Mod/ModVersionTest.php
Normal file
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
use App\Models\ModVersion;
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('resolves spt versions when mod version is created', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
});
|
||||
|
||||
it('resolves spt versions when constraint is updated', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
|
||||
$modVersion->spt_version_constraint = '~1.2.0';
|
||||
$modVersion->save();
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(1)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.2.0');
|
||||
});
|
||||
|
||||
it('resolves spt versions when spt version is created', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
|
||||
SptVersion::factory()->create(['version' => '1.1.2']);
|
||||
|
||||
$modVersion->refresh();
|
||||
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(3)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1', '1.1.2');
|
||||
});
|
||||
|
||||
it('resolves spt versions when spt version is deleted', function () {
|
||||
SptVersion::factory()->create(['version' => '1.0.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.0']);
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
$sptVersion = SptVersion::factory()->create(['version' => '1.1.2']);
|
||||
|
||||
$modVersion = ModVersion::factory()->create(['spt_version_constraint' => '~1.1.0']);
|
||||
|
||||
$modVersion->refresh();
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(3)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1', '1.1.2');
|
||||
|
||||
$sptVersion->delete();
|
||||
|
||||
$modVersion->refresh();
|
||||
$sptVersions = $modVersion->sptVersions;
|
||||
|
||||
expect($sptVersions)->toHaveCount(2)
|
||||
->and($sptVersions->pluck('version'))->toContain('1.1.0', '1.1.1');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
32
tests/Feature/Mod/SptVersionTest.php
Normal file
32
tests/Feature/Mod/SptVersionTest.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Models\SptVersion;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it("returns true if the version is part of the latest version's minor releases", function () {
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||
$version = SptVersion::factory()->create(['version' => '1.3.0']);
|
||||
SptVersion::factory()->create(['version' => '1.3.2']);
|
||||
SptVersion::factory()->create(['version' => '1.3.3']);
|
||||
|
||||
expect($version->isLatestMinor())->toBeTrue();
|
||||
});
|
||||
|
||||
it("returns false if the version is not part of the latest version's minor releases", function () {
|
||||
SptVersion::factory()->create(['version' => '1.1.1']);
|
||||
SptVersion::factory()->create(['version' => '1.2.0']);
|
||||
$version = SptVersion::factory()->create(['version' => '1.2.1']);
|
||||
SptVersion::factory()->create(['version' => '1.3.2']);
|
||||
SptVersion::factory()->create(['version' => '1.3.3']);
|
||||
|
||||
expect($version->isLatestMinor())->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false if there is no latest version in the database', function () {
|
||||
$version = SptVersion::factory()->make(['version' => '1.0.0']);
|
||||
|
||||
expect($version->isLatestMinor())->toBeFalse();
|
||||
});
|
@ -1,238 +0,0 @@
|
||||
<?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);
|
@ -1,42 +0,0 @@
|
||||
<?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)");
|
||||
});
|
@ -1,39 +0,0 @@
|
||||
<?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();
|
||||
});
|
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
use function Pest\Stressless\stress;
|
||||
|
||||
it('homepage has a fast response time', function () {
|
||||
$result = stress('/');
|
||||
|
||||
expect($result->requests()->duration()->med())->toBeLessThan(100);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user