Implemented theming. A default "base" theme is provided that all themes can extend and override parts of if necessary without having to define every single screen.

Renamed Photo Perfect to Blue Twilight.
This commit is contained in:
Andy Heathershaw 2016-09-02 10:42:05 +01:00
parent 932f7017dc
commit 8baa0b06e7
91 changed files with 325 additions and 65 deletions

4
.idea/webServers.xml generated
View File

@ -2,8 +2,8 @@
<project version="4">
<component name="WebServers">
<option name="servers">
<webServer id="b14a34b0-0127-4886-964a-7be75a2281ac" name="Development" url="http://photoperfect-dev.andys.eu">
<fileTransfer host="photoperfect-dev.andys.eu" port="22" privateKey="C:\Users\andyheathershaw\.ssh\id_rsa" rootFolder="/srv/www/photoperfect" accessType="SFTP" passphrase="dff4dfcbdfc2df98df92dfe0dfe2df98df9adfdddfcedfdddf8e" username="root" keyPair="true">
<webServer id="b14a34b0-0127-4886-964a-7be75a2281ac" name="Development" url="http://blue-twilight-dev.andys.eu">
<fileTransfer host="andyhaa1.miniserver.com" port="22" privateKey="C:\Users\andyheathershaw\.ssh\id_rsa" rootFolder="/srv/www/blue-twilight-dev" accessType="SFTP" keyPair="true">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" />
</advancedOptions>

35
app/Configuration.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
class Configuration extends Model
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'key', 'value'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'configuration';
}

18
app/Facade/Theme.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Facade;
use Illuminate\Support\Facades\Facade;
class Theme extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'theme';
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Helpers;
use App\Configuration;
class ThemeHelper
{
const DEFAULT_THEME = 'base';
public function current()
{
return $this->getThemeName();
}
public function info()
{
$themeName = $this->getThemeName();
$jsonFile = sprintf('%s/%s/theme.json', $this->getThemeBasePath(), $this->sanitiseThemeName($themeName));
if (file_exists($jsonFile))
{
return json_decode(file_get_contents($jsonFile), true);
}
return array();
}
public function render($viewPath, array $viewData = array())
{
$themeName = $this->getThemeName();
// First see if the current theme has the specified file
$requestedViewFilename = $this->getRealFilePath($themeName, $viewPath);
if (!file_exists($requestedViewFilename))
{
// If it doesn't, revert to the base theme
// TODO allow themes to specify another theme as the parent, and traverse up the theme tree to find the
// relevant file - this allows for sub-themes
$themeName = ThemeHelper::DEFAULT_THEME;
}
return view(sprintf('themes.%s.%s', $themeName, $viewPath), $viewData);
}
private function getRealFilePath($themeName, $viewPath)
{
return sprintf(
'%s/%s/%s.blade.php',
$this->getThemeBasePath(),
$themeName,
str_replace('.', DIRECTORY_SEPARATOR, $viewPath)
);
}
private function getThemeBasePath()
{
return sprintf('%s/resources/views/themes', dirname(dirname(__DIR__)));
}
private function getThemeName()
{
$themeName = ThemeHelper::DEFAULT_THEME;
$currentTheme = Configuration::all()->where('key', 'theme')->first();
if (!is_null($currentTheme))
{
$themeName = $currentTheme->value;
}
return $this->sanitiseThemeName($themeName);
}
private function sanitiseThemeName($themeName)
{
// Ensure nasty people can't try and trick us into traversing the directory tree
return preg_replace('/[\\\.\/]/', '', strtolower($themeName));
}
}

View File

