diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..6a5efa1 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..127fbcd --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +

+ +# Voyager Themes + +This is a theme hook for voyager and the hook system + +![Voyager Themes Admin Page](https://i.imgur.com/uG78r50.png) + +## Installing the hook + +You can use the artisan command below to install this hook + +```bash +php artisan hook:install voyager-themes +``` + +## Adding Themes + +The **voyager-themes** hook will look inside of the `resources/themes` folder for any folder that has a `.json` file inside of it with the same name. *(You can change the theme folder location in the config)* + +As an example if you have a folder called **sample-theme** and inside that folder you have another file called **sample-theme.json** with the following contents: + +``` +{ + "name": "Sample Theme", + "version": "1.0" +} +``` + +Voyager Themes will detect this as a new theme. You can also include a sample screenshot of your theme, which would be **sample-theme.jpg** *(800x500px) for best results* + +In fact, you can checkout the sample-theme repo here: [https://github.com/thedevdojo/sample-theme](https://github.com/thedevdojo/sample-theme) + +You can activate this theme inside of Voyager and then when you want to tell your application which view to load you can use: + +``` +return view('theme::welcome') +``` + +This will then look in the current active theme folder for a new view called `welcome.blade.php` :D + +## Theme Configs + +You may choose to publish a config to your project by running: + +``` +php artisan vendor:publish +``` + +You will want to publish the `voyager-themes-config`, and you will now see a new config located at `config/themes.php`, which will look like the following: + +``` + resource_path('views/themes'), + 'publish_assets' => true + +]; +``` + +Now, you can choose an alternate location for your themes folder. By default this will be put into the `resources/views` folder; however, you can change that to any location you would like. + +Additionally, you can set **publish_assets** to *true* or *false*, if it is set to *true* anytime the themes directory is scanned it will publish the `assets` folder in your theme to the public folder inside a new `themes` folder. Set this to *false* and this will no longer happen. + +## Theme Options + +You can also easily add a number of options by including another file in the theme folder called `options.blade.php` + +![Voyager Theme Options Page](https://i.imgur.com/eAoNt0W.png) + +Inside the `options.blade.php` file you can now add a new field as simple as: + +``` +{!! theme_field('text', 'title') !!} +``` + +This will now add a new **text field** and store it with a **key** of *title*. So, now if you wanted to reference this value anywhere in your theme files you can simple echo it out like so: + +``` +{{ theme('title') }} +``` + +Couldn't be easier, right! + +Take a look at all the following explanation of the `theme_field` function. + +### The theme_field() function + +The `theme_field()` function can be used to display fields in our theme options page. Take a look at the function DEFINITION, EXAMPLE, EXPLANATION, and TYPES OF FIELDS below: + +**DEFINITION:** + + theme_field( + $type, + $key, + $title = '', + $content = '', + $details = '', + $placeholder = '', + $required = 1) + +**EXAMPLE** of a textbox asking for headline: + + {!! theme_field( + 'text', + 'headline', + 'My Aweseome Headline', + '{}', + 'Add your Headline here', + 0) + !!} + +Only the first 2 are arguments are required + + {!! theme_field('text', 'headline') !!} + +**EXPLANATION:** + + $type + This is the type of field you want to display, you can + take a look at all the fields from the TYPES OF FIELDS + section below. + $key + This is the key you want to create to reference the + field in your theme. + $title + This is the title or the label above the field + $content + The current contents or value of the field, if the field + has already been created in the db, the value in the + database will be used instead + $details + The details of the field in JSON. You can find more + info about the details from the following URL: + https://voyager.readme.io/docs/additional-field-options + $placeholder + The placeholder value of the field + $required + Whether or not this field is required + +**TYPES OF FIELDS** + + checkbox, color, date, file, image, multiple_images, + number, password, radio_btn, rich_text_box, code_editor, + markdown_editor, select_dropdown, select_multiple, text, + text_area, timestamp, hidden, coordinates + +--- + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5a5025d --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ + +{ + "name": "Themes", + "description": "This is a themes package for Laravel", + "require": {}, + "autoload": { + "psr-4": { + "Themes\\": "src/" + } + }, + "extra": { + "hook": { + "providers": [ + "Themes\\ThemesServiceProvider" + ] + } + } +} \ No newline at end of file diff --git a/config/themes.php b/config/themes.php new file mode 100644 index 0000000..6ed000e --- /dev/null +++ b/config/themes.php @@ -0,0 +1,8 @@ + resource_path('views/themes'), + 'publish_assets' => true + +]; \ No newline at end of file diff --git a/resources/.DS_Store b/resources/.DS_Store new file mode 100644 index 0000000..b5cab1f Binary files /dev/null and b/resources/.DS_Store differ diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php new file mode 100644 index 0000000..b818327 --- /dev/null +++ b/resources/views/index.blade.php @@ -0,0 +1,252 @@ +@extends('voyager::master') + +@section('css') + +@endsection + +@section('content') + +
+ +
+ +

