Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions minecraft-modrinth/lang/de/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
'update_failed_body' => 'Bei der Aktualisierung ist ein Fehler aufgetreten. Bitte versuche es erneut oder wende dich an den Support, wenn das Problem weiterhin besteht.',
'uninstall_success' => 'Deinstallation abgeschlossen',
'uninstall_success_body' => ':name erfolgreich deinstalliert',
'uninstall_partial' => 'Deinstallation unvollständig',
'uninstall_partial_body' => 'Die Datei von :name wurde gelöscht, konnte aber nicht aus der Liste der installierten Mods/Plugins entfernt werden. Sie wird eventuell weiterhin als installiert angezeigt.',
'uninstall_failed' => 'Deinstallation fehlgeschlagen',
'uninstall_failed_body' => 'Bei der Deinstallation ist ein Fehler aufgetreten. Bitte versuche es erneut oder wende dich an den Support, wenn das Problem weiterhin besteht.',
],
Expand Down
2 changes: 2 additions & 0 deletions minecraft-modrinth/lang/en/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
'update_failed_body' => 'An error occurred during the update. Please try again or contact support if the issue persists.',
'uninstall_success' => 'Uninstall completed',
'uninstall_success_body' => 'Successfully uninstalled :name',
'uninstall_partial' => 'Uninstall incomplete',
'uninstall_partial_body' => 'The file for :name was deleted, but it could not be removed from the installed list. It may still appear as installed.',
'uninstall_failed' => 'Uninstall failed',
'uninstall_failed_body' => 'An error occurred during uninstallation. Please try again or contact support if the issue persists.',
],
Expand Down
5 changes: 5 additions & 0 deletions minecraft-modrinth/src/Facades/MinecraftModrinth.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@
* @method static array{hits: array<int, array<string, mixed>>, total_hits: int} getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null)
* @method static array<int, array<string, mixed>> getInstalledModsFromModrinth(array<int, InstalledModMetadata> $installedMods, int $page = 1)
* @method static array<int, ModrinthVersion> getProjectVersions(string $projectId, Server $server)
* @method static array<string, array<int, ModrinthVersion>> getProjectVersionsBulk(array<int, string> $projectIds, Server $server)
* @method static array<int, InstalledModMetadata> getInstalledModsMetadata(Server $server, ModrinthProjectType $modrinthProjectType)
* @method static bool saveModMetadata(Server $server, ModrinthProjectType $modrinthProjectType, string $projectId, string $projectSlug, string $projectTitle, string $versionId, string $versionNumber, string $filename, ?string $author = null)
* @method static bool removeModMetadata(Server $server, ModrinthProjectType $modrinthProjectType, string $projectId)
* @method static void downloadFile(Server $server, string $url, string $folder, string $filename)
* @method static void deleteFile(Server $server, string $folder, string $filename)
* @method static bool fileExists(Server $server, string $folder, string $filename)
* @method static array<string, mixed> listFolder(Server $server, string $folder)
* @method static InstalledModMetadata|null getInstalledMod(Server $server, ModrinthProjectType $modrinthProjectType, string $projectId)
* @method static bool isUpdateAvailable(array{version_id: string, version_number: string} $installedMod, array<int, array{id: string, version_number: string}> $availableVersions)
* @method static array<string> getInstalledMods(Server $server, ModrinthProjectType $modrinthProjectType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use App\Filament\Server\Resources\Files\Pages\ListFiles;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Traits\Filament\BlockAccessInConflict;
use Boy132\MinecraftModrinth\Enums\ModrinthProjectType;
use Boy132\MinecraftModrinth\Facades\MinecraftModrinth;
Expand All @@ -27,7 +26,6 @@
use Filament\Tables\Table;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\HtmlString;

Expand Down Expand Up @@ -132,6 +130,42 @@ protected function getCachedVersions(string $projectId): array
return $this->versionsCache[$projectId];
}

