Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

@@ -232,7 +232,15 @@ class _ajax extends \IPS\Dispatcher\Controller
{
if ( $result['template_user_edited'] )
{
$outOfDate = $result['template_user_edited'] < \IPS\Application::load( $result['template_app'] )->long_version;
try
{
$outOfDate = $result['template_user_edited'] < \IPS\Application::load( $result['template_app'] )->long_version;
}
catch( \UnexpectedValueException | \OutOfRangeException $e )
{
unset( $return[ $result['template_app'] ][ $result['template_location'] ][ $result['template_group'] ][ $result['template_name'] ] );
continue;
}
if ( ( $outOfDate and !\in_array( 'outofdate', explode( ',', \IPS\Request::i()->filters ) ) ) or ( !$outOfDate and !\in_array( 'modified', explode( ',', \IPS\Request::i()->filters ) ) ) )
{
@@ -313,8 +321,16 @@ class _ajax extends \IPS\Dispatcher\Controller
{
if ( $result['css_user_edited'] )
{
$outOfDate = $result['css_user_edited'] < \IPS\Application::load( $result['css_app'] )->long_version;
try
{
$outOfDate = $result['css_user_edited'] < \IPS\Application::load( $result['css_app'] )->long_version;
}
catch( \UnexpectedValueException | \OutOfRangeException $e )
{
unset( $return[ $result['css_app'] ][ $result['css_location'] ][ $result['css_path'] ][ $result['css_name'] ] );
continue;
}
if ( !$result['css_added_to'] and ( ( $outOfDate and !\in_array( 'outofdate', explode( ',', \IPS\Request::i()->filters ) ) ) or ( !$outOfDate and !\in_array( 'modified', explode( ',', \IPS\Request::i()->filters ) ) ) ) )
{
unset( $return[ $result['css_app'] ][ $result['css_location'] ][ $result['css_path'] ][ $result['css_name'] ] );
@@ -370,7 +370,7 @@ class _emails extends \IPS\Dispatcher\Controller
*/
public function emailPreview()
{
$email = \IPS\Email::buildFromContent( '', \IPS\Member::loggedIn()->language()->addToStack( 'email_preview_content' ) );
$email = \IPS\Email::buildFromContent( '', \IPS\Member::loggedIn()->language()->addToStack( 'email_preview_content' ), NULL, \IPS\Email::TYPE_TRANSACTIONAL );
\IPS\Output::i()->sendOutput( $email->compileContent( 'html' ) );
}
}
@@ -58,6 +58,10 @@ class _icons extends \IPS\Dispatcher\Controller
$homeScreen = json_decode( \IPS\Settings::i()->icons_homescreen, TRUE ) ?? array();
$form->add( new \IPS\Helpers\Form\Upload( 'icons_homescreen', ( isset( $homeScreen['original'] ) ) ? \IPS\File::get( 'core_Icons', $homeScreen['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons' ) ) );
/* Apple startup screen logo - we accept one upload and create the images we need */
$apple = json_decode( \IPS\Settings::i()->icons_apple_startup, TRUE ) ?? array();
$form->add( new \IPS\Helpers\Form\Upload( 'icons_apple_startup', ( isset( $apple['original'] ) ) ? \IPS\File::get( 'core_Icons', $apple['original'] ) : NULL, FALSE, array( 'image' => true, 'storageExtension' => 'core_Icons' ) ) );
/* Safari pinned tabs icon and highlight color */
$form->add( new \IPS\Helpers\Form\Upload( 'icons_mask_icon', \IPS\Settings::i()->icons_mask_icon ? \IPS\File::get( 'core_Icons', \IPS\Settings::i()->icons_mask_icon ) : NULL, FALSE, array( 'allowedFileTypes' => array( 'svg' ), 'storageExtension' => 'core_Icons', 'obscure' => FALSE ) ) );
$form->add( new \IPS\Helpers\Form\Color( 'icons_mask_color', \IPS\Settings::i()->icons_mask_color, FALSE ) );
@@ -129,9 +133,34 @@ class _icons extends \IPS\Dispatcher\Controller
}
}
/* Homescreen icons are the hardest part, as we need to generate different sizes.. */
/* Homescreen icon is the hardest part, as we need to generate different sizes.. */
$values = static::processApplicationIcon( $values, $homeScreen );
/* The start screen requires more different sizes so we'll rebuild that after submit */
$rebuildStartScreen = FALSE;
if( $values['icons_apple_startup'] AND ( !isset( $apple['original'] ) OR !$apple['original'] OR (string) $values['icons_apple_startup'] != $apple['original'] ) )
{
$rebuildStartScreen = TRUE;
}
if( ( !isset( $manifestDetails['background_color'] ) AND $values['manifest_bgcolor'] ) OR ( isset( $manifestDetails['background_color'] ) AND !$values['manifest_bgcolor'] ) OR ( isset( $manifestDetails['background_color'] ) AND $values['manifest_bgcolor'] != $manifestDetails['background_color'] ) )
{
if( $values['icons_apple_startup'] )
{
$rebuildStartScreen = TRUE;
}
}
if( $rebuildStartScreen )
{
$values = static::processAppleStartupScreen( $values, $apple );
}
else
{
$values['icons_apple_startup'] = json_encode( $apple );
}
/* We need the string value of this uploaded file as well */
$values['icons_mask_icon'] = (string) $values['icons_mask_icon'];
@@ -166,7 +195,14 @@ class _icons extends \IPS\Dispatcher\Controller
\IPS\Session::i()->log( 'acplogs__icons_and_logos' );
/* And Redirect */
\IPS\Output::i()->redirect( $this->url , 'saved' );
if( $rebuildStartScreen === TRUE )
{
\IPS\Output::i()->redirect( $this->url->setQueryString( 'do', 'buildStartupScreenImages' ), 'saved' );
}
else
{
\IPS\Output::i()->redirect( $this->url, 'saved' );
}
}
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__core_customization_icons');
@@ -253,4 +289,182 @@ class _icons extends \IPS\Dispatcher\Controller
return $values;
}
/**
* @brief Minimum padding on either side of startup image (in px)
*/
const MINIMUM_STARTUP_IMAGE_PADDING = 50;
/**
* Process Apple startup screen images
*
* @param array $values Values from form submission
* @param array $apple Existing values, if any
* @return array
*/
public static function processAppleStartupScreen( $values, $apple = array() )
{
if( $values['icons_apple_startup'] )
{
$values['icons_apple_startup'] = json_encode( array( 'original' => (string) $values['icons_apple_startup'] ) );
}
else
{
/* Delete any images that may already exist */
foreach( $apple as $key => $image )
{
try
{
\IPS\File::get( 'core_Icons', ( $key == 'original' ) ? $image : $image['url'] )->delete();
}
catch( \Exception $e ){}
}
$values['icons_apple_startup'] = '';
}
return $values;
}
/**
* Process Apple startup screen images
*
* @param array $values Values from form submission
* @param array $apple Existing values, if any
* @return array
*/
public function buildStartupScreenImages()
{
$self = $this;
$multiRedirect = new \IPS\Helpers\MultipleRedirect(
$this->url->setQueryString('do', 'buildStartupScreenImages'),
function( $data )
{
/* Get the necessary data */
$manifestDetails = json_decode( \IPS\Settings::i()->manifest_details, TRUE );
$setting = json_decode( \IPS\Settings::i()->icons_apple_startup, TRUE ) ?? array();
if( isset( $setting['original'] ) )
{
$sizes = array(
'apple-startup-1136x640' => array( 320, 568, 2, 'landscape' ),
'apple-startup-2436x1125' => array( 375, 812, 3, 'landscape' ),
'apple-startup-1792x828' => array( 414, 896, 2, 'landscape' ),
'apple-startup-828x1792' => array( 414, 896, 2, 'portrait' ),
'apple-startup-1334x750' => array( 375, 667, 2, 'landscape' ),
'apple-startup-1242x2688' => array( 414, 896, 3, 'portrait' ),
'apple-startup-2208x1242' => array( 414, 736, 3, 'landscape' ),
'apple-startup-1125x2436' => array( 375, 812, 3, 'portrait' ),
'apple-startup-1242x2208' => array( 414, 736, 3, 'portrait' ),
'apple-startup-2732x2048' => array( 1024, 1366, 2, 'landscape' ),
'apple-startup-2688x1242' => array( 414, 896, 3, 'landscape' ),
'apple-startup-2224x1668' => array( 834, 1112, 2, 'landscape' ),
'apple-startup-750x1334' => array( 375, 667, 2, 'portrait' ),
'apple-startup-2048x2732' => array( 1024, 1366, 2, 'portrait' ),
'apple-startup-2388x1668' => array( 834, 1194, 2, 'landscape' ),
'apple-startup-1668x2224' => array( 834, 1112, 2, 'portrait' ),
'apple-startup-640x1136' => array( 320, 568, 2, 'portrait' ),
'apple-startup-1668x2388' => array( 834, 1194, 2, 'portrait' ),
'apple-startup-2048x1536' => array( 768, 1024, 2, 'landscape' ),
'apple-startup-1536x2048' => array( 768, 1024, 2, 'portrait' ),
'apple-startup-2360x1640' => array( 1180, 820, 2, 'landscape' ),
'apple-startup-1640x2360' => array( 1180, 820, 2, 'portrait' ),
'apple-startup-2160x1620' => array( 1080, 810, 2, 'landscape' ),
'apple-startup-1620x2160' => array( 1080, 810, 2, 'portrait' ),
'apple-startup-2778x1284' => array( 428, 926, 3, 'landscape' ),
'apple-startup-1284x2778' => array( 428, 926, 3, 'portrait' ),
'apple-startup-2532x1170' => array( 390, 844, 3, 'landscape' ),
'apple-startup-1170x2532' => array( 390, 844, 3, 'portrait' ),
'apple-startup-2340x1080' => array( 360, 780, 3, 'landscape' ),
'apple-startup-1080x2340' => array( 360, 780, 3, 'portrait' ),
);
$file = \IPS\File::get( 'core_Icons', $setting['original'] );
$originalContents = $file->contents();
$backgroundColor = ( isset( $manifestDetails['background_color'] ) ) ? str_replace( '#', '', $manifestDetails['background_color'] ) : 'FFFFFF';
$rgb = array();
if ( \strlen( $backgroundColor ) == 3 )
{
$rgb[] = hexdec( \substr( $backgroundColor, 0, 1 ) . \substr( $backgroundColor, 0, 1 ) ); // R
$rgb[] = hexdec( \substr( $backgroundColor, 1, 1 ) . \substr( $backgroundColor, 1, 1 ) ); // G
$rgb[] = hexdec( \substr( $backgroundColor, 2, 1 ) . \substr( $backgroundColor, 2, 1 ) ); // B
}
else
{
$rgb[] = hexdec( \substr( $backgroundColor, 0, 2 ) ); // R
$rgb[] = hexdec( \substr( $backgroundColor, 2, 2 ) ); // G
$rgb[] = hexdec( \substr( $backgroundColor, 4, 2 ) ); // B
}
$did = 0;
foreach( $sizes as $filename => $_sizes )
{
$did++;
if( isset( $setting[ $filename ] ) )
{
continue;
}
try
{
$width = ( $_sizes[3] == 'landscape' ) ? $_sizes[1] * $_sizes[2] : $_sizes[0] * $_sizes[2];
$height = ( $_sizes[3] == 'portrait' ) ? $_sizes[1] * $_sizes[2] : $_sizes[0] * $_sizes[2];
$logoImage = \IPS\Image::create( $originalContents );
$canvas = \IPS\Image::newImageCanvas( $width, $height, $rgb );
$logoImage->resizeToMax( $width - static::MINIMUM_STARTUP_IMAGE_PADDING, $height - static::MINIMUM_STARTUP_IMAGE_PADDING );
$xPos = ( $width - $logoImage->width ) / 2;
$yPos = ( $height - $logoImage->height ) / 2;
$canvas->impose( $logoImage, $xPos, $yPos );
if( $canvas::exifSupported() )
{
$canvas->setExifData( $originalContents );
}
$setting[ $filename ] = array(
'url' => (string) \IPS\File::create( 'core_Icons', $filename . '.png', (string) $canvas, NULL, TRUE, NULL, FALSE ),
'width' => $canvas->width,
'height' => $canvas->height,
'density' => $_sizes[2],
'orientation' => $_sizes[3],
);
}
catch ( \Exception $e )
{
\IPS\Log::log( $e, 'apple-startup-image' );
}
break;
}
/* Are we done? */
if( $did == \count( $sizes ) )
{
return NULL;
}
\IPS\Settings::i()->changeValues( array( 'icons_apple_startup' => json_encode( $setting ) ) );
}
return array( $data, \IPS\Member::loggedIn()->language()->addToStack('build_start_images_title'), ( $did ) ? round( ( 100 / \count( $sizes ) * $did ), 2 ) : 0 );
},
function() use( $self )
{
\IPS\Output::i()->redirect( $self->url, 'completed' );
}
);
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('build_start_images_title');
\IPS\Output::i()->output = $multiRedirect;
}
}
@@ -546,7 +546,7 @@ class _themes extends \IPS\Node\Controller
}
/* Start importing */
$data = array( 'buildingDesignersFiles' => true, 'toBuild' => $toBuild, 'extensions' => $extensions, 'totalItems' => \count( array_keys( $toBuild ) ) + \count( array_keys( $extensions ) ), 'totalDone' => 0, 'extra' => array() );
$data = array( 'buildingDesignersFiles' => true, 'toBuild' => $toBuild, 'extensions' => $extensions, 'totalItems' => \count( array_keys( $toBuild ) ) * 3 + \count( array_keys( $extensions ) ), 'totalDone' => 0, 'extra' => array() );
return array( $data, \IPS\Member::loggedIn()->language()->addToStack('theme_dm_building') );
}
@@ -664,7 +664,7 @@ class _themes extends \IPS\Node\Controller
$table->parsers = array(
'resource_data' => function( $val, $row ) use ($set)
{
if ( \in_array( mb_substr( $row['resource_name'], mb_strrpos( $row['resource_name'], '.' ) + 1 ), array_merge( \IPS\Image::$imageExtensions, array( 'svg' ) ) ) )
if ( \in_array( mb_substr( $row['resource_name'], mb_strrpos( $row['resource_name'], '.' ) + 1 ), array_merge( \IPS\Image::supportedExtensions(), array( 'svg' ) ) ) )
{
$row['resource_filename'] = $set->resource( ltrim( $row['resource_path'], '/' ) . $row['resource_name'], $row['resource_app'], $row['resource_location'] );
}
@@ -674,7 +674,7 @@ class _themes extends \IPS\Node\Controller
'resource_name' => function( $val, $row ) use ($set)
{
if ( \in_array( mb_substr( $row['resource_name'], mb_strrpos( $row['resource_name'], '.' ) + 1 ), array_merge( \IPS\Image::$imageExtensions, array( 'svg' ) ) ) )
if ( \in_array( mb_substr( $row['resource_name'], mb_strrpos( $row['resource_name'], '.' ) + 1 ), array_merge( \IPS\Image::supportedExtensions(), array( 'svg' ) ) ) )
{
$row['resource_filename'] = $set->resource( ltrim( $row['resource_path'], '/' ) . $row['resource_name'], $row['resource_app'], $row['resource_location'] );
}
@@ -864,7 +864,7 @@ class _themes extends \IPS\Node\Controller
\IPS\Member::loggedIn()->language()->words['core_theme_resource_name_choice_o_filename_desc'] = sprintf( \IPS\Member::loggedIn()->language()->get('core_theme_resource_name_choice_o_filename_desc'), $current['resource_name'] );
}
$form->add( new \IPS\Helpers\Form\Upload( 'core_theme_resource_filename', ( isset( $current['resource_filename'] ) ? \IPS\File::get( 'core_Theme', $current['resource_filename'] ) : NULL ), TRUE, array( 'obscure' => FALSE, 'maxFileSize' => 1.2, 'storageExtension' => 'core_Theme', 'storageContainer' => 'set_resources_' . $set->_id ), NULL, NULL, NULL, 'core_theme_resource_filename' ) );
$form->add( new \IPS\Helpers\Form\Upload( 'core_theme_resource_filename', ( isset( $current['resource_filename'] ) ? \IPS\File::get( 'core_Theme', $current['resource_filename'] ) : NULL ), TRUE, array( 'obscure' => ( $id ) ? TRUE : FALSE, 'maxFileSize' => 1.2, 'storageExtension' => 'core_Theme', 'storageContainer' => 'set_resources_' . $set->_id ), NULL, NULL, NULL, 'core_theme_resource_filename' ) );
if ( $values = $form->values() )
{
@@ -919,12 +919,14 @@ class _themes extends \IPS\Node\Controller
/* Resource may be used in CSS */
\IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $set->id );
$set->css_map = array();
$set->css_updated = time();
$set->save();
foreach( $set->children() as $child )
{
\IPS\File::getClass('core_Theme')->deleteContainer( 'css_built_' . $child->id );
$child->css_map = array();
$child->css_updated = time();
$child->save();
}
@@ -1101,8 +1103,7 @@ class _themes extends \IPS\Node\Controller
$masterCss = \IPS\Theme::load( \IPS\Request::i()->id )->getRawCss( null, null, null, \IPS\Theme::RETURN_BIT_NAMES, false );
/* Open XML file */
$xml = new \IPS\Xml\XMLReader;
$xml->open( \IPS\Request::i()->file );
$xml = \IPS\Xml\XMLReader::safeOpen( \IPS\Request::i()->file );
if ( ! @$xml->read() )
{
@@ -1489,11 +1490,7 @@ class _themes extends \IPS\Node\Controller
$url = (string) \IPS\File::create( 'core_Theme', $name, base64_decode( $xml->readString() ) );
$image = \IPS\Image::create( base64_decode( $xml->readString() ) );
$width = $image->width;
$height = $image->height;
$set->saveSet( array( 'logo' => array( $type => array( 'url' => $url, 'width' => $width, 'height' => $height ) ) ) );
$set->saveSet( array( 'logo' => array( $type => array( 'url' => $url, 'height' => $xml->getAttribute('height') ?? 100 ) ) ) );
}
$xml->next();
@@ -1528,6 +1525,9 @@ class _themes extends \IPS\Node\Controller
\IPS\Theme::setThemeCustomized( $set->id );
$set->resetCacheKey();
/* Update children */
$set->updateChildrenThemeSettings();
/* Conflicts to fix? */
if ( \IPS\Db::i()->select( 'count(*)', 'core_theme_conflict', array( 'conflict_set_id=?', \IPS\Request::i()->id ) )->first() )
@@ -2014,6 +2014,10 @@ class _themes extends \IPS\Node\Controller
$xml->text( $file );
$xml->endAttribute();
$xml->startAttribute('height');
$xml->text( $data['height'] );
$xml->endAttribute();
/* Get image data */
$original = \IPS\File::get( 'core_Theme', $data['url'] );
@@ -2311,10 +2315,8 @@ class _themes extends \IPS\Node\Controller
/* Finish */
$xml->endDocument();
$name = addslashes( str_replace( array( ' ', '.', ',' ), '_', \IPS\Member::loggedIn()->language()->get('core_theme_set_title_' . $themeSet->_id ) ) . '.xml' );
\IPS\Output::i()->sendOutput( $xml->outputMemory(), 200, 'application/xml', array( 'Content-Disposition' => \IPS\Output::getContentDisposition( 'attachment', $name ), FALSE, FALSE ) );
\IPS\Output::i()->sendOutput( $xml->outputMemory(), 200, 'application/xml', array( 'Content-Disposition' => \IPS\Output::getContentDisposition( 'attachment', \IPS\Member::loggedIn()->language()->get('core_theme_set_title_' . $themeSet->_id ) . " {$version}.xml" ), FALSE, FALSE ) );
}
/* Display */
@@ -3035,7 +3037,7 @@ class _themes extends \IPS\Node\Controller
{
$form = new \IPS\Helpers\Form;
$form->hiddenvalues['id'] = \IPS\Request::i()->id;
$form->add( new \IPS\Helpers\Form\Select( 'member_reset_where', '*', TRUE, array( 'options' => \IPS\Member\Group::groups( TRUE, FALSE ), 'multiple' => TRUE, 'parse' => 'normal', 'unlimited' => '*', 'unlimitedLang' => 'all' ) ) );
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'member_reset_where', '*', TRUE, array( 'options' => \IPS\Member\Group::groups( TRUE, FALSE ), 'multiple' => TRUE, 'parse' => 'normal', 'unlimited' => '*', 'unlimitedLang' => 'all', 'impliedUnlimited' => TRUE ) ) );
if ( $values = $form->values() )
{
@@ -286,7 +286,7 @@ class _themesettings extends \IPS\Dispatcher\Controller
$form = new \IPS\Helpers\Form();
$form->hiddenValues['set_id'] = $setId;
$form->add( new \IPS\Helpers\Form\Text( 'theme_custom_setting_title', $current['sc_title'], TRUE ) );
$form->add( new \IPS\Helpers\Form\Text( 'theme_custom_setting_title', $id ? $current['sc_title'] : null, TRUE ) );
$form->add( new \IPS\Helpers\Form\Select( 'theme_custom_setting_app', $id ? $current['sc_app'] : null, TRUE, array( 'options' => $apps ) ) );
@@ -469,7 +469,7 @@ class _themesettings extends \IPS\Dispatcher\Controller
}
/* Default is changed, change value? */
if ( $save['sc_default'] !== $current['sc_default'] )
if ( $current and ( $save['sc_default'] !== $current['sc_default'] ) )
{
try
{
@@ -501,6 +501,9 @@ class _themesettings extends \IPS\Dispatcher\Controller
$set->template_settings = json_encode( $themeSettings );
$set->save();
}
/* Update children */
$set->updateChildrenThemeSettings();
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=customization&controller=themesettings&set_id=' . $setId ), 'saved' );
}