BloomFeed Upgrade v0.21

This commit is contained in:
Esteban
2026-07-04 03:07:54 +02:00
parent d73c25cbbb
commit 28ba35da13
36 changed files with 1981 additions and 274 deletions
+5 -5
View File
@@ -6,7 +6,7 @@
<div class="form-status">{{ session('status') }}</div>
@endif
<a href="{{ url()->previous(route('dashboard')) }}" class="back-link">&larr; Retour aux articles</a>
<a href="{{ url()->previous(route('dashboard')) }}" class="back-link">&larr; {{ __('app.back_to_articles') }}</a>
<article class="card reader-article">
<h1 class="reader-title">{{ $article->title }}</h1>
@@ -15,15 +15,15 @@
@if ($article->published_at)
<span>&middot; {{ $article->published_at->diffForHumans() }}</span>
@endif
<a href="{{ $article->link }}" target="_blank" rel="noopener" class="reader-source-link">Voir la source originale &#8599;</a>
<a href="{{ $article->link }}" target="_blank" rel="noopener" class="reader-source-link">{{ __('app.view_original') }} &#8599;</a>
</div>
@if ($article->full_content_error)
<div class="reader-error">
<p>Impossible de récupérer le contenu complet de cet article ({{ $article->full_content_error }}).</p>
<p>{{ __('app.fetch_error', ['error' => $article->full_content_error]) }}</p>
<form method="POST" action="{{ route('articles.retry', $article) }}">
@csrf
<button type="submit" class="btn btn-ghost btn-sm">Réessayer</button>
<button type="submit" class="btn btn-ghost btn-sm">{{ __('app.retry') }}</button>
</form>
@if ($article->excerpt)
<p class="mt-2">{{ $article->excerpt }}</p>
@@ -32,7 +32,7 @@
@elseif ($contentHtml)
<div class="prose">{!! $contentHtml !!}</div>
@else
<p class="text-muted">Récupération du contenu en cours…</p>
<p class="text-muted">{{ __('app.fetching') }}</p>
@endif
</article>
</div>
+9 -9
View File
@@ -1,4 +1,4 @@
@extends('layouts.guest', ['title' => 'Connexion — BloomFeed'])
@extends('layouts.guest', ['title' => __('app.login_title').' — BloomFeed'])
@section('content')
<div class="auth-card">
@@ -6,8 +6,8 @@
<img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light">
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</div>
<h1>Connexion</h1>
<p class="subtitle">Accédez à vos flux et à votre lecture approfondie</p>
<h1>{{ __('app.login_title') }}</h1>
<p class="subtitle">{{ __('app.login_subtitle') }}</p>
@if (session('status'))
<div class="form-status">{{ session('status') }}</div>
@@ -20,20 +20,20 @@
<form method="POST" action="{{ route('login') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="email">Adresse e-mail</label>
<label for="email">{{ __('app.email') }}</label>
<input type="email" name="email" id="email" class="form-input" value="{{ old('email') }}" required autofocus>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<label for="password">{{ __('app.password') }}</label>
<input type="password" name="password" id="password" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Se connecter</button>
<button type="submit" class="btn btn-primary btn-block">{{ __('app.login') }}</button>
</form>
@unless (\App\Models\User::query()->exists())
@if (! \App\Models\User::query()->exists() || \App\Models\Setting::registrationsOpen())
<div class="switch-link">
Pas encore de compte ? <a href="{{ route('register') }}">Créer un compte</a>
{{ __('app.no_account_yet') }} <a href="{{ route('register') }}">{{ __('app.create_account') }}</a>
</div>
@endunless
@endif
</div>
@endsection
+11 -9
View File
@@ -1,4 +1,6 @@
@extends('layouts.guest', ['title' => 'Créer un compte — BloomFeed'])
@extends('layouts.guest', ['title' => __('app.create_account').' — BloomFeed'])
@php($isFirstUser = ! \App\Models\User::query()->exists())
@section('content')
<div class="auth-card">
@@ -6,8 +8,8 @@
<img src="{{ asset('images/logo.png') }}" alt="BloomFeed" class="brand-logo brand-logo-light">
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</div>
<h1>Configurer votre instance</h1>
<p class="subtitle">Ce compte sera le seul compte propriétaire de cette instance BloomFeed</p>
<h1>{{ $isFirstUser ? __('app.register_owner_title') : __('app.register_title') }}</h1>
<p class="subtitle">{{ $isFirstUser ? __('app.register_owner_subtitle') : __('app.register_subtitle') }}</p>
@if ($errors->any())
<div class="form-error mt-1">{{ $errors->first() }}</div>
@@ -16,26 +18,26 @@
<form method="POST" action="{{ route('register') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="name">Nom</label>
<label for="name">{{ __('app.name') }}</label>
<input type="text" name="name" id="name" class="form-input" value="{{ old('name') }}" required autofocus>
</div>
<div class="form-group">
<label for="email">Adresse e-mail</label>
<label for="email">{{ __('app.email') }}</label>
<input type="email" name="email" id="email" class="form-input" value="{{ old('email') }}" required>
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<label for="password">{{ __('app.password') }}</label>
<input type="password" name="password" id="password" class="form-input" required>
</div>
<div class="form-group">
<label for="password_confirmation">Confirmer le mot de passe</label>
<label for="password_confirmation">{{ __('app.confirm_password') }}</label>
<input type="password" name="password_confirmation" id="password_confirmation" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary btn-block">Créer mon compte</button>
<button type="submit" class="btn btn-primary btn-block">{{ __('app.register_button') }}</button>
</form>
<div class="switch-link">
Déjà un compte ? <a href="{{ route('login') }}">Se connecter</a>
{{ __('app.already_account') }} <a href="{{ route('login') }}">{{ __('app.login') }}</a>
</div>
</div>
@endsection
+25 -10
View File
@@ -1,4 +1,4 @@
@extends('layouts.app', ['title' => 'Catalogue — BloomFeed'])
@extends('layouts.app', ['title' => __('app.catalog').' — BloomFeed'])
@section('content')
<div class="main-column" style="width:100%;">
@@ -8,22 +8,37 @@
<div class="market-header">
<div>
<h2 style="margin:0;">Catalogue de flux</h2>
<h2 style="margin:0;">{{ __('app.catalog_title') }}</h2>
<p class="text-muted text-sm" style="margin:4px 0 0;">
Une sélection de flux RSS livrée avec BloomFeed ajoutez-les à votre collection en un clic.
{{ __('app.catalog_lede') }}
</p>
</div>
</div>
@if ($feeds->isEmpty())
<div class="card empty-state">
Le catalogue est vide pour le moment.
{{ __('app.catalog_empty') }}
</div>
@else
<input type="search" id="market-search" class="form-input market-search" placeholder="Rechercher un flux… (nom, description, catégorie)">
@php($languageNames = [
'fr' => 'Français', 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch',
'it' => 'Italiano', 'pt' => 'Português', 'nl' => 'Nederlands', 'ja' => '日本語',
'ru' => 'Русский', 'pl' => 'Polski', 'sv' => 'Svenska',
])
@php($languages = $feeds->flatten(1)->pluck('lang')->unique()->sort()->values())
<div class="market-toolbar">
<input type="search" id="market-search" class="form-input market-search" placeholder="{{ __('app.search_placeholder') }}">
<select id="market-lang" class="form-input market-lang-select">
<option value="">{{ __('app.all_languages') }}</option>
@foreach ($languages as $lang)
<option value="{{ $lang }}">{{ $languageNames[$lang] ?? strtoupper($lang) }}</option>
@endforeach
</select>
</div>
<div class="market-chips" id="market-chips">
<button type="button" class="chip active" data-category="">Tout</button>
<button type="button" class="chip active" data-category="">{{ __('app.all') }}</button>
@foreach ($feeds->keys() as $category)
<button type="button" class="chip" data-category="{{ $category }}">{{ $category }}</button>
@endforeach
@@ -32,7 +47,7 @@
<div class="market-grid" id="market-grid">
@foreach ($feeds as $category => $items)
@foreach ($items as $item)
<div class="market-card" data-category="{{ $category }}" data-search="{{ Str::lower($item['title'].' '.$item['description'].' '.$category) }}">
<div class="market-card" data-category="{{ $category }}" data-lang="{{ $item['lang'] }}" data-search="{{ Str::lower($item['title'].' '.$item['description'].' '.$category) }}">
<div class="market-card-head">
<span class="market-card-title">{{ $item['title'] }}</span>
<span class="chip chip-static">{{ $category }}</span>
@@ -41,13 +56,13 @@
<div class="market-card-foot">
<span class="market-lang">{{ strtoupper($item['lang']) }}</span>
@if (in_array($item['url'], $subscribedUrls))
<span class="market-added"> Ajouté</span>
<span class="market-added"> {{ __('app.added') }}</span>
@else
<form method="POST" action="{{ route('feeds.store') }}">
@csrf
<input type="hidden" name="url" value="{{ $item['url'] }}">
<input type="hidden" name="title" value="{{ $item['title'] }}">
<button type="submit" class="btn btn-primary btn-sm">+ Ajouter</button>
<button type="submit" class="btn btn-primary btn-sm">+ {{ __('app.add') }}</button>
</form>
@endif
</div>
@@ -56,7 +71,7 @@
@endforeach
</div>
<div class="card empty-state" id="market-empty" hidden>Aucun flux ne correspond à votre recherche.</div>
<div class="card empty-state" id="market-empty" hidden>{{ __('app.no_match') }}</div>
@endif
</div>
@endsection
+5 -5
View File
@@ -1,4 +1,4 @@
@extends('layouts.app', ['title' => 'Articles — BloomFeed'])
@extends('layouts.app', ['title' => __('app.articles').' — BloomFeed'])
@section('content')
<div class="sidebar">
@@ -11,11 +11,11 @@
@endif
<div class="flex-between">
<h2 style="margin:0;">Derniers articles</h2>
<h2 style="margin:0;">{{ __('app.latest_articles') }}</h2>
<div class="flex" style="gap:8px;">
<a href="{{ route('dashboard', array_filter(['feed' => request('feed'), 'unread' => request('unread') ? null : 1])) }}"
class="btn btn-ghost btn-sm">
{{ request('unread') ? 'Tout afficher' : 'Non lus uniquement' }}
{{ request('unread') ? __('app.show_all') : __('app.unread_only') }}
</a>
</div>
</div>
@@ -25,9 +25,9 @@
@empty
<div class="card empty-state">
@if ($feeds->isEmpty())
Ajoutez votre premier flux RSS depuis la barre latérale pour commencer.
{{ __('app.empty_no_feeds') }}
@else
Aucun article pour le moment. Essayez d'actualiser vos flux.
{{ __('app.empty_no_articles') }}
@endif
</div>
@endforelse
+11 -11
View File
@@ -1,4 +1,4 @@
@extends('layouts.app', ['title' => 'Mes flux — BloomFeed'])
@extends('layouts.app', ['title' => __('app.my_feeds').' — BloomFeed'])
@section('content')
<div class="main-column" style="width:100%;">
@@ -7,19 +7,19 @@
@endif
<div class="card">
<h2>Ajouter un flux</h2>
<h2>{{ __('app.add_feed') }}</h2>
<form method="POST" action="{{ route('feeds.store') }}" class="inline-form">
@csrf
<input type="url" name="url" class="form-input" placeholder="https://exemple.com/rss.xml" required>
<input type="text" name="title" class="form-input" placeholder="Titre personnalisé (optionnel)" style="max-width:220px;">
<button type="submit" class="btn btn-primary">Ajouter</button>
<input type="url" name="url" class="form-input" placeholder="{{ __('app.feed_url_placeholder') }}" required>
<input type="text" name="title" class="form-input" placeholder="{{ __('app.custom_title_optional') }}" style="max-width:220px;">
<button type="submit" class="btn btn-primary">{{ __('app.add') }}</button>
</form>
@error('url')
<div class="form-error mt-1">{{ $message }}</div>
@enderror
</div>
<h2 class="mt-3">Mes flux</h2>
<h2 class="mt-3">{{ __('app.my_feeds') }}</h2>
@forelse ($feeds as $feed)
<div class="card flex-between">
@@ -27,9 +27,9 @@
<div style="font-weight:600;">{{ $feed->displayTitle() }}</div>
<div class="text-sm text-muted">{{ $feed->url }}</div>
<div class="text-sm text-muted mt-1">
{{ $feed->articles_count }} article(s)
{{ __('app.articles_count', ['count' => $feed->articles_count]) }}
@if ($feed->last_fetched_at)
&middot; actualisé {{ $feed->last_fetched_at->diffForHumans() }}
&middot; {{ __('app.refreshed_ago', ['time' => $feed->last_fetched_at->diffForHumans()]) }}
@endif
@if ($feed->last_fetch_status && $feed->last_fetch_status !== 'ok')
&middot; <span style="color:var(--danger);">{{ $feed->last_fetch_status }}</span>
@@ -39,17 +39,17 @@
<div class="flex" style="gap:8px;">
<form method="POST" action="{{ route('feeds.refresh', $feed) }}">
@csrf
<button type="submit" class="btn btn-ghost btn-sm">Actualiser</button>
<button type="submit" class="btn btn-ghost btn-sm">{{ __('app.refresh') }}</button>
</form>
<form method="POST" action="{{ route('feeds.destroy', $feed) }}">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger btn-sm" data-confirm="Supprimer ce flux et tous ses articles ?">Supprimer</button>
<button type="submit" class="btn btn-danger btn-sm" data-confirm="{{ __('app.confirm_delete_feed') }}">{{ __('app.delete') }}</button>
</form>
</div>
</div>
@empty
<div class="card empty-state">Vous n'avez pas encore ajouté de flux.</div>
<div class="card empty-state">{{ __('app.no_feeds_yet') }}</div>
@endforelse
</div>
@endsection
+14 -17
View File
@@ -1,21 +1,18 @@
@extends('layouts.marketing', ['title' => 'BloomFeed — votre agrégateur RSS auto-hébergé'])
@extends('layouts.marketing', ['title' => 'BloomFeed'])
@section('content')
<section class="hero">
<div class="hero-inner">
<span class="eyebrow">Auto-hébergé &middot; Vie privée</span>
<h1>Vos flux RSS, lus en profondeur, sans jamais quitter BloomFeed.</h1>
<p class="hero-lede">
BloomFeed centralise vos flux préférés et en extrait le contenu complet à la demande,
directement sur votre propre serveur. Aucune donnée envoyée à des tiers.
</p>
<span class="eyebrow">{{ __('app.hero_eyebrow') }}</span>
<h1>{{ __('app.hero_title') }}</h1>
<p class="hero-lede">{{ __('app.hero_lede') }}</p>
<div class="hero-cta">
@if ($hasOwner)
<a href="{{ route('login') }}" class="btn btn-primary btn-lg">Se connecter</a>
@if ($hasOwner && ! \App\Models\Setting::registrationsOpen())
<a href="{{ route('login') }}" class="btn btn-primary btn-lg">{{ __('app.login') }}</a>
@else
<a href="{{ route('register') }}" class="btn btn-primary btn-lg">Créer un compte</a>
<a href="{{ route('register') }}" class="btn btn-primary btn-lg">{{ __('app.create_account') }}</a>
@endif
<a href="{{ route('self-hosting') }}" class="btn btn-ghost btn-lg">Comment l'auto-héberger ?</a>
<a href="{{ route('self-hosting') }}" class="btn btn-ghost btn-lg">{{ __('app.how_to_self_host') }}</a>
</div>
</div>
</section>
@@ -23,18 +20,18 @@
<section class="feature-grid">
<div class="feature-card">
<div class="feature-icon">📰</div>
<h3>Flux personnalisés</h3>
<p>Ajoutez n'importe quel flux RSS ou Atom, organisez-les et suivez vos non-lus en un coup d'œil.</p>
<h3>{{ __('app.feature_feeds_title') }}</h3>
<p>{{ __('app.feature_feeds_text') }}</p>
</div>
<div class="feature-card">
<div class="feature-icon">📖</div>
<h3>Lecture approfondie intégrée</h3>
<p>BloomFeed récupère la page source, en extrait le contenu principal et l'affiche proprement — plus besoin d'ouvrir dix onglets.</p>
<h3>{{ __('app.feature_reader_title') }}</h3>
<p>{{ __('app.feature_reader_text') }}</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<h3>Chez vous, pour vous</h3>
<p>Une instance BloomFeed = un propriétaire. Pas d'inscriptions publiques, pas de tracking, votre serveur, vos données.</p>
<h3>{{ __('app.feature_home_title') }}</h3>
<p>{{ __('app.feature_home_text') }}</p>
</div>
</section>
@endsection
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
+5 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -20,6 +20,10 @@
</script>
</head>
<body>
<div class="guest-topbar">
@include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
</div>
<div class="centered-shell">
@yield('content')
</div>
+10 -9
View File
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="fr">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'BloomFeed' }}</title>
<meta name="description" content="BloomFeed est un agrégateur RSS personnel et auto-hébergé, avec lecture approfondie intégrée.">
<meta name="description" content="{{ __('app.meta_description') }}">
<link rel="icon" href="{{ asset('images/icon.png') }}">
<link rel="apple-touch-icon" href="{{ asset('images/icon.png') }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -27,12 +27,13 @@
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</a>
<div class="navbar-links">
<a href="{{ route('self-hosting') }}" class="{{ request()->routeIs('self-hosting') ? 'active' : '' }}">Self-hosting</a>
<button type="button" class="theme-toggle" data-theme-toggle title="Changer de thème"></button>
@if (\App\Models\User::query()->exists())
<a href="{{ route('login') }}" class="btn btn-primary btn-sm">Se connecter</a>
<a href="{{ route('self-hosting') }}" class="{{ request()->routeIs('self-hosting') ? 'active' : '' }}">{{ __('app.self_hosting') }}</a>
@include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
@if (\App\Models\User::query()->exists() && ! \App\Models\Setting::registrationsOpen())
<a href="{{ route('login') }}" class="btn btn-primary btn-sm">{{ __('app.login') }}</a>
@else
<a href="{{ route('register') }}" class="btn btn-primary btn-sm">Créer un compte</a>
<a href="{{ route('register') }}" class="btn btn-primary btn-sm">{{ __('app.create_account') }}</a>
@endif
</div>
</nav>
@@ -42,8 +43,8 @@
</main>
<footer class="site-footer">
<span>BloomFeed projet open-source, auto-hébergé.</span>
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">Code source</a>
<span>{{ __('app.footer_tagline') }}</span>
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">{{ __('app.source_code') }}</a>
</footer>
<script src="{{ asset('js/theme-toggle.js') }}" defer></script>
@@ -1,9 +1,9 @@
<div class="card">
<div class="sidebar-title">Flux</div>
<div class="sidebar-title">{{ __('app.feeds_label') }}</div>
<ul class="feed-list">
<li>
<a href="{{ route('dashboard') }}" class="{{ ! request('feed') ? 'active' : '' }}">
Tous les articles
{{ __('app.all_articles') }}
</a>
</li>
@foreach ($feeds as $feed)
@@ -21,18 +21,18 @@
<form method="POST" action="{{ route('feeds.store') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="sidebar-url">Ajouter un flux</label>
<input type="url" name="url" id="sidebar-url" class="form-input" placeholder="https://exemple.com/rss.xml" required>
<label for="sidebar-url">{{ __('app.add_feed') }}</label>
<input type="url" name="url" id="sidebar-url" class="form-input" placeholder="{{ __('app.feed_url_placeholder') }}" required>
</div>
<button type="submit" class="btn btn-primary btn-block btn-sm">Ajouter</button>
<button type="submit" class="btn btn-primary btn-block btn-sm">{{ __('app.add') }}</button>
</form>
<form method="POST" action="{{ route('feeds.refreshAll') }}" class="mt-2">
@csrf
<button type="submit" class="btn btn-ghost btn-block btn-sm">Tout actualiser</button>
<button type="submit" class="btn btn-ghost btn-block btn-sm">{{ __('app.refresh_all') }}</button>
</form>
<a href="{{ route('feeds.index') }}" class="text-sm text-muted mt-1" style="display:block; text-align:center;">
Gérer mes flux
{{ __('app.manage_feeds') }}
</a>
</div>
@@ -0,0 +1,8 @@
<form method="POST" action="{{ route('locale.update') }}" class="locale-form">
@csrf
<select name="locale" class="locale-select" onchange="this.form.submit()" title="{{ __('app.language') }}">
@foreach (['fr' => 'Français', 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch'] as $code => $label)
<option value="{{ $code }}" @selected(app()->getLocale() === $code)>{{ $label }}</option>
@endforeach
</select>
</form>
+9 -5
View File
@@ -4,13 +4,17 @@
<img src="{{ asset('images/logo-dark.png') }}" alt="BloomFeed" class="brand-logo brand-logo-dark">
</a>
<div class="navbar-links">
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">Articles</a>
<a href="{{ route('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">Catalogue</a>
<a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">Mes flux</a>
<button type="button" class="theme-toggle" data-theme-toggle title="Changer de thème"></button>
<a href="{{ route('dashboard') }}" class="{{ request()->routeIs('dashboard') ? 'active' : '' }}">{{ __('app.articles') }}</a>
<a href="{{ route('catalog') }}" class="{{ request()->routeIs('catalog') ? 'active' : '' }}">{{ __('app.catalog') }}</a>
<a href="{{ route('feeds.index') }}" class="{{ request()->routeIs('feeds.*') ? 'active' : '' }}">{{ __('app.my_feeds') }}</a>
@if (auth()->user()?->is_owner)
<a href="{{ route('settings') }}" class="{{ request()->routeIs('settings') ? 'active' : '' }}">{{ __('app.settings') }}</a>
@endif
@include('partials.locale-selector')
<button type="button" class="theme-toggle" data-theme-toggle title="{{ __('app.change_theme') }}"></button>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-ghost btn-sm">Déconnexion</button>
<button type="submit" class="btn btn-ghost btn-sm">{{ __('app.logout') }}</button>
</form>
</div>
</nav>
+34 -29
View File
@@ -1,55 +1,60 @@
@extends('layouts.marketing', ['title' => 'Auto-héberger BloomFeed'])
@extends('layouts.marketing', ['title' => __('app.selfhost_title').' — BloomFeed'])
@section('content')
<section class="docs-shell">
<h1>Auto-héberger BloomFeed</h1>
<p class="hero-lede">
BloomFeed est pensé pour être installé sur votre propre serveur, pour vous (ou votre foyer).
Une instance = un seul compte propriétaire. Tout tourne dans Docker : l'application
et la base de données MariaDB.
</p>
<h1>{{ __('app.selfhost_title') }}</h1>
<p class="hero-lede">{{ __('app.selfhost_intro') }}</p>
<div class="card mt-3">
<h2>Prérequis</h2>
<h2>{{ __('app.prereq_title') }}</h2>
<ul class="docs-list">
<li>Un serveur Linux avec <strong>Docker</strong> et le plugin <strong>Docker Compose</strong>.</li>
<li>Le port <strong>7081</strong> libre (modifiable dans <code>docker-compose.yml</code>).</li>
<li>Optionnel : un nom de domaine et un reverse proxy si vous voulez du HTTPS.</li>
<li>{{ __('app.prereq_docker') }}</li>
<li>{{ __('app.prereq_port') }}</li>
<li>{{ __('app.prereq_proxy') }}</li>
</ul>
</div>
<div class="card mt-2">
<h2>Installation</h2>
<h2>{{ __('app.install_title') }}</h2>
<ol class="docs-list docs-list-numbered">
<li>Clonez le dépôt sur votre serveur.</li>
<li>
Lancez le script de déploiement :
<pre class="code-block">bash scriptsite.sh</pre>
Il crée automatiquement un fichier <code>.env</code> (à partir de <code>.env.example</code>) et y génère
des secrets aléatoires (<code>APP_KEY</code>, mots de passe de base de données) s'ils sont absents
ils ne sont jamais régénérés lors des déploiements suivants.
{{ __('app.install_step_clone') }}
<pre class="code-block">git clone https://git.eldorianet.work/esteban/BloomFeed-RSS bloomfeed
cd bloomfeed</pre>
</li>
<li>Le script construit les images et démarre les conteneurs (application, planificateur, MariaDB).</li>
<li>Ouvrez <code>http://votre-serveur:7081</code> et créez le compte propriétaire cette option disparaît ensuite définitivement.</li>
<li>
{{ __('app.install_step_script') }}
<pre class="code-block">bash scriptsite.sh</pre>
{{ __('app.install_env_note') }}
</li>
<li>{{ __('app.install_step_containers') }}</li>
<li>{{ __('app.install_step_owner') }}</li>
</ol>
</div>
<div class="card mt-2">
<h2>Mettre à jour</h2>
<p>Relancez simplement <code>bash scriptsite.sh</code> : il récupère les derniers changements et reconstruit la stack sans toucher à vos données.</p>
<h2>{{ __('app.config_title') }}</h2>
<ul class="docs-list">
<li>{{ __('app.config_registrations') }}</li>
<li>{{ __('app.config_language') }}</li>
<li>{{ __('app.config_catalog') }}</li>
</ul>
</div>
<div class="card mt-2">
<h2>Désinstaller</h2>
<p>
Le script <code>uninstall.sh</code> arrête et supprime proprement la stack Docker.
Il demande une confirmation explicite avant de supprimer vos données (base de données)
et ne supprime jamais le code source sans confirmation séparée.
</p>
<h2>{{ __('app.update_title') }}</h2>
<p>{{ __('app.update_text') }}</p>
<pre class="code-block">bash scriptsite.sh</pre>
</div>
<div class="card mt-2">
<h2>Code source</h2>
<h2>{{ __('app.uninstall_title') }}</h2>
<p>{{ __('app.uninstall_text') }}</p>
<pre class="code-block">bash uninstall.sh</pre>
</div>
<div class="card mt-2">
<h2>{{ __('app.source_title') }}</h2>
<p>
<a href="https://git.eldorianet.work/esteban/BloomFeed-RSS" target="_blank" rel="noopener">git.eldorianet.work/esteban/BloomFeed-RSS</a>
</p>
+106
View File
@@ -0,0 +1,106 @@
@extends('layouts.app', ['title' => __('app.settings').' — BloomFeed'])
@section('content')
<div class="main-column" style="width:100%; max-width:720px; margin:0 auto;">
@if (session('status'))
<div class="form-status">{{ session('status') }}</div>
@endif
<h2 style="margin:0;">{{ __('app.settings_title') }}</h2>
<div class="card">
<h3>{{ __('app.registrations_section') }}</h3>
<p class="text-muted text-sm">{{ __('app.registrations_desc') }}</p>
<div class="flex-between mt-2">
<span class="text-sm" style="font-weight:600; color: {{ $registrationsOpen ? 'var(--green)' : 'var(--text-muted)' }};">
{{ $registrationsOpen ? __('app.registrations_state_open') : __('app.registrations_state_closed') }}
</span>
<form method="POST" action="{{ route('settings.toggleRegistrations') }}">
@csrf
<button type="submit" class="btn {{ $registrationsOpen ? 'btn-danger' : 'btn-primary' }} btn-sm">
{{ $registrationsOpen ? __('app.close_registrations') : __('app.open_registrations') }}
</button>
</form>
</div>
</div>
<div class="card">
<h3>{{ __('app.create_user_section') }}</h3>
<p class="text-muted text-sm">{{ __('app.create_user_desc') }}</p>
@if ($errors->any())
<div class="form-error mt-1">{{ $errors->first() }}</div>
@endif
<form method="POST" action="{{ route('settings.createUser') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="new-name">{{ __('app.name') }}</label>
<input type="text" name="name" id="new-name" class="form-input" value="{{ old('name') }}" required>
</div>
<div class="form-group">
<label for="new-email">{{ __('app.email') }}</label>
<input type="email" name="email" id="new-email" class="form-input" value="{{ old('email') }}" required>
</div>
<div class="form-group">
<label for="new-password">{{ __('app.password') }}</label>
<input type="text" name="password" id="new-password" class="form-input" minlength="8" required autocomplete="off">
</div>
<button type="submit" class="btn btn-primary">{{ __('app.create_account') }}</button>
</form>
</div>
<div class="card">
<h3>{{ __('app.catalog_sources_section') }}</h3>
<p class="text-muted text-sm">{{ __('app.catalog_sources_desc') }}</p>
@error('source_url')
<div class="form-error mt-1">{{ $message }}</div>
@enderror
@if (empty($catalogSources))
<p class="text-muted text-sm mt-2">{{ __('app.no_sources') }}</p>
@else
<ul class="feed-list mt-2">
@foreach ($catalogSources as $source)
<li style="display:flex; align-items:center; justify-content:space-between; gap:12px; padding:8px 10px;">
<span class="text-sm" style="overflow-wrap:anywhere;">{{ $source }}</span>
<form method="POST" action="{{ route('settings.removeSource') }}">
@csrf
@method('DELETE')
<input type="hidden" name="source_url" value="{{ $source }}">
<button type="submit" class="btn btn-danger btn-sm">{{ __('app.remove') }}</button>
</form>
</li>
@endforeach
</ul>
@endif
<form method="POST" action="{{ route('settings.addSource') }}" class="mt-2">
@csrf
<div class="form-group">
<label for="source-url">{{ __('app.source_url_label') }}</label>
<input type="url" name="source_url" id="source-url" class="form-input" placeholder="https://git.example.com/user/repo/raw/branch/main/bloomflux.json" required>
</div>
<button type="submit" class="btn btn-primary btn-sm">{{ __('app.add_source') }}</button>
</form>
</div>
<div class="card">
<h3>{{ __('app.users_section') }}</h3>
<ul class="feed-list mt-1">
@foreach ($users as $user)
<li style="display:flex; align-items:center; justify-content:space-between; padding:8px 10px;">
<span>
<strong>{{ $user->name }}</strong>
<span class="text-muted text-sm">&middot; {{ $user->email }}</span>
</span>
@if ($user->is_owner)
<span class="pill">{{ __('app.owner_badge') }}</span>
@endif
</li>
@endforeach
</ul>
</div>
</div>
@endsection