/**
* Fetch the versions of every project on the current page up front.
*
* Each installed row asks for its versions to decide between the "installed" and "update"
* action, so without this the installed tab does one Modrinth request per row, one after
* the other, and a full page can take longer than the request is allowed to run.
*
* @param array<int|string, string> $projectIds
*/
protected function primeVersionsCache(array $projectIds): void
{
$projectIds = array_values(array_diff(array_unique(array_filter($projectIds)), array_keys($this->versionsCache)));

if (count($projectIds) < 2) {
return;
}

/** @var Server $server */
$server = Filament::getTenant();

$this->versionsCache += MinecraftModrinth::getProjectVersionsBulk($projectIds, $server);
}

/**
* Drop everything read from the server so the next render reflects what is actually on disk.
*/
protected function forgetInstalledState(bool $refresh = true): void
{
$this->installedModsMetadata = null;
$this->versionsCache = [];

if ($refresh) {
$this->js('$wire.$refresh()');
}
}

/**
* @param array<int, array{primary: bool, filename: string, url: string}> $files
* @return array{primary: bool, filename: string, url: string}|null
Expand Down Expand Up @@ -174,17 +208,12 @@ private function performInstallOrUpdate(
array $primaryFile,
?array $installedMod = null
): void {
$fileRepository = app(DaemonFileRepository::class);

$safeNewFilename = $this->validateFilename($primaryFile['filename']);
$oldFilename = $installedMod ? $this->validateFilename($installedMod['filename']) : null;

$folder = static::$modrinthProjectType->getFolder();

$fileRepository
->setServer($server)
->pull($primaryFile['url'], $folder)
->throw();
MinecraftModrinth::downloadFile($server, $primaryFile['url'], $folder, $safeNewFilename);

$saved = MinecraftModrinth::saveModMetadata(
$server,
Expand All @@ -201,12 +230,7 @@ private function performInstallOrUpdate(
if (!$saved) {
if (!$oldFilename || $oldFilename !== $safeNewFilename) {
try {
Http::daemon($server->node)
->post("/api/servers/{$server->uuid}/files/delete", [
'root' => '/',
'files' => [$folder . '/' . $safeNewFilename],
])
->throw();
MinecraftModrinth::deleteFile($server, $folder, $safeNewFilename);
} catch (Exception $rollbackException) {
report($rollbackException);
}
Expand All @@ -217,20 +241,10 @@ private function performInstallOrUpdate(

if ($oldFilename && $oldFilename !== $safeNewFilename) {
try {
Http::daemon($server->node)
->post("/api/servers/{$server->uuid}/files/delete", [
'root' => '/',
'files' => [$folder . '/' . $oldFilename],
])
->throw();
MinecraftModrinth::deleteFile($server, $folder, $oldFilename);
} catch (Exception $deleteException) {
try {
Http::daemon($server->node)
->post("/api/servers/{$server->uuid}/files/delete", [
'root' => '/',
'files' => [$folder . '/' . $safeNewFilename],
])
->throw();
MinecraftModrinth::deleteFile($server, $folder, $safeNewFilename);
} catch (Exception $rollbackException) {
report($rollbackException);
}
Expand Down Expand Up @@ -275,12 +289,21 @@ public function table(Table $table): Table
}));
}

// Sort by title so a row keeps its place instead of moving around whenever its
// metadata entry is rewritten by an install, update or failed update.
usort($installedMods, fn (array $a, array $b) => strcasecmp($a['project_title'], $b['project_title']));

$projects = MinecraftModrinth::getInstalledModsFromModrinth($installedMods, $page);

$this->primeVersionsCache(array_column($projects, 'project_id'));

return new LengthAwarePaginator($projects, count($installedMods), 20, $page);
} else {
$response = MinecraftModrinth::getProjects($server, static::$modrinthProjectType, $page, $search);

$installedIds = array_column($this->getInstalledModsMetadata(), 'project_id');
$this->primeVersionsCache(array_intersect(array_column($response['hits'], 'project_id'), $installedIds));

return new LengthAwarePaginator($response['hits'], $response['total_hits'], 20, $page);
}
})
Expand Down Expand Up @@ -383,9 +406,7 @@ public function table(Table $table): Table

$this->performInstallOrUpdate($server, $record, $versionData, $primaryFile, $installedMod);

$this->installedModsMetadata = null;
$this->versionsCache = [];
$this->js('$wire.$refresh()');
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.install_success'))
Expand All @@ -398,9 +419,7 @@ public function table(Table $table): Table
} catch (Exception $exception) {
report($exception);

$this->installedModsMetadata = null;
$this->versionsCache = [];
$this->js('$wire.$refresh()');
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.install_failed'))
Expand Down Expand Up @@ -459,8 +478,7 @@ public function table(Table $table): Table

$this->performInstallOrUpdate($server, $record, $latestVersion, $primaryFile);

$this->installedModsMetadata = null;
$this->versionsCache = [];
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.install_success'))
Expand All @@ -473,8 +491,7 @@ public function table(Table $table): Table
} catch (Exception $exception) {
report($exception);

$this->installedModsMetadata = null;
$this->versionsCache = [];
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.install_failed'))
Expand Down Expand Up @@ -541,8 +558,7 @@ public function table(Table $table): Table

$this->performInstallOrUpdate($server, $record, $latestVersion, $primaryFile, $installedMod);

$this->installedModsMetadata = null;
$this->versionsCache = [];
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.update_success'))
Expand All @@ -554,8 +570,7 @@ public function table(Table $table): Table
} catch (Exception $exception) {
report($exception);

$this->installedModsMetadata = null;
$this->versionsCache = [];
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.update_failed'))
Expand Down Expand Up @@ -611,35 +626,29 @@ public function table(Table $table): Table

$folder = static::$modrinthProjectType->getFolder();

Http::daemon($server->node)
->post("/api/servers/{$server->uuid}/files/delete", [
'root' => '/',
'files' => [$folder . '/' . $safeFilename],
])
->throw();
MinecraftModrinth::deleteFile($server, $folder, $safeFilename);

$metadataRemoved = MinecraftModrinth::removeModMetadata($server, static::$modrinthProjectType, $record['project_id']);

$this->forgetInstalledState();

if (!$metadataRemoved) {
Log::warning('Failed to remove mod metadata after successful file deletion', [
'project_id' => $record['project_id'],
'server_id' => $server->id,
]);

if (is_array($this->installedModsMetadata)) {
$this->installedModsMetadata = array_values(
array_filter($this->installedModsMetadata, fn ($mod) => $mod['project_id'] !== $record['project_id'])
);
}

unset($this->versionsCache[$record['project_id']]);
} else {
$this->installedModsMetadata = null;
$this->versionsCache = [];
}

if ($this->activeTab === 'installed') {
$this->js('$wire.$refresh()');
// The jar is gone but the metadata file still lists it, so the row
// stays. Say so instead of reporting a clean uninstall.
Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.uninstall_partial'))
->body(trans('minecraft-modrinth::strings.notifications.uninstall_partial_body', [
'name' => $record['title'],
]))
->warning()
->send();

return;
}

Notification::make()
Expand All @@ -652,12 +661,7 @@ public function table(Table $table): Table
} catch (Exception $exception) {
report($exception);

$this->installedModsMetadata = null;
$this->versionsCache = [];

if ($this->activeTab === 'installed') {
$this->js('$wire.$refresh()');
}
$this->forgetInstalledState();

Notification::make()
->title(trans('minecraft-modrinth::strings.notifications.uninstall_failed'))
Expand Down Expand Up @@ -703,23 +707,9 @@ public function content(Schema $schema): Schema
->badge(),
TextEntry::make('installed')
->label(fn () => trans('minecraft-modrinth::strings.page.installed', ['type' => static::$modrinthProjectType?->getLabel() ?? 'Modrinth']))
->state(function (DaemonFileRepository $fileRepository) use ($server) {
try {
$files = $fileRepository->setServer($server)->getDirectory(static::$modrinthProjectType->getFolder());

if (isset($files['error'])) {
throw new Exception($files['error']);
}

return collect($files)
->filter(fn ($file) => $file['mime'] === 'application/jar' || str($file['name'])->lower()->endsWith('.jar'))
->count();
} catch (Exception $exception) {
report($exception);

return trans('minecraft-modrinth::strings.page.unknown');
}
})
->state(fn () => collect(MinecraftModrinth::listFolder($server, static::$modrinthProjectType->getFolder()))
->filter(fn ($file) => ($file['mime'] ?? null) === 'application/jar' || str($file['name'] ?? '')->lower()->endsWith('.jar'))
->count())
->badge(),
]),
$this->getTabsContentComponent(),
Expand Down
Loading