Overhaul the plugin installation process in the back-end
Themes can now be installed via the back-end
This commit is contained in:
parent
9e9aa97101
commit
c54821f175
|
|
@ -0,0 +1,126 @@
|
|||
<?php namespace Cms\Classes;
|
||||
|
||||
use File;
|
||||
use ApplicationException;
|
||||
use System\Models\Parameters;
|
||||
use Cms\Classes\Theme as CmsTheme;
|
||||
|
||||
/**
|
||||
* Theme manager
|
||||
*
|
||||
* @package october\cms
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class ThemeManager
|
||||
{
|
||||
use \October\Rain\Support\Traits\Singleton;
|
||||
|
||||
//
|
||||
// Gateway spawned
|
||||
//
|
||||
|
||||
/**
|
||||
* Returns a collection of themes installed via the update gateway
|
||||
* @return array
|
||||
*/
|
||||
public function getInstalled()
|
||||
{
|
||||
return Parameters::get('system::theme.history', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a theme has ever been installed before.
|
||||
* @param string $name Theme code
|
||||
* @return boolean
|
||||
*/
|
||||
public function isInstalled($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 setInstalled($code, $dirName = null)
|
||||
{
|
||||
if (!$dirName) {
|
||||
$dirName = strtolower(str_replace('.', '-', $code));
|
||||
}
|
||||
|
||||
$history = Parameters::get('system::theme.history', []);
|
||||
$history[$code] = $dirName;
|
||||
Parameters::set('system::theme.history', $history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags a theme as being uninstalled.
|
||||
* @param string $name Theme code
|
||||
*/
|
||||
public function setUninstalled($code)
|
||||
{
|
||||
$history = Parameters::get('system::theme.history', []);
|
||||
if (array_key_exists($code, $history)) {
|
||||
unset($history[$code]);
|
||||
}
|
||||
|
||||
Parameters::set('system::theme.history', $history);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an installed theme's code from it's dirname.
|
||||
* @return string
|
||||
*/
|
||||
public function findByDirName($dirName)
|
||||
{
|
||||
$installed = $this->getInstalled();
|
||||
foreach ($installed as $code => $name) {
|
||||
if ($dirName == $name) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// Management
|
||||
//
|
||||
|
||||
/**
|
||||
* Completely delete a theme from the system.
|
||||
* @param string $id Theme code/namespace
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTheme($theme)
|
||||
{
|
||||
if (!$theme) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($theme)) {
|
||||
$theme = CmsTheme::load($theme);
|
||||
}
|
||||
|
||||
if ($theme->isActiveTheme()) {
|
||||
throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed'));
|
||||
}
|
||||
|
||||
/*
|
||||
* Delete from file system
|
||||
*/
|
||||
$themePath = $theme->getPath();
|
||||
if (File::isDirectory($themePath)) {
|
||||
File::deleteDirectory($themePath);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set uninstalled
|
||||
*/
|
||||
if ($themeCode = $this->findByDirName($theme->getDirName())) {
|
||||
$this->setUninstalled($themeCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ use Cms\Models\ThemeData;
|
|||
use Cms\Models\ThemeExport;
|
||||
use Cms\Models\ThemeImport;
|
||||
use Cms\Classes\Theme as CmsTheme;
|
||||
use Cms\Classes\ThemeManager;
|
||||
use System\Classes\SettingsManager;
|
||||
use Backend\Classes\Controller;
|
||||
use Exception;
|
||||
|
|
@ -71,16 +72,7 @@ class Themes extends Controller
|
|||
|
||||
public function index_onDelete()
|
||||
{
|
||||
$theme = $this->findThemeObject();
|
||||
|
||||
if ($theme->isActiveTheme()) {
|
||||
throw new ApplicationException(trans('cms::lang.theme.delete_active_theme_failed'));
|
||||
}
|
||||
|
||||
$themePath = $theme->getPath();
|
||||
if (File::isDirectory($themePath)) {
|
||||
File::deleteDirectory($themePath);
|
||||
}
|
||||
ThemeManager::instance()->deleteTheme(post('theme'));
|
||||
|
||||
Flash::success(trans('cms::lang.theme.delete_theme_success'));
|
||||
return Redirect::refresh();
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@
|
|||
data-control="popup"
|
||||
data-handler="onLoadCreateForm"
|
||||
data-size="huge"
|
||||
href="javascript:;"
|
||||
target="_blank">
|
||||
href="javascript:;">
|
||||
<?= e(trans('cms::lang.theme.create_new_blank_theme')) ?>
|
||||
</a>
|
||||
<a
|
||||
class="find-more-themes"
|
||||
href="http://octobercms.com/themes"
|
||||
target="_blank">
|
||||
href="<?= Backend::url('system/updates/install/themes') ?>">
|
||||
<?= e(trans('cms::lang.theme.find_more_themes')) ?>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ return [
|
|||
'new_directory_name_comment' => 'Provide a new directory name for the duplicated theme.',
|
||||
'dir_name_invalid' => 'Name can contain only digits, Latin letters and the following symbols: _-',
|
||||
'dir_name_taken' => 'Desired theme directory already exists.',
|
||||
'find_more_themes' => 'Find more themes on OctoberCMS Theme Marketplace',
|
||||
'find_more_themes' => 'Find more themes',
|
||||
'return' => 'Return to themes list',
|
||||
],
|
||||
'maintenance' => [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,276 @@
|
|||
.product-list-empty {
|
||||
padding: 5px 0;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
.product-list {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
overflow: hidden;
|
||||
/* clearfix */
|
||||
}
|
||||
.product-list li button,
|
||||
.product-list li .image,
|
||||
.product-list li .details {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
-moz-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear;
|
||||
}
|
||||
.product-list li button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0;
|
||||
outline: none;
|
||||
}
|
||||
.product-list li:hover button {
|
||||
opacity: .3;
|
||||
}
|
||||
.product-list li:hover button:hover {
|
||||
opacity: .8;
|
||||
}
|
||||
.plugin-list li {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #E6E9E9;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.plugin-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.plugin-list li .image {
|
||||
float: left;
|
||||
margin-right: 15px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.plugin-list li .image img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.plugin-list li .details p {
|
||||
padding: 0;
|
||||
margin: 3px 0 0 0;
|
||||
color: #808C8D;
|
||||
}
|
||||
.plugin-list li h4 {
|
||||
padding: 5px 0 0;
|
||||
margin: 0;
|
||||
color: #C03F31;
|
||||
font-weight: 400;
|
||||
}
|
||||
.theme-list li {
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 0 10px 10px 0;
|
||||
list-style: none;
|
||||
border: 1px solid #E6E9E9;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.theme-list li:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
.theme-list li {
|
||||
-webkit-transition: border .2s linear;
|
||||
-moz-transition: border .2s linear;
|
||||
transition: border .2s linear;
|
||||
}
|
||||
.theme-list li .image {
|
||||
padding: 5px;
|
||||
}
|
||||
.theme-list li .image img {
|
||||
width: 210px;
|
||||
height: 140px;
|
||||
}
|
||||
.theme-list li:hover .image {
|
||||
opacity: 0;
|
||||
}
|
||||
.theme-list li .details {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-list li:hover .details {
|
||||
opacity: 1;
|
||||
}
|
||||
.theme-list li h4 {
|
||||
padding: 15px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
.theme-list li p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
.suggested-products {
|
||||
padding: 0;
|
||||
}
|
||||
.suggested-products .product {
|
||||
padding: 0;
|
||||
}
|
||||
.suggested-products .image img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.suggested-themes .image img {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
}
|
||||
.suggested-products .image {
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
.suggested-products .details {
|
||||
margin-left: 50px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.suggested-themes .details {
|
||||
margin-left: 70px;
|
||||
}
|
||||
.suggested-products .details h5 {
|
||||
margin: 0 0 3px;
|
||||
font-size: 14px;
|
||||
color: #C03F31;
|
||||
font-weight: 400;
|
||||
}
|
||||
.suggested-products .details p {
|
||||
font-size: 12px;
|
||||
}
|
||||
.suggested-products a {
|
||||
color: #777;
|
||||
background: #fff;
|
||||
padding: 5px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #E6E9E9;
|
||||
}
|
||||
.suggested-products a:hover {
|
||||
color: #333;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.suggested-products a:hover .image:after {
|
||||
content: "+";
|
||||
color: #999;
|
||||
font-size: 32px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 0;
|
||||
}
|
||||
.suggested-products a:hover .image img {
|
||||
opacity: .5;
|
||||
}
|
||||
/*!
|
||||
* Typeahead
|
||||
*/
|
||||
.product-search {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0 auto 0 auto;
|
||||
text-align: left;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.twitter-typeahead {
|
||||
width: 100%;
|
||||
}
|
||||
.typeahead,
|
||||
.tt-hint {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
padding: 8px 12px;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
border: 1px solid #024e6a;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
.typeahead {
|
||||
background-color: #fff;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
.tt-input {
|
||||
font-weight: 200;
|
||||
}
|
||||
.tt-input:focus {
|
||||
border-color: #E6E9E9;
|
||||
}
|
||||
.tt-hint {
|
||||
color: #999;
|
||||
font-weight: 200;
|
||||
}
|
||||
.tt-dropdown-menu {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.tt-suggestion {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.tt-suggestion + .tt-suggestion {
|
||||
font-size: 14px;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
.tt-suggestions .product-details {
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.tt-suggestions .product-image {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.tt-suggestions .product-image img {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
}
|
||||
.tt-suggestions .product-name {
|
||||
font-size: 20px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.tt-suggestion.tt-cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tt-suggestion.tt-cursor .product-details {
|
||||
color: #333;
|
||||
background: #f9f9f9;
|
||||
border-color: #f0f0f0;
|
||||
}
|
||||
.tt-suggestion.tt-cursor .product-details .product-image:after {
|
||||
content: "+";
|
||||
color: #999;
|
||||
font-size: 38px;
|
||||
display: block;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
line-height: 45px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
.tt-suggestion.tt-cursor .product-details .product-image img {
|
||||
opacity: .5;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../../backend/assets/less/core/boot.less";
|
||||
@import "../../../../backend/assets/less/core/boot.less";
|
||||
|
||||
.control-settings {
|
||||
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
@import "../../../../backend/assets/less/core/boot.less";
|
||||
|
||||
.product-list-empty {
|
||||
padding: 5px 0;
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
}
|
||||
.product-list {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
overflow: hidden; /* clearfix */
|
||||
}
|
||||
.product-list li button,
|
||||
.product-list li .image,
|
||||
.product-list li .details {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
-moz-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear;
|
||||
}
|
||||
.product-list li button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0;
|
||||
outline: none;
|
||||
}
|
||||
.product-list li:hover button {
|
||||
opacity: .3;
|
||||
}
|
||||
.product-list li:hover button:hover {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.plugin-list {
|
||||
|
||||
}
|
||||
.plugin-list li {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #E6E9E9;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.plugin-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.plugin-list li .image {
|
||||
float: left;
|
||||
margin-right: 15px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.plugin-list li .image img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.plugin-list li .details p {
|
||||
padding: 0;
|
||||
margin: 3px 0 0 0;
|
||||
color: #808C8D;
|
||||
}
|
||||
.plugin-list li h4 {
|
||||
padding: 5px 0 0;
|
||||
margin: 0;
|
||||
color: #C03F31;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.theme-list li {
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 0 10px 10px 0;
|
||||
list-style: none;
|
||||
border: 1px solid #E6E9E9;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.theme-list li:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
.theme-list li {
|
||||
-webkit-transition: border .2s linear;
|
||||
-moz-transition: border .2s linear;
|
||||
transition: border .2s linear;
|
||||
}
|
||||
.theme-list li .image {
|
||||
padding: 5px;
|
||||
}
|
||||
.theme-list li .image img {
|
||||
width: 210px;
|
||||
height: 140px;
|
||||
}
|
||||
.theme-list li:hover .image {
|
||||
opacity: 0;
|
||||
}
|
||||
.theme-list li .details {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-list li:hover .details {
|
||||
opacity: 1;
|
||||
}
|
||||
.theme-list li h4 {
|
||||
padding: 15px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
.theme-list li p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #999;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.suggested-products-container {
|
||||
}
|
||||
.suggested-products {
|
||||
padding: 0;
|
||||
}
|
||||
.suggested-products .product {
|
||||
padding: 0;
|
||||
}
|
||||
.suggested-products .image img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.suggested-themes .image img {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
}
|
||||
.suggested-products .image {
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
.suggested-products .details {
|
||||
margin-left: 50px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.suggested-themes .details {
|
||||
margin-left: 70px;
|
||||
}
|
||||
.suggested-products .details h5 {
|
||||
margin: 0 0 3px;
|
||||
font-size: 14px;
|
||||
color: #C03F31;
|
||||
font-weight: 400;
|
||||
}
|
||||
.suggested-products .details p {
|
||||
font-size: 12px;
|
||||
}
|
||||
.suggested-products a {
|
||||
color: #777;
|
||||
background: #fff;
|
||||
padding: 5px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #E6E9E9;
|
||||
}
|
||||
.suggested-products a:hover {
|
||||
color: #333;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.suggested-products a:hover .image {
|
||||
}
|
||||
.suggested-products a:hover .image:after {
|
||||
content: "+";
|
||||
color: #999;
|
||||
font-size: 32px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 0;
|
||||
}
|
||||
.suggested-products a:hover .image img {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Typeahead
|
||||
*/
|
||||
|
||||
.product-search {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0 auto 0 auto;
|
||||
text-align: left;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.twitter-typeahead {
|
||||
width: 100%;
|
||||
}
|
||||
.typeahead,
|
||||
.tt-hint {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
padding: 8px 12px;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
border: 1px solid #024e6a;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
}
|
||||
.typeahead {
|
||||
background-color: #fff;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
.tt-input {
|
||||
font-weight: 200;
|
||||
}
|
||||
.tt-input:focus {
|
||||
border-color: #E6E9E9;
|
||||
}
|
||||
.tt-hint {
|
||||
color: #999;
|
||||
font-weight: 200;
|
||||
}
|
||||
.tt-dropdown-menu {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
}
|
||||
.tt-suggestion {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.tt-suggestion + .tt-suggestion {
|
||||
font-size: 14px;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
.tt-suggestions .product-details {
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.tt-suggestions .product-image {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.tt-suggestions .product-image img {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
}
|
||||
.tt-suggestions .product-name {
|
||||
font-size: 20px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.tt-suggestions .product-description {
|
||||
}
|
||||
|
||||
.tt-suggestion.tt-cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
.tt-suggestion.tt-cursor .product-details {
|
||||
color: #333;
|
||||
background: #f9f9f9;
|
||||
border-color: #f0f0f0;
|
||||
}
|
||||
.tt-suggestion.tt-cursor .product-details .product-image:after {
|
||||
content: "+";
|
||||
color: #999;
|
||||
font-size: 38px;
|
||||
display: block;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
line-height: 45px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
}
|
||||
.tt-suggestion.tt-cursor .product-details .product-image img {
|
||||
opacity: .5;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@import "../../../backend/assets/less/core/boot.less";
|
||||
@import "../../../../backend/assets/less/core/boot.less";
|
||||
|
||||
.control-updatelist {
|
||||
|
||||
|
|
@ -645,4 +645,40 @@ class PluginManager
|
|||
|
||||
return $result;
|
||||
}
|
||||
|
||||
//
|
||||
// Management
|
||||
//
|
||||
|
||||
/**
|
||||
* Completely roll back and delete a plugin from the system.
|
||||
* @param string $id Plugin code/namespace
|
||||
* @return void
|
||||
*/
|
||||
public function deletePlugin($id)
|
||||
{
|
||||
/*
|
||||
* Rollback plugin
|
||||
*/
|
||||
UpdateManager::instance()->rollbackPlugin($id);
|
||||
|
||||
/*
|
||||
* Delete from file system
|
||||
*/
|
||||
if ($pluginPath = PluginManager::instance()->getPluginPath($id)) {
|
||||
File::deleteDirectory($pluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears down a plugin's database tables and rebuilds them.
|
||||
* @param string $id Plugin code/namespace
|
||||
* @return void
|
||||
*/
|
||||
public function refreshPlugin($id)
|
||||
{
|
||||
$manager = UpdateManager::instance();
|
||||
$manager->rollbackPlugin($id);
|
||||
$manager->updatePlugin($id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ use URL;
|
|||
use File;
|
||||
use Lang;
|
||||
use Http;
|
||||
use Cache;
|
||||
use Schema;
|
||||
use Config;
|
||||
use Carbon\Carbon;
|
||||
use ApplicationException;
|
||||
use Cms\Classes\ThemeManager;
|
||||
use System\Models\Parameters;
|
||||
use System\Models\PluginVersion;
|
||||
use ApplicationException;
|
||||
use System\Helpers\Cache as CacheHelper;
|
||||
use October\Rain\Filesystem\Zip;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
|
|
@ -48,6 +50,11 @@ class UpdateManager
|
|||
*/
|
||||
protected $pluginManager;
|
||||
|
||||
/**
|
||||
* @var Cms\Classes\ThemeManager
|
||||
*/
|
||||
protected $themeManager;
|
||||
|
||||
/**
|
||||
* @var System\Classes\VersionManager
|
||||
*/
|
||||
|
|
@ -68,12 +75,18 @@ class UpdateManager
|
|||
*/
|
||||
protected $disableCoreUpdates = false;
|
||||
|
||||
/**
|
||||
* @var array Cache of gateway products
|
||||
*/
|
||||
protected $productCache;
|
||||
|
||||
/**
|
||||
* Initialize this singleton.
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
$this->pluginManager = PluginManager::instance();
|
||||
$this->themeManager = ThemeManager::instance();
|
||||
$this->versionManager = VersionManager::instance();
|
||||
$this->tempDirectory = temp_path();
|
||||
$this->baseDirectory = base_path();
|
||||
|
|
@ -235,7 +248,7 @@ class UpdateManager
|
|||
*/
|
||||
$themes = [];
|
||||
foreach (array_get($result, 'themes', []) as $code => $info) {
|
||||
if (!$this->isThemeInstalled($code)) {
|
||||
if (!$this->themeManager->isInstalled($code)) {
|
||||
$themes[$code] = $info;
|
||||
}
|
||||
}
|
||||
|
|
@ -492,6 +505,17 @@ class UpdateManager
|
|||
// Themes
|
||||
//
|
||||
|
||||
/**
|
||||
* Looks up a theme from the update server.
|
||||
* @param string $name Theme name.
|
||||
* @return array Details about the theme.
|
||||
*/
|
||||
public function requestThemeDetails($name)
|
||||
{
|
||||
$result = $this->requestServerData('theme/detail', ['name' => $name]);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a theme from the update server.
|
||||
* @param string $name Theme name.
|
||||
|
|
@ -516,29 +540,118 @@ class UpdateManager
|
|||
throw new ApplicationException(Lang::get('system::lang.zip.extract_failed', ['file' => $filePath]));
|
||||
}
|
||||
|
||||
$this->setThemeInstalled($name);
|
||||
$this->themeManager->setInstalled($name);
|
||||
@unlink($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a theme has ever been installed before.
|
||||
* @param string $name Theme code
|
||||
* @return boolean
|
||||
*/
|
||||
public function isThemeInstalled($name)
|
||||
//
|
||||
// Products
|
||||
//
|
||||
|
||||
public function requestProductDetails($codes, $type = null)
|
||||
{
|
||||
return array_key_exists($name, Parameters::get('system::theme.history', []));
|
||||
if ($type != 'plugin' && $type != 'theme')
|
||||
$type = 'plugin';
|
||||
|
||||
$codes = (array) $codes;
|
||||
$this->loadProductDetailCache();
|
||||
|
||||
/*
|
||||
* New products requested
|
||||
*/
|
||||
$newCodes = array_diff($codes, array_keys($this->productCache[$type]));
|
||||
if (count($newCodes)) {
|
||||
$dataCodes = [];
|
||||
$data = $this->requestServerData($type.'/details', ['names' => $newCodes]);
|
||||
foreach ($data as $product) {
|
||||
$code = array_get($product, 'code', -1);
|
||||
$this->cacheProductDetail($type, $code, $product);
|
||||
$dataCodes[] = $code;
|
||||
}
|
||||
|
||||
/*
|
||||
* Cache unknown products
|
||||
*/
|
||||
$unknownCodes = array_diff($newCodes, $dataCodes);
|
||||
foreach ($unknownCodes as $code) {
|
||||
$this->cacheProductDetail($type, $code, -1);
|
||||
}
|
||||
|
||||
$this->saveProductDetailCache();
|
||||
}
|
||||
|
||||
/*
|
||||
* Build details from cache
|
||||
*/
|
||||
$result = [];
|
||||
$requestedDetails = array_intersect_key($this->productCache[$type], array_flip($codes));
|
||||
|
||||
foreach ($requestedDetails as $detail) {
|
||||
if ($detail === -1) continue;
|
||||
$result[] = $detail;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags a theme as being installed, so it is not downloaded twice.
|
||||
* @param string $name Theme code
|
||||
* Returns popular themes found on the marketplace.
|
||||
*/
|
||||
public function setThemeInstalled($name)
|
||||
public function requestPopularProducts($type = null)
|
||||
{
|
||||
$history = Parameters::get('system::theme.history', []);
|
||||
$history[$name] = Carbon::now()->timestamp;
|
||||
Parameters::set('system::theme.history', $history);
|
||||
if ($type != 'plugin' && $type != 'theme')
|
||||
$type = 'plugin';
|
||||
|
||||
$cacheKey = 'system-updates-popular-'.$type;
|
||||
|
||||
if (Cache::has($cacheKey)) {
|
||||
return @unserialize(Cache::get($cacheKey)) ?: [];
|
||||
}
|
||||
|
||||
$data = $this->requestServerData($type.'/popular');
|
||||
Cache::put($cacheKey, serialize($data), 60);
|
||||
|
||||
foreach ($data as $product) {
|
||||
$code = array_get($product, 'code', -1);
|
||||
$this->cacheProductDetail($type, $code, $product);
|
||||
}
|
||||
|
||||
$this->saveProductDetailCache();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function loadProductDetailCache()
|
||||
{
|
||||
$defaultCache = ['theme' => [], 'plugin' => []];
|
||||
$cacheKey = 'system-updates-product-details';
|
||||
|
||||
if (Cache::has($cacheKey)) {
|
||||
$this->productCache = @unserialize(Cache::get($cacheKey)) ?: $defaultCache;
|
||||
}
|
||||
else {
|
||||
$this->productCache = $defaultCache;
|
||||
}
|
||||
}
|
||||
|
||||
protected function saveProductDetailCache()
|
||||
{
|
||||
if ($this->productCache === null) {
|
||||
$this->loadProductDetailCache();
|
||||
}
|
||||
|
||||
$cacheKey = 'system-updates-product-details';
|
||||
$expiresAt = Carbon::now()->addDays(2);
|
||||
Cache::put($cacheKey, serialize($this->productCache), $expiresAt);
|
||||
}
|
||||
|
||||
protected function cacheProductDetail($type, $code, $data)
|
||||
{
|
||||
if ($this->productCache === null) {
|
||||
$this->loadProductDetailCache();
|
||||
}
|
||||
|
||||
$this->productCache[$type][$code] = $data;
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Settings extends Controller
|
|||
$this->requiredPermissions = null;
|
||||
}
|
||||
|
||||
$this->addCss('/modules/system/assets/css/settings.css', 'core');
|
||||
$this->addCss('/modules/system/assets/css/settings/settings.css', 'core');
|
||||
|
||||
BackendMenu::setContext('October.System', 'system', 'settings');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ use Flash;
|
|||
use Config;
|
||||
use Backend;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use BackendMenu;
|
||||
use Cms\Classes\ThemeManager;
|
||||
use Backend\Classes\Controller;
|
||||
use System\Models\Parameters;
|
||||
use System\Models\PluginVersion;
|
||||
|
|
@ -37,7 +39,7 @@ class Updates extends Controller
|
|||
parent::__construct();
|
||||
|
||||
$this->addJs('/modules/system/assets/js/updates/updates.js', 'core');
|
||||
$this->addCss('/modules/system/assets/css/updates.css', 'core');
|
||||
$this->addCss('/modules/system/assets/css/updates/updates.css', 'core');
|
||||
|
||||
BackendMenu::setContext('October.System', 'system', 'updates');
|
||||
SettingsManager::setContext('October.System', 'updates');
|
||||
|
|
@ -67,6 +69,31 @@ class Updates extends Controller
|
|||
return $this->asExtension('ListController')->index();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install new plugins / themes
|
||||
*/
|
||||
public function install($tab = null)
|
||||
{
|
||||
if (get('search')) {
|
||||
return Response::make($this->onSearchProducts());
|
||||
}
|
||||
|
||||
try {
|
||||
$this->bodyClass = 'compact-container breadcrumb-flush';
|
||||
$this->pageTitle = 'Install products';
|
||||
|
||||
$this->addJs('/modules/system/assets/js/updates/install.js', 'core');
|
||||
$this->addCss('/modules/system/assets/css/updates/install.css', 'core');
|
||||
|
||||
$this->vars['activeTab'] = $tab ?: 'plugins';
|
||||
$this->vars['installedPlugins'] = $this->getInstalledPlugins();
|
||||
$this->vars['installedThemes'] = $this->getInstalledThemes();
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->handleError($ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
|
@ -132,12 +159,12 @@ class Updates extends Controller
|
|||
case 'completeUpdate':
|
||||
$manager->update();
|
||||
Flash::success(Lang::get('system::lang.updates.update_success'));
|
||||
return Backend::redirect('system/updates');
|
||||
return Redirect::refresh();
|
||||
|
||||
case 'completeInstall':
|
||||
$manager->update();
|
||||
Flash::success(Lang::get('system::lang.install.install_success'));
|
||||
return Backend::redirect('system/updates');
|
||||
return Redirect::refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +469,7 @@ class Updates extends Controller
|
|||
}
|
||||
|
||||
/**
|
||||
* Removes or purges plugins from the system.
|
||||
* Rollback and remove plugins from the system.
|
||||
* @return void
|
||||
*/
|
||||
public function onRemovePlugins()
|
||||
|
|
@ -454,18 +481,7 @@ class Updates extends Controller
|
|||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Rollback plugin
|
||||
*/
|
||||
$pluginCode = $object->code;
|
||||
UpdateManager::instance()->rollbackPlugin($pluginCode);
|
||||
|
||||
/*
|
||||
* Delete from file system
|
||||
*/
|
||||
if ($pluginPath = PluginManager::instance()->getPluginPath($pluginCode)) {
|
||||
File::deleteDirectory($pluginPath);
|
||||
}
|
||||
PluginManager::instance()->deletePlugin($object->code);
|
||||
}
|
||||
|
||||
Flash::success(Lang::get('system::lang.plugins.remove_success'));
|
||||
|
|
@ -474,6 +490,22 @@ class Updates extends Controller
|
|||
return $this->listRefresh('manage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback and remove a single plugin from the system.
|
||||
* @return void
|
||||
*/
|
||||
public function onRemovePlugin()
|
||||
{
|
||||
if ($pluginCode = post('code')) {
|
||||
|
||||
PluginManager::instance()->deletePlugin($pluginCode);
|
||||
|
||||
Flash::success(Lang::get('system::lang.plugins.remove_success'));
|
||||
}
|
||||
|
||||
return Redirect::refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds plugin database migrations.
|
||||
* @return void
|
||||
|
|
@ -482,19 +514,12 @@ class Updates extends Controller
|
|||
{
|
||||
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
|
||||
|
||||
$manager = UpdateManager::instance();
|
||||
|
||||
foreach ($checkedIds as $objectId) {
|
||||
if (!$object = PluginVersion::find($objectId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Refresh plugin
|
||||
*/
|
||||
$pluginCode = $object->code;
|
||||
$manager->rollbackPlugin($pluginCode);
|
||||
$manager->updatePlugin($pluginCode);
|
||||
PluginManager::instance()->refreshPlugin($object->code);
|
||||
}
|
||||
|
||||
Flash::success(Lang::get('system::lang.plugins.refresh_success'));
|
||||
|
|
@ -548,4 +573,145 @@ class Updates extends Controller
|
|||
|
||||
return Backend::redirect('system/updates/manage');
|
||||
}
|
||||
|
||||
//
|
||||
// Theme management
|
||||
//
|
||||
|
||||
/**
|
||||
* Validate the theme code and execute the theme installation
|
||||
*/
|
||||
public function onInstallTheme()
|
||||
{
|
||||
try {
|
||||
if (!$code = trim(post('code'))) {
|
||||
throw new ApplicationException(Lang::get('system::lang.install.missing_theme_name'));
|
||||
}
|
||||
|
||||
$manager = UpdateManager::instance();
|
||||
$result = $manager->requestThemeDetails($code);
|
||||
|
||||
if (!isset($result['code']) || !isset($result['hash'])) {
|
||||
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
|
||||
}
|
||||
|
||||
$name = $result['code'];
|
||||
$hash = $result['hash'];
|
||||
$themes = [$name => $hash];
|
||||
$plugins = [];
|
||||
|
||||
/*
|
||||
* Update steps
|
||||
*/
|
||||
$updateSteps = $this->buildUpdateSteps(null, $plugins, $themes);
|
||||
|
||||
/*
|
||||
* Finish up
|
||||
*/
|
||||
$updateSteps[] = [
|
||||
'code' => 'completeInstall',
|
||||
'label' => Lang::get('system::lang.install.install_completing'),
|
||||
];
|
||||
|
||||
$this->vars['updateSteps'] = $updateSteps;
|
||||
|
||||
return $this->makePartial('execute');
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->handleError($ex);
|
||||
return $this->makePartial('theme_form');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a single theme from the system.
|
||||
* @return void
|
||||
*/
|
||||
public function onRemoveTheme()
|
||||
{
|
||||
if ($themeCode = post('code')) {
|
||||
|
||||
ThemeManager::instance()->deleteTheme($themeCode);
|
||||
|
||||
Flash::success(trans('cms::lang.theme.delete_theme_success'));
|
||||
}
|
||||
|
||||
return Redirect::refresh();
|
||||
}
|
||||
|
||||
//
|
||||
// Product install
|
||||
//
|
||||
|
||||
public function install_onSearchProducts()
|
||||
{
|
||||
$searchType = get('search', 'plugins');
|
||||
$serverUri = $searchType == 'plugins' ? 'plugin/search' : 'theme/search';
|
||||
|
||||
$manager = UpdateManager::instance();
|
||||
return $manager->requestServerData($serverUri, ['query' => get('query')]);
|
||||
}
|
||||
|
||||
public function install_onGetPopularPlugins()
|
||||
{
|
||||
$installed = $this->getInstalledPlugins();
|
||||
$popular = UpdateManager::instance()->requestPopularProducts('plugin');
|
||||
$popular = $this->filterPopularProducts($popular, $installed);
|
||||
|
||||
return ['result' => $popular];
|
||||
}
|
||||
|
||||
public function install_onGetPopularThemes()
|
||||
{
|
||||
$installed = $this->getInstalledThemes();
|
||||
$popular = UpdateManager::instance()->requestPopularProducts('theme');
|
||||
$popular = $this->filterPopularProducts($popular, $installed);
|
||||
|
||||
return ['result' => $popular];
|
||||
}
|
||||
|
||||
protected function getInstalledPlugins()
|
||||
{
|
||||
$installed = PluginVersion::lists('code');
|
||||
$manager = UpdateManager::instance();
|
||||
return $manager->requestProductDetails($installed, 'plugin');
|
||||
}
|
||||
|
||||
protected function getInstalledThemes()
|
||||
{
|
||||
$history = Parameters::get('system::theme.history', []);
|
||||
$manager = UpdateManager::instance();
|
||||
$installed = $manager->requestProductDetails(array_keys($history), 'theme');
|
||||
|
||||
/*
|
||||
* Splice in the directory names
|
||||
*/
|
||||
foreach ($installed as $key => $data) {
|
||||
$code = array_get($data, 'code');
|
||||
$installed[$key]['dirName'] = array_get($history, $code, $code);
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove installed products from the collection
|
||||
*/
|
||||
protected function filterPopularProducts($popular, $installed)
|
||||
{
|
||||
$installedArray = [];
|
||||
foreach ($installed as $product) {
|
||||
$installedArray[] = array_get($product, 'code', -1);
|
||||
}
|
||||
|
||||
foreach ($popular as $key => $product) {
|
||||
$code = array_get($product, 'code');
|
||||
if (in_array($code, $installedArray)) {
|
||||
unset($popular[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $popular;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
|
||||
<div>
|
||||
|
||||
<!-- Search -->
|
||||
<form
|
||||
id="installPluginsForm"
|
||||
data-handler="onInstallPlugin"
|
||||
onsubmit="$.oc.installProcess.searchSubmit(this); return false">
|
||||
<div class="product-search">
|
||||
<input
|
||||
name="code"
|
||||
id="pluginSearchInput"
|
||||
class="product-search-input search-input-lg typeahead"
|
||||
placeholder="search plugins to install..."
|
||||
data-search-type="plugins"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-7">
|
||||
|
||||
<!-- Installed plugins -->
|
||||
<div id="pluginList"
|
||||
class="product-list-manager">
|
||||
|
||||
<h4 class="section-header">
|
||||
<a href="<?= Backend::url('system/updates') ?>">Installed plugins</a>
|
||||
<small>(<span class="product-counter"><?= count($installedPlugins) ?></span>)</small>
|
||||
</h4>
|
||||
|
||||
<?php if (!count($installedPlugins)): ?>
|
||||
<div class="product-list-empty">
|
||||
<p>There are no plugins installed from the marketplace.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="product-list plugin-list">
|
||||
<?php foreach ($installedPlugins as $plugin): ?>
|
||||
|
||||
<li data-code="<?= $plugin['code'] ?>">
|
||||
<div class="image">
|
||||
<img src="<?= $plugin['image'] ?>" alt="">
|
||||
</div>
|
||||
<div class="details">
|
||||
<h4><?= $plugin['name'] ?></h4>
|
||||
<p>by <?= $plugin['author'] ?></p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
aria-hidden="true"
|
||||
data-request="onRemovePlugin"
|
||||
data-request-data="code: '<?= $plugin['code'] ?>'"
|
||||
data-request-confirm="Are you sure you want to remove this?"
|
||||
data-stripe-load-indicator>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
|
||||
<!-- Recommended extras -->
|
||||
<div class="suggested-products-container">
|
||||
<h4 class="section-header">Recommended</h4>
|
||||
<div class="scroll-panel">
|
||||
<div
|
||||
id="suggestedPlugins"
|
||||
class="suggested-products suggested-plugins"
|
||||
data-handler="onGetPopularPlugins"
|
||||
data-view="plugin/suggestion"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/template" data-partial="plugin/suggestion">
|
||||
<div class="product">
|
||||
<a
|
||||
data-control="popup"
|
||||
data-handler="onInstallPlugin"
|
||||
data-request-data="code: '{{code}}'"
|
||||
href="javascript:;">
|
||||
<div class="image"><img src="{{image}}" alt=""></div>
|
||||
<div class="details">
|
||||
<h5 class="text-overflow">{{code}}</h5>
|
||||
<p>{{description}}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
<div>
|
||||
|
||||
<!-- Search -->
|
||||
<form
|
||||
id="installThemesForm"
|
||||
data-handler="onInstallTheme"
|
||||
onsubmit="$.oc.installProcess.searchSubmit(this); return false">
|
||||
<div class="product-search">
|
||||
<input
|
||||
name="code"
|
||||
id="themeSearchInput"
|
||||
class="product-search-input search-input-lg typeahead"
|
||||
placeholder="search themes to install..."
|
||||
data-search-type="themes"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-7">
|
||||
|
||||
<!-- Installed themes -->
|
||||
<div id="themeList"
|
||||
class="product-list-manager"
|
||||
data-handler="onGetInstalledThemes"
|
||||
data-view="product/theme">
|
||||
|
||||
<h4 class="section-header">
|
||||
<a href="<?= Backend::url('cms/themes') ?>">Installed themes</a>
|
||||
<small>(<span class="product-counter">0</span>)</small>
|
||||
</h4>
|
||||
|
||||
<?php if (!count($installedThemes)): ?>
|
||||
<div class="product-list-empty">
|
||||
<p>There are no themes installed from the marketplace.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="product-list theme-list">
|
||||
<?php foreach ($installedThemes as $theme): ?>
|
||||
|
||||
<li data-code="<?= $theme['code'] ?>">
|
||||
<div class="image">
|
||||
<img src="<?= $theme['image'] ?>" alt="">
|
||||
</div>
|
||||
<div class="details">
|
||||
<h4><?= $theme['name'] ?></h4>
|
||||
<p>by <?= $theme['author'] ?></p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
aria-hidden="true"
|
||||
data-request="onRemoveTheme"
|
||||
data-request-data="code: '<?= $theme['dirName'] ?>'"
|
||||
data-request-confirm="Are you sure you want to remove this?"
|
||||
data-stripe-load-indicator>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
|
||||
<!-- Recommended extras -->
|
||||
<div class="suggested-products-container">
|
||||
<h4 class="section-header">Recommended</h4>
|
||||
<div class="scroll-panel">
|
||||
<div
|
||||
id="suggestedThemes"
|
||||
class="suggested-products suggested-themes"
|
||||
data-handler="onGetPopularThemes"
|
||||
data-view="theme/suggestion"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/template" data-partial="theme/suggestion">
|
||||
<div class="product">
|
||||
<a
|
||||
data-control="popup"
|
||||
data-handler="onInstallTheme"
|
||||
data-request-data="code: '{{code}}'"
|
||||
href="javascript:;">
|
||||
<div class="image"><img src="{{image}}" alt=""></div>
|
||||
<div class="details">
|
||||
<h5 class="text-overflow">{{code}}</h5>
|
||||
<p>{{description}}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</script>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?= Form::open() ?>
|
||||
|
||||
<div class="input-group" style="width: 207px">
|
||||
<input
|
||||
placeholder="<?= e(trans('backend::lang.list.search_prompt')) ?>"
|
||||
type="text"
|
||||
name="code"
|
||||
value=""
|
||||
class="form-control icon plus growable pull-right"
|
||||
autocomplete="off" />
|
||||
|
||||
<span class="input-group-btn">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
id="installPluginButton"
|
||||
data-control="popup"
|
||||
data-handler="onInstallPlugin">
|
||||
<?= e(trans('system::lang.install.plugin_label')) ?>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?= Form::close() ?>
|
||||
|
|
@ -11,4 +11,9 @@
|
|||
class="btn btn-default oc-icon-puzzle-piece">
|
||||
<?= e(trans('system::lang.plugins.manage')) ?>
|
||||
</a>
|
||||
<a
|
||||
href="<?= Backend::url('system/updates/install') ?>"
|
||||
class="btn btn-success oc-icon-plus">
|
||||
Install plugins
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<?= Form::open(['id' => 'themeForm']) ?>
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans('system::lang.install.theme_label')) ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<?php if ($this->fatalError): ?>
|
||||
<p class="flash-message static error"><?= $fatalError ?></p>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="themeCode"><?= e(trans('system::lang.theme.name.label')) ?></label>
|
||||
<input
|
||||
name="code"
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="themeCode"
|
||||
value="<?= post('code') ?>" />
|
||||
<p class="help-block"><?= e(trans('system::lang.theme.name.help')) ?></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
data-dismiss="popup"
|
||||
data-control="popup"
|
||||
data-handler="onInstallTheme">
|
||||
<?= e(trans('system::lang.install.theme_label')) ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.cancel')) ?>
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(
|
||||
function(){ $('#themeCode').select() },
|
||||
310
|
||||
)
|
||||
</script>
|
||||
<?= Form::close() ?>
|
||||
|
|
@ -9,4 +9,3 @@ noRecordsMessage: backend::lang.list.no_records
|
|||
|
||||
toolbar:
|
||||
buttons: list_toolbar
|
||||
search: list_search
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php Block::put('breadcrumb') ?>
|
||||
<ul>
|
||||
<li><a href="<?= Backend::url('system/updates') ?>"><?= e(trans('system::lang.updates.menu_label')) ?></a></li>
|
||||
<li><?= e(trans($this->pageTitle)) ?></li>
|
||||
</ul>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
|
||||
<div class="control-tabs content-tabs tabs-flush" data-control="tab">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="<?= $activeTab == 'plugins' ? 'active' : '' ?>">
|
||||
<a
|
||||
href="#tabPlugins"
|
||||
data-tab-url="<?= Backend::url('system/updates/install/plugins') ?>">
|
||||
Plugins
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $activeTab == 'themes' ? 'active' : '' ?>">
|
||||
<a
|
||||
href="#tabThemes"
|
||||
data-tab-url="<?= Backend::url('system/updates/install/themes') ?>">
|
||||
Themes
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane <?= $activeTab == 'plugins' ? 'active' : '' ?>">
|
||||
<div class="padded-container">
|
||||
<?= $this->makePartial('install_plugins') ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane <?= $activeTab == 'themes' ? 'active' : '' ?>">
|
||||
<div class="padded-container">
|
||||
<?= $this->makePartial('install_themes') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="padded-container">
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= Backend::url('system/updates') ?>" class="btn btn-default"><?= e(trans('system::lang.settings.return')) ?></a></p>
|
||||
<p><a href="<?= Backend::url('cms/themes') ?>" class="btn btn-default"><?= e(trans('cms::lang.theme.return')) ?></a></p>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
|
|
@ -52,6 +52,12 @@ return [
|
|||
'my_settings' => 'My Settings'
|
||||
]
|
||||
],
|
||||
'theme' => [
|
||||
'name' => [
|
||||
'label' => 'Theme Name',
|
||||
'help' => 'Name the theme by its unique code. For example, RainLab.Vanilla'
|
||||
]
|
||||
],
|
||||
'plugin' => [
|
||||
'unnamed' => 'Unnamed plugin',
|
||||
'name' => [
|
||||
|
|
@ -156,7 +162,9 @@ return [
|
|||
'install' => [
|
||||
'project_label' => 'Attach to Project',
|
||||
'plugin_label' => 'Install Plugin',
|
||||
'theme_label' => 'Install Theme',
|
||||
'missing_plugin_name' => 'Please specify a Plugin name to install.',
|
||||
'missing_theme_name' => 'Please specify a Theme name to install.',
|
||||
'install_completing' => 'Finishing installation process',
|
||||
'install_success' => 'The plugin has been installed successfully.'
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in New Issue