@ -3,6 +3,7 @@
namespace app\Http\Controllers\Admin;
use App\Album;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
use App\Http\Requests;
use Illuminate\Http\Request;
@ -21,7 +22,7 @@ class AlbumController extends Controller
$albums = Album::all()->sortBy('name');
return view('admin.list_albums', [
return Theme::render('admin.list_albums', [
'albums' => $albums
]);
}
@ -35,7 +36,7 @@ class AlbumController extends Controller
{
$this->authorize('admin-access');
return view('admin.create_album');
return Theme::render('admin.create_album');
}
public function delete($id)
@ -44,7 +45,7 @@ class AlbumController extends Controller
$album = $this->loadAlbum($id);
return view('admin.delete_album', ['album' => $album]);
return Theme::render('admin.delete_album', ['album' => $album]);
}
/**
@ -75,7 +76,7 @@ class AlbumController extends Controller
$album = $this->loadAlbum($id);
return view('admin.show_album', ['album' => $album]);
return Theme::render('admin.show_album', ['album' => $album]);
}
/**
@ -90,7 +91,7 @@ class AlbumController extends Controller
$album = $this->loadAlbum($id);
return view('admin.edit_album', ['album' => $album]);
return Theme::render('admin.edit_album', ['album' => $album]);
}
/**
@ -107,7 +108,7 @@ class AlbumController extends Controller
$album = $this->loadAlbum($id);
$album->fromRequest($request)->save();
return view('admin.show_album', ['album' => $album]);
return Theme::render('admin.show_album', ['album' => $album]);
}
/**

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Admin;
use App\Album;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
@ -14,7 +15,7 @@ class DefaultController extends Controller
$albumCount = Album::all()->count();
return view('admin.index', [
return Theme::render('admin.index', [
'album_count' => $albumCount
]);
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
@ -29,4 +30,14 @@ class ForgotPasswordController extends Controller
{
$this->middleware('guest');
}
/**
* Display the form to request a password reset link.
*
* @return \Illuminate\Http\Response
*/
public function showLinkRequestForm()
{
return Theme::render('auth.passwords.email');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
@ -36,4 +37,14 @@ class LoginController extends Controller
{
$this->middleware('guest', ['except' => 'logout']);
}
/**
* Show the application's login form.
*
* @return \Illuminate\Http\Response
*/
public function showLoginForm()
{
return Theme::render('auth.login');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use App\Facade\Theme;
use App\User;
use Validator;
use App\Http\Controllers\Controller;
@ -68,4 +69,14 @@ class RegisterController extends Controller
'password' => bcrypt($data['password']),
]);
}
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm()
{
return Theme::render('auth.register');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
@ -29,4 +30,20 @@ class ResetPasswordController extends Controller
{
$this->middleware('guest');
}
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* @param \Illuminate\Http\Request $request
* @param string|null $token
* @return \Illuminate\Http\Response
*/
public function showResetForm(Request $request, $token = null)
{
return Theme::render('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
}

View File

@ -2,12 +2,13 @@
namespace App\Http\Controllers\Gallery;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
class DefaultController extends Controller
{
public function index()
{
return view('gallery.index');
return Theme::render('gallery.index');
}
}

View File

@ -2,6 +2,9 @@
namespace App\Providers;
use App\Facade\Theme;
use App\Helpers\ThemeHelper;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -13,7 +16,12 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
//
$this->app->singleton('theme', function($app)
{
return new ThemeHelper();
});
$this->addThemeInfoToView();
}
/**
@ -25,4 +33,23 @@ class AppServiceProvider extends ServiceProvider
{
//
}
private function addThemeInfoToView()
{
$themeInfo = Theme::info();
$themeInfoKeys = array('name', 'version');
// Add each valid theme info element to the view - prefixing with theme_
// e.g. $themeInfo['name'] becomes $theme_name in the view
foreach ($themeInfoKeys as $key)
{
if (isset($themeInfo[$key]))
{
View::share('theme_' . $key, $themeInfo[$key]);
}
}
// Also add a theme_url key
View::share('theme_url', sprintf('themes/%s', Theme::current()));
}
}

View File

@ -14,7 +14,7 @@ return [
| any other location as required by the application or its packages.
*/
'name' => 'Photo Perfect',
'name' => 'Blue Twilight',
/*
|--------------------------------------------------------------------------
@ -168,7 +168,6 @@ return [
/*
* Package Service Providers...
*/
Collective\Html\HtmlServiceProvider::class,
/*
@ -227,8 +226,10 @@ return [
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
// Additional aliases added by AH
'Form' => Collective\Html\FormFacade::class,
'Html' => Collective\Html\HtmlFacade::class,
'Theme' => App\Facade\Theme::class,
],
];

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateConfigTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('configuration', function (Blueprint $table) {
$table->increments('id');
$table->string('key');
$table->text('value');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('configuration');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 382 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View File

@ -1,5 +1,6 @@
<?php
return [
'app_name' => 'Blue Twilight',
'nav_admin' => 'Manage',
'nav_admin_control' => 'Control Panel',
'nav_admin_albums' => 'Albums'

View File

@ -1,2 +0,0 @@
@extends('layouts.app')
@section('title', 'Welcome')

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="{{ config('app.name') }} v{{ config('app.version') }} (framework v{{ App::VERSION() }})">
<title>@yield('title') | {{ config('app.name') }}</title>
<base href="{{ url('/') }}">
<link href="bootstrap/css/bootstrap.min.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
<link href="font-awesome/css/font-awesome.min.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
<link href="css/app.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
@stack('styles')
</head>
<body>
@include('partials.navbar')
<div class="container-fluid">
@yield('content')
</div>
<script src="js/jquery.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="bootstrap/js/bootstrap.min.js?v={{ urlencode(config('app.version')) }}"></script>
<script src="js/app.js?v={{ urlencode(config('app.version')) }}"></script>
@stack('scripts')
</body>
</html>

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('title', 'Gallery Admin')
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('title', trans('admin.delete_album', ['name' => $album->name]))
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('title', 'Gallery Admin')
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('title', 'Gallery Admin')
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('title', 'Gallery Admin')
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('title', $album->name)
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('content')
<div class="container">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
<!-- Main Content -->
@section('content')

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('content')
<div class="container">

View File

@ -1,4 +1,4 @@
@extends('layouts.app')
@extends('themes.base.layout')
@section('content')
<div class="container">

View File

@ -0,0 +1,2 @@
@extends('themes.base.layout')
@section('title', 'Welcome')

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="generator" content="{{ config('app.name') }} v{{ config('app.version') }} (framework v{{ App::VERSION() }})">
<title>@yield('title') | @lang('global.app_name')</title>
<base href="{{ url('/') }}">
{{-- Cannot use $theme_url here: if a theme uses the base layout, it would also have to provide all these dependencies! --}}
<link href="themes/base/bootstrap/css/bootstrap.min.css?v={{ urlencode($theme_version) }}" rel="stylesheet">
<link href="themes/base/font-awesome/css/font-awesome.min.css?v={{ urlencode($theme_version) }}" rel="stylesheet">
<link href="themes/base/css/app.css?v={{ urlencode($theme_version) }}" rel="stylesheet">
@stack('styles')
</head>
<body>
@include('themes.base.partials.navbar')
<div class="container-fluid">
@yield('content')
</div>
{{-- Cannot use $theme_url here: if a theme uses the base layout, it would also have to provide all these dependencies! --}}
<script src="themes/base/js/jquery.min.js?v={{ urlencode($theme_version) }}"></script>
<script src="themes/base/bootstrap/js/bootstrap.min.js?v={{ urlencode($theme_version) }}"></script>
<script src="themes/base/js/app.js?v={{ urlencode($theme_version) }}"></script>
@stack('scripts')
</body>
</html>

View File

@ -8,7 +8,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ route('home') }}">{{ config('app.name') }}</a>
<a class="navbar-brand" href="{{ route('home') }}">@lang('global.app_name')</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->

View File

@ -0,0 +1,5 @@
The "base" theme is actually using Bootstrap3, however in the UI the "base" theme is not visible - it's used to provide
other themes a sensible default so they don't have to override every single view file.
Therefore this folder exists to allow users to select a theme called "bootstrap3" to get the default look-and-feel, but
in actual fact, it's driven by the "base" theme.

View File

@ -0,0 +1,6 @@
{
"name": "Bootstrap 3",
"version": "1.0",
"author": "Andy Heathershaw",
"author_email": "andy@andys.eu"
}