diff --git a/modules/backend/assets/css/october.css b/modules/backend/assets/css/october.css index 674351cf2..0c1df8bc3 100644 --- a/modules/backend/assets/css/october.css +++ b/modules/backend/assets/css/october.css @@ -7323,6 +7323,9 @@ body { .layout > .layout-row > .layout-cell.min-size { width: 0; } +.layout > .layout-row > .layout-cell.min-height { + height: 0; +} .layout > .layout-row > .layout-cell.center { text-align: center; } @@ -7363,6 +7366,9 @@ body { .layout > .layout-row > .layout-cell.min-size { width: 0; } +.layout > .layout-row > .layout-cell.min-height { + height: 0; +} .layout > .layout-row > .layout-cell.center { text-align: center; } @@ -7411,6 +7417,9 @@ body { .layout > .layout-cell.min-size { width: 0; } +.layout > .layout-cell.min-height { + height: 0; +} .layout > .layout-cell.center { text-align: center; } @@ -11521,6 +11530,9 @@ ul.status-list li span.status.info { .control-breadcrumb + .content-tabs { margin-top: -20px; } +.control-breadcrumb.no-bottom-margin { + margin-bottom: 0; +} body.slim-container .control-breadcrumb { margin-left: 0; margin-right: 0; diff --git a/modules/backend/assets/less/controls/breadcrumb.less b/modules/backend/assets/less/controls/breadcrumb.less index 48316b278..549383a5e 100644 --- a/modules/backend/assets/less/controls/breadcrumb.less +++ b/modules/backend/assets/less/controls/breadcrumb.less @@ -43,6 +43,10 @@ + .content-tabs { margin-top: -20px; } + + &.no-bottom-margin { + margin-bottom: 0; + } } body.slim-container { diff --git a/modules/backend/assets/less/layout/layout.less b/modules/backend/assets/less/layout/layout.less index fb23adf52..985358e88 100644 --- a/modules/backend/assets/less/layout/layout.less +++ b/modules/backend/assets/less/layout/layout.less @@ -68,6 +68,10 @@ body { width: 0; } + &.min-height { + height: 0; + } + &.center { text-align: center; } diff --git a/modules/cms/ServiceProvider.php b/modules/cms/ServiceProvider.php index 2a528b8b4..78557ac8b 100644 --- a/modules/cms/ServiceProvider.php +++ b/modules/cms/ServiceProvider.php @@ -6,6 +6,7 @@ use BackendMenu; use BackendAuth; use Backend\Classes\WidgetManager; use October\Rain\Support\ModuleServiceProvider; +use System\Classes\SettingsManager; class ServiceProvider extends ModuleServiceProvider { @@ -89,6 +90,7 @@ class ServiceProvider extends ModuleServiceProvider 'cms.manage_pages' => ['label' => 'Manage pages', 'tab' => 'Cms'], 'cms.manage_layouts' => ['label' => 'Manage layouts', 'tab' => 'Cms'], 'cms.manage_partials' => ['label' => 'Manage partials', 'tab' => 'Cms'], + 'cms.manage_themes' => ['label' => 'Manage themes', 'tab' => 'Cms'] ]); }); @@ -99,6 +101,21 @@ class ServiceProvider extends ModuleServiceProvider $manager->registerFormWidget('Cms\FormWidgets\Components'); }); + /* + * Register settings + */ + SettingsManager::instance()->registerCallback(function($manager){ + $manager->registerSettingItems('October.Cms', [ + 'theme' => [ + 'label' => 'cms::lang.theme.settings_menu', + 'description' => 'cms::lang.theme.settings_menu_description', + 'category' => 'CMS', + 'icon' => 'icon-picture-o', + 'url' => Backend::URL('cms/theme'), + 'sort' => 200 + ] + ]); + }); } /** diff --git a/modules/cms/assets/css/october.theme-selector.css b/modules/cms/assets/css/october.theme-selector.css new file mode 100644 index 000000000..92d8cca90 --- /dev/null +++ b/modules/cms/assets/css/october.theme-selector.css @@ -0,0 +1,112 @@ +.theme-selector-layout .layout-cell { + padding: 24px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.theme-selector-layout .theme-thumbnail { + width: 288px; + background: #ecf0f1; + border-bottom: 1px solid #e3e7e9; +} +.theme-selector-layout .theme-thumbnail img { + opacity: 0.6; + filter: alpha(opacity=60); + width: 240px; +} +.theme-selector-layout .theme-description { + border-bottom: 1px solid #f2f3f4; +} +.theme-selector-layout .theme-description h3, +.theme-selector-layout .theme-description p { + opacity: 0.6; + filter: alpha(opacity=60); +} +.theme-selector-layout .theme-description h3 { + margin: 0 0 25px 0; + font-size: 28px; + color: #2b3e50; + display: inline-block; +} +.theme-selector-layout .theme-description p.author { + font-size: 13px; + display: inline-block; + color: #808c8d; +} +.theme-selector-layout .theme-description p.description { + color: #2b3e50; + font-size: 14px; + line-height: 180%; + margin-bottom: 30px; +} +.theme-selector-layout .theme-description .controls button i.icon-star { + margin-right: 5px; + color: #f1a84e; + font-size: 16px; + vertical-align: middle; +} +.theme-selector-layout .layout-row.active .theme-thumbnail { + background: #bdc3c7; + border-bottom-color: #bdc3c7; +} +.theme-selector-layout .layout-row.active .thumbnail-container { + position: relative; +} +.theme-selector-layout .layout-row.active .thumbnail-container:after { + content: ''; + display: block; + width: 0; + height: 0; + border-top: 14px solid transparent; + border-bottom: 14px solid transparent; + border-left: 15px solid #bdc3c7; + position: absolute; + right: -35px; + top: 50%; + margin-top: -14px; +} +.theme-selector-layout .layout-row.active .theme-description h3, +.theme-selector-layout .layout-row:hover .theme-description h3, +.theme-selector-layout .layout-row.active .theme-description p, +.theme-selector-layout .layout-row:hover .theme-description p { + opacity: 1; + filter: alpha(opacity=100); +} +.theme-selector-layout .layout-row.active .theme-thumbnail img, +.theme-selector-layout .layout-row:hover .theme-thumbnail img { + opacity: 1; + filter: alpha(opacity=100); +} +.theme-selector-layout .layout-row.last .theme-description, +.theme-selector-layout .layout-row.last .theme-thumbnail { + border-bottom: none!important; +} +.theme-selector-layout .find-more-themes { + background: #ecf0f1; + color: #2b3e50; + text-decoration: none; + display: block; + padding: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.theme-selector-layout .find-more-themes:hover { + background: #1795f1; + color: white; +} +@media (max-width: 768px) { + .theme-selector-layout .layout-cell, + .theme-selector-layout .layout-row { + display: block!important; + width: auto!important; + height: auto!important; + } + .theme-selector-layout .theme-thumbnail img { + width: 100%; + } + .theme-selector-layout .layout-row.links .theme-thumbnail { + background: transparent; + padding: 0; + } +} diff --git a/modules/cms/assets/images/default-theme-preview.png b/modules/cms/assets/images/default-theme-preview.png new file mode 100644 index 000000000..37d340d02 Binary files /dev/null and b/modules/cms/assets/images/default-theme-preview.png differ diff --git a/modules/cms/assets/less/october.theme-selector.less b/modules/cms/assets/less/october.theme-selector.less new file mode 100644 index 000000000..1da56c885 --- /dev/null +++ b/modules/cms/assets/less/october.theme-selector.less @@ -0,0 +1,139 @@ +@import "../../../backend/assets/less/core/boot.less"; + +.theme-selector-layout { + .layout-cell { + padding: 24px; + .box-sizing(border-box); + } + + .theme-thumbnail { + width: 288px; + background: #ecf0f1; + border-bottom: 1px solid #e3e7e9; + + img { + .opacity(0.6); + width: 240px; + } + } + + .theme-description { + border-bottom: 1px solid #f2f3f4; + + h3, p { + .opacity(0.6); + } + + h3 { + margin: 0 0 25px 0; + font-size: 28px; + color: #2b3e50; + display: inline-block; + } + + p.author { + font-size: 13px; + display: inline-block; + color: #808c8d; + } + + p.description { + color: #2b3e50; + font-size: 14px; + line-height: 180%; + margin-bottom: 30px; + } + + .controls { + button i.icon-star { + margin-right: 5px; + color: #f1a84e; + font-size: 16px; + vertical-align: middle; + } + } + } + + .layout-row.active { + .theme-thumbnail { + background: #bdc3c7; + border-bottom-color: #bdc3c7; + + } + + .thumbnail-container { + position: relative; + + &:after { + .triangle(right, 15px, 28px, #bdc3c7); + position: absolute; + right: -35px; + top: 50%; + margin-top: -14px; + } + } + } + + .layout-row { + &.active, &:hover { + .theme-description { + h3, p { + .opacity(1); + } + } + + .theme-thumbnail { + img { + .opacity(1); + } + } + } + + &.last { + .theme-description, .theme-thumbnail { + border-bottom: none!important; + } + } + } + + .find-more-themes { + background: #ecf0f1; + color: #2b3e50; + text-decoration: none; + display: block; + padding: 20px; + .border-radius(4px); + + &:hover { + background: @link-color; + color: white; + } + } +} + +// +// Screen specific +// + +@media (max-width: @screen-sm) { + .theme-selector-layout { + .layout-cell, .layout-row { + display: block!important; + width: auto!important; + height: auto!important; + } + + .theme-thumbnail { + img { + width: 100%; + } + } + + .layout-row.links { + .theme-thumbnail { + background: transparent; + padding: 0; + } + } + } +} \ No newline at end of file diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index 338cc9d89..9e5dc8e77 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -4,7 +4,12 @@ use File; use Lang; use Event; use Config; +use URL; +use Cache; +use DB; +use DirectoryIterator; use System\Classes\SystemException; +use October\Rain\Support\Yaml; /** * This class represents the CMS theme. @@ -21,6 +26,11 @@ class Theme */ protected $dirName; + /** + * @var mixed Keeps the cached configuration file values. + */ + protected $configCache = null; + /** * Loads the theme. */ @@ -84,7 +94,16 @@ class Theme */ public static function getActiveTheme() { - $activeTheme = Config::get('cms.activeTheme'); + $configKey = 'cms.activeTheme'; + + $activeTheme = Config::get($configKey); + + $dbResult = DB::table('system_settings') + ->where('item', '=', $configKey) + ->remember(1440, $configKey) + ->pluck('value'); + if ($dbResult !== null) + $activeTheme = $dbResult; $apiResult = Event::fire('cms.activeTheme', [], true); if ($apiResult !== null) @@ -101,6 +120,20 @@ class Theme return $theme; } + /** + * Sets the active theme. + * The active theme code is stored in the database and overrides the configuration cms.activeTheme parameter. + * @param string $code Specifies the active theme code. + */ + public static function setActiveTheme($code) + { + $configKey = 'cms.activeTheme'; + + DB::table('system_settings')->where('item', '=', $configKey)->delete(); + DB::table('system_settings')->insert(['item'=>$configKey, 'value'=>$code]); + Cache::forget($configKey); + } + /** * Returns the edit theme. * By default the edit theme is loaded from the cms.editTheme parameter, @@ -130,4 +163,75 @@ class Theme return $theme; } + + /** + * Returns a list of all themes. + * @return array Returns an array of the Theme objects. + */ + public static function all() + { + $path = base_path().Config::get('cms.themesDir'); + + $it = new DirectoryIterator($path); + $result = []; + foreach ($it as $fileinfo) { + if ($fileinfo->isDot() ||(substr($fileinfo->getFilename(), 0, 1) == '.')) + continue; + + $theme = new self(); + $theme->load($fileinfo->getFilename()); + + $result[] = $theme; + + + } + + return $result; + } + + /** + * Reads the theme.yaml file and returns the theme configuration values. + * @return array Returns the parsed configuration file values. + */ + public function getConfig() + { + if ($this->configCache !== null) + return $this->configCache; + + $path = $this->getPath().'/theme.yaml'; + if (!File::exists($path)) + return $this->configCache = []; + + return $this->configCache = Yaml::parseFile($path); + } + + /** + * Returns a value from the theme configuration file by its name. + * @param string $name Specifies the configuration parameter name. + * @param mixed $default Specifies the default value to return in case if the parameter doesn't exist in the configuration file. + * @return mixed Returns the parameter value or a default value + */ + public function getConfigValue($name, $default = null) + { + $config = $this->getConfig(); + if (isset($config[$name])) + return $config[$name]; + + return $default; + } + + /** + * Returns the theme preview image URL. + * If the image file doesn't exist returns the placeholder image URL. + * @return string Returns the image URL. + */ + public function getPreviewImageUrl() + { + $previewPath = '/assets/images/theme-preview.png'; + $path = $this->getPath().$previewPath; + if (!File::exists($path)) + return URL::asset('modules/cms/assets/images/default-theme-preview.png'); + + return URL::asset('themes/'.$this->getDirName().$previewPath); + } } diff --git a/modules/cms/controllers/Theme.php b/modules/cms/controllers/Theme.php new file mode 100644 index 000000000..99c72c7be --- /dev/null +++ b/modules/cms/controllers/Theme.php @@ -0,0 +1,50 @@ +addJs('/modules/cms/assets/js/themeselector/themeselector.js', 'core'); + $this->addCss('/modules/cms/assets/css/october.theme-selector.css', 'core'); + + $this->pageTitle = Lang::get('cms::lang.theme.settings_menu'); + BackendMenu::setContext('October.System', 'system', 'settings'); + } + + public function index() + { + + } + + public function index_onSetActiveTheme() + { + CmsTheme::setActiveTheme(Input::get('theme')); + + return [ + '#theme-list' => $this->makePartial('theme_list') + ]; + } +} \ No newline at end of file diff --git a/modules/cms/controllers/theme/_theme_list.htm b/modules/cms/controllers/theme/_theme_list.htm new file mode 100644 index 000000000..f7622ae35 --- /dev/null +++ b/modules/cms/controllers/theme/_theme_list.htm @@ -0,0 +1,51 @@ +$theme): + $isLast = $index == $cnt-1; + $isActive = $activeTheme->getDirName() == $theme->getDirName(); + $author = $theme->getConfigValue('author'); +?> +
= e($theme->getConfigValue('description', 'The theme description is not provided.')) ?>
+