diff --git a/app/Http/Controllers/Admin/DownloadProductController.php b/app/Http/Controllers/Admin/DownloadProductController.php
index e8e7701..9e5a45b 100644
--- a/app/Http/Controllers/Admin/DownloadProductController.php
+++ b/app/Http/Controllers/Admin/DownloadProductController.php
@@ -8,18 +8,23 @@ use Illuminate\Support\Facades\Storage;
final class DownloadProductController extends Controller {
public function index() { return view('admin.download-products.index', ['products'=>DownloadProduct::latest()->get()]); }
public function store(Request $request) {
- $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'required|file|max:512000','is_published'=>'sometimes|boolean']);
+ $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'required|file|max:1048576','is_published'=>'sometimes|boolean']);
$file=$request->file('package');
$path=$file->store('download-products','local');
DownloadProduct::create(['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'is_published'=>$request->boolean('is_published')]);
return back()->with('status','Product added.');
}
public function update(Request $request, DownloadProduct $product) {
- $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'nullable|file|max:512000','is_published'=>'sometimes|boolean']);
+ $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'nullable|file|max:1048576','is_published'=>'sometimes|boolean']);
$changes=['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'is_published'=>$request->boolean('is_published')];
if ($request->hasFile('package')) { $file=$request->file('package'); $path=$file->store('download-products','local'); $old=$product->file_path; $changes+=['file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize()]; $product->update($changes); if($old) Storage::disk('local')->delete($old); }
else $product->update($changes);
return back()->with('status','Product updated.');
}
+ public function publication(Request $request, DownloadProduct $product) {
+ $data = $request->validate(['is_published' => 'required|boolean']);
+ $product->update(['is_published' => (bool) $data['is_published']]);
+ return back()->with('status', $product->is_published ? 'Product published.' : 'Product unpublished.');
+ }
public function destroy(DownloadProduct $product) { if($product->file_path) Storage::disk('local')->delete($product->file_path); $product->delete(); return back()->with('status','Product deleted.'); }
}
diff --git a/app/Http/Controllers/ReleaseController.php b/app/Http/Controllers/ReleaseController.php
index b961246..b51954b 100644
--- a/app/Http/Controllers/ReleaseController.php
+++ b/app/Http/Controllers/ReleaseController.php
@@ -15,9 +15,13 @@ final class ReleaseController extends Controller
{
$latest = SonarDeskRelease::published()->where('is_featured', true)->orderByDesc('published_at')->first()
?? SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->orderByDesc('id')->first();
- $releases = SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);
+ $releases = SonarDeskRelease::published()
+ ->when($latest, fn ($query) => $query->whereKeyNot($latest->getKey()))
+ ->orderByDesc('published_at')->paginate(12);
$settings = $this->settings();
- $products = DownloadProduct::where('is_published', true)->whereNotNull('file_path')->orderBy('name')->get();
+ $products = DownloadProduct::where('is_published', true)->whereNotNull('file_path')
+ ->when($latest, fn ($query) => $query->whereRaw('LOWER(name) <> ?', ['sonardesk']))
+ ->orderBy('name')->get();
return view('downloads.index', compact('latest', 'releases', 'settings', 'products'));
}
diff --git a/resources/views/admin/download-products/index.blade.php b/resources/views/admin/download-products/index.blade.php
index 3ab2973..c63541b 100644
--- a/resources/views/admin/download-products/index.blade.php
+++ b/resources/views/admin/download-products/index.blade.php
@@ -1,12 +1,15 @@
@extends('layouts.app', ['title' => 'Download products — Admin'])
@section('content')
-
-@if(session('status'))
@endif
-@if($errors->any())@foreach($errors->all() as $error)- {{ $error }}
@endforeach
@endif
-
-@foreach($products as $product)
-
-@endforeach
+
@endsection
diff --git a/resources/views/downloads/index.blade.php b/resources/views/downloads/index.blade.php
index 166b5b5..f360b60 100644
--- a/resources/views/downloads/index.blade.php
+++ b/resources/views/downloads/index.blade.php
@@ -2,8 +2,10 @@
@section('fullbleed')
{{ $settings['eyebrow'] }}{{ $settings['title'] }}
{{ $settings['intro'] }}
@if($latest)
✓ Signed distribution✓ SHA-256 verified✓ Secure licence activation
@else
The next SonarDesk release is being prepared.Check back shortly or contact support.
@endif
SonarDesk
Project healthAll systems monitored
NativeBuilt for macOS
Native macOSFast, focused desktop workflow
SonarQubeYour server remains the source of truth
SecurityTokens protected by macOS Keychain
UpdatesProfessional in-app release delivery
-@if($latest)Latest stable releaseSonarDesk {{ $latest->version }}
Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}
Download {{ $latest->version }}{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}
SHA-256{{ $latest->sha256 }}
@endif
+@if($latest)Latest stable releaseSonarDesk {{ $latest->version }}
Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}
Download {{ $latest->version }}{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}
SHA-256 checksum · Verify download
{{ $latest->sha256 }} @endif
@if($products->isNotEmpty())
+Software catalogueMore downloads
Additional tools and installers available from our team.
@foreach($products as $product)
{{ $product->platform ?: 'Software' }}{{ $product->version ? 'v'.$product->version : 'Latest' }}
↓
{{ $product->name }}
{{ $product->description ?: 'Download the latest available package.' }}
@endforeach
+@endif
More downloadsOther products
Explore our other available software and downloads.
@foreach($products as $product)
{{ $product->name }}@if($product->version) {{ $product->version }}@endif{{ $product->description }}@if($product->platform){{ $product->platform }}@endif
@endforeach
@endif
Release archivePrevious versions
Published SonarDesk builds remain available here for customers who need a previous release.
@forelse($releases as $release)
SonarDesk {{ $release->version }}Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}
@empty
No published releases are available yet.
@endforelse
{{ $releases->links() }}
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
index 32426c2..3a379d0 100755
--- a/resources/views/layouts/app.blade.php
+++ b/resources/views/layouts/app.blade.php
@@ -198,6 +198,10 @@
.home-download-showcase{display:grid;grid-template-columns:1.25fr .75fr;gap:36px;align-items:center;margin:0 0 64px;padding:38px;background:linear-gradient(135deg,#f8fafc,#eff6ff);border:1px solid #dbeafe;border-radius:22px}.home-download-copy h2{font-size:38px;letter-spacing:-.035em;margin:7px 0 10px}.home-download-copy p{color:#667085;font-size:16px;max-width:680px}.home-download-meta{background:#0b1220;color:#fff;border-radius:16px;padding:24px;box-shadow:0 18px 40px rgba(15,23,42,.16)}.home-download-meta>span{display:block;color:#93c5fd;text-transform:uppercase;font-size:9px;font-weight:900;letter-spacing:.12em}.home-download-meta>strong{display:block;font-size:24px;margin:7px 0 3px}.home-download-meta>small{color:#94a3b8}.home-download-meta>div{margin-top:20px;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.home-download-meta b,.home-download-meta code{display:block}.home-download-meta b{font-size:9px;color:#64748b;letter-spacing:.1em}.home-download-meta code{font-size:10px;color:#cbd5e1;margin-top:5px}@media(max-width:760px){.home-download-showcase{grid-template-columns:1fr;padding:24px}}
.seo-admin-form{display:grid;gap:18px}.seo-section{padding:28px}.seo-section h2{margin:4px 0 4px;font-size:22px;letter-spacing:-.02em}.seo-section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;padding-bottom:20px;margin-bottom:22px;border-bottom:1px solid var(--line)}.seo-section-head p{margin:0;color:var(--muted);max-width:680px}.seo-badge{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:var(--blue-soft);color:var(--blue-dark);font-size:11px;font-weight:850;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.seo-badge.success{background:var(--green-soft);color:var(--green)}.seo-dns-callout{display:grid;gap:5px;margin-bottom:22px;padding:16px 18px;border:1px solid #bfdbfe;border-radius:12px;background:#eff6ff;color:#1e3a8a}.seo-dns-callout span{color:#475569}.seo-dns-callout code{font-weight:800}.seo-savebar{position:sticky;bottom:18px;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:15px 18px;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.96);backdrop-filter:blur(14px);box-shadow:0 14px 38px rgba(15,23,42,.14)}.seo-savebar strong,.seo-savebar span{display:block}.seo-savebar span{font-size:12px;color:var(--muted);margin-top:2px}.seo-preview{border:1px solid var(--line);border-radius:12px;padding:18px;background:#fff}.seo-preview-label{font-size:10px;font-weight:850;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin-bottom:10px}.seo-preview-title{color:#1a0dab;font-size:19px;line-height:1.3}.seo-preview-url{color:#188038;font-size:12px;margin:4px 0}.seo-preview-description{color:#4d5156;font-size:13px;line-height:1.45}@media(max-width:700px){.seo-section-head,.seo-savebar{align-items:stretch;flex-direction:column}.seo-savebar .btn{width:100%}}
+
+/* Download catalogue and distribution admin */
+.distribution-admin{max-width:1200px;margin:auto;padding-bottom:55px}.distribution-header{display:flex;justify-content:space-between;align-items:end;gap:24px}.distribution-header-actions{display:flex;gap:10px;flex-wrap:wrap}.distribution-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:25px 0}.distribution-summary>div{background:#fff;border:1px solid #e4e7ec;border-radius:14px;padding:18px 20px}.distribution-summary span,.distribution-summary strong{display:block}.distribution-summary span{font-size:12px;color:#667085}.distribution-summary strong{font-size:28px;color:#101828;margin-top:5px}.distribution-notice,.distribution-errors{padding:14px 18px;border-radius:10px;margin:16px 0;background:#ecfdf3;border:1px solid #a6f4c5;color:#067647}.distribution-errors{background:#fef3f2;border-color:#fecdca;color:#b42318}.distribution-table{margin-bottom:22px}.distribution-row-actions{display:flex;gap:14px;justify-content:flex-end;align-items:center;white-space:nowrap}.distribution-row-actions form{margin:0}.dist-action{background:none;border:0;padding:4px 0;color:#245bc6;font-weight:750;font-size:13px;cursor:pointer;text-decoration:none}.dist-unpublish{color:#b54708}.dist-publish{color:#067647}.distribution-panel{background:#fff;border:1px solid #e4e7ec;border-radius:14px;margin:12px 0;overflow:hidden}.distribution-panel>summary{list-style:none;display:flex;justify-content:space-between;align-items:center;gap:12px;cursor:pointer;padding:19px 24px}.distribution-panel>summary::-webkit-details-marker{display:none}.distribution-panel>summary strong,.distribution-panel>summary small{display:block}.distribution-panel>summary strong{font-size:16px;color:#101828}.distribution-panel>summary small{font-size:12px;color:#667085;margin-top:4px}.dist-chevron{font-size:22px;color:#667085}.distribution-panel[open] .dist-chevron{transform:rotate(45deg)}.distribution-form{padding:24px;border-top:1px solid #eef2f6;display:grid;gap:16px}.distribution-form label{display:grid;gap:6px;font-size:13px;font-weight:750;color:#344054}.distribution-form input:not([type=checkbox]),.distribution-form textarea{width:100%;border:1px solid #d0d5dd;border-radius:9px;padding:11px 12px;background:#fff;color:#101828;font:inherit}.distribution-form .form-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.distribution-file{border:1px dashed #cbd5e1;border-radius:12px;padding:16px;background:#f8fafc}.distribution-file small{color:#667085;font-weight:400}.distribution-file input{background:transparent!important;border:0!important;padding-left:0!important}.distribution-check{display:flex!important;align-items:center;gap:12px!important;padding:12px 0}.distribution-check input{width:18px;height:18px;flex:none}.distribution-check strong,.distribution-check small{display:block}.distribution-check small{font-weight:400;color:#667085;margin-top:3px}.distribution-form>.btn{justify-self:start}.distribution-delete{padding:0 24px 22px}.distribution-catalogue{background:#f6f8fc;padding:76px 0;border-top:1px solid #e4e7ec}.distribution-product-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;margin-top:28px}.distribution-product-card{background:#fff;border:1px solid #e4e7ec;border-radius:18px;padding:26px;display:flex;flex-direction:column;box-shadow:0 8px 30px rgba(16,24,40,.04)}.distribution-product-top,.distribution-product-footer{display:flex;justify-content:space-between;align-items:center;gap:12px}.distribution-platform{background:#eff6ff;color:#175cd3;border-radius:20px;padding:5px 10px;font-size:11px;font-weight:800}.distribution-version{font-size:12px;color:#667085}.distribution-product-icon{display:grid;place-items:center;width:48px;height:48px;background:#0b1730;color:#e8bc66;border-radius:12px;font-size:23px;margin:26px 0 12px}.distribution-product-card h3{font-size:22px;letter-spacing:-.03em;margin:0 0 8px;color:#101828}.distribution-product-card p{font-size:14px;line-height:1.6;color:#667085;flex:1;margin:0 0 28px}.distribution-product-footer{border-top:1px solid #eef2f6;padding-top:20px}.distribution-product-footer>span{font-size:12px;color:#667085}.checksum-box summary{cursor:pointer;font-size:12px;font-weight:750;color:#475467}.checksum-box code{display:block;margin-top:12px}.download-primary{background:#e8bc66!important;color:#101828!important}.download-icon{background:#101828!important;color:#e8bc66!important}
+@media(max-width:900px){.distribution-product-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.distribution-header{align-items:start;flex-direction:column}}@media(max-width:640px){.distribution-product-grid,.distribution-summary{grid-template-columns:1fr}.distribution-form .form-grid{grid-template-columns:1fr}.distribution-row-actions{flex-wrap:wrap}.distribution-header-actions{width:100%}.distribution-table .pro-table{min-width:730px}}
@stack('head')
diff --git a/routes/web.php b/routes/web.php
index f1ae5c0..a25dd50 100755
--- a/routes/web.php
+++ b/routes/web.php
@@ -79,6 +79,7 @@ Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name
Route::get('/download-products', [DownloadProductController::class, 'index'])->name('download-products.index');
Route::post('/download-products', [DownloadProductController::class, 'store'])->name('download-products.store');
Route::put('/download-products/{product}', [DownloadProductController::class, 'update'])->name('download-products.update');
+ Route::patch('/download-products/{product}/publication', [DownloadProductController::class, 'publication'])->name('download-products.publication');
Route::delete('/download-products/{product}', [DownloadProductController::class, 'destroy'])->name('download-products.destroy');
Route::get('/releases', [AdminReleaseController::class, 'index'])->name('releases.index');
Route::get('/releases/create', [AdminReleaseController::class, 'create'])->name('releases.create');