+ Themes + Choose a theme below +

+ + @if(count($themes) < 1) +
+ Wuh oh! +

It doesn't look like you have any themes available in your theme folder located at

+
+ @endif + +
+
+ +
+ + @if(count($themes) < 1) +
+

No Themes Found

+

That's ok, you can download a sample theme here, or download the default pages here. Make sure to download the theme and place it in your themes folder.

+
+ @endif + + @foreach($themes as $theme) + +
+
+ +
+

{{ $theme->name }}@if(isset($theme->version)){{ 'version ' . $theme->version }}@endif

+ + @if($theme->active) + Active + @else + Activate Theme + @endif + +
+ + +
+
+
+ + @endforeach +
+ +
+
+ +
+ + {{-- Single delete modal --}} + + +
+ +@endsection + +@section('javascript') + + + +@endsection diff --git a/resources/views/options.blade.php b/resources/views/options.blade.php new file mode 100644 index 0000000..207e62d --- /dev/null +++ b/resources/views/options.blade.php @@ -0,0 +1,84 @@ +@extends('voyager::master') + +@section('css') + + + +@endsection + +@section('content') + +
+ +
+ +

+ {{ $theme->name }} Theme Options + Options and settings for the {{ $theme->name }} theme. +

+ +
+
+ + @if(file_exists(config('themes.themes_folder', resource_path('views/themes')) . '/' . $theme->folder . '/options.blade.php')) + folder); } ?> +
+ + @include('themes_folder::' . $theme->folder . '.options') + {{ csrf_field() }} + +
+ @else +

No options file for {{ $theme->name }} theme.

+ @endif + +
+
+ +
+ +
+ +@endsection + +@section('javascript') + +@endsection diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..c859423 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/Http/.DS_Store b/src/Http/.DS_Store new file mode 100644 index 0000000..1466b0a Binary files /dev/null and b/src/Http/.DS_Store differ diff --git a/src/Http/Controllers/ThemesController.php b/src/Http/Controllers/ThemesController.php new file mode 100644 index 0000000..581aeae --- /dev/null +++ b/src/Http/Controllers/ThemesController.php @@ -0,0 +1,240 @@ +themes_folder = config('themes.themes_folder', resource_path('views/themes')); + } + + public function index(){ + + // Anytime the admin visits the theme page we will check if we + // need to add any more themes to the database + $this->installThemes(); + $themes = Theme::all(); + + return view('themes::index', compact('themes')); + } + + private function getThemesFromFolder(){ + $themes = array(); + + if(!file_exists($this->themes_folder)){ + mkdir($this->themes_folder); + } + + $scandirectory = scandir($this->themes_folder); + + if(isset($scandirectory)){ + + foreach($scandirectory as $folder){ + //dd($theme_folder . '/' . $folder . '/' . $folder . '.json'); + $json_file = $this->themes_folder . '/' . $folder . '/' . $folder . '.json'; + if(file_exists($json_file)){ + $themes[$folder] = json_decode(file_get_contents($json_file), true); + $themes[$folder]['folder'] = $folder; + $themes[$folder] = (object)$themes[$folder]; + } + } + + } + + return (object)$themes; + } + + private function installThemes() { + + $themes = $this->getThemesFromFolder(); + + foreach($themes as $theme){ + if(isset($theme->folder)){ + $theme_exists = Theme::where('folder', '=', $theme->folder)->first(); + // If the theme does not exist in the database, then update it. + if(!isset($theme_exists->id)){ + $version = isset($theme->version) ? $theme->version : ''; + Theme::create(['name' => $theme->name, 'folder' => $theme->folder, 'version' => $version]); + if(config('themes.publish_assets', true)){ + $this->publishAssets($theme->folder); + } + } else { + // If it does exist, let's make sure it's been updated + $theme_exists->name = $theme->name; + $theme_exists->version = isset($theme->version) ? $theme->version : ''; + $theme_exists->save(); + if(config('themes.publish_assets', true)){ + $this->publishAssets($theme->folder); + } + } + } + } + } + + public function activate($theme_folder){ + + $theme = Theme::where('folder', '=', $theme_folder)->first(); + + if(isset($theme->id)){ + $this->deactivateThemes(); + $theme->active = 1; + $theme->save(); + return redirect() + ->route("voyager.theme.index") + ->with([ + 'message' => "Successfully activated " . $theme->name . " theme.", + 'alert-type' => 'success', + ]); + } else { + return redirect() + ->route("voyager.theme.index") + ->with([ + 'message' => "Could not find theme " . $theme_folder . ".", + 'alert-type' => 'error', + ]); + } + + } + + public function delete(Request $request){ + $theme = Theme::find($request->id); + if(!isset($theme)){ + return redirect() + ->route("voyager.theme.index") + ->with([ + 'message' => "Could not find theme to delete", + 'alert-type' => 'error', + ]); + } + + $theme_name = $theme->name; + + // if the folder exists delete it + if(file_exists($this->themes_folder.'/'.$theme->folder)){ + File::deleteDirectory($this->themes_folder.'/'.$theme->folder, false); + } + + $theme->delete(); + + return redirect() + ->back() + ->with([ + 'message' => "Successfully deleted theme " . $theme_name, + 'alert-type' => 'success', + ]); + + } + + public function options($theme_folder){ + + $theme = Theme::where('folder', '=', $theme_folder)->first(); + + if(isset($theme->id)){ + + $options = []; + + return view('themes::options', compact('options', 'theme')); + + } else { + return redirect() + ->route("voyager.theme.index") + ->with([ + 'message' => "Could not find theme " . $theme_folder . ".", + 'alert-type' => 'error', + ]); + } + } + + public function options_save(Request $request, $theme_folder){ + $theme = Theme::where('folder', '=', $theme_folder)->first(); + + if(!isset($theme->id)){ + return redirect() + ->route("voyager.theme.index") + ->with([ + 'message' => "Could not find theme " . $theme_folder . ".", + 'alert-type' => 'error', + ]); + } + + foreach($request->all() as $key => $content){ + + // If we have a type checkbox and it is unchecked we need to set a value to null + if($content == 'checkbox'){ + $field = str_replace('_type__theme_field', '', $key); + if(!isset($request->{$field})){ + $request->request->add([$field => null]); + $key = $field; + } + } + + + if(!$this->stringEndsWith($key, '_details__theme_field') && !$this->stringEndsWith($key, '_type__theme_field') && $key != '_token'){ + + $type = $request->{$key.'_type__theme_field'}; + $details = $request->{$key.'_details__theme_field'}; + $row = (object)['field' => $key, 'type' => $type, 'details' => $details]; + + + + $value = $this->getContentBasedOnType($request, 'themes', $row); + + $option = ThemeOptions::where('voyager_theme_id', '=', $theme->id)->where('key', '=', $key)->first(); + + + // If we already have this key with the Theme ID we can update the value + if(isset($option->id)){ + $option->value = $value; + $option->save(); + } else { + ThemeOptions::create(['voyager_theme_id' => $theme->id, 'key' => $key, 'value' => $value]); + } + } + } + + + return redirect() + ->back() + ->with([ + 'message' => "Successfully Saved Theme Options", + 'alert-type' => 'success', + ]); + + + } + + function stringEndsWith($haystack, $needle) + { + $length = strlen($needle); + + return $length === 0 || + (substr($haystack, -$length) === $needle); + } + + private function deactivateThemes(){ + Theme::query()->update(['active' => 0]); + } + + private function publishAssets($theme) { + $theme_path = public_path('themes/'.$theme); + + if(!file_exists($theme_path)){ + if(!file_exists(public_path('themes'))){ + mkdir(public_path('themes')); + } + mkdir($theme_path); + } + + File::copyDirectory($this->themes_folder.'/'.$theme.'/assets', public_path('themes/'.$theme)); + File::copy($this->themes_folder.'/'.$theme.'/'.$theme.'.jpg', public_path('themes/'.$theme.'/'.$theme.'.jpg')); + } +} diff --git a/src/Models/Theme.php b/src/Models/Theme.php new file mode 100644 index 0000000..69d611c --- /dev/null +++ b/src/Models/Theme.php @@ -0,0 +1,16 @@ +hasMany('\Themes\Models\ThemeOptions', 'theme_id'); + } +} diff --git a/src/Models/ThemeOptions.php b/src/Models/ThemeOptions.php new file mode 100644 index 0000000..b6883bb --- /dev/null +++ b/src/Models/ThemeOptions.php @@ -0,0 +1,11 @@ +is(config('voyager.prefix')) || request()->is(config('voyager.prefix').'/*')) { + $this->addThemesTable(); + + app(Dispatcher::class)->listen('voyager.menu.display', function ($menu) { + $this->addThemeMenuItem($menu); + }); + + app(Dispatcher::class)->listen('voyager.admin.routing', function ($router) { + $this->addThemeRoutes($router); + }); + } + + // publish config + $this->publishes([dirname(__DIR__).'/config/themes.php' => config_path('themes.php')], 'themes-config'); + + // load helpers + @include __DIR__.'/helpers.php'; + } + + /** + * Register the menu options and selected theme. + * + * @return void + */ + public function boot() + { + try{ + + $this->loadViewsFrom(__DIR__.'/../resources/views', 'themes'); + + $theme = ''; + + if (Schema::hasTable('themes')) { + $theme = $this->rescue(function () { + return \Themes\Models\Theme::where('active', '=', 1)->first(); + }); + if(Cookie::get('theme')){ + $theme_cookied = \Themes\Models\Theme::where('folder', '=', Cookie::get('theme'))->first(); + if(isset($theme_cookied->id)){ + $theme = $theme_cookied; + } + } + } + + view()->share('theme', $theme); + + $this->themes_folder = config('themes.themes_folder', resource_path('views/themes')); + + $this->loadDynamicMiddleware($this->themes_folder, $theme); + + // Make sure we have an active theme + if (isset($theme)) { + $this->loadViewsFrom($this->themes_folder.'/'.@$theme->folder, 'theme'); + } + $this->loadViewsFrom($this->themes_folder, 'themes_folder'); + + } catch(\Exception $e){ + return $e->getMessage(); + } + } + + /** + * Admin theme routes. + * + * @param $router + */ + public function addThemeRoutes($router) + { + $namespacePrefix = '\\Themes\\Http\\Controllers\\'; + $router->get('themes', ['uses' => $namespacePrefix.'ThemesController@index', 'as' => 'theme.index']); + $router->get('themes/activate/{theme}', ['uses' => $namespacePrefix.'ThemesController@activate', 'as' => 'theme.activate']); + $router->get('themes/options/{theme}', ['uses' => $namespacePrefix.'ThemesController@options', 'as' => 'theme.options']); + $router->post('themes/options/{theme}', ['uses' => $namespacePrefix.'ThemesController@options_save', 'as' => 'theme.options.post']); + $router->get('themes/options', function () { + return redirect(route('voyager.theme.index')); + }); + $router->delete('themes/delete', ['uses' => $namespacePrefix.'ThemesController@delete', 'as' => 'theme.delete']); + } + + /** + * Adds the Theme icon to the admin menu. + * + * @param TCG\Voyager\Models\Menu $menu + */ + public function addThemeMenuItem(Menu $menu) + { + if ($menu->name == 'admin') { + $url = route('voyager.theme.index', [], false); + $menuItem = $menu->items->where('url', $url)->first(); + if (is_null($menuItem)) { + $menu->items->add(MenuItem::create([ + 'menu_id' => $menu->id, + 'url' => $url, + 'title' => 'Themes', + 'target' => '_self', + 'icon_class' => 'voyager-paint-bucket', + 'color' => null, + 'parent_id' => null, + 'order' => 98, + ])); + $this->ensurePermissionExist(); + + return redirect()->back(); + } + } + } + + /** + * Add Permissions for themes if they do not exist yet. + * + * @return none + */ + protected function ensurePermissionExist() + { + $permission = Permission::firstOrNew([ + 'key' => 'browse_themes', + 'table_name' => 'admin', + ]); + if (!$permission->exists) { + $permission->save(); + $role = Role::where('name', 'admin')->first(); + if (!is_null($role)) { + $role->permissions()->attach($permission); + } + } + } + + private function loadDynamicMiddleware($themes_folder, $theme){ + if (empty($theme)) { + return; + } + $middleware_folder = $themes_folder . '/' . $theme->folder . '/middleware'; + if(file_exists( $middleware_folder )){ + $middleware_files = scandir($middleware_folder); + foreach($middleware_files as $middleware){ + if($middleware != '.' && $middleware != '..'){ + include($middleware_folder . '/' . $middleware); + $middleware_classname = 'Themes\\Middleware\\' . str_replace('.php', '', $middleware); + if(class_exists($middleware_classname)){ + // Dynamically Load The Middleware + $this->app->make('Illuminate\Contracts\Http\Kernel')->prependMiddleware($middleware_classname); + } + } + } + } + } + + /** + * Add the necessary Themes tables if they do not exist. + */ + private function addThemesTable() + { + if (!Schema::hasTable('themes')) { + Schema::create('themes', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('folder', 191)->unique(); + $table->boolean('active')->default(false); + $table->string('version')->default(''); + $table->timestamps(); + }); + + Schema::create('theme_options', function (Blueprint $table) { + $table->increments('id'); + $table->integer('theme_id')->unsigned()->index(); + $table->foreign('theme_id')->references('id')->on('themes')->onDelete('cascade'); + $table->string('key'); + $table->text('value')->nullable(); + $table->timestamps(); + }); + } + } + + // Duplicating the rescue function that's available in 5.5, just in case + // A user wants to use this hook with 5.4 + + function rescue(callable $callback, $rescue = null) + { + try { + return $callback(); + } catch (Throwable $e) { + report($e); + return value($rescue); + } + } +} diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 0000000..92b2e39 --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,89 @@ +first(); + + $option_exists = $theme->options->where('key', '=', $key)->first(); + + if(isset($option_exists->value)){ + $content = $option_exists->value; + } + + $row = (object)['required' => $required, 'field' => $key, 'type' => $type, 'details' => $details, 'display_name' => $placeholder]; + $dataTypeContent = (object)[$key => $content]; + $label = ''; + $details = ''; + $type = ''; + return $label . app('voyager')->formField($row, '', $dataTypeContent) . $details . $type . '
'; + } + +} + +if (!function_exists(theme)){ + + function theme($key, $default = ''){ + $theme = \Themes\Models\Theme::where('active', '=', 1)->first(); + + if(Cookie::get('theme')){ + $theme_cookied = \Themes\Models\Theme::where('folder', '=', Cookie::get('theme'))->first(); + if(isset($theme_cookied->id)){ + $theme = $theme_cookied; + } + } + + $value = $theme->options->where('key', '=', $key)->first(); + + if(isset($value)) { + return $value->value; + } + + return $default; + } + +} + +if(!function_exists(theme_folder)){ + function theme_folder($folder_file = ''){ + + if(defined('THEME_FOLDER') && THEME_FOLDER){ + return 'themes/' . THEME_FOLDER . $folder_file; + } + + $theme = \Themes\Models\Theme::where('active', '=', 1)->first(); + + if(Cookie::get('theme')){ + $theme_cookied = \Themes\Models\Theme::where('folder', '=', Cookie::get('theme'))->first(); + if(isset($theme_cookied->id)){ + $theme = $theme_cookied; + } + } + + define('THEME_FOLDER', $theme->folder); + return 'themes/' . $theme->folder . $folder_file; + } +} + +if(!function_exists(theme_folder_url)){ + function theme_folder_url($folder_file = ''){ + + if(defined('THEME_FOLDER') && THEME_FOLDER){ + return url('themes/' . THEME_FOLDER . $folder_file); + } + + $theme = \Themes\Models\Theme::where('active', '=', 1)->first(); + + if(Cookie::get('theme')){ + $theme_cookied = \Themes\Models\Theme::where('folder', '=', Cookie::get('theme'))->first(); + if(isset($theme_cookied->id)){ + $theme = $theme_cookied; + } + } + + define('THEME_FOLDER', $theme->folder); + return url('themes/' . $theme->folder . $folder_file); + } +}