Merge pull request #457 from octobercms/develop

* Build 125
This commit is contained in:
Samuel Georges 2014-07-24 20:26:20 +10:00
commit 61fcd5fd76
52 changed files with 1122 additions and 92 deletions

View File

@ -1,3 +1,16 @@
* **Build 125** (2014-07-24)
- Theme support added.
- Added new Theme picker to the backend via Settings > Front-end theme
- New shorthand method for `$this->getClassExtension('Backend.Behaviors.FormController')` becomes `$this->asExtension('FormController')`.
- Buttons inside a popup support new `data-popup-load-indicator` attribute.
- Added a new config item to disable core updates completely (see config cms.disableCoreUpdates).
- Added a unique alternate favicon to the Back-end area.
* **Build 124** (2014-07-17)
- Improvements to Twig functions and filters.
- URL, HTML and Form helpers are now available in Twig.
- The DataGrid form widget has been moved to a standard widget called Grid.
* **Build 122** (2014-07-15)
- Restyled the CMS tabs

View File

@ -33,6 +33,20 @@ return array(
*/
'disablePlugins' => [],
/*
|--------------------------------------------------------------------------
| Prevents application updates
|--------------------------------------------------------------------------
|
| If using composer or git to download updates to the core files, set this
| value to 'true' to prevent the update gateway from trying to download
| these files again as part of the application update process. Plugins
| and themes will still be downloaded.
|
*/
'disableCoreUpdates' => false,
/*
|--------------------------------------------------------------------------
| Back-end URI prefix

View File

@ -48,7 +48,10 @@ Log::useFiles(storage_path().'/logs/system.log');
App::error(function(Exception $exception, $code)
{
Log::error($exception);
/*
* October uses a custom error handler, see
* System\Classes\ErrorHandler::handleException
*/
});
/*

View File

@ -82,7 +82,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => 'My Settings',
'icon' => 'icon-code',
'url' => Backend::URL('backend/editorpreferences'),
'sort' => 200,
'order' => 600,
'context' => 'mysettings'
],
'backend_preferences' => [
@ -91,7 +91,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => 'My Settings',
'icon' => 'icon-laptop',
'class' => 'Backend\Models\BackendPreferences',
'sort' => 200,
'order' => 500,
'context' => 'mysettings'
],
'myaccount' => [
@ -100,7 +100,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => 'My Settings',
'icon' => 'icon-user',
'url' => Backend::URL('backend/users/myaccount'),
'sort' => 200,
'order' => 400,
'context' => 'mysettings'
],
]);

View File

@ -7271,6 +7271,7 @@ body {
/* The html and body elements cannot have any padding or margin. */
}
body {
webkit-font-smoothing: antialiased;
background: #fafafa;
}
#layout-canvas {
@ -7323,6 +7324,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 +7367,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 +7418,9 @@ body {
.layout > .layout-cell.min-size {
width: 0;
}
.layout > .layout-cell.min-height {
height: 0;
}
.layout > .layout-cell.center {
text-align: center;
}
@ -10506,6 +10516,19 @@ html.cssanimations .cursor-loading-indicator.hide {
.dropdown-menu .dropdown-container > ul li.divider {
margin: 0;
}
.dropdown-menu.pull-right .dropdown-container > ul:after {
left: auto;
right: 15px;
}
.dropdown-menu.pull-right .dropdown-container > ul:before {
left: auto;
right: 14px;
}
.dropdown-menu.pull-right .dropdown-container > ul li.first-item a:hover:after,
.dropdown-menu.pull-right .dropdown-container > ul li.first-item a:focus:after {
left: auto;
right: 15px;
}
.dropdown-menu.top .dropdown-container > ul:after {
content: '';
display: block;
@ -10944,6 +10967,7 @@ body.dropdown-open .dropdown-overlay {
.control-tabs.content-tabs > .tab-content > .tab-pane {
padding-top: 0;
}
.control-tabs.content-tabs > .tab-content > .tab-pane div.list-header,
.control-tabs.content-tabs > .tab-content > .tab-pane div.padded-container,
.control-tabs.content-tabs > .tab-content > .tab-pane div.toolbar-widget {
background: #ffffff;
@ -11518,9 +11542,13 @@ ul.status-list li span.status.info {
.control-breadcrumb li:last-child:after {
content: '';
}
.control-breadcrumb + .content-tabs {
.control-breadcrumb + .content-tabs,
.control-breadcrumb + .padded-container {
margin-top: -20px;
}
.control-breadcrumb.no-bottom-margin {
margin-bottom: 0;
}
body.slim-container .control-breadcrumb {
margin-left: 0;
margin-right: 0;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -101,7 +101,7 @@
// LOADINDICATOR DATA-API
// ==============
$(document)
.on('ajaxPromise', '[data-load-indicator]', function() {
var

View File

@ -38,6 +38,11 @@
this.$modal = this.$target.modal({ show: false, backdrop: false, keyboard: this.options.keyboard })
this.isAjax = this.options.handler || this.options.ajax
/*
* Duplicate the popup reference on the .control-popup container
*/
this.$target.data('oc.popup', this)
/*
* Hook in to BS Modal events
*/
@ -50,7 +55,7 @@
setTimeout(function() { self.$content.empty() }, 500)
}
})
this.$modal.on('show.bs.modal', function(){
self.isOpen = true
self.setBackdrop(true)
@ -156,7 +161,7 @@
this.$backdrop.addClass('in')
this.$backdrop.append($('<div class="popup-loading-indicator modal-content" />'))
this.$backdrop.append($('<div class="modal-content popup-loading-indicator" />'))
}
else if (!val && this.$backdrop) {
this.$backdrop.remove()
@ -168,15 +173,24 @@
if (!this.$backdrop)
return;
var self = this;
var self = this
if (val) {
setTimeout(function(){ self.$backdrop.addClass('loading'); }, 100)
}
}
else {
this.$backdrop.removeClass('loading');
}
}
Popup.prototype.hideLoading = function(val) {
this.setLoading(false)
// Wait for animations to complete
var self = this
setTimeout(function() { self.setBackdrop(false) }, 250)
setTimeout(function() { self.hide() }, 500)
}
Popup.prototype.triggerEvent = function(eventName, params) {
if (!params)
params = [this.$el, this.$modal]
@ -265,4 +279,15 @@
return false
});
$(document)
.on('ajaxPromise', '[data-popup-load-indicator]', function() {
$(this).closest('.control-popup').removeClass('in').popup('setLoading', true)
})
.on('ajaxFail', '[data-popup-load-indicator]', function() {
$(this).closest('.control-popup').addClass('in').popup('setLoading', false)
})
.on('ajaxDone', '[data-popup-load-indicator]', function() {
$(this).closest('.control-popup').popup('hideLoading')
})
}(window.jQuery);

View File

@ -8,11 +8,14 @@
* Supported data attributes:
* - data-trigger-type, values: display, hide, enable, disable
* - data-trigger: a CSS selector for elements that trigger the action (checkboxes)
* - data-trigger-condition, values: checked (more conditions to add later) - determines the condition the elements
* specified in the data-trigger should satisfy in order the condition to be considered as "true".
* - data-trigger-condition, values:
* - checked: determines the condition the elements specified in the data-trigger
* should satisfy in order the condition to be considered as "true".
* - value[somevalue]: determines if the value of data-trigger equals the specified value (somevalue)
* the condition is considered "true".
*
* Example: <input type="button" class="btn disabled"
* data-trigger-type="enable"
* data-trigger-type="enable"
* data-trigger="#cblist input[type=checkbox]"
* data-trigger-condition="checked" ... >
*
@ -28,7 +31,7 @@
var TriggerOn = function (element, options) {
var $el = this.$el = $(element);
this.options = options || {};
if (this.options.triggerCondition === false)
@ -40,10 +43,20 @@
if (this.options.triggerType === false)
throw new Error('Trigger type is not specified.')
if (this.options.triggerCondition == 'checked')
this.triggerCondition = this.options.triggerCondition
if (this.options.triggerCondition.indexOf('value') == 0) {
var match = this.options.triggerCondition.match(/[^[\]]+(?=])/g)
if (match) {
this.triggerConditionValue = match
this.triggerCondition = 'value'
}
}
if (this.triggerCondition == 'checked' || this.triggerCondition == 'value')
$(document).on('change', this.options.trigger, $.proxy(this.onConditionChanged, this))
var self = this;
var self = this
$el.on('oc.triggerOn.update', function(e){
e.stopPropagation()
self.onConditionChanged()
@ -53,8 +66,12 @@
}
TriggerOn.prototype.onConditionChanged = function() {
if (this.options.triggerCondition == 'checked')
this.updateTarget($(this.options.trigger + ':checked').length > 0);
if (this.triggerCondition == 'checked') {
this.updateTarget($(this.options.trigger + ':checked').length > 0)
}
else if (this.triggerCondition == 'value') {
this.updateTarget($(this.options.trigger).val() == this.triggerConditionValue)
}
}
TriggerOn.prototype.updateTarget = function(status) {

View File

@ -5,8 +5,8 @@
margin: -20px -20px 20px -20px;
background-color: @color-breadcrumb-background;
ul {
padding: 0;
ul {
padding: 0;
margin: 0;
}
@ -40,9 +40,13 @@
}
}
+ .content-tabs {
+ .content-tabs, + .padded-container {
margin-top: -20px;
}
&.no-bottom-margin {
margin-bottom: 0;
}
}
body.slim-container {
@ -50,4 +54,4 @@ body.slim-container {
margin-left: 0;
margin-right: 0;
}
}
}

View File

@ -91,8 +91,29 @@
}
}
&.pull-right {
.dropdown-container > ul {
&:after {
left: auto;
right: 15px;
}
&:before {
left: auto;
right: 14px;
}
li.first-item a {
&:hover, &:focus {
&:after {
left: auto;
right: 15px;
}
}
}
}
}
&.top {
.dropdown-container {
.dropdown-container {
> ul {
&:after {
.triangle(down, 15px, 8px, @dropdown-bg);
@ -110,7 +131,7 @@
}
}
.touch .dropdown-menu .dropdown-container > ul li {
.touch .dropdown-menu .dropdown-container > ul li {
a:hover {
color: @dropdown-link-color;
background: white;

View File

@ -64,7 +64,7 @@
background-color: rgba(0,0,0,.2);
.opacity(1);
.popup-loading-indicator {
.popup-loading-indicator {
display: block;
width: 100px;
height: 100px;

View File

@ -293,6 +293,7 @@
> .tab-content > .tab-pane {
padding-top: 0;
div.list-header,
div.padded-container,
div.toolbar-widget {
background: @color-tab-content-active-bg;

View File

@ -28,6 +28,7 @@ body {
}
body {
webkit-font-smoothing: antialiased;
background: @color-body-bg;
}
@ -68,6 +69,10 @@ body {
width: 0;
}
&.min-height {
height: 0;
}
&.center {
text-align: center;
}

View File

@ -70,9 +70,6 @@ class UserPreferencesModel extends SettingsModel
*/
public function beforeModelSave()
{
// Purge the field values from the attributes
$this->model->attributes = array_diff_key($this->model->attributes, $this->fieldValues);
$preferences = UserPreferences::forUser();
list($namespace, $group, $item) = $preferences->parseKey($this->recordCode);
$this->model->item = $item;

View File

@ -379,7 +379,7 @@ class Controller extends Extendable
return Response::make(Lang::get('backend::lang.model.mass_assignment_failed', ['attribute' => $ex->getMessage()]), 500);
}
catch (Exception $ex) {
return Response::make($ex->getMessage(), 500);
return Response::make(sprintf('"%s" on line %s of %s', $ex->getMessage(), $ex->getLine(), $ex->getFile()), 500);
}
}
@ -494,8 +494,11 @@ class Controller extends Extendable
* @param array $params Extra parameters
* @return string
*/
public function makeHintPartial($name, $partial, array $params = [])
public function makeHintPartial($name, $partial = null, array $params = [])
{
if (!$partial)
$partial = $name;
return $this->makeLayoutPartial('hint', [
'hintName' => $name,
'hintPartial' => $partial,

View File

@ -254,11 +254,16 @@ class FormField
/**
* Returns a value suitable for the field name property.
* @param string $arrayName Specify a custom array name
* @return string
*/
public function getName()
public function getName($arrayName = null)
{
if ($this->arrayName)
return $this->arrayName.'['.implode('][', Str::evalHtmlArray($this->columnName)).']';
if ($arrayName === null)
$arrayName = $this->arrayName;
if ($arrayName)
return $arrayName.'['.implode('][', Str::evalHtmlArray($this->columnName)).']';
else
return $this->columnName;
}

View File

@ -1,5 +1,6 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0">
<link rel="icon" type="image/png" href="<?= URL::asset('modules/backend/assets/images/favicon.png') ?>" />
<title data-title-template="<?= empty($this->pageTitleTemplate) ? '%s | October CMS' : e($this->pageTitleTemplate) ?>">
<?= $this->pageTitle ?> | October CMS
</title>

View File

@ -51,15 +51,25 @@ class Grid extends WidgetBase
protected $disableToolbar = false;
/**
* @var mixed Array of data, or callable for data source.
* @var array Provided data set, cannot use with dataLocker or useDataSource.
*/
protected $dataSource;
protected $data;
/**
* @var string HTML element that can [re]store the grid data.
* @var string HTML element that can [re]store the grid data, cannot use with data or useDataSource.
*/
protected $dataLocker;
/**
* @var boolean Get data from AJAX callback (onDataSource), cannot use with data or dataLocker.
*/
protected $useDataSource = false;
/**
* @var boolean Sends an AJAX callback (onDataChanged) any time a field is changed.
*/
protected $monitorChanges = true;
/**
* Initialize the widget, called by the constructor and free from its parameters.
*/
@ -70,8 +80,10 @@ class Grid extends WidgetBase
$this->allowInsert = $this->getConfig('allowInsert', $this->allowInsert);
$this->allowRemove = $this->getConfig('allowRemove', $this->allowRemove);
$this->disableToolbar = $this->getConfig('disableToolbar', $this->disableToolbar);
$this->data = $this->getConfig('data', $this->data);
$this->dataLocker = $this->getConfig('dataLocker', $this->dataLocker);
$this->dataSource = $this->getConfig('dataSource', $this->dataSource);
$this->useDataSource = $this->getConfig('useDataSource', $this->useDataSource);
$this->monitorChanges = $this->getConfig('monitorChanges', $this->monitorChanges);
}
/**
@ -97,7 +109,10 @@ class Grid extends WidgetBase
$this->vars['allowInsert'] = $this->allowInsert;
$this->vars['allowRemove'] = $this->allowRemove;
$this->vars['disableToolbar'] = $this->disableToolbar;
$this->vars['data'] = $this->data;
$this->vars['dataLocker'] = $this->dataLocker;
$this->vars['useDataSource'] = $this->useDataSource;
$this->vars['monitorChanges'] = $this->monitorChanges;
}
protected function makeToolbarWidget()
@ -129,12 +144,32 @@ class Grid extends WidgetBase
return ['result' => $result];
}
public function onDataSource()
public function onDataChanged()
{
if ($this->dataLocker)
if (!$this->monitorChanges)
return;
$result = $this->dataSource;
/*
* Changes array, each array item will contain:
* ['rowData' => [...], 'keyName' => 'changedColumn', 'oldValue' => 'was', 'newValue' => 'is']
*/
$changes = post('changes');
$this->fireEvent('grid.dataChanged', [$changes]);
}
public function onDataSource()
{
if (!$this->useDataSource)
return;
$result = [];
if ($_result = $this->fireEvent('grid.dataSource', [], true))
$result = $_result;
if (!is_array($result))
$result = [];
return ['result' => $result];
}

View File

@ -63,7 +63,6 @@
}
// FORM WIDGET PLUGIN DEFINITION
// ============================
@ -97,7 +96,7 @@
// FORM WIDGET DATA-API
// ==============
$(document).render(function(){
$('[data-control="formwidget"]').formWidget();
})

View File

@ -11,6 +11,7 @@
class="form-control"
autocomplete="off"
maxlength="255"
pattern="\d+"
<?= HTML::attributes($field->attributes) ?>
/>
<?php endif ?>
<?php endif ?>

View File

@ -3,13 +3,16 @@
*
* Data attributes:
* - data-control="datagrid" - enables the plugin on an element
* - data-option="value" - an option with a value
* - data-allow-remove="true" - allow rows to be removed
* - data-autocomplete-handler="onAutocomplete" - AJAX handler for autocomplete values
* - data-data-locker="input#locker" - Input element to store and restore grid data as JSON
* - data-source-handler="onGetData" - AJAX handler for obtaining grid data
*
* JavaScript API:
* $('a#someElement').dataGrid({ option: 'value' })
* $('div#someElement').dataGrid({ option: 'value' })
*
* Dependences:
* - Some other plugin (filename.js)
* Dependences:
* - Handsontable (handsontable.js)
*/
+function ($) { "use strict";
@ -22,13 +25,16 @@
this.options = options
this.$el = $(element)
this.columnHeaders = this.options.columnHeaders
this.staticWidths = this.options.columnWidths
this.gridInstance = null
this.columns = validateColumns(this.options.columns)
// Init
var handsontableOptions = {
colHeaders: this.options.columnHeaders,
colHeaders: function(columnIndex) {
return self.columnHeaders[columnIndex]
},
colWidths: function(columnIndex) {
return self.staticWidths[columnIndex]
},
@ -40,6 +46,19 @@
// rowHeaders: false,
// manualColumnMove: true,
// manualRowMove: true,
afterChange: function(changes, source) {
if (source === 'loadData')
return
/*
* changes - is a 2D array containing information about each of the edited cells
* [ [row, prop, oldVal, newVal], ... ].
*
* source - is one of the strings: "alter", "empty", "edit", "populateFromArray",
* "loadData", "autofill", "paste".
*/
self.$el.trigger('datagrid.change', [changes, source])
},
fillHandle: false,
multiSelect: false,
removeRowPlugin: this.options.allowRemove
@ -48,15 +67,24 @@
if (this.options.autoInsertRows)
handsontableOptions.minSpareRows = 1
if (this.options.dataLocker) {
/*
* Data provided
*/
if (this.options.data) {
handsontableOptions.data = this.options.data
}
/*
* Data from a data locker
*/
else if (this.options.dataLocker) {
/*
* Event to update the data locker
*/
this.$dataLocker = $(this.options.dataLocker)
handsontableOptions.afterChange = function(changes, source) {
self.$el.on('datagrid.change', function(event, eventData) {
if (!self.gridInstance) return
self.$dataLocker.val(JSON.stringify(self.getData()))
}
})
/*
* Populate existing data
@ -68,14 +96,44 @@
delete handsontableOptions.data
}
}
/*
* Data from an AJAX data source
*/
else if (this.options.sourceHandler) {
$.request(self.options.sourceHandler, {
success: function(data, textStatus, jqXHR){
self.gridInstance.loadData(data.result)
self.refreshDataSource()
}
/*
* Monitor for data changes
*/
if (this.options.changeHandler) {
self.$el.on('datagrid.change', function(event, changes, source) {
var changeData = [];
$.each(changes, function(index, change){
var changeObj = {}
changeObj.keyName = change[1]
changeObj.oldValue = change[2]
changeObj.newValue = change[3]
if (changeObj.oldValue == changeObj.newValue)
return; // continue
changeObj.rowData = self.getDataAtRow(change[0])
changeData.push(changeObj)
})
if (changeData.length > 0) {
self.$el.request(self.options.changeHandler, {
data: { changes: changeData }
})
}
})
}
/*
* Create up Handson table and validate columns
*/
this.$el.handsontable(handsontableOptions)
this.gridInstance = this.$el.handsontable('getInstance')
@ -103,6 +161,9 @@
return columns
}
/*
* Auto complete
*/
var autocompleteLastQuery = '',
autocompleteInterval = 300,
autocompleteInputTimer
@ -137,7 +198,10 @@
}
DataGrid.DEFAULTS = {
data: null,
dataLocker: null,
sourceHandler: null,
changeHandler: null,
startRows: 1,
minRows: 1,
autoInsertRows: false,
@ -145,11 +209,14 @@
columnWidths: null,
columns: null,
autocompleteHandler: null,
sourceHandler: null,
allowRemove: true,
confirmMessage: 'Are you sure?'
}
DataGrid.prototype.setHeaderValue = function(index, value) {
this.columnHeaders[index] = value
}
DataGrid.prototype.getDataAtRow = function(row) {
if (!row && row !== 0)
row = this.getSelectedRow()
@ -157,6 +224,18 @@
return $.extend(true, {}, this.gridInstance.getDataAtRow(row))
}
DataGrid.prototype.refreshDataSource = function() {
var self = this
this.$el.request(self.options.sourceHandler, {
success: function(data, textStatus, jqXHR){
self.setData(data.result)
}
})
}
DataGrid.prototype.setData = function(data) {
this.gridInstance.loadData(data)
}
DataGrid.prototype.getData = function() {
var self = this,
results = [],

View File

@ -124,9 +124,6 @@
.handsontable tbody th:last-of-type {
border-right: 1px solid #e2e2e2 !important;
}
.handsontable tbody td:first-of-type.currentRow {
border-left: 3px solid #ff9933;
}
.handsontable th.active {
/*background-color: #CCC;*/
color: #666;

View File

@ -15,7 +15,12 @@
* - Use autocomplete plugin instead of typeahead
* - Custom checkboxes
* - Removed native scrollbars
*
*
* @todo
* - Add a Column strategy for even distribution of 0 width columns
* - Replace dragdealer with October scrollbars
* - Explore mobile support, currently non-existent
*
*/
var Handsontable = { //class namespace

View File

@ -147,9 +147,13 @@
border-right: 1px solid @color-handsontable-border !important;
}
td:first-of-type.currentRow {
border-left: 3px solid @color-list-stripe-active;
}
//td:first-of-type {
// border-left: 3px solid transparent;
//}
// td:first-of-type.currentRow {
// border-left: 3px solid @color-list-stripe-active;
// }
}
th.active {

View File

@ -4,20 +4,22 @@
<?php endif ?>
<div
id="<?= $this->getId('grid') ?>"
id="<?= $this->getId() ?>"
style="width:100%"
class="control-datagrid"
data-control="datagrid"
data-allow-remove="<?= $allowRemove ? 'true' : 'false' ?>"
<?php if ($dataLocker): ?>data-data-locker="<?= $dataLocker ?>"<?php endif ?>
data-autocomplete-handler="<?= $this->getEventHandler('onAutocomplete') ?>"
data-source-handler="<?= $this->getEventHandler('onDataSource') ?>"
<?php if ($dataLocker): ?>data-data-locker="<?= $dataLocker ?>"<?php endif ?>
<?php if ($useDataSource): ?>data-source-handler="<?= $this->getEventHandler('onDataSource') ?>"<?php endif ?>
<?php if ($monitorChanges): ?>data-change-handler="<?= $this->getEventHandler('onDataChanged') ?>"<?php endif ?>
></div>
</div>
<script>
$('#<?= $this->getId('grid') ?>')
$('#<?= $this->getId() ?>')
.data('columns', <?= json_encode($columnDefinitions) ?>)
.data('columnHeaders', <?= json_encode($columnHeaders) ?>)
.data('columnWidths', <?= json_encode($columnWidths) ?>)
<?php if ($data): ?>.data('data', <?= json_encode($data) ?>)<?php endif ?>
</script>

View File

@ -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/themes'),
'order' => 200
]
]);
});
}
/**

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -93,6 +93,9 @@ class Controller extends BaseController
public function __construct($theme = null)
{
$this->theme = $theme ? $theme : Theme::getActiveTheme();
if (!$this->theme)
throw new CmsException(Lang::get('cms::lang.theme.active.not_found'));
$this->assetPath = Config::get('cms.themesDir').'/'.$this->theme->getDirName();
$this->router = new Router($this->theme);
$this->initTwigEnvironment();

View File

@ -205,8 +205,12 @@ class Router
*/
$pages = $this->theme->listPages();
$map = [];
foreach ($pages as $page)
foreach ($pages as $page) {
if (!$page->url)
continue;
$map[] = ['file' => $page->getFileName(), 'pattern' => $page->url];
}
self::$urlMap = $map;
if ($cacheable)

View File

@ -1,10 +1,15 @@
<?php namespace Cms\Classes;
use URL;
use File;
use Lang;
use Cache;
use Event;
use Config;
use October\Rain\Support\Yaml;
use System\Models\Parameters;
use System\Classes\SystemException;
use DirectoryIterator;
/**
* 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.
*/
@ -58,7 +68,7 @@ class Theme
*/
public static function exists($dirName)
{
$theme = new self;
$theme = new static;
$path = $theme->getPath($dirName);
return File::isDirectory($path);
@ -84,8 +94,17 @@ class Theme
*/
public static function getActiveTheme()
{
$paramKey = 'cms::theme.active';
$activeTheme = Config::get('cms.activeTheme');
$dbResult = Parameters::findRecord($paramKey)
->remember(1440, $paramKey)
->pluck('value')
;
if ($dbResult !== null)
$activeTheme = $dbResult;
$apiResult = Event::fire('cms.activeTheme', [], true);
if ($apiResult !== null)
$activeTheme = $apiResult;
@ -93,7 +112,7 @@ class Theme
if (!strlen($activeTheme))
throw new SystemException(Lang::get('cms::lang.theme.active.not_set'));
$theme = new self();
$theme = new static;
$theme->load($activeTheme);
if (!File::isDirectory($theme->getPath()))
return null;
@ -101,6 +120,18 @@ 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)
{
$paramKey = 'cms::theme.active';
Parameters::set($paramKey, $code);
Cache::forget($paramKey);
}
/**
* Returns the edit theme.
* By default the edit theme is loaded from the cms.editTheme parameter,
@ -123,11 +154,81 @@ class Theme
if (!strlen($editTheme))
throw new SystemException(Lang::get('cms::lang.theme.edit.not_set'));
$theme = new self();
$theme = new static;
$theme->load($editTheme);
if (!File::isDirectory($theme->getPath()))
return null;
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);
$it->rewind();
$result = [];
foreach ($it as $fileinfo) {
if (!$fileinfo->isDir() || $fileinfo->isDot())
continue;
$theme = new static;
$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);
}
}

View File

@ -0,0 +1,49 @@
<?php namespace Cms\Controllers;
use Lang;
use Config;
use BackendMenu;
use Input;
use Backend\Classes\Controller;
use Cms\Classes\Theme as CmsTheme;
/**
* Theme selector controller
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*
*/
class Themes extends Controller
{
public $requiredPermissions = ['cms.manage_themes'];
public $bodyClass = 'slim-container';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$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')
];
}
}

View File

@ -0,0 +1,51 @@
<?php
$themes = Cms\Classes\Theme::all();
$activeTheme = Cms\Classes\Theme::getActiveTheme();
$cnt = count($themes);
foreach ($themes as $index=>$theme):
$isLast = $index == $cnt-1;
$isActive = $activeTheme->getDirName() == $theme->getDirName();
$author = $theme->getConfigValue('author');
?>
<div class="layout-row <?= $isLast ? 'last' : null ?> min-size <?= $isActive ? 'active' : null ?>">
<div class="layout-cell min-height theme-thumbnail">
<div class="thumbnail-container"><img src="<?= $theme->getPreviewImageUrl() ?>"/></div>
</div>
<div class="layout-cell min-height theme-description">
<h3><?= e($theme->getConfigValue('name', $theme->getDirName())) ?></h3>
<?php if (strlen($author)): ?>
<p class="author">by <a href="<?= e($theme->getConfigValue('authorUrl', '#')) ?>"><?= e($author) ?></a></p>
<?php endif ?>
<p class="description"><?= e($theme->getConfigValue('description', 'The theme description is not provided.')) ?></p>
<div class="controls">
<?php if ($isActive): ?>
<button
type="submit"
disabled
class="btn btn-disabled">
<i class="icon-star"></i>
<?= e(trans('cms::lang.theme.active_button')) ?>
</button>
<?php else: ?>
<button
type="submit"
data-request="onSetActiveTheme"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
data-stripe-load-indicator
class="btn btn-primary">
<?= e(trans('cms::lang.theme.activate_button')) ?>
</button>
<?php endif ?>
</div>
</div>
</div>
<?php endforeach ?>
<div class="layout-row links">
<div class="layout-cell theme-thumbnail">
</div>
<div class="layout-cell theme-description">
<a class="find-more-themes" href="http://octobercms.com/themes"><?= e(trans('cms::lang.theme.find_more_themes')) ?></a>
</div>
</div>

View File

@ -0,0 +1,21 @@
<?= Block::put('body') ?>
<div class="layout">
<div class="layout-row min-size">
<div class="control-breadcrumb no-bottom-margin">
<ul>
<li><a href="<?= Backend::url('system/settings') ?>"><?= e(trans('system::lang.settings.menu_label')) ?></a></li>
<li><?= e($this->pageTitle) ?></li>
</ul>
</div>
</div>
<div class="layout-row">
<?= Form::open(['onsubmit'=>'return false']) ?>
<div class="layout theme-selector-layout" id="theme-list">
<?= $this->makePartial('theme_list') ?>
</div>
<?= Form::close() ?>
</div>
</div>
<?= Block::endPut() ?>

View File

@ -15,12 +15,18 @@ return [
'theme' => [
'active' => [
'not_set' => "The active theme is not set.",
'not_found' => "The active theme is not found.",
],
'edit' => [
'not_set' => "The edit theme is not set.",
'not_found' => "The edit theme is not found.",
'not_match' => "The object you're trying to access doesn't belong to the theme being edited. Please reload the page."
]
],
'settings_menu' => 'Front-end theme',
'settings_menu_description' => 'Preview the list of installed themes and select an active theme.',
'find_more_themes' => 'Find more themes on OctoberCMS Theme Marketplace.',
'activate_button' => 'Activate',
'active_button' => 'Activate',
],
'page' => [
'not_found' => [

View File

@ -73,7 +73,6 @@ class AssetList extends WidgetBase
{
$this->addCss('css/assetlist.css', 'core');
$this->addJs('js/assetlist.js', 'core');
$this->addJs('/modules/backend/widgets/form/assets/js/form.js', 'core');
}
/**

View File

@ -229,7 +229,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => 'System',
'icon' => 'icon-envelope',
'class' => 'System\Models\MailSettings',
'sort' => 100
'order' => 400,
],
'mail_templates' => [
'label' => 'system::lang.mail_templates.menu_label',
@ -237,7 +237,7 @@ class ServiceProvider extends ModuleServiceProvider
'category' => 'System',
'icon' => 'icon-envelope-square',
'url' => Backend::url('system/mailtemplates'),
'sort' => 100
'order' => 400,
],
]);
});

View File

@ -14,6 +14,10 @@
.control-updatelist h5:first-of-type {
border-top: none;
}
.control-updatelist h5 i {
margin-right: 7px;
color: #405261;
}
.control-updatelist h5 small {
text-transform: none;
float: right;

View File

@ -18,6 +18,11 @@
border-top: none;
}
i {
margin-right: 7px;
color: @color-text-title;
}
small {
text-transform: none;
float: right;

View File

@ -8,7 +8,7 @@ use System\Classes\ApplicationException;
*
* Usage:
*
* In the model class definition:
* In the model class definition:
*
* public $implement = ['System.Behaviors.SettingsModel'];
* public $settingsCode = 'author_plugin_code';
@ -200,4 +200,4 @@ class SettingsModel extends ModelBehavior
{
return $this->fieldConfig;
}
}
}

View File

@ -36,7 +36,7 @@ class SettingsManager
'icon' => null,
'url' => null,
'permissions' => [],
'order' => 100,
'order' => 500,
'context' => 'system',
];

View File

@ -206,6 +206,16 @@ class UpdateManager
}
$result['plugins'] = $plugins;
/*
* Strip out themes that have been installed before
*/
$themes = [];
foreach (array_get($result, 'themes', []) as $code => $info) {
if (!$this->isThemeInstalled($code))
$themes[$code] = $info;
}
$result['themes'] = $themes;
Parameters::set('system::update.count', array_get($result, 'update', 0));
return $result;
@ -433,6 +443,57 @@ class UpdateManager
@unlink($filePath);
}
//
// Themes
//
/**
* Downloads a theme from the update server.
* @param string $name Theme name.
* @param string $hash Expected file hash.
* @return self
*/
public function downloadTheme($name, $hash)
{
$fileCode = $name . $hash;
$this->requestServerFile('theme/get', $fileCode, $hash, ['name' => $name]);
}
/**
* Extracts a theme after it has been downloaded.
*/
public function extractTheme($name, $hash)
{
$fileCode = $name . $hash;
$filePath = $this->getFilePath($fileCode);
if (!Zip::extract($filePath, $this->baseDirectory . '/themes/'))
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
@unlink($filePath);
}
/**
* Checks if a theme has ever been installed before.
* @param string $name Theme code
* @return boolean
*/
public function isThemeInstalled($name)
{
return array_key_exists($name, Parameters::get('system::theme.history', []));
}
/**
* Flags a theme as being installed, so it is not downloaded twice.
* @param string $name Theme code
*/
public function setThemeInstalled($name)
{
$history = Parameters::get('system::theme.history', []);
$history[$name] = Carbon::now()->timestamp;
Parameters::set('system::theme.history', $history);
}
//
// Notes
//

View File

@ -1,6 +1,7 @@
<?php namespace System\Console;
use Str;
use Config;
use Illuminate\Console\Command;
use System\Classes\UpdateManager;
use Symfony\Component\Console\Input\InputOption;
@ -35,9 +36,29 @@ class OctoberUpdate extends Command
$this->output->writeln('<info>Updating October...</info>');
$manager = UpdateManager::instance()->resetNotes();
$forceUpdate = $this->option('force');
$pluginsOnly = $this->option('plugins');
$coreOnly = $this->option('core');
/*
* Check for disabilities
*/
$disableCore = $disablePlugins = $disableThemes = false;
if ($this->option('plugins')) {
$disableCore = true;
$disableThemes = true;
}
if ($this->option('core')) {
$disablePlugins = true;
$disableThemes = true;
}
if (Config::get('cms.disableCoreUpdates', false)) {
$disableCore = true;
}
/*
* Perform update
*/
$updateList = $manager->requestUpdateList($forceUpdate);
$updates = (int)array_get($updateList, 'update', 0);
@ -49,7 +70,7 @@ class OctoberUpdate extends Command
$this->output->writeln(sprintf('<info>Found %s new %s!</info>', $updates, Str::plural('update', $updates)));
}
$coreHash = $pluginsOnly ? null : array_get($updateList, 'core.hash');
$coreHash = $disableCore ? null : array_get($updateList, 'core.hash');
$coreBuild = array_get($updateList, 'core.build');
if ($coreHash) {
@ -57,7 +78,7 @@ class OctoberUpdate extends Command
$manager->downloadCore($coreHash);
}
$plugins = $coreOnly ? [] : array_get($updateList, 'plugins');
$plugins = $disablePlugins ? [] : array_get($updateList, 'plugins');
foreach ($plugins as $code => $plugin) {
$pluginName = array_get($plugin, 'name');
$pluginHash = array_get($plugin, 'hash');

View File

@ -4,6 +4,7 @@ use Str;
use Lang;
use File;
use Flash;
use Config;
use Backend;
use Redirect;
use BackendMenu;
@ -31,6 +32,11 @@ class Updates extends Controller
public $listConfig = ['list' => 'config_list.yaml', 'manage' => 'config_manage_list.yaml'];
/**
* @var boolean If set to true, core updates will not be downloaded or extracted.
*/
protected $disableCoreUpdates = false;
public function __construct()
{
parent::__construct();
@ -38,6 +44,8 @@ class Updates extends Controller
$this->addCss('/modules/system/assets/css/updates.css', 'core');
BackendMenu::setContext('October.System', 'system', 'updates');
$this->disableCoreUpdates = Config::get('cms.disableCoreUpdates', false);
}
/**
@ -98,10 +106,12 @@ class Updates extends Controller
switch ($stepCode) {
case 'downloadCore':
if ($this->disableCoreUpdates) return;
$manager->downloadCore(post('hash'));
break;
case 'extractCore':
if ($this->disableCoreUpdates) return;
$manager->extractCore(post('hash'), post('build'));
break;
@ -109,10 +119,18 @@ class Updates extends Controller
$manager->downloadPlugin(post('name'), post('hash'));
break;
case 'downloadTheme':
$manager->downloadTheme(post('name'), post('hash'));
break;
case 'extractPlugin':
$manager->extractPlugin(post('name'), post('hash'));
break;
case 'extractTheme':
$manager->extractTheme(post('name'), post('hash'));
break;
case 'completeUpdate':
$manager->update();
Flash::success(Lang::get('system::lang.updates.update_success'));
@ -148,7 +166,8 @@ class Updates extends Controller
$this->vars['core'] = array_get($result, 'core', false);
$this->vars['hasUpdates'] = array_get($result, 'update', false);
$this->vars['updateList'] = array_get($result, 'plugins', []);
$this->vars['pluginList'] = array_get($result, 'plugins', []);
$this->vars['themeList'] = array_get($result, 'themes', []);
}
catch (Exception $ex) {
$this->handleError($ex);
@ -204,18 +223,20 @@ class Updates extends Controller
public function onApplyUpdates()
{
try {
$plugins = post('plugins', []);
if (!is_array($plugins))
$plugins = [];
$coreHash = post('hash');
$coreBuild = post('build');
$core = [$coreHash, $coreBuild];
$plugins = post('plugins', []);
if (!is_array($plugins)) $plugins = [];
$themes = post('themes', []);
if (!is_array($themes)) $themes = [];
/*
* Update steps
*/
$updateSteps = $this->buildUpdateSteps($core, $plugins);
$updateSteps = $this->buildUpdateSteps($core, $plugins, $themes);
/*
* Finish up
@ -234,13 +255,16 @@ class Updates extends Controller
return $this->makePartial('execute');
}
private function buildUpdateSteps($core, $plugins)
private function buildUpdateSteps($core, $plugins, $themes)
{
if (!is_array($core))
$core = [null, null];
if (!is_array($plugins))
$plugins = [];
if (!is_array($core))
$core = [null, null];
if (!is_array($themes))
$themes = [];
$updateSteps = [];
list($coreHash, $coreBuild) = $core;
@ -265,6 +289,15 @@ class Updates extends Controller
];
}
foreach ($themes as $name => $hash) {
$updateSteps[] = [
'code' => 'downloadTheme',
'label' => Lang::get('system::lang.updates.theme_downloading', compact('name')),
'name' => $name,
'hash' => $hash
];
}
/*
* Extract
*/
@ -286,6 +319,15 @@ class Updates extends Controller
];
}
foreach ($themes as $name => $hash) {
$updateSteps[] = [
'code' => 'extractTheme',
'label' => Lang::get('system::lang.updates.theme_extracting', compact('name')),
'name' => $name,
'hash' => $hash
];
}
return $updateSteps;
}

View File

@ -13,6 +13,7 @@
<?php if ($core): ?>
<h5>
<i class="icon-cube"></i>
<?= e(trans('system::lang.system.name')) ?>
<?php if ($core['old_build']): ?>
<?= e(trans('system::lang.updates.core_build_old', ['build'=>$core['old_build']])) ?>
@ -26,8 +27,23 @@
<input type="hidden" name="build" value="<?= e($core['build']) ?>" />
<?php endif ?>
<?php foreach ($updateList as $code => $plugin): ?>
<?php foreach ($themeList as $code => $theme): ?>
<h5>
<i class="icon-picture-o"></i>
<?= e(array_get($theme, 'name', 'Unknown')) ?>
<small><?= e(trans('system::lang.updates.theme_label')) ?></small>
</h5>
<dl>
<dt><?= e(array_get($theme, 'version', 'v1.0.0')) ?></dt>
<dd><?= e(trans('system::lang.updates.theme_new_install')) ?></dd>
</dl>
<input type="hidden" name="themes[<?= e($code) ?>]" value="<?= e($theme['hash']) ?>" />
<?php endforeach ?>
<?php foreach ($pluginList as $code => $plugin): ?>
<h5>
<i class="icon-puzzle-piece"></i>
<?= e($plugin['name']) ?>
<?php if ($plugin['old_version']): ?>

View File

@ -137,6 +137,10 @@ return [
'plugin_version_none' => 'New plugin',
'plugin_version_old' => 'Current v:version',
'plugin_version_new' => 'v:version',
'theme_label' => 'Theme',
'theme_new_install' => 'New theme installation.',
'theme_downloading' => 'Downloading theme: :name',
'theme_extracting' => 'Unpacking theme: :name',
'update_label' => 'Update software',
'update_completing' => 'Finishing update process',
'update_loading' => 'Loading available updates...',

View File

@ -17,7 +17,7 @@ class ThemeTest extends TestCase
$it->setMaxDepth(1);
$it->rewind();
while($it->valid()) {
while ($it->valid()) {
if (!$it->isDot() && !$it->isDir() && $it->getExtension() == 'htm')
$result++;

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

4
themes/demo/theme.yaml Normal file
View File

@ -0,0 +1,4 @@
name: Demo
description: Demo OctoberCMS theme. Demonstrates the basic concepts of the front-end theming: layouts, pages, partials, components, content blocks, AJAX framework.
author: OctoberCMS
authorUrl: 'http://octobercms.com'