intial push
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
# Blockland Config
|
||||||
|
|
||||||
|
**Version 1.7.0** · Author: RallyBlock
|
||||||
|
|
||||||
|
A Pelican Panel plugin for managing Blockland dedicated server configuration directly from the panel UI.
|
||||||
|
|
||||||
|
**Compatibility:** Pelican Panel (Filament 4.x, Laravel 12.x)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Add-Ons
|
||||||
|
- Toggle server add-ons on/off, grouped by category (Bricks, Vehicles, Weapons, etc.)
|
||||||
|
- Categories are automatically detected from add-on names
|
||||||
|
- Orphaned entries (add-ons removed from the server) are automatically cleaned up
|
||||||
|
- Save changes or **Save & Restart** to apply immediately
|
||||||
|
|
||||||
|
### ColorSet
|
||||||
|
- Browse installed ColorSet add-ons and activate them with one click
|
||||||
|
- Read-only preview of the currently active color palette with color swatches
|
||||||
|
- Server restarts automatically after activating a new ColorSet
|
||||||
|
- Supports up to 64 colors (Blockland's maximum)
|
||||||
|
|
||||||
|
### Preferences
|
||||||
|
- Edit server preferences (`prefs.cs`) with typed form fields (toggles, text, numeric, password)
|
||||||
|
- Preferences are grouped by category (General, Bricks, Network, Quotas, etc.)
|
||||||
|
- **Save** syncs prefs to server vars and runs `webcom_postserver()` for live effect; **Save & Restart** for immediate full reload
|
||||||
|
|
||||||
|
### Admins
|
||||||
|
- Manage AutoAdminList and AutoSuperAdminList in `prefs.cs`
|
||||||
|
- **Add by BL_ID** with role (Admin or Super Admin) and mode: **Auto** (saved to prefs) or **Manual** (session only, no rejoin persistence)
|
||||||
|
- **Demote by BL_ID** for manually granted admins (player must be online)
|
||||||
|
- Apply changes to connected players immediately when adding Auto admins—no rejoin required (vanilla TorqueScript: `findClientByBL_ID`, `commandToClient setAdminLevel`, `messageAll`)
|
||||||
|
- **Remove** button to remove BL_IDs from Super Admins or Admins lists; demotes connected players instantly if online
|
||||||
|
- **Silent mode** toggle—when enabled, no `messageAll` announcements are sent for promotions, demotions, or preference updates
|
||||||
|
|
||||||
|
### Add-On Browser
|
||||||
|
- Browse and install community add-ons directly from [bl.kenko.dev](https://bl.kenko.dev/Add-ons/Retail/)
|
||||||
|
- Browse by category with a searchable dropdown
|
||||||
|
- Search and filter add-ons by name
|
||||||
|
- Paginated results with a configurable page size (10, 25, 50, or 100 per page)
|
||||||
|
- One-click install downloads add-ons directly into the server's `Add-Ons/` folder
|
||||||
|
- **Note:** These add-ons are for Retail Blockland (v21+), are community-created, and have not been tested for safety
|
||||||
|
|
||||||
|
### Save Loader
|
||||||
|
- List .bls save files from the server's `saves/` folder with optional preview thumbnails
|
||||||
|
- Copy any save to `base/server/temp/temp.bls` — then run /ReloadBricks in-game to load it
|
||||||
|
- Upload new .bls files into the saves folder; optionally add a preview image (.jpg, .jpeg, or .png with the same filename)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Pelican Panel with a Blockland server egg (must have `blockland` in egg tags or features)
|
||||||
|
|
||||||
|
The plugin is **standalone** and works with vanilla Blockland—no Blockland Glass or Support Preferences (BLPrefs) required.
|
||||||
|
|
||||||
|
## Enabling Pages Per Server
|
||||||
|
|
||||||
|
Pages are controlled by **egg features** so you can choose which pages each server gets. Add these feature keys to your Blockland egg's `features` array:
|
||||||
|
|
||||||
|
| Feature | Page | Description |
|
||||||
|
|---------|------|-------------|
|
||||||
|
| `blockland-all` | All pages | Enable all Blockland pages at once |
|
||||||
|
| `blockland-addons` | Add-Ons | Toggle add-ons on/off, save or restart |
|
||||||
|
| `blockland-colorset` | ColorSet | Activate ColorSet add-ons, preview palette |
|
||||||
|
| `blockland-prefs` | Preferences | Edit prefs.cs with typed form fields |
|
||||||
|
| `blockland-admins` | Admins | Manage admin lists, add/remove by BL_ID, demote, silent mode |
|
||||||
|
| `blockland-addon-browser` | Add-On Browser | Browse and install add-ons from bl.kenko.dev |
|
||||||
|
| `blockland-save-loader` | Save Loader | List saves with previews, copy to temp.bls, upload .bls and preview images |
|
||||||
|
|
||||||
|
Only pages whose features are present on the server's egg will appear in the server panel. For example, adding only `blockland-addons` and `blockland-prefs` will show just those two pages for servers using that egg.
|
||||||
|
|
||||||
|
## Server File Paths
|
||||||
|
|
||||||
|
The plugin reads and writes the following files on the game server:
|
||||||
|
|
||||||
|
- `config/server/ADD_ON_LIST.cs` — Add-on enable/disable list
|
||||||
|
- `config/server/colorSet.txt` — Active color palette
|
||||||
|
- `config/server/prefs.cs` — Server preferences
|
||||||
|
- `config/server/.blockland-panel` — Panel config (e.g. silent mode)
|
||||||
|
- `Add-Ons/` — Installed add-ons (folders or .zip files)
|
||||||
|
- `Add-Ons/Colorset_*/` — Installed ColorSet add-ons
|
||||||
|
- `saves/` — Save files (.bls) and optional preview images (.jpg, .jpeg, .png); Save Loader lists, uploads, and displays thumbnails
|
||||||
|
- `base/server/temp/temp.bls` — Target for Save Loader "Copy to temp.bls" (run /ReloadBricks in-game to load)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Page enable/disable is controlled via egg features. Add the following
|
||||||
|
// to your Blockland egg's "features" array to enable each page per server:
|
||||||
|
//
|
||||||
|
// blockland-all — Enable all Blockland pages
|
||||||
|
// blockland-addons — Add-Ons page
|
||||||
|
// blockland-colorset — ColorSet page
|
||||||
|
// blockland-prefs — Preferences page
|
||||||
|
// blockland-admins — Admins page
|
||||||
|
// blockland-addon-browser — Add-On Browser page
|
||||||
|
// blockland-save-loader — Save Loader page
|
||||||
|
];
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Loading states
|
||||||
|
'loading' => 'Loading...',
|
||||||
|
'loading_addons' => 'Loading add-ons...',
|
||||||
|
'loading_admins' => 'Loading admins...',
|
||||||
|
'loading_colorset' => 'Loading ColorSet...',
|
||||||
|
'loading_prefs' => 'Loading preferences...',
|
||||||
|
'loading_categories' => 'Loading categories...',
|
||||||
|
'loading_saves' => 'Loading saves...',
|
||||||
|
|
||||||
|
// Page titles
|
||||||
|
'pages' => [
|
||||||
|
'addons' => 'Add-Ons',
|
||||||
|
'colorset' => 'ColorSet',
|
||||||
|
'preferences' => 'Preferences',
|
||||||
|
'admins' => 'Admins',
|
||||||
|
'addon_browser' => 'Add-On Browser',
|
||||||
|
'save_loader' => 'Save Loader',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
'actions' => [
|
||||||
|
'save' => 'Save',
|
||||||
|
'save_and_restart' => 'Save & Restart',
|
||||||
|
'activate_colorset' => 'Activate ColorSet Add-On',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
'modals' => [
|
||||||
|
'restart_heading' => 'Save & Restart Server',
|
||||||
|
'restart_description' => 'This will save your add-on changes and restart the server. Players will be disconnected.',
|
||||||
|
'prefs_restart_description' => 'This will save your preference changes and restart the server. Players will be disconnected.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Add-On categories (known Blockland categories; unknown ones fall back to raw name)
|
||||||
|
'categories' => [
|
||||||
|
'ai' => 'AI',
|
||||||
|
'bot' => 'Bots',
|
||||||
|
'brick' => 'Bricks',
|
||||||
|
'chatemotes' => 'Chat Emotes',
|
||||||
|
'client' => 'Client',
|
||||||
|
'colorset' => 'ColorSets',
|
||||||
|
'content' => 'Content',
|
||||||
|
'daycycle' => 'Day Cycles',
|
||||||
|
'decal' => 'Decals',
|
||||||
|
'emote' => 'Emotes',
|
||||||
|
'event' => 'Events',
|
||||||
|
'face' => 'Faces',
|
||||||
|
'gamemode' => 'Game Modes',
|
||||||
|
'ground' => 'Grounds',
|
||||||
|
'hat' => 'Hats',
|
||||||
|
'hatmod' => 'Hat Mods',
|
||||||
|
'instrument' => 'Instruments',
|
||||||
|
'item' => 'Items',
|
||||||
|
'jvs' => 'JVS',
|
||||||
|
'langscript' => 'Language Scripts',
|
||||||
|
'light' => 'Lights',
|
||||||
|
'map' => 'Maps',
|
||||||
|
'particle' => 'Particles',
|
||||||
|
'player' => 'Player',
|
||||||
|
'print' => 'Prints',
|
||||||
|
'projectile' => 'Projectiles',
|
||||||
|
'prop' => 'Props',
|
||||||
|
'rp' => 'Roleplay',
|
||||||
|
'script' => 'Scripts',
|
||||||
|
'server' => 'Server',
|
||||||
|
'sky' => 'Skies',
|
||||||
|
'sound' => 'Sounds',
|
||||||
|
'staticmap' => 'Static Maps',
|
||||||
|
'sunflare' => 'Sun Flares',
|
||||||
|
'support' => 'Support',
|
||||||
|
'system' => 'System',
|
||||||
|
'theme' => 'Themes',
|
||||||
|
'tool' => 'Tools',
|
||||||
|
'vehicle' => 'Vehicles',
|
||||||
|
'water' => 'Water',
|
||||||
|
'weapon' => 'Weapons',
|
||||||
|
],
|
||||||
|
|
||||||
|
// ColorSet
|
||||||
|
'colorset' => [
|
||||||
|
'select_addon' => 'Choose a ColorSet add-on to replace the active palette',
|
||||||
|
'active_palette' => 'Active Palette',
|
||||||
|
'color_count' => ':count / :max colors',
|
||||||
|
'no_colorset' => 'No active colorset found.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Prefs
|
||||||
|
'prefs' => [
|
||||||
|
'restart_hint' => 'Restart required for this preference to take effect.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
'validation' => [
|
||||||
|
'max_length' => 'Must be at most :max characters.',
|
||||||
|
'must_be_numeric' => 'Must be a number.',
|
||||||
|
'min_value' => 'Must be at least :min.',
|
||||||
|
'max_value' => 'Must be at most :max.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Pref groups
|
||||||
|
'pref_groups' => [
|
||||||
|
'general' => 'General',
|
||||||
|
'bricks' => 'Bricks',
|
||||||
|
'newduplicator' => 'New Duplicator',
|
||||||
|
'newbricktool' => 'New Brick Tool',
|
||||||
|
'newpaintcan' => 'New Paint Can',
|
||||||
|
'quotas' => 'Quotas',
|
||||||
|
'lan_quotas' => 'LAN Quotas',
|
||||||
|
'network' => 'Network',
|
||||||
|
'other' => 'Other',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
'notifications' => [
|
||||||
|
'addons_saved' => 'Add-on list saved successfully.',
|
||||||
|
'addons_saved_restarting' => 'Add-on list saved. Server is restarting...',
|
||||||
|
'addons_cleaned' => 'Add-on list cleaned up.',
|
||||||
|
'addons_cleaned_detail' => 'Removed :count add-on(s) that no longer exist in the Add-Ons folder.',
|
||||||
|
'colorset_activated' => 'ColorSet activated successfully.',
|
||||||
|
'colorset_activated_restarting' => 'ColorSet activated. Server is restarting...',
|
||||||
|
'colorset_activate_failed' => 'Failed to activate colorset.',
|
||||||
|
'colorset_too_many_colors' => 'The selected colorset exceeds the maximum of :max colors.',
|
||||||
|
'colorset_not_found' => 'Could not find a colorSet.txt in the selected add-on. It may be corrupted or missing.',
|
||||||
|
'prefs_saved' => 'Preferences saved successfully.',
|
||||||
|
'prefs_saved_restarting' => 'Preferences saved. Server is restarting...',
|
||||||
|
'prefs_applied_live' => 'Changes applied to the running server.',
|
||||||
|
'prefs_restart_to_apply' => 'Restart the server for changes to take effect.',
|
||||||
|
'save_failed' => 'Failed to save changes.',
|
||||||
|
'addon_install_started' => 'Download started.',
|
||||||
|
'addon_install_started_detail' => ':name is being downloaded to your Add-Ons folder.',
|
||||||
|
'addon_install_failed' => 'Failed to install add-on.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Egg feature keys (for documentation; add these to egg features to enable pages per server)
|
||||||
|
'features' => [
|
||||||
|
'all' => 'blockland-all',
|
||||||
|
'addons' => 'blockland-addons',
|
||||||
|
'colorset' => 'blockland-colorset',
|
||||||
|
'prefs' => 'blockland-prefs',
|
||||||
|
'admins' => 'blockland-admins',
|
||||||
|
'addon_browser' => 'blockland-addon-browser',
|
||||||
|
'save_loader' => 'blockland-save-loader',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Save Loader
|
||||||
|
'save_loader' => [
|
||||||
|
'upload' => 'Upload to Saves',
|
||||||
|
'upload_hint' => 'Choose a .bls file from your computer, then click Upload to add it to your server\'s saves folder.',
|
||||||
|
'upload_drop_or_click' => 'Drop .bls here or click to choose',
|
||||||
|
'upload_file_types' => '.bls files only',
|
||||||
|
'upload_change' => 'Change',
|
||||||
|
'upload_button' => 'Upload to saves folder',
|
||||||
|
'upload_preview_optional' => 'Optional: Add a preview image (same filename as the save, but .jpg, .jpeg, or .png)',
|
||||||
|
'upload_preview_add' => 'Add preview image',
|
||||||
|
'upload_preview_change' => 'Change',
|
||||||
|
'upload_success' => 'File uploaded to saves folder.',
|
||||||
|
'upload_failed' => 'Upload failed.',
|
||||||
|
'upload_no_file' => 'Please select a file first.',
|
||||||
|
'upload_invalid_type' => 'File must have a .bls extension.',
|
||||||
|
'saves_folder' => 'Saves Folder',
|
||||||
|
'copy_hint' => 'Copy a save to base/server/temp/temp.bls. Then run /ReloadBricks in-game.',
|
||||||
|
'copy_to_temp' => 'Copy to temp.bls',
|
||||||
|
'copy_success' => 'Save copied to temp.bls. Run /ReloadBricks in-game to load it.',
|
||||||
|
'copy_failed' => 'Failed to copy save.',
|
||||||
|
'no_saves' => 'No .bls save files found in the saves folder.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Add-On Browser
|
||||||
|
'browser' => [
|
||||||
|
'warning' => 'These add-ons are for Retail Blockland (v21+) and may not work on older versions. They are sourced from bl.kenko.dev, community-created, and have not been tested for safety. Use at your own risk.',
|
||||||
|
'category' => 'Category',
|
||||||
|
'select_category' => 'Choose a category...',
|
||||||
|
'select_category_prompt' => 'Select a category above to browse available add-ons.',
|
||||||
|
'select_category_or_search_prompt' => 'Select a category or search above to find add-ons.',
|
||||||
|
'search' => 'Search',
|
||||||
|
'search_placeholder' => 'Filter add-ons...',
|
||||||
|
'addon_name' => 'Name',
|
||||||
|
'addon_size' => 'Size',
|
||||||
|
'install' => 'Install',
|
||||||
|
'install_heading' => 'Install :name',
|
||||||
|
'install_description' => 'This will download the add-on to your server\'s Add-Ons folder. You may need to restart the server for it to take effect.',
|
||||||
|
'showing_count' => ':count add-on(s) available',
|
||||||
|
'showing_range' => 'Showing :from to :to of :total',
|
||||||
|
'per_page' => 'Per page',
|
||||||
|
'no_addons' => 'No add-ons found.',
|
||||||
|
'searching_addons' => 'Searching add-ons...',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Admins
|
||||||
|
'admins' => [
|
||||||
|
'online_players' => 'Online Players',
|
||||||
|
'refresh' => 'Refresh',
|
||||||
|
'refresh_players' => 'Refresh player list',
|
||||||
|
'online_loading' => 'Loading players...',
|
||||||
|
'online_empty' => 'No players online.',
|
||||||
|
'players_online' => 'Players online',
|
||||||
|
'make_admin' => 'Make Admin',
|
||||||
|
'make_admin_manual' => 'Make Admin (Manual)',
|
||||||
|
'make_super_admin' => 'Make Super Admin',
|
||||||
|
'make_super_admin_manual' => 'Make Super Admin (Manual)',
|
||||||
|
'promote_to_super_admin' => 'Promote to Super Admin',
|
||||||
|
'promote_to_super_admin_manual' => 'Promote to Super Admin (Manual)',
|
||||||
|
'role_super_admin' => 'Super Admin',
|
||||||
|
'role_admin' => 'Admin',
|
||||||
|
'role_player' => 'Player',
|
||||||
|
'super_admins' => 'Super Admins',
|
||||||
|
'admins' => 'Admins',
|
||||||
|
'announcements' => 'Announcements',
|
||||||
|
'announcement_message' => 'Message',
|
||||||
|
'announcement_placeholder' => 'Message to send to all players...',
|
||||||
|
'announcement_empty' => 'Please enter a message.',
|
||||||
|
'announcement_sent' => 'Announcement sent.',
|
||||||
|
'send_announcement' => 'Send',
|
||||||
|
'add_by_blid' => 'Add by BL_ID',
|
||||||
|
'bl_id' => 'BL_ID',
|
||||||
|
'mode' => 'Mode',
|
||||||
|
'mode_auto' => 'Auto (saved)',
|
||||||
|
'mode_auto_checkbox' => 'Auto (saved)',
|
||||||
|
'mode_manual' => 'Manual (session only)',
|
||||||
|
'session_only' => '(session only)',
|
||||||
|
'role' => 'Role',
|
||||||
|
'add' => 'Add',
|
||||||
|
'remove' => 'Remove',
|
||||||
|
'demote' => 'Demote',
|
||||||
|
'revoke' => 'Revoke',
|
||||||
|
'cancel' => 'Cancel',
|
||||||
|
'confirm_demote' => 'Demote BL_ID :bl_id? They will lose admin for this session.',
|
||||||
|
'confirm_revoke' => 'Revoke admin for BL_ID :bl_id? They will lose admin for this session.',
|
||||||
|
'confirm_remove' => 'Remove BL_ID :bl_id from :role?',
|
||||||
|
'demote_by_blid' => 'Demote by BL_ID',
|
||||||
|
'revoke_by_blid' => 'Revoke by BL_ID',
|
||||||
|
'demote_by_blid_hint' => 'For manually granted admins. Applies only if player is online.',
|
||||||
|
'revoke_by_blid_hint' => 'For manually granted admins. Applies only if player is online.',
|
||||||
|
'demote_from' => 'Demote from',
|
||||||
|
'revoke_from' => 'Revoke from',
|
||||||
|
'no_super_admins' => 'No super admins.',
|
||||||
|
'no_admins' => 'No admins.',
|
||||||
|
'added' => ':name added as :role.',
|
||||||
|
'removed' => 'BL_ID :id removed from :role.',
|
||||||
|
'server_offline' => 'Server must be running to apply changes to connected players.',
|
||||||
|
'manual_requires_online' => 'Player must be online for Manual promotion.',
|
||||||
|
'demoted' => 'Player demoted.',
|
||||||
|
'revoked' => 'Admin privileges have been revoked.',
|
||||||
|
'banned' => 'Player banned.',
|
||||||
|
'unbanned' => 'Player unbanned.',
|
||||||
|
'ban_list' => 'Ban List',
|
||||||
|
'ban' => 'Ban',
|
||||||
|
'unban' => 'Unban',
|
||||||
|
'no_bans' => 'No players banned.',
|
||||||
|
'ban_time' => 'Time (minutes)',
|
||||||
|
'ban_reason' => 'Reason',
|
||||||
|
'silent_mode' => 'Silent mode',
|
||||||
|
'silent_mode_tooltip' => 'When on, no messageAll announcements are sent for promotes, revokes, or preference updates',
|
||||||
|
'silent_enabled' => 'Silent mode enabled. No in-game announcements will be sent.',
|
||||||
|
'silent_disabled' => 'Silent mode disabled. In-game announcements will be sent.',
|
||||||
|
],
|
||||||
|
];
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"id": "blockland-config",
|
||||||
|
"name": "Blockland Config",
|
||||||
|
"author": "RallyBlock",
|
||||||
|
"version": "1.7.0",
|
||||||
|
"description": "Manage Blockland server add-ons, color sets, and preferences",
|
||||||
|
"category": "plugin",
|
||||||
|
"url": null,
|
||||||
|
"update_url": null,
|
||||||
|
"namespace": "Pelican\\BlocklandConfig",
|
||||||
|
"class": "BlocklandConfigPlugin",
|
||||||
|
"panels": [
|
||||||
|
"admin",
|
||||||
|
"server"
|
||||||
|
],
|
||||||
|
"panel_version": null,
|
||||||
|
"composer_packages": null,
|
||||||
|
"meta": {
|
||||||
|
"status": "enabled",
|
||||||
|
"status_message": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<x-filament-panels::page wire:init="loadCategories">
|
||||||
|
@if(!$categoriesLoaded)
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.loading_categories') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
|
||||||
|
{{-- Warning banner --}}
|
||||||
|
<div class="rounded-xl border border-amber-400/30 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-950/30 px-6 py-4 flex items-start gap-4 shrink-0">
|
||||||
|
<x-filament::icon icon="tabler-alert-triangle" class="h-5 w-5 text-amber-500 dark:text-amber-400 shrink-0 mt-0.5" />
|
||||||
|
<p class="text-sm text-amber-800 dark:text-amber-200">
|
||||||
|
{{ trans('blockland-config::blockland-config.browser.warning') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Category & search filters --}}
|
||||||
|
<div>
|
||||||
|
{{ $this->form }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative min-h-[12rem]">
|
||||||
|
<div wire:loading.delay.shortest class="block w-full">
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.browser.searching_addons') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
</div>
|
||||||
|
<div wire:loading.remove.delay.shortest>
|
||||||
|
@if($this->selectedCategory || $this->search !== '')
|
||||||
|
@php $paginated = $this->getPaginatedData(); @endphp
|
||||||
|
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="afterHeader">
|
||||||
|
<div class="flex items-center justify-between gap-4 flex-wrap w-full">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.browser.showing_count', ['count' => $paginated['total']]) }}</p>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<label for="perPage" class="text-sm font-medium text-gray-950 dark:text-white">{{ trans('blockland-config::blockland-config.browser.per_page') }}</label>
|
||||||
|
<x-filament::input.wrapper class="!w-auto min-w-[5rem]">
|
||||||
|
<x-filament::input.select wire:model.live="perPage" id="perPage">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</x-filament::input.select>
|
||||||
|
</x-filament::input.wrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
@if(count($paginated['items']) > 0)
|
||||||
|
<div class="divide-y divide-transparent dark:divide-white/10">
|
||||||
|
@foreach($paginated['items'] as $addOn)
|
||||||
|
<div class="flex items-center justify-between py-4 gap-4" wire:key="addon-{{ md5($addOn['filename']) }}">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-950 dark:text-white truncate">{{ $addOn['name'] }}</p>
|
||||||
|
@if($addOn['size'])
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $addOn['size'] }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<x-filament::button
|
||||||
|
size="sm"
|
||||||
|
color="gray"
|
||||||
|
outlined
|
||||||
|
icon="tabler-download"
|
||||||
|
wire:click="installAddOn('{{ $addOn['url'] }}', '{{ $addOn['filename'] }}', '{{ addslashes($addOn['name']) }}')"
|
||||||
|
wire:loading.attr="disabled"
|
||||||
|
wire:target="installAddOn"
|
||||||
|
>
|
||||||
|
{{ trans('blockland-config::blockland-config.browser.install') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($paginated['lastPage'] > 1)
|
||||||
|
<div class="pt-4 mt-4 border-t border-transparent dark:border-white/10 flex items-center justify-between gap-4">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ trans('blockland-config::blockland-config.browser.showing_range', [
|
||||||
|
'from' => $paginated['from'],
|
||||||
|
'to' => $paginated['to'],
|
||||||
|
'total' => $paginated['total'],
|
||||||
|
]) }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<x-filament::icon-button
|
||||||
|
icon="tabler-chevron-left"
|
||||||
|
color="gray"
|
||||||
|
wire:click="previousPage"
|
||||||
|
:disabled="$paginated['page'] <= 1"
|
||||||
|
/>
|
||||||
|
@for($p = 1; $p <= $paginated['lastPage']; $p++)
|
||||||
|
@if($paginated['lastPage'] <= 7 || $p <= 2 || $p >= $paginated['lastPage'] - 1 || abs($p - $paginated['page']) <= 1)
|
||||||
|
<x-filament::button
|
||||||
|
:color="$p === $paginated['page'] ? 'primary' : 'gray'"
|
||||||
|
:outlined="$p !== $paginated['page']"
|
||||||
|
size="sm"
|
||||||
|
wire:click="goToPage({{ $p }})"
|
||||||
|
class="!px-3 !py-1 min-w-[2rem]"
|
||||||
|
>
|
||||||
|
{{ $p }}
|
||||||
|
</x-filament::button>
|
||||||
|
@elseif($p === 3 || $p === $paginated['lastPage'] - 2)
|
||||||
|
<span class="px-2 text-sm text-gray-400 dark:text-gray-500">…</span>
|
||||||
|
@endif
|
||||||
|
@endfor
|
||||||
|
<x-filament::icon-button
|
||||||
|
icon="tabler-chevron-right"
|
||||||
|
color="gray"
|
||||||
|
wire:click="nextPage"
|
||||||
|
:disabled="$paginated['page'] >= $paginated['lastPage']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<x-filament::empty-state
|
||||||
|
icon="tabler-package-off"
|
||||||
|
:heading="trans('blockland-config::blockland-config.browser.no_addons')"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<x-filament::empty-state
|
||||||
|
icon="tabler-package"
|
||||||
|
:heading="trans('blockland-config::blockland-config.browser.select_category_or_search_prompt')"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<x-filament-panels::page wire:init="loadAddOns">
|
||||||
|
@if(!$addOnsLoaded)
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.loading_addons') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="flex flex-col gap-10">
|
||||||
|
{{ $this->form }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<x-filament-panels::page wire:init="loadAdmins">
|
||||||
|
@if(!$adminsLoaded)
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.loading_admins') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
|
{{-- Confirmation modal popup --}}
|
||||||
|
@if($this->confirmingDemote || $this->confirmingRemove)
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
@keydown.escape.window="$wire.cancelConfirm()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/50"
|
||||||
|
wire:click="cancelConfirm"
|
||||||
|
></div>
|
||||||
|
<div class="relative z-10 w-full max-w-md rounded-xl bg-white dark:bg-gray-900 shadow-xl dark:ring-1 dark:ring-white/10 p-6 text-center">
|
||||||
|
@if($this->confirmingDemote)
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-8">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.confirm_demote', ['bl_id' => $demoteBlId]) }}
|
||||||
|
</p>
|
||||||
|
@else
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-8">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.confirm_remove', ['bl_id' => $this->confirmingRemove['blId'], 'role' => $this->confirmingRemove['list'] === 'super' ? trans('blockland-config::blockland-config.admins.super_admins') : trans('blockland-config::blockland-config.admins.admins')]) }}
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
<div class="flex justify-center gap-3">
|
||||||
|
<x-filament::button color="gray" outlined icon="tabler-x" wire:click="cancelConfirm">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.cancel') }}
|
||||||
|
</x-filament::button>
|
||||||
|
@if($this->confirmingDemote)
|
||||||
|
<x-filament::button color="danger" icon="tabler-user-minus" wire:click="demoteByBlId" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.demote') }}
|
||||||
|
</x-filament::button>
|
||||||
|
@else
|
||||||
|
<x-filament::button color="danger" icon="tabler-user-minus" wire:click="confirmRemoveFromList" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.remove') }}
|
||||||
|
</x-filament::button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{-- Add by BL_ID --}}
|
||||||
|
<x-filament::section :heading="trans('blockland-config::blockland-config.admins.add_by_blid')">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
{{ $this->addForm }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 pt-6">
|
||||||
|
<x-filament::button icon="tabler-plus" wire:click="addByBlId" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.add') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
{{-- Demote by BL_ID --}}
|
||||||
|
<x-filament::section
|
||||||
|
:heading="trans('blockland-config::blockland-config.admins.demote_by_blid')"
|
||||||
|
:description="trans('blockland-config::blockland-config.admins.demote_by_blid_hint')"
|
||||||
|
>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
{{ $this->demoteForm }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 pt-6">
|
||||||
|
<x-filament::button color="danger" icon="tabler-user-minus" wire:click="askConfirmDemote" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.demote') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
{{-- Super Admins --}}
|
||||||
|
<x-filament::section :heading="trans('blockland-config::blockland-config.admins.super_admins')" collapsible collapsed>
|
||||||
|
@if(count($this->superAdmins) > 0)
|
||||||
|
<div class="divide-y divide-transparent dark:divide-white/10">
|
||||||
|
@foreach($this->superAdmins as $blId)
|
||||||
|
<div class="flex items-center justify-between py-4 gap-4">
|
||||||
|
<span class="text-sm text-gray-950 dark:text-white">BL_ID {{ $blId }}</span>
|
||||||
|
<x-filament::button color="danger" size="sm" icon="tabler-user-minus" wire:click="askRemoveFromList('{{ $blId }}', 'super')" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.remove') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.admins.no_super_admins') }}</p>
|
||||||
|
@endif
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
{{-- Admins --}}
|
||||||
|
<x-filament::section :heading="trans('blockland-config::blockland-config.admins.admins')" collapsible collapsed>
|
||||||
|
@if(count($this->admins) > 0)
|
||||||
|
<div class="divide-y divide-transparent dark:divide-white/10">
|
||||||
|
@foreach($this->admins as $blId)
|
||||||
|
<div class="flex items-center justify-between py-4 gap-4">
|
||||||
|
<span class="text-sm text-gray-950 dark:text-white">BL_ID: {{ $blId }}</span>
|
||||||
|
<x-filament::button color="danger" size="sm" icon="tabler-user-minus" wire:click="askRemoveFromList('{{ $blId }}', 'admin')" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.admins.remove') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.admins.no_admins') }}</p>
|
||||||
|
@endif
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<x-filament-panels::page wire:init="loadColorSet">
|
||||||
|
@if(!$colorSetLoaded)
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.loading_colorset') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
{{-- Color count summary --}}
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<x-filament::icon icon="tabler-palette" class="h-5 w-5 text-primary-500 dark:text-primary-400 shrink-0" />
|
||||||
|
<span class="text-sm font-medium text-gray-950 dark:text-white">{{ trans('blockland-config::blockland-config.colorset.active_palette') }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.colorset.color_count', ['count' => $this->totalColors, 'max' => 64]) }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
{{-- Swatch preview per section --}}
|
||||||
|
@forelse($this->colorSetPreview as $section)
|
||||||
|
<x-filament::section
|
||||||
|
:heading="$section['label']"
|
||||||
|
:description="count($section['colors']) . ' ' . (count($section['colors']) === 1 ? 'color' : 'colors')"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach($section['colors'] as $color)
|
||||||
|
<div
|
||||||
|
class="shrink-0 w-10 h-10 rounded-lg border-0 dark:border dark:border-gray-600 shadow-sm"
|
||||||
|
style="background-color: rgba({{ $color['r'] }}, {{ $color['g'] }}, {{ $color['b'] }}, {{ round($color['a'] / 255, 2) }});"
|
||||||
|
title="R:{{ $color['r'] }} G:{{ $color['g'] }} B:{{ $color['b'] }} A:{{ $color['a'] }}"
|
||||||
|
></div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@empty
|
||||||
|
<x-filament::empty-state
|
||||||
|
icon="tabler-palette-off"
|
||||||
|
:heading="trans('blockland-config::blockland-config.colorset.no_colorset')"
|
||||||
|
/>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<x-filament-panels::page wire:init="loadPrefs">
|
||||||
|
@if(!$prefsLoaded)
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.loading_prefs') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="flex flex-col gap-10">
|
||||||
|
{{ $this->form }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<x-filament-panels::page wire:init="loadSaves">
|
||||||
|
@if(!$savesLoaded)
|
||||||
|
<x-filament::section>
|
||||||
|
<div class="flex items-center justify-center gap-2 py-12">
|
||||||
|
<x-filament::loading-indicator class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.loading_saves') }}</span>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
{{-- Upload to Saves --}}
|
||||||
|
<x-filament::section :heading="trans('blockland-config::blockland-config.save_loader.upload')" :description="trans('blockland-config::blockland-config.save_loader.upload_hint')">
|
||||||
|
<div class="rounded-xl border-2 transition {{ $uploadedFile ? 'border-gray-200 bg-gray-50 px-6 py-4 dark:border-gray-700 dark:bg-gray-800/50' : 'border-dashed border-gray-300 dark:border-gray-600' }}">
|
||||||
|
@if($uploadedFile)
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<label class="flex min-w-0 cursor-pointer items-center gap-3">
|
||||||
|
<x-filament::icon icon="tabler-file" class="h-6 w-6 shrink-0 text-primary-500 dark:text-primary-400" />
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate text-sm font-medium text-gray-950 dark:text-white">{{ $uploadedFile->getClientOriginalName() }}</p>
|
||||||
|
<span class="text-xs text-primary-500 hover:underline dark:text-primary-400">{{ trans('blockland-config::blockland-config.save_loader.upload_change') }}</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" wire:model="uploadedFile" accept=".bls" class="absolute h-px w-px overflow-hidden opacity-0" aria-hidden="true" tabindex="-1" />
|
||||||
|
</label>
|
||||||
|
<x-filament::button icon="tabler-upload" wire:click="uploadSave" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.save_loader.upload_button') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-white/10">
|
||||||
|
<p class="mb-2 text-xs text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.save_loader.upload_preview_optional') }}</p>
|
||||||
|
@if($uploadedPreviewFile)
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<label class="flex min-w-0 flex-1 cursor-pointer items-center gap-3">
|
||||||
|
<div class="flex h-6 w-8 shrink-0 items-center justify-center overflow-hidden rounded bg-gray-200 dark:bg-gray-700">
|
||||||
|
<img src="{{ $uploadedPreviewFile->temporaryUrl() }}" alt="" class="h-full w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate text-sm font-medium text-gray-950 dark:text-white">{{ $uploadedPreviewFile->getClientOriginalName() }}</p>
|
||||||
|
<span class="text-xs text-primary-500 hover:underline dark:text-primary-400">{{ trans('blockland-config::blockland-config.save_loader.upload_preview_change') }}</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" wire:model="uploadedPreviewFile" accept=".jpg,.jpeg,.png" class="absolute h-px w-px overflow-hidden opacity-0" aria-hidden="true" tabindex="-1" />
|
||||||
|
</label>
|
||||||
|
<button type="button" wire:click="removeUploadedPreviewFile" class="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
<x-filament::icon icon="tabler-x" class="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<label class="flex cursor-pointer items-center gap-2 rounded-lg border border-dashed border-gray-300 px-3 py-2 text-sm transition dark:border-gray-600 hover:border-primary-500 dark:hover:border-primary-500">
|
||||||
|
<x-filament::icon icon="tabler-photo-plus" class="h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||||||
|
<span class="text-gray-600 dark:text-gray-300">{{ trans('blockland-config::blockland-config.save_loader.upload_preview_add') }}</span>
|
||||||
|
<input type="file" wire:model="uploadedPreviewFile" accept=".jpg,.jpeg,.png" class="absolute h-px w-px overflow-hidden opacity-0" aria-hidden="true" />
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<label class="relative flex cursor-pointer flex-col items-center justify-center gap-2 px-6 py-8 sm:flex-row sm:py-6">
|
||||||
|
<x-filament::icon icon="tabler-upload" class="h-10 w-10 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||||
|
<div class="text-center sm:text-left">
|
||||||
|
<p class="text-sm font-medium text-gray-950 dark:text-white">{{ trans('blockland-config::blockland-config.save_loader.upload_drop_or_click') }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ trans('blockland-config::blockland-config.save_loader.upload_file_types') }}</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" wire:model="uploadedFile" accept=".bls" class="absolute inset-0 h-full w-full cursor-pointer opacity-0" aria-hidden="true" />
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
{{-- Saves Folder --}}
|
||||||
|
<x-filament::section :heading="trans('blockland-config::blockland-config.save_loader.saves_folder')" :description="trans('blockland-config::blockland-config.save_loader.copy_hint')">
|
||||||
|
@if(count($saves) > 0)
|
||||||
|
<div class="divide-y divide-transparent dark:divide-white/10">
|
||||||
|
@foreach($saves as $save)
|
||||||
|
<div class="flex items-center justify-between gap-4 py-4" wire:key="save-{{ $save['path'] }}">
|
||||||
|
<div class="flex min-w-0 flex-1 items-center gap-4">
|
||||||
|
<div class="flex h-6 w-8 shrink-0 items-center justify-center overflow-hidden rounded bg-gray-100 dark:bg-gray-800">
|
||||||
|
@if(isset($save['preview_url']))
|
||||||
|
<img src="{{ $save['preview_url'] }}" alt="" class="h-full w-full object-cover" loading="lazy" />
|
||||||
|
@else
|
||||||
|
<x-filament::icon icon="tabler-file" class="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<span class="truncate text-sm font-medium text-gray-950 dark:text-white">{{ $save['name'] }}</span>
|
||||||
|
</div>
|
||||||
|
<x-filament::button size="sm" icon="tabler-copy" wire:click="copyToTemp('{{ addslashes($save['path']) }}')" wire:loading.attr="disabled">
|
||||||
|
{{ trans('blockland-config::blockland-config.save_loader.copy_to_temp') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<x-filament::empty-state
|
||||||
|
icon="tabler-folder-off"
|
||||||
|
:heading="trans('blockland-config::blockland-config.save_loader.no_saves')"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
</x-filament::section>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament-panels::page>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\Activity\ServerSubject;
|
||||||
|
use App\Http\Middleware\Api\Client\Server\AuthenticateServerAccess;
|
||||||
|
use App\Http\Middleware\Api\Client\Server\ResourceBelongsToServer;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Pelican\BlocklandConfig\Http\Controllers\Api\Client\Servers\SavePreviewController;
|
||||||
|
|
||||||
|
Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, AuthenticateServerAccess::class, ResourceBelongsToServer::class])->group(function () {
|
||||||
|
Route::get('/blockland-save-preview', SavePreviewController::class)->name('api:client:server.blockland-save-preview');
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig;
|
||||||
|
|
||||||
|
use Filament\Contracts\Plugin;
|
||||||
|
use Filament\Panel;
|
||||||
|
use Pelican\BlocklandConfig\Filament\Server\Pages\BlocklandAddOnBrowserPage;
|
||||||
|
use Pelican\BlocklandConfig\Filament\Server\Pages\BlocklandAddOnsPage;
|
||||||
|
use Pelican\BlocklandConfig\Filament\Server\Pages\BlocklandAdminsPage;
|
||||||
|
use Pelican\BlocklandConfig\Filament\Server\Pages\BlocklandColorSetPage;
|
||||||
|
use Pelican\BlocklandConfig\Filament\Server\Pages\BlocklandPrefsPage;
|
||||||
|
use Pelican\BlocklandConfig\Filament\Server\Pages\BlocklandSaveLoaderPage;
|
||||||
|
|
||||||
|
class BlocklandConfigPlugin implements Plugin
|
||||||
|
{
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'blockland-config';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(Panel $panel): void
|
||||||
|
{
|
||||||
|
if ($panel->getId() === 'server') {
|
||||||
|
$panel->pages([
|
||||||
|
BlocklandAddOnsPage::class,
|
||||||
|
BlocklandColorSetPage::class,
|
||||||
|
BlocklandAdminsPage::class,
|
||||||
|
BlocklandPrefsPage::class,
|
||||||
|
BlocklandAddOnBrowserPage::class,
|
||||||
|
BlocklandSaveLoaderPage::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(Panel $panel): void {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Facades;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static bool isBlocklandServer(Server $server)
|
||||||
|
* @method static bool hasBlocklandFeature(Server $server, string $feature)
|
||||||
|
* @method static array getAddOns(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static void saveAddOns(Server $server, DaemonFileRepository $fileRepository, array $addOns)
|
||||||
|
* @method static array parseAddOnList(string $content)
|
||||||
|
* @method static string serializeAddOnList(array $addOns)
|
||||||
|
* @method static array groupAddOnsByCategory(array $addOns)
|
||||||
|
* @method static array listInstalledAddOnNames(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static array cleanupOrphanedAddOns(array $addOns, array $installedNames)
|
||||||
|
* @method static array parseColorSet(string $content)
|
||||||
|
* @method static string serializeColorSet(array $sections)
|
||||||
|
* @method static array parsePrefs(string $content)
|
||||||
|
* @method static string serializePrefs(array $prefs)
|
||||||
|
* @method static array getPrefs(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static void savePrefs(Server $server, DaemonFileRepository $fileRepository, array $prefs)
|
||||||
|
* @method static array groupPrefs(array $prefs)
|
||||||
|
* @method static array listColorsetAddOns(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static string|null readColorsetAddOn(Server $server, DaemonFileRepository $fileRepository, string $path, bool $isZip)
|
||||||
|
* @method static string activateColorset(Server $server, DaemonFileRepository $fileRepository, string $path, bool $isZip)
|
||||||
|
* @method static array getActiveColorSet(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static bool saveActiveColorSet(Server $server, DaemonFileRepository $fileRepository, array $sections)
|
||||||
|
* @method static void sendConsoleCommands(Server $server, DaemonFileRepository $fileRepository, array $commands)
|
||||||
|
* @method static string|null validatePrefValue(string $key, string $value)
|
||||||
|
* @method static bool requiresRestart(string $key)
|
||||||
|
* @method static array getAdminLists(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static void saveAdminLists(Server $server, DaemonFileRepository $fileRepository, array $superAdmins, array $admins)
|
||||||
|
* @method static array getOnlinePlayers(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static array fetchRetailCategories()
|
||||||
|
* @method static array fetchRetailAddOns(string $category)
|
||||||
|
* @method static void installRetailAddOn(Server $server, DaemonFileRepository $fileRepository, string $url, ?string $filename = null)
|
||||||
|
* @method static bool isSilentMode(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static void setSilentMode(Server $server, DaemonFileRepository $fileRepository, bool $silent)
|
||||||
|
* @method static array buildPrefConsoleCommands(array $prefs)
|
||||||
|
* @method static array listSaveFiles(Server $server, DaemonFileRepository $fileRepository)
|
||||||
|
* @method static void copySaveToTemp(Server $server, DaemonFileRepository $fileRepository, string $path)
|
||||||
|
* @method static void uploadSaveFile(Server $server, DaemonFileRepository $fileRepository, string $content, string $filename)
|
||||||
|
*
|
||||||
|
* @see BlocklandConfigService
|
||||||
|
*/
|
||||||
|
class BlocklandConfig extends Facade
|
||||||
|
{
|
||||||
|
protected static function getFacadeAccessor(): string
|
||||||
|
{
|
||||||
|
return BlocklandConfigService::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Filament\Server\Pages;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Pelican\BlocklandConfig\Facades\BlocklandConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Schema $form
|
||||||
|
*/
|
||||||
|
class BlocklandAddOnBrowserPage extends Page
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected string $view = 'blockland-config::pages.addon-browser';
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'tabler-download';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'blockland-addon-browser';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 23;
|
||||||
|
|
||||||
|
public ?string $selectedCategory = null;
|
||||||
|
|
||||||
|
public string $search = '';
|
||||||
|
|
||||||
|
public int $perPage = 25;
|
||||||
|
|
||||||
|
public int $page = 1;
|
||||||
|
|
||||||
|
public bool $categoriesLoaded = false;
|
||||||
|
|
||||||
|
public array $categories = [];
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
/** @var Server|null $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::canAccess()
|
||||||
|
&& BlocklandConfig::isBlocklandServer($server)
|
||||||
|
&& BlocklandConfig::hasBlocklandFeature($server, \Pelican\BlocklandConfig\Services\BlocklandConfigService::FEATURE_ADDON_BROWSER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return trans('blockland-config::blockland-config.pages.addon_browser');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string|Htmlable
|
||||||
|
{
|
||||||
|
return static::getNavigationLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSelectedCategory(): void
|
||||||
|
{
|
||||||
|
$this->page = 1;
|
||||||
|
$this->search = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSearch(): void
|
||||||
|
{
|
||||||
|
$this->page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedPerPage(): void
|
||||||
|
{
|
||||||
|
$this->page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadCategories(): void
|
||||||
|
{
|
||||||
|
$this->categories = BlocklandConfig::fetchRetailCategories();
|
||||||
|
$this->categoriesLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $form): Schema
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
foreach ($this->categories as $cat) {
|
||||||
|
$options[$cat['name']] = $cat['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Select::make('selectedCategory')
|
||||||
|
->label(trans('blockland-config::blockland-config.browser.category'))
|
||||||
|
->options($options)
|
||||||
|
->searchable()
|
||||||
|
->placeholder(trans('blockland-config::blockland-config.browser.select_category'))
|
||||||
|
->live(),
|
||||||
|
TextInput::make('search')
|
||||||
|
->label(trans('blockland-config::blockland-config.browser.search'))
|
||||||
|
->placeholder(trans('blockland-config::blockland-config.browser.search_placeholder'))
|
||||||
|
->live(debounce: 300),
|
||||||
|
])
|
||||||
|
->statePath(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllAddOns(): array
|
||||||
|
{
|
||||||
|
if ($this->selectedCategory) {
|
||||||
|
$addOns = BlocklandConfig::fetchRetailAddOns($this->selectedCategory);
|
||||||
|
} elseif ($this->search !== '') {
|
||||||
|
$addOns = $this->fetchAddOnsFromAllCategories();
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->search !== '') {
|
||||||
|
$needle = strtolower($this->search);
|
||||||
|
$addOns = array_filter($addOns, fn ($a) => str_contains(strtolower($a['name']), $needle));
|
||||||
|
$addOns = array_values($addOns);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $addOns;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchAddOnsFromAllCategories(): array
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($this->categories as $cat) {
|
||||||
|
$addOns = BlocklandConfig::fetchRetailAddOns($cat['name']);
|
||||||
|
foreach ($addOns as $addOn) {
|
||||||
|
$key = strtolower($addOn['filename']);
|
||||||
|
if (isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$key] = true;
|
||||||
|
$all[] = $addOn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usort($all, fn ($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||||
|
|
||||||
|
return array_values($all);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPaginatedData(): array
|
||||||
|
{
|
||||||
|
$all = $this->getAllAddOns();
|
||||||
|
$total = count($all);
|
||||||
|
$lastPage = max(1, (int) ceil($total / $this->perPage));
|
||||||
|
|
||||||
|
if ($this->page > $lastPage) {
|
||||||
|
$this->page = $lastPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = ($this->page - 1) * $this->perPage;
|
||||||
|
$items = array_slice($all, $offset, $this->perPage);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $items,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $this->page,
|
||||||
|
'lastPage' => $lastPage,
|
||||||
|
'perPage' => $this->perPage,
|
||||||
|
'from' => $total > 0 ? $offset + 1 : 0,
|
||||||
|
'to' => min($offset + $this->perPage, $total),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function previousPage(): void
|
||||||
|
{
|
||||||
|
$this->page = max(1, $this->page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextPage(): void
|
||||||
|
{
|
||||||
|
$data = $this->getPaginatedData();
|
||||||
|
$this->page = min($data['lastPage'], $this->page + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToPage(int $page): void
|
||||||
|
{
|
||||||
|
$this->page = max(1, $page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function installAddOn(string $url, string $filename, string $name): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
BlocklandConfig::installRetailAddOn($server, $fileRepository, $url, $filename);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.addon_install_started'))
|
||||||
|
->body(trans('blockland-config::blockland-config.notifications.addon_install_started_detail', [
|
||||||
|
'name' => $name,
|
||||||
|
]))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.addon_install_failed'))
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Filament\Server\Pages;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Pelican\BlocklandConfig\Facades\BlocklandConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Schema $form
|
||||||
|
*/
|
||||||
|
class BlocklandAddOnsPage extends Page
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected string $view = 'blockland-config::pages.addons-page';
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'tabler-puzzle';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'blockland-addons';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 20;
|
||||||
|
|
||||||
|
public ?array $data = [];
|
||||||
|
|
||||||
|
public bool $addOnsLoaded = false;
|
||||||
|
|
||||||
|
public array $groupedAddOns = [];
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
/** @var Server|null $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::canAccess()
|
||||||
|
&& BlocklandConfig::isBlocklandServer($server)
|
||||||
|
&& BlocklandConfig::hasBlocklandFeature($server, \Pelican\BlocklandConfig\Services\BlocklandConfigService::FEATURE_ADDONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return trans('blockland-config::blockland-config.pages.addons');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return static::getNavigationLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadAddOns(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$addOns = BlocklandConfig::getAddOns($server, $fileRepository);
|
||||||
|
$installedNames = BlocklandConfig::listInstalledAddOnNames($server, $fileRepository);
|
||||||
|
$result = BlocklandConfig::cleanupOrphanedAddOns($addOns, $installedNames);
|
||||||
|
|
||||||
|
if (!empty($result['removed'])) {
|
||||||
|
BlocklandConfig::saveAddOns($server, $fileRepository, $result['cleaned']);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.addons_cleaned'))
|
||||||
|
->body(trans('blockland-config::blockland-config.notifications.addons_cleaned_detail', [
|
||||||
|
'count' => count($result['removed']),
|
||||||
|
]))
|
||||||
|
->info()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
$addOns = $result['cleaned'];
|
||||||
|
$this->groupedAddOns = BlocklandConfig::groupAddOnsByCategory($addOns);
|
||||||
|
$this->addOnsLoaded = true;
|
||||||
|
|
||||||
|
$formData = [];
|
||||||
|
foreach ($addOns as $name => $value) {
|
||||||
|
$formData['addon_' . $name] = $value === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->form->fill($formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $form): Schema
|
||||||
|
{
|
||||||
|
if (!$this->addOnsLoaded || empty($this->groupedAddOns)) {
|
||||||
|
return $form->schema([])->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections = [];
|
||||||
|
foreach ($this->groupedAddOns as $category => $items) {
|
||||||
|
$toggles = [];
|
||||||
|
foreach ($items as $name => $value) {
|
||||||
|
$label = str_replace('_', ' ', preg_replace('/^' . preg_quote($category, '/') . '_/', '', $name));
|
||||||
|
$toggles[] = Toggle::make('addon_' . $name)
|
||||||
|
->label($label)
|
||||||
|
->inline(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transKey = 'blockland-config::blockland-config.categories.' . strtolower($category);
|
||||||
|
$translated = trans($transKey);
|
||||||
|
$sectionLabel = $translated === $transKey ? $category : $translated;
|
||||||
|
|
||||||
|
$sections[] = Section::make($sectionLabel)
|
||||||
|
->schema($toggles)
|
||||||
|
->collapsible()
|
||||||
|
->collapsed()
|
||||||
|
->columns(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $form
|
||||||
|
->schema($sections)
|
||||||
|
->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('save')
|
||||||
|
->label(trans('blockland-config::blockland-config.actions.save'))
|
||||||
|
->icon('tabler-device-floppy')
|
||||||
|
->disabled(fn () => !$this->addOnsLoaded)
|
||||||
|
->action(function () {
|
||||||
|
$this->saveAddOns();
|
||||||
|
}),
|
||||||
|
Action::make('save_and_restart')
|
||||||
|
->label(trans('blockland-config::blockland-config.actions.save_and_restart'))
|
||||||
|
->icon('tabler-refresh')
|
||||||
|
->color('warning')
|
||||||
|
->disabled(fn () => !$this->addOnsLoaded)
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading(trans('blockland-config::blockland-config.modals.restart_heading'))
|
||||||
|
->modalDescription(trans('blockland-config::blockland-config.modals.restart_description'))
|
||||||
|
->action(function () {
|
||||||
|
$this->saveAddOns(restart: true);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveAddOns(bool $restart = false): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$currentAddOns = BlocklandConfig::getAddOns($server, $fileRepository);
|
||||||
|
$installedNames = BlocklandConfig::listInstalledAddOnNames($server, $fileRepository);
|
||||||
|
$result = BlocklandConfig::cleanupOrphanedAddOns($currentAddOns, $installedNames);
|
||||||
|
$currentAddOns = $result['cleaned'];
|
||||||
|
|
||||||
|
$formData = $this->form->getState();
|
||||||
|
$updatedAddOns = [];
|
||||||
|
|
||||||
|
foreach ($currentAddOns as $name => $oldValue) {
|
||||||
|
$formKey = 'addon_' . $name;
|
||||||
|
$updatedAddOns[$name] = !empty($formData[$formKey]) ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocklandConfig::saveAddOns($server, $fileRepository, $updatedAddOns);
|
||||||
|
|
||||||
|
if ($restart) {
|
||||||
|
/** @var DaemonServerRepository $serverRepository */
|
||||||
|
$serverRepository = app(DaemonServerRepository::class);
|
||||||
|
$serverRepository->setServer($server)->power('restart');
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.addons_saved_restarting'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.addons_saved'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Filament\Server\Pages;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Forms\Components\Checkbox;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Pelican\BlocklandConfig\Facades\BlocklandConfig;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Schema $addForm
|
||||||
|
* @property Schema $demoteForm
|
||||||
|
*/
|
||||||
|
class BlocklandAdminsPage extends Page
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
protected string $view = 'blockland-config::pages.admins-page';
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'tabler-shield';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'blockland-admins';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 21;
|
||||||
|
|
||||||
|
public array $superAdmins = [];
|
||||||
|
|
||||||
|
public array $admins = [];
|
||||||
|
|
||||||
|
public string $newBlId = '';
|
||||||
|
|
||||||
|
public string $newRole = 'admin';
|
||||||
|
|
||||||
|
public bool $newModeAuto = false;
|
||||||
|
|
||||||
|
public string $demoteBlId = '';
|
||||||
|
|
||||||
|
public string $demoteFromRole = 'admin';
|
||||||
|
|
||||||
|
public bool $confirmingDemote = false;
|
||||||
|
|
||||||
|
/** @var array{blId: string, list: string}|null */
|
||||||
|
public ?array $confirmingRemove = null;
|
||||||
|
|
||||||
|
public bool $silentMode = false;
|
||||||
|
|
||||||
|
public bool $adminsLoaded = false;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
/** @var Server|null $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
if (! $server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::canAccess()
|
||||||
|
&& BlocklandConfig::isBlocklandServer($server)
|
||||||
|
&& BlocklandConfig::hasBlocklandFeature($server, \Pelican\BlocklandConfig\Services\BlocklandConfigService::FEATURE_ADMINS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return trans('blockland-config::blockland-config.pages.admins');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return static::getNavigationLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getForms(): array
|
||||||
|
{
|
||||||
|
return ['addForm', 'demoteForm'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addForm(Schema $form): Schema
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Grid::make(2)->schema([
|
||||||
|
TextInput::make('newBlId')
|
||||||
|
->label(trans('blockland-config::blockland-config.admins.bl_id'))
|
||||||
|
->placeholder('BL_ID')
|
||||||
|
->live(),
|
||||||
|
Select::make('newRole')
|
||||||
|
->label(trans('blockland-config::blockland-config.admins.role'))
|
||||||
|
->options([
|
||||||
|
'admin' => trans('blockland-config::blockland-config.admins.admins'),
|
||||||
|
'super' => trans('blockland-config::blockland-config.admins.super_admins'),
|
||||||
|
])
|
||||||
|
->placeholder(trans('blockland-config::blockland-config.admins.role'))
|
||||||
|
->position('bottom')
|
||||||
|
->live(),
|
||||||
|
]),
|
||||||
|
Checkbox::make('newModeAuto')
|
||||||
|
->label(trans('blockland-config::blockland-config.admins.mode_auto_checkbox')),
|
||||||
|
])
|
||||||
|
->statePath(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function demoteForm(Schema $form): Schema
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
TextInput::make('demoteBlId')
|
||||||
|
->label(trans('blockland-config::blockland-config.admins.bl_id'))
|
||||||
|
->placeholder('BL_ID')
|
||||||
|
->live(),
|
||||||
|
Select::make('demoteFromRole')
|
||||||
|
->label(trans('blockland-config::blockland-config.admins.demote_from'))
|
||||||
|
->options([
|
||||||
|
'admin' => trans('blockland-config::blockland-config.admins.admins'),
|
||||||
|
'super' => trans('blockland-config::blockland-config.admins.super_admins'),
|
||||||
|
])
|
||||||
|
->placeholder(trans('blockland-config::blockland-config.admins.demote_from'))
|
||||||
|
->position('bottom')
|
||||||
|
->live(),
|
||||||
|
])
|
||||||
|
->statePath(null)
|
||||||
|
->columns(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadAdmins(): void
|
||||||
|
{
|
||||||
|
$this->loadAdminLists();
|
||||||
|
$this->loadSilentMode();
|
||||||
|
$this->adminsLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadSilentMode(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
$this->silentMode = BlocklandConfig::isSilentMode($server, $fileRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleSilentMode(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
$this->silentMode = ! $this->silentMode;
|
||||||
|
BlocklandConfig::setSilentMode($server, $fileRepository, $this->silentMode);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title($this->silentMode
|
||||||
|
? trans('blockland-config::blockland-config.admins.silent_enabled')
|
||||||
|
: trans('blockland-config::blockland-config.admins.silent_disabled'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadAdminLists(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$lists = BlocklandConfig::getAdminLists($server, $fileRepository);
|
||||||
|
$this->superAdmins = $lists['superAdmins'];
|
||||||
|
$this->admins = $lists['admins'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function askConfirmDemote(): void
|
||||||
|
{
|
||||||
|
$blId = trim($this->demoteBlId);
|
||||||
|
if ($blId === '' || ! ctype_digit($blId)) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.bl_id') . ' must be a numeric BL_ID.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->confirmingDemote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function askRemoveFromList(string $blId, string $list): void
|
||||||
|
{
|
||||||
|
$this->confirmingRemove = ['blId' => $blId, 'list' => $list];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancelConfirm(): void
|
||||||
|
{
|
||||||
|
$this->confirmingDemote = false;
|
||||||
|
$this->confirmingRemove = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirmRemoveFromList(): void
|
||||||
|
{
|
||||||
|
if ($this->confirmingRemove === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$blId = $this->confirmingRemove['blId'];
|
||||||
|
$list = $this->confirmingRemove['list'];
|
||||||
|
$this->confirmingRemove = null;
|
||||||
|
$this->removeFromList($blId, $list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeFromList(string $blId, string $list): void
|
||||||
|
{
|
||||||
|
$superAdmins = $this->superAdmins;
|
||||||
|
$admins = $this->admins;
|
||||||
|
|
||||||
|
$fromLevel = $list === 'super' ? 2 : 1;
|
||||||
|
|
||||||
|
if ($list === 'super') {
|
||||||
|
$superAdmins = array_values(array_filter($superAdmins, fn ($id) => $id !== $blId));
|
||||||
|
} else {
|
||||||
|
$admins = array_values(array_filter($admins, fn ($id) => $id !== $blId));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->saveAndApply($superAdmins, $admins);
|
||||||
|
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
$commands = BlocklandConfig::buildDemoteAdminCommands(
|
||||||
|
[$blId],
|
||||||
|
[$blId => $fromLevel],
|
||||||
|
BlocklandConfig::isSilentMode($server, $fileRepository)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
BlocklandConfig::sendConsoleCommands($server, $fileRepository, $commands);
|
||||||
|
} catch (Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleLabel = $list === 'super' ? trans('blockland-config::blockland-config.admins.super_admins') : trans('blockland-config::blockland-config.admins.admins');
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.prefs_saved'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.removed', ['id' => $blId, 'role' => $roleLabel]))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addByBlId(): void
|
||||||
|
{
|
||||||
|
$blId = trim($this->newBlId);
|
||||||
|
if ($blId === '' || ! ctype_digit($blId)) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.bl_id') . ' must be a numeric BL_ID.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$manual = ! $this->newModeAuto;
|
||||||
|
|
||||||
|
if ($manual) {
|
||||||
|
$level = $this->newRole === 'super' ? 2 : 1;
|
||||||
|
$commands = BlocklandConfig::buildApplyAdminCommands(
|
||||||
|
[$blId],
|
||||||
|
$level,
|
||||||
|
[$blId => 0],
|
||||||
|
manual: true,
|
||||||
|
silent: BlocklandConfig::isSilentMode(Filament::getTenant(), app(DaemonFileRepository::class))
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
BlocklandConfig::sendConsoleCommands(
|
||||||
|
Filament::getTenant(),
|
||||||
|
app(DaemonFileRepository::class),
|
||||||
|
$commands
|
||||||
|
);
|
||||||
|
} catch (Exception) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.server_offline'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$superAdmins = $this->superAdmins;
|
||||||
|
$admins = $this->admins;
|
||||||
|
|
||||||
|
if ($this->newRole === 'super') {
|
||||||
|
$admins = array_values(array_filter($admins, fn ($id) => $id !== $blId));
|
||||||
|
if (! in_array($blId, $superAdmins, true)) {
|
||||||
|
$superAdmins[] = $blId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$superAdmins = array_values(array_filter($superAdmins, fn ($id) => $id !== $blId));
|
||||||
|
if (! in_array($blId, $admins, true)) {
|
||||||
|
$admins[] = $blId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->saveAndApply($superAdmins, $admins);
|
||||||
|
|
||||||
|
$level = $this->newRole === 'super' ? 2 : 1;
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
$commands = BlocklandConfig::buildApplyAdminCommands(
|
||||||
|
[$blId],
|
||||||
|
$level,
|
||||||
|
[$blId => 0],
|
||||||
|
manual: false,
|
||||||
|
silent: BlocklandConfig::isSilentMode($server, $fileRepository)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
BlocklandConfig::sendConsoleCommands($server, $fileRepository, $commands);
|
||||||
|
} catch (Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newBlId = '';
|
||||||
|
|
||||||
|
$role = $this->newRole === 'super' ? 'Super Admin' : 'Admin';
|
||||||
|
$suffix = $manual ? ' ' . trans('blockland-config::blockland-config.admins.session_only') : '';
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.prefs_saved'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.added', ['name' => "BL_ID {$blId}", 'role' => $role . $suffix]))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function demoteByBlId(): void
|
||||||
|
{
|
||||||
|
$blId = trim($this->demoteBlId);
|
||||||
|
if ($blId === '' || ! ctype_digit($blId)) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.bl_id') . ' must be a numeric BL_ID.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fromLevel = $this->demoteFromRole === 'super' ? 2 : 1;
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$commands = BlocklandConfig::buildDemoteAdminCommands(
|
||||||
|
[$blId],
|
||||||
|
[$blId => $fromLevel],
|
||||||
|
BlocklandConfig::isSilentMode($server, $fileRepository)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
BlocklandConfig::sendConsoleCommands($server, $fileRepository, $commands);
|
||||||
|
} catch (Exception) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.admins.server_offline'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->demoteBlId = '';
|
||||||
|
$this->confirmingDemote = false;
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.admins.demoted'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function saveAndApply(array $superAdmins, array $admins): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
BlocklandConfig::saveAdminLists($server, $fileRepository, $superAdmins, $admins);
|
||||||
|
$this->superAdmins = $superAdmins;
|
||||||
|
$this->admins = $admins;
|
||||||
|
|
||||||
|
try {
|
||||||
|
BlocklandConfig::sendConsoleCommands($server, $fileRepository, ['exec("config/server/prefs.cs");']);
|
||||||
|
} catch (Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('silent_mode')
|
||||||
|
->label(trans('blockland-config::blockland-config.admins.silent_mode'))
|
||||||
|
->icon(fn () => $this->silentMode ? 'tabler-volume-off' : 'tabler-volume')
|
||||||
|
->color(fn () => $this->silentMode ? 'success' : 'gray')
|
||||||
|
->tooltip(trans('blockland-config::blockland-config.admins.silent_mode_tooltip'))
|
||||||
|
->action(fn () => $this->toggleSilentMode()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Filament\Server\Pages;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Pelican\BlocklandConfig\Facades\BlocklandConfig;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
|
||||||
|
class BlocklandColorSetPage extends Page
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected string $view = 'blockland-config::pages.colorset-page';
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'tabler-palette';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'blockland-colorset';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 21;
|
||||||
|
|
||||||
|
public array $colorSetPreview = [];
|
||||||
|
|
||||||
|
public int $totalColors = 0;
|
||||||
|
|
||||||
|
public bool $colorSetLoaded = false;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
/** @var Server|null $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::canAccess()
|
||||||
|
&& BlocklandConfig::isBlocklandServer($server)
|
||||||
|
&& BlocklandConfig::hasBlocklandFeature($server, \Pelican\BlocklandConfig\Services\BlocklandConfigService::FEATURE_COLORSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return trans('blockland-config::blockland-config.pages.colorset');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return static::getNavigationLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadColorSet(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$colorSet = BlocklandConfig::getActiveColorSet($server, $fileRepository);
|
||||||
|
$this->colorSetPreview = $colorSet['sections'];
|
||||||
|
$this->totalColors = $colorSet['totalColors'];
|
||||||
|
$this->colorSetLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('activate_colorset')
|
||||||
|
->label(trans('blockland-config::blockland-config.actions.activate_colorset'))
|
||||||
|
->icon('tabler-palette')
|
||||||
|
->color('success')
|
||||||
|
->disabled(fn () => !$this->colorSetLoaded)
|
||||||
|
->form([
|
||||||
|
Select::make('colorset')
|
||||||
|
->label(trans('blockland-config::blockland-config.colorset.select_addon'))
|
||||||
|
->options(fn () => $this->getColorsetOptions())
|
||||||
|
->searchable()
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->modalCancelAction(fn (Action $action) => $action
|
||||||
|
->extraAttributes(['class' => '!bg-white dark:!bg-gray-800 dark:!ring-gray-600 dark:hover:!bg-gray-900 !text-gray-950 dark:!text-white'], merge: true))
|
||||||
|
->action(function (array $data) {
|
||||||
|
$this->activateColorsetAddOn($data['colorset']);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getColorsetOptions(): array
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$addOns = BlocklandConfig::listColorsetAddOns($server, $fileRepository);
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
foreach ($addOns as $addOn) {
|
||||||
|
$key = $addOn['path'] . '|' . ($addOn['isZip'] ? '1' : '0');
|
||||||
|
$options[$key] = $addOn['name'] . ($addOn['isZip'] ? ' (.zip)' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activateColorsetAddOn(string $selection): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
[$path, $isZipStr] = explode('|', $selection, 2);
|
||||||
|
$isZip = $isZipStr === '1';
|
||||||
|
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$result = BlocklandConfig::activateColorset($server, $fileRepository, $path, $isZip);
|
||||||
|
|
||||||
|
if ($result === 'ok') {
|
||||||
|
/** @var DaemonServerRepository $serverRepository */
|
||||||
|
$serverRepository = app(DaemonServerRepository::class);
|
||||||
|
$serverRepository->setServer($server)->power('restart');
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.colorset_activated_restarting'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
$this->redirect(static::getUrl());
|
||||||
|
} elseif ($result === 'too_many_colors') {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.colorset_activate_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.notifications.colorset_too_many_colors', ['max' => BlocklandConfigService::MAX_COLORS]))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.colorset_activate_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.notifications.colorset_not_found'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.colorset_activate_failed'))
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Filament\Server\Pages;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use App\Repositories\Daemon\DaemonServerRepository;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pelican\BlocklandConfig\Facades\BlocklandConfig;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Schema $form
|
||||||
|
*/
|
||||||
|
class BlocklandPrefsPage extends Page
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected string $view = 'blockland-config::pages.prefs-page';
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'tabler-adjustments';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'blockland-prefs';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 22;
|
||||||
|
|
||||||
|
public ?array $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the original pref metadata (key, rawKey, quoted, etc.) for serialization.
|
||||||
|
*
|
||||||
|
* @var array<int, array{key: string, rawKey: string, value: string, quoted: bool}>
|
||||||
|
*/
|
||||||
|
public array $prefsMeta = [];
|
||||||
|
|
||||||
|
public bool $prefsLoaded = false;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
/** @var Server|null $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::canAccess()
|
||||||
|
&& BlocklandConfig::isBlocklandServer($server)
|
||||||
|
&& BlocklandConfig::hasBlocklandFeature($server, \Pelican\BlocklandConfig\Services\BlocklandConfigService::FEATURE_PREFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return trans('blockland-config::blockland-config.pages.preferences');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return static::getNavigationLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadPrefs(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$prefs = BlocklandConfig::getPrefs($server, $fileRepository);
|
||||||
|
$this->prefsMeta = $prefs;
|
||||||
|
$this->prefsLoaded = true;
|
||||||
|
|
||||||
|
$formData = [];
|
||||||
|
foreach ($prefs as $i => $pref) {
|
||||||
|
$fieldType = $this->detectFieldType($pref);
|
||||||
|
$formKey = 'pref_' . $i;
|
||||||
|
|
||||||
|
if ($fieldType === 'boolean') {
|
||||||
|
$formData[$formKey] = in_array($pref['value'], ['1', 'true'], true);
|
||||||
|
} else {
|
||||||
|
$formData[$formKey] = $pref['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->form->fill($formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function detectFieldType(array $pref): string
|
||||||
|
{
|
||||||
|
$key = $pref['key'];
|
||||||
|
$value = $pref['value'];
|
||||||
|
|
||||||
|
$booleanKeys = [
|
||||||
|
'AllowMultiClient', 'AllowColorLoading', 'ETardFilter', 'FallingDamage',
|
||||||
|
'RandomBrickColor', 'ResetOnEmpty', 'WrenchEventsAdminOnly',
|
||||||
|
'ClearEventsOnClientExit', 'UseRTB',
|
||||||
|
'AdminOnly', 'AdminTrustBypass1', 'AdminTrustBypass2',
|
||||||
|
'FillBricksAdminOnly', 'FloatAdminOnly', 'LoadAdminOnly',
|
||||||
|
'PaintAdminOnly', 'PaintFxAdminOnly', 'PlayMenuSounds',
|
||||||
|
'SaveAdminOnly', 'ScatterGhostBricks', 'SelectPublicBricks',
|
||||||
|
'SymTableOnStart', 'WrenchAdminOnly',
|
||||||
|
'AdminOrb', 'AllowRepeat', 'IgnoreRayCasting', 'IgnoreRaycasting',
|
||||||
|
'AdminOrbPainting', 'ColorPick', 'DefaultToPaint', 'ShowBricks',
|
||||||
|
'SuperAdmin',
|
||||||
|
];
|
||||||
|
|
||||||
|
$shortKey = preg_replace('/^.*::/', '', $key);
|
||||||
|
|
||||||
|
foreach ($booleanKeys as $bk) {
|
||||||
|
if ($shortKey === $bk) {
|
||||||
|
return 'boolean';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$pref['quoted'] && is_numeric($value)) {
|
||||||
|
return 'numeric';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/(Password|WelcomeMessage|Info|Name|AutoAdminList|AutoSuperAdminList|ETardList|ConnectionError)$/', $shortKey)) {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHumanLabel(string $key): string
|
||||||
|
{
|
||||||
|
$short = preg_replace('/^[Pp]ref::(Server|Net)::/', '', $key);
|
||||||
|
|
||||||
|
$short = str_replace('::', ' > ', $short);
|
||||||
|
|
||||||
|
return $short;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $form): Schema
|
||||||
|
{
|
||||||
|
if (!$this->prefsLoaded || empty($this->prefsMeta)) {
|
||||||
|
return $form->schema([])->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefs = $this->prefsMeta;
|
||||||
|
$grouped = BlocklandConfig::groupPrefs($prefs);
|
||||||
|
|
||||||
|
$globalIndexMap = [];
|
||||||
|
foreach ($prefs as $i => $pref) {
|
||||||
|
$globalIndexMap[$pref['key']] = $i;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections = [];
|
||||||
|
|
||||||
|
foreach ($grouped as $groupName => $groupPrefs) {
|
||||||
|
$fields = [];
|
||||||
|
|
||||||
|
foreach ($groupPrefs as $pref) {
|
||||||
|
$i = $globalIndexMap[$pref['key']] ?? null;
|
||||||
|
if ($i === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortKey = preg_replace('/^.*::/', '', $pref['key']);
|
||||||
|
if (in_array($shortKey, BlocklandConfigService::PREF_ADMIN_KEYS, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formKey = 'pref_' . $i;
|
||||||
|
$label = $this->getHumanLabel($pref['key']);
|
||||||
|
$fieldType = $this->detectFieldType($pref);
|
||||||
|
$validationRules = BlocklandConfigService::PREF_VALIDATION[$shortKey] ?? null;
|
||||||
|
$restartHint = BlocklandConfig::requiresRestart($pref['key']);
|
||||||
|
|
||||||
|
if ($fieldType === 'boolean') {
|
||||||
|
$fields[] = Toggle::make($formKey)
|
||||||
|
->label($label)
|
||||||
|
->inline(false)
|
||||||
|
->helperText($restartHint ? trans('blockland-config::blockland-config.prefs.restart_hint') : null);
|
||||||
|
} elseif ($fieldType === 'numeric') {
|
||||||
|
$input = TextInput::make($formKey)
|
||||||
|
->label($label)
|
||||||
|
->numeric();
|
||||||
|
if ($validationRules) {
|
||||||
|
if (isset($validationRules['min'])) {
|
||||||
|
$input->minValue($validationRules['min']);
|
||||||
|
}
|
||||||
|
if (isset($validationRules['max'])) {
|
||||||
|
$input->maxValue($validationRules['max']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fields[] = $input->helperText($restartHint ? trans('blockland-config::blockland-config.prefs.restart_hint') : null);
|
||||||
|
} elseif (preg_match('/(WelcomeMessage|Info)$/', $pref['key'])) {
|
||||||
|
$input = Textarea::make($formKey)
|
||||||
|
->label($label)
|
||||||
|
->rows(2)
|
||||||
|
->default('');
|
||||||
|
if ($validationRules && isset($validationRules['maxLength'])) {
|
||||||
|
$input->maxLength($validationRules['maxLength']);
|
||||||
|
}
|
||||||
|
$fields[] = $input->helperText($restartHint ? trans('blockland-config::blockland-config.prefs.restart_hint') : null);
|
||||||
|
} elseif (preg_match('/(Password|SuperAdminPassword|AdminPassword)$/', $pref['key'])) {
|
||||||
|
$input = TextInput::make($formKey)
|
||||||
|
->label($label)
|
||||||
|
->password()
|
||||||
|
->revealable()
|
||||||
|
->default('');
|
||||||
|
if ($validationRules && isset($validationRules['maxLength'])) {
|
||||||
|
$input->maxLength($validationRules['maxLength']);
|
||||||
|
}
|
||||||
|
$fields[] = $input->helperText($restartHint ? trans('blockland-config::blockland-config.prefs.restart_hint') : null);
|
||||||
|
} else {
|
||||||
|
$input = TextInput::make($formKey)
|
||||||
|
->label($label)
|
||||||
|
->default('');
|
||||||
|
if ($validationRules && isset($validationRules['maxLength'])) {
|
||||||
|
$input->maxLength($validationRules['maxLength']);
|
||||||
|
}
|
||||||
|
$fields[] = $input->helperText($restartHint ? trans('blockland-config::blockland-config.prefs.restart_hint') : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($fields) === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$translationKey = 'blockland-config::blockland-config.pref_groups.' . strtolower(str_replace(' ', '_', $groupName));
|
||||||
|
$label = trans($translationKey);
|
||||||
|
if ($label === $translationKey) {
|
||||||
|
$label = Str::title(str_replace('_', ' ', Str::snake($groupName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections[] = Section::make($label)
|
||||||
|
->schema($fields)
|
||||||
|
->collapsible()
|
||||||
|
->collapsed()
|
||||||
|
->columns(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $form
|
||||||
|
->schema($sections)
|
||||||
|
->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Action::make('save')
|
||||||
|
->label(trans('blockland-config::blockland-config.actions.save'))
|
||||||
|
->icon('tabler-device-floppy')
|
||||||
|
->disabled(fn () => !$this->prefsLoaded)
|
||||||
|
->action(function () {
|
||||||
|
$this->savePrefs();
|
||||||
|
}),
|
||||||
|
Action::make('save_and_restart')
|
||||||
|
->label(trans('blockland-config::blockland-config.actions.save_and_restart'))
|
||||||
|
->icon('tabler-refresh')
|
||||||
|
->color('warning')
|
||||||
|
->disabled(fn () => !$this->prefsLoaded)
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading(trans('blockland-config::blockland-config.modals.restart_heading'))
|
||||||
|
->modalDescription(trans('blockland-config::blockland-config.modals.prefs_restart_description'))
|
||||||
|
->action(function () {
|
||||||
|
$this->savePrefs(restart: true);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function savePrefs(bool $restart = false): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$formData = $this->form->getState();
|
||||||
|
$updatedPrefs = [];
|
||||||
|
|
||||||
|
foreach ($this->prefsMeta as $i => $meta) {
|
||||||
|
$shortKey = preg_replace('/^.*::/', '', $meta['key']);
|
||||||
|
if (in_array($shortKey, BlocklandConfigService::PREF_ADMIN_KEYS, true)) {
|
||||||
|
$updatedPrefs[] = [
|
||||||
|
'key' => $meta['key'],
|
||||||
|
'rawKey' => $meta['rawKey'],
|
||||||
|
'value' => $meta['value'],
|
||||||
|
'quoted' => $meta['quoted'],
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formKey = 'pref_' . $i;
|
||||||
|
$fieldType = $this->detectFieldType($meta);
|
||||||
|
|
||||||
|
if ($fieldType === 'boolean') {
|
||||||
|
$newValue = !empty($formData[$formKey]) ? '1' : '0';
|
||||||
|
} else {
|
||||||
|
$newValue = array_key_exists($formKey, $formData)
|
||||||
|
? (string) ($formData[$formKey] ?? '')
|
||||||
|
: $meta['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = BlocklandConfig::validatePrefValue($meta['key'], $newValue);
|
||||||
|
if ($error !== null) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body($error)
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedPrefs[] = [
|
||||||
|
'key' => $meta['key'],
|
||||||
|
'rawKey' => $meta['rawKey'],
|
||||||
|
'value' => $newValue,
|
||||||
|
'quoted' => $meta['quoted'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
BlocklandConfig::savePrefs($server, $fileRepository, $updatedPrefs);
|
||||||
|
|
||||||
|
$oldMeta = $this->prefsMeta;
|
||||||
|
$this->prefsMeta = $updatedPrefs;
|
||||||
|
|
||||||
|
$changedPrefs = [];
|
||||||
|
foreach ($updatedPrefs as $i => $pref) {
|
||||||
|
$shortKey = preg_replace('/^.*::/', '', $pref['key'] ?? '');
|
||||||
|
if (in_array($shortKey, BlocklandConfigService::PREF_ADMIN_KEYS, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$orig = $oldMeta[$i] ?? null;
|
||||||
|
if ($orig === null || (string) ($orig['value'] ?? '') !== (string) ($pref['value'] ?? '')) {
|
||||||
|
$changedPrefs[] = $pref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($restart) {
|
||||||
|
/** @var DaemonServerRepository $serverRepository */
|
||||||
|
$serverRepository = app(DaemonServerRepository::class);
|
||||||
|
$serverRepository->setServer($server)->power('restart');
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.prefs_saved_restarting'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($changedPrefs)) {
|
||||||
|
try {
|
||||||
|
$commands = array_merge(
|
||||||
|
BlocklandConfig::buildPrefConsoleCommands($changedPrefs),
|
||||||
|
['webcom_postserver();']
|
||||||
|
);
|
||||||
|
if (!BlocklandConfig::isSilentMode($server, $fileRepository)) {
|
||||||
|
$commands[] = 'messageAll(\'MsgAdminForce\', "\\c2Server settings have been updated");';
|
||||||
|
}
|
||||||
|
BlocklandConfig::sendConsoleCommands($server, $fileRepository, $commands);
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.prefs_saved'))
|
||||||
|
->body(trans('blockland-config::blockland-config.notifications.prefs_applied_live'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (Exception) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.prefs_saved'))
|
||||||
|
->body(trans('blockland-config::blockland-config.notifications.prefs_restart_to_apply'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.prefs_saved'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.notifications.save_failed'))
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Filament\Server\Pages;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use Exception;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
|
use Livewire\WithFileUploads;
|
||||||
|
use Pelican\BlocklandConfig\Facades\BlocklandConfig;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
|
||||||
|
class BlocklandSaveLoaderPage extends Page
|
||||||
|
{
|
||||||
|
use WithFileUploads;
|
||||||
|
|
||||||
|
protected string $view = 'blockland-config::pages.save-loader-page';
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'tabler-folder-open';
|
||||||
|
|
||||||
|
protected static ?string $slug = 'blockland-save-loader';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 24;
|
||||||
|
|
||||||
|
public bool $savesLoaded = false;
|
||||||
|
|
||||||
|
/** @var array<int, array{name: string, path: string}> */
|
||||||
|
public array $saves = [];
|
||||||
|
|
||||||
|
public ?TemporaryUploadedFile $uploadedFile = null;
|
||||||
|
|
||||||
|
public ?TemporaryUploadedFile $uploadedPreviewFile = null;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
/** @var Server|null $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
if (!$server) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::canAccess()
|
||||||
|
&& BlocklandConfig::isBlocklandServer($server)
|
||||||
|
&& BlocklandConfig::hasBlocklandFeature($server, BlocklandConfigService::FEATURE_SAVE_LOADER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return trans('blockland-config::blockland-config.pages.save_loader');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return static::getNavigationLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadSaves(): void
|
||||||
|
{
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
$saves = BlocklandConfig::listSaveFiles($server, $fileRepository);
|
||||||
|
foreach ($saves as &$save) {
|
||||||
|
if (isset($save['preview_path'])) {
|
||||||
|
$save['preview_url'] = route('api:client:server.blockland-save-preview', ['server' => $server->uuid]) . '?file=' . rawurlencode($save['preview_path']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->saves = $saves;
|
||||||
|
$this->savesLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function copyToTemp(string $path): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
BlocklandConfig::copySaveToTemp($server, $fileRepository, $path);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.copy_success'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.copy_failed'))
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploadSave(): void
|
||||||
|
{
|
||||||
|
$file = $this->uploadedFile ?? null;
|
||||||
|
if (!$file instanceof TemporaryUploadedFile || !$file->isValid()) {
|
||||||
|
if ($file && !$file->isValid()) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.upload_failed'))
|
||||||
|
->body($file->getErrorMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
} else {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.upload_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.save_loader.upload_no_file'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = $file->getClientOriginalName();
|
||||||
|
if (!str_ends_with(strtolower($filename), '.bls')) {
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.upload_failed'))
|
||||||
|
->body(trans('blockland-config::blockland-config.save_loader.upload_invalid_type'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$content = file_get_contents($file->getRealPath());
|
||||||
|
|
||||||
|
/** @var Server $server */
|
||||||
|
$server = Filament::getTenant();
|
||||||
|
|
||||||
|
/** @var DaemonFileRepository $fileRepository */
|
||||||
|
$fileRepository = app(DaemonFileRepository::class);
|
||||||
|
|
||||||
|
BlocklandConfig::uploadSaveFile($server, $fileRepository, $content, $filename);
|
||||||
|
|
||||||
|
$base = substr($filename, 0, -4);
|
||||||
|
$previewFile = $this->uploadedPreviewFile;
|
||||||
|
if ($previewFile instanceof TemporaryUploadedFile && $previewFile->isValid()) {
|
||||||
|
$previewName = $previewFile->getClientOriginalName();
|
||||||
|
$previewLower = strtolower($previewName);
|
||||||
|
if (preg_match('/\.(jpg|jpeg|png)$/', $previewLower)) {
|
||||||
|
$expectedBase = $base;
|
||||||
|
$actualBase = preg_replace('/\.(jpg|jpeg|png)$/i', '', $previewName);
|
||||||
|
if (strcasecmp($actualBase, $expectedBase) === 0) {
|
||||||
|
try {
|
||||||
|
$previewContent = file_get_contents($previewFile->getRealPath());
|
||||||
|
BlocklandConfig::uploadPreviewFile($server, $fileRepository, $previewContent, $previewName);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->uploadedFile = null;
|
||||||
|
$this->uploadedPreviewFile = null;
|
||||||
|
$this->loadSaves();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.upload_success'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
report($e);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(trans('blockland-config::blockland-config.save_loader.upload_failed'))
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeUploadedFile(): void
|
||||||
|
{
|
||||||
|
$this->uploadedFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeUploadedPreviewFile(): void
|
||||||
|
{
|
||||||
|
$this->uploadedPreviewFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Http\Controllers\Api\Client\Servers;
|
||||||
|
|
||||||
|
use App\Enums\SubuserPermission;
|
||||||
|
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Repositories\Daemon\DaemonFileRepository;
|
||||||
|
use Illuminate\Contracts\Routing\ResponseFactory;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
class SavePreviewController extends ClientApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private DaemonFileRepository $fileRepository
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(Request $request, Server $server): Response|ResponseFactory
|
||||||
|
{
|
||||||
|
abort_unless(user()?->can(SubuserPermission::FileReadContent, $server), 403);
|
||||||
|
|
||||||
|
$file = $request->query('file');
|
||||||
|
if (!is_string($file) || $file === '') {
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = rawurldecode($file);
|
||||||
|
if (!str_starts_with($path, BlocklandConfigService::SAVES_DIR . '/')) {
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
$lower = strtolower($path);
|
||||||
|
if (!preg_match('/\.(jpg|jpeg|png)$/', $lower)) {
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$content = $this->fileRepository->setServer($server)->getContent(
|
||||||
|
$path,
|
||||||
|
BlocklandConfigService::MAX_PREVIEW_SIZE_BYTES
|
||||||
|
);
|
||||||
|
} catch (\Exception) {
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$mime = match (strtolower(pathinfo($path, PATHINFO_EXTENSION))) {
|
||||||
|
'jpg', 'jpeg' => 'image/jpeg',
|
||||||
|
'png' => 'image/png',
|
||||||
|
default => 'application/octet-stream',
|
||||||
|
};
|
||||||
|
|
||||||
|
return response($content, 200, [
|
||||||
|
'Content-Type' => $mime,
|
||||||
|
'Cache-Control' => 'private, max-age=300',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Pelican\BlocklandConfig\Services\BlocklandConfigService;
|
||||||
|
|
||||||
|
class BlocklandConfigProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(__DIR__ . '/../../config/blockland-config.php', 'blockland-config');
|
||||||
|
$this->app->singleton(BlocklandConfigService::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->loadViewsFrom(__DIR__ . '/../../resources/views', 'blockland-config');
|
||||||
|
$this->loadTranslationsFrom(__DIR__ . '/../../lang', 'blockland-config');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pelican\BlocklandConfig\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
class BlocklandConfigRoutesProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->routes(function () {
|
||||||
|
Route::middleware(['api', 'client-api', 'throttle:api.client'])
|
||||||
|
->prefix('/api/client')
|
||||||
|
->scopeBindings()
|
||||||
|
->group(plugin_path('blockland-config', 'routes/api-client.php'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user