Version 5.0.0 release

This commit is contained in:
Neo committed 2025-12-19 18:37:58 -08:00
1 parent 90e8c7e903
commit a3a4a9ccbd
651 files changed
+7275 -9481

No files matched your search

+5 -33
View File
@@ -21,10 +21,8 @@ use IPS\Helpers\Form;
use IPS\Helpers\Form\Icon;
use IPS\Helpers\Form\Node;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Translatable;
use IPS\Http\Url;
use IPS\Lang;
use IPS\Member;
use IPS\Node\Model;
use IPS\Request;
use IPS\Settings;
@@ -74,11 +72,6 @@ class Category extends Model
*/
public static ?string $databaseColumnParent = 'parent';
/**
* @brief [Node] Title prefix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}" as the key
*/
public static ?string $titleLangPrefix = 'themeeditor_cat_';
/**
* @brief [Node] Show forms modally?
*/
@@ -106,7 +99,7 @@ class Category extends Model
*/
protected function get__title(): string
{
return Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->key, NULL, array( 'escape' => TRUE ) );
return $this->name ?? '';
}
/**
@@ -119,7 +112,7 @@ class Category extends Model
*/
public function getTitleForLanguage( Lang $language, array $options=array() ): string
{
return $language->addToStack( static::$titleLangPrefix . $this->key, NULL, $options );
return $this->name;
}
/**
@@ -129,7 +122,7 @@ class Category extends Model
*/
protected function get__titleLanguageKey(): ?string
{
return static::$titleLangPrefix . $this->key;
return $this->name;
}
/**
@@ -316,10 +309,7 @@ class Category extends Model
*/
public function form( Form &$form ) : void
{
$form->add( new Translatable( 'themeeditor_cat_name', null, true, array(
'app' => 'core',
'key' => ( $this->id ? 'themeeditor_cat_' . $this->key : null )
) ) );
$form->add( new Text( 'themeeditor_cat_name', $this->name, true ) );
$form->add( new Text( 'themeeditor_cat_key', $this->key, true, array(
'regex' => '/[A-Z]+/i',
@@ -375,12 +365,6 @@ class Category extends Model
*/
public function formatFormValues( array $values ): array
{
if( !$this->id )
{
$this->key = $values['themeeditor_cat_key'];
$this->save();
}
$icon = $values['themeeditor_cat_icon'][0];
if( $values['themeeditor_cat_icon_extra'] )
{
@@ -388,6 +372,7 @@ class Category extends Model
}
$return = [
'cat_name' => $values['themeeditor_cat_name'],
'cat_key' => $values['themeeditor_cat_key'],
'cat_icon' => json_encode( $icon ),
'cat_parent' => ( $values['themeeditor_cat_parent'] instanceof Model ? $values['themeeditor_cat_parent']->id : 0 ),
@@ -399,8 +384,6 @@ class Category extends Model
$return['cat_app'] = $values['themeeditor_cat_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $values['themeeditor_cat_key'], $values['themeeditor_cat_name'] );
return $return;
}
@@ -527,15 +510,4 @@ class Category extends Model
}
return $return;
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete(): void
{
parent::delete();
Lang::deleteCustom( 'core', static::$titleLangPrefix . $this->key );
}
}
+8 -66
View File
@@ -25,7 +25,6 @@ use IPS\Helpers\Form\Node;
use IPS\Helpers\Form\Number;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\Text;
use IPS\Helpers\Form\Translatable;
use IPS\Helpers\Form\YesNo;
use IPS\Http\Url;
use IPS\IPS;
@@ -90,16 +89,6 @@ class Setting extends Model
*/
public static ?string $databaseColumnOrder = 'position';
/**
* @brief [Node] Title prefix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}" as the key
*/
public static ?string $titleLangPrefix = 'themeeditor_var_';
/**
* @brief [Node] Description suffix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}_{$descriptionLangSuffix}" as the key
*/
public static ?string $descriptionLangSuffix = '_desc';
/**
* @brief [Node] Parent Node ID Database Column
*/
@@ -134,7 +123,7 @@ class Setting extends Model
*/
protected function get__title(): string
{
return Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->key, NULL, array( 'escape' => TRUE ) );
return $this->name ?? '';
}
/**
@@ -147,7 +136,7 @@ class Setting extends Model
*/
public function getTitleForLanguage( Lang $language, array $options=array() ): string
{
return $language->addToStack( static::$titleLangPrefix . $this->key, NULL, $options );
return $this->_title;
}
/**
@@ -157,7 +146,7 @@ class Setting extends Model
*/
protected function get__titleLanguageKey(): ?string
{
return static::$titleLangPrefix . $this->key;
return $this->_title;
}
/**
@@ -167,7 +156,7 @@ class Setting extends Model
*/
protected function get_description(): ?string
{
return Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->key . static::$descriptionLangSuffix );
return $this->desc;
}
/**
@@ -237,27 +226,6 @@ class Setting extends Model
return $return;
}
/**
* @return string
*/
public function get_name() : string
{
return Member::loggedIn()->language()->addToStack( 'themeeditor_var_' . $this->key );
}
/**
* @return string
*/
public function get_desc() : string
{
if( Member::loggedIn()->language()->checkKeyExists( 'themeeditor_var_' . $this->key . '_desc' ) )
{
return Member::loggedIn()->language()->addToStack( 'themeeditor_var_' . $this->key . '_desc' );
}
return '';
}
/**
* @return array
*/
@@ -427,10 +395,6 @@ class Setting extends Model
switch( $this->type )
{
case static::SETTING_IMAGE:
$value = 'url("' . $value . '")';
break;
case static::SETTING_TEXT:
$value = strip_tags( $value );
$value = str_replace( array( '/*', ';' ), '', $value );
@@ -495,16 +459,8 @@ class Setting extends Model
*/
public function form( Form &$form ) : void
{
$form->add( new Translatable( 'themeeditor_setting_name', null, true, array(
'app' => 'core',
'key' => ( $this->id ? 'themeeditor_var_' . $this->key : null )
) ) );
$form->add( new Translatable( 'themeeditor_setting_desc', null, false, array(
'app' => 'core',
'key' => ( $this->id ? 'themeeditor_var_' . $this->key . '_desc' : null )
) ) );
$form->add( new Text( 'themeeditor_setting_name', $this->name, true ) );
$form->add( new Text( 'themeeditor_setting_desc', $this->desc, false ) );
$form->add( new Text( 'themeeditor_setting_key', $this->key, true, array(
'regex' => '/[A-Z]+/i',
'maxLength' => 150
@@ -606,6 +562,8 @@ class Setting extends Model
}
$return = [
'setting_name' => $values['themeeditor_setting_name'],
'setting_desc' => $values['themeeditor_setting_desc'],
'setting_key' => $values['themeeditor_setting_key'],
'setting_category_id' => ( $values['themeeditor_setting_category'] instanceof Category ) ? $values['themeeditor_setting_category']->id : 0,
'setting_type' => $values['themeeditor_setting_type'],
@@ -651,22 +609,6 @@ class Setting extends Model
$return['setting_app'] = $values['themeeditor_setting_app']->directory;
}
Lang::saveCustom( 'core', static::$titleLangPrefix . $values['themeeditor_setting_key'], $values['themeeditor_setting_name'] );
Lang::saveCustom( 'core', static::$titleLangPrefix . $values['themeeditor_setting_key'] . '_desc', $values['themeeditor_setting_desc'] );
return $return;
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete(): void
{
parent::delete();
Lang::deleteCustom( 'core', static::$titleLangPrefix . $this->key );
Lang::deleteCustom( 'core', static::$titleLangPrefix . $this->key . '_desc' );
}
}
+119 -38
View File
@@ -35,6 +35,7 @@ use IPS\Member\Group;
use IPS\Node\Model;
use IPS\Patterns\ActiveRecord;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Theme\Editor\Category;
use IPS\Theme\Editor\Setting;
use IPS\Theme\SandboxedTemplate;
use IPS\Theme\Dev\Theme as DevTheme;
@@ -755,7 +756,6 @@ class Theme extends Model
/* Are these core IPS apps? If so, we store these in the static directory and won't be in the resource map */
if( in_array( $app, IPS::$ipsApps ) and str_replace( '/', '', $path ) !== 'custom' )
{
$url = Url::internal( 'static/resources/' . $app . '_' . static::makeBuiltTemplateLookupHash($app, $location, $path) . '_' .$name, 'static' );
if( $noProtocol )
@@ -766,7 +766,6 @@ class Theme extends Model
return $url;
}
if ( in_array( $key, array_keys( $this->resource_map ) ) )
{
if ( $this->resource_map[ $key ] === NULL )
@@ -777,7 +776,7 @@ class Theme extends Model
{
$url = File::get( 'core_Theme', $this->resource_map[ $key ] )->url;
if( $noProtocol and $url instanceof Url)
if( $noProtocol and $url instanceof Url )
{
$url = $url->setScheme(NULL);
}
@@ -816,7 +815,7 @@ class Theme extends Model
static::unlock( $flagKey );
if( in_array( $app, IPS::$ipsApps ) )
if( in_array( $app, IPS::$ipsApps ) and $resource['resource_path'] !== '/custom/' )
{
$url = rtrim( Settings::i()->base_url, '/' ) . '/' . $resourceMap[ $key ];
}
@@ -1011,33 +1010,6 @@ class Theme extends Model
}
}
if ( $editorData = json_decode( $obj->_data['theme_editor_data'], true ) )
{
$obj->_data['theme_editor_data'] = $editorData;
if ( ! isset( $obj->_data['theme_editor_data']['header'] ) )
{
$obj->_data['theme_editor_data']['header'] = [
'logo' => 4,
'navigation' => 5,
'user' => 6,
'breadcrumb' => 7,
'search' => 9,
];
}
}
else
{
/* Set up defaults */
$obj->_data['theme_editor_data'] = [ 'header' => [
'logo' => 4,
'navigation' => 5,
'user' => 6,
'breadcrumb' => 7,
'search' => 9,
] ];
}
if ( ! is_array( $obj->_data['resource_map'] ) )
{
if( $obj->id > 0 )
@@ -1142,6 +1114,24 @@ class Theme extends Model
*/
public function getCustomCssForOutput(): string
{
/* Use pending changes, if we have any */
if ( Member::loggedIn()->isEditingTheme() and isset( Request::i()->cookie['theme_editor_vars'] ) and $cookie = json_decode( Request::i()->cookie['theme_editor_vars'], true ) )
{
if( array_key_exists( 'custom', $cookie ) )
{
/* Compile the pending CSS but don't store it */
$functionName = "css_" . uniqid();
static::makeProcessFunction( static::fixResourceTags( (string) $cookie['custom'], 'front' ), $functionName, '', FALSE, TRUE );
$fqFunc = 'IPS\\Theme\\'. $functionName;
$content = static::minifyCss( $fqFunc() );
/* Replace any <fileStore.xxx> tags in the CSS */
Output::i()->parseFileObjectUrls( $content );
return $content;
}
}
$key = $this->_id . '_theme_editor_custom_css';
if ( ! isset( Store::i()->$key ) )
{
@@ -1307,6 +1297,43 @@ class Theme extends Model
return NULL;
}
/**
* Store the default so that we can determine if this is customized or not
*
* @var array[]
*/
public static array $defaultThemeEditorData = [
'header' => [
'logo' => 4,
'navigation' => 5,
'user' => 6,
'breadcrumb' => 7,
'search' => 9,
]
];
/**
* @return array
*/
public function get_theme_editor_data() : array
{
$editorData = isset( $this->_data['theme_editor_data'] ) ? json_decode( $this->_data['theme_editor_data'], true ) : [];
if( !isset( $editorData['header'] ) )
{
$editorData['header'] = static::$defaultThemeEditorData['header'];
}
return $editorData;
}
/**
* @param mixed $val
* @return void
*/
public function set_theme_editor_data( mixed $val ) : void
{
$this->_data['theme_editor_data'] = is_array( $val ) ? json_encode( $val ) : null;
}
/**
* [Node] Return the custom badge for each row
*
@@ -1440,6 +1467,9 @@ class Theme extends Model
/* Copy logos */
$this->logo_data = NULL;
$this->copyLogosFromSet( $originalId );
/* Copy custom settings and categories */
$this->copyEditorSettings( $originalId );
Session::i()->log( 'acplogs__themeset_created', array( sprintf( Member::loggedIn()->language()->get( 'theme_clone_copy_of' ), Member::loggedIn()->language()->get( 'core_theme_set_title_' . $originalId ) ) => FALSE ) );
}
@@ -2053,6 +2083,46 @@ class Theme extends Model
$this->save();
}
/**
* Copy the editor settings and categories from the specified theme
*
* @param int $id
* @return void
*/
public function copyEditorSettings( int $id ) : void
{
$categoryMapping = [];
foreach( new ActiveRecordIterator(
Db::i()->select( '*', Category::$databaseTable, [ 'cat_set_id=?', $id ], 'cat_parent, cat_position' ),
Category::class
) as $cat )
{
/* Copy the category and change the theme ID */
$newCategory = clone $cat;
$newCategory->set_id = $this->_id;
if( $newCategory->parent and array_key_exists( $cat->parent, $categoryMapping ) )
{
$newCategory->parent = $categoryMapping[ $cat->parent ];
}
$newCategory->save();
/* Map the category IDs, we'll need them later */
$categoryMapping[ $cat->id ] = $newCategory->id;
}
/* Now loop through the settings */
foreach( new ActiveRecordIterator(
Db::i()->select( '*', Setting::$databaseTable, [ 'setting_set_id=?', $id ], 'setting_position' ),
Setting::class
) as $setting )
{
$newSetting = clone $setting;
$newSetting->set_id = $this->_id;
$newSetting->category_id = $categoryMapping[ $newSetting->category_id ] ?? 0;
$newSetting->save();
}
}
/**
* Install the theme editor settings for this theme
*
@@ -2742,7 +2812,10 @@ EOF;
{
foreach( $cookie as $group => $vars )
{
$customValues = array_merge( $customValues, $vars );
if( is_array( $vars ) )
{
$customValues = array_merge( $customValues, $vars );
}
}
}
@@ -2839,7 +2912,17 @@ EOF;
/* Parse the value based on the setting type */
try
{
$value = Setting::load( $key, 'setting_key' )->parsedValue( $value );
$setting = Setting::load( $key, 'setting_key' );
$value = $setting->parsedValue( $value );
if( $setting->type == Setting::SETTING_IMAGE )
{
if( empty( $value ) )
{
continue;
}
$value = "url('" . $value . "')";
}
}
catch( OutOfRangeException ){}
@@ -3894,9 +3977,7 @@ EOF;
if( $functionName == 'theme_core_front_global_footer' or ( $functionName == 'footer' and $app == 'core' and $location == 'front' and $group == 'global' ) )
{
$content = $content . "\n<p class='ipsCopyright'>
<span class='ipsCopyright__user'>{lang=\"copyright_line_value\"}</span>
{{if !\$licenseData = \IPS\IPS::licenseKey() or !isset(\$licenseData['products']['copyright']) or !\$licenseData['products']['copyright']}}<a rel='nofollow' title='Invision Community' href='https://www.invisioncommunity.com/'>Powered by <span translate='no'>Invision Community</span></a>{{endif}}
</p>";
<span class='ipsCopyright__user'>{lang=\"copyright_line_value\"}</span></p>";
}
/* Expand hook points */
@@ -4607,7 +4688,7 @@ PHP;
$this->custom_css = '';
$this->css_variables = null;
$this->view_options = null;
$this->theme_editor_data = json_encode( $data );
$this->theme_editor_data = $data;
$this->save();
}
@@ -4746,7 +4827,7 @@ PHP;
*/
public function isCustomized(): bool
{
if ( is_array( $this->set_theme_editor_data ) and count( $this->set_theme_editor_data ) )
if( $this->theme_editor_data !== static::$defaultThemeEditorData )
{
return true;
}