Browse Source

Version 1.3.7: Image Field for Themes and Plugins

trendschau 5 years ago
parent
commit
b41ca0a3a1

+ 1 - 1
content/index.md

@@ -1,4 +1,4 @@
 # Typemill
 # Typemill
 
 
-*Typemill is a user-friendly and lightweight open source CMS for publishing text-works like prose, lyrics, manuals, documentations, studies and more. Just download and start.*
+Typemill is a user-friendly and lightweight open source CMS for publishing text-works like prose, lyrics, manuals, documentations, studies and more. Just download and start.
 
 

+ 86 - 16
system/Controllers/SettingsController.php

@@ -172,7 +172,7 @@ class SettingsController extends Controller
 	
 	
 	public function showThemes($request, $response, $args)
 	public function showThemes($request, $response, $args)
 	{
 	{
-		$userSettings 	= $this->c->get('settings');		
+		$userSettings 	= $this->c->get('settings');
 		$themes 		= $this->getThemes();
 		$themes 		= $this->getThemes();
 		$themedata		= array();
 		$themedata		= array();
 		$fieldsModel	= new Fields();
 		$fieldsModel	= new Fields();
@@ -332,7 +332,7 @@ class SettingsController extends Controller
 			$userInput		= isset($params[$themeName]) ? $params[$themeName] : false;
 			$userInput		= isset($params[$themeName]) ? $params[$themeName] : false;
 			$validate		= new Validation();
 			$validate		= new Validation();
 			$themeSettings 	= \Typemill\Settings::getObjectSettings('themes', $themeName);
 			$themeSettings 	= \Typemill\Settings::getObjectSettings('themes', $themeName);
-			
+
 			if(isset($themeSettings['settings']['images']))
 			if(isset($themeSettings['settings']['images']))
 			{	
 			{	
 				# get the default settings
 				# get the default settings
@@ -375,12 +375,23 @@ class SettingsController extends Controller
 
 
 			if($userInput)
 			if($userInput)
 			{
 			{
-				/* validate the user-input */
-				$this->validateInput('themes', $themeName, $userInput, $validate);
-				
+				# validate the user-input and return image-fields if they are defined
+				$imageFields = $this->validateInput('themes', $themeName, $userInput, $validate);
+
 				/* set user input as theme settings */
 				/* set user input as theme settings */
 				$userSettings['themes'][$themeName] = $userInput;
 				$userSettings['themes'][$themeName] = $userInput;
 			}
 			}
+
+			# handle images
+			$images = $request->getUploadedFiles();
+
+			if(!isset($_SESSION['errors']) && isset($images[$themeName]))
+			{
+				$userInput = $this->saveImages($imageFields, $userInput, $userSettings, $images[$themeName]);
+
+				# set user input as theme settings
+				$userSettings['themes'][$themeName] = $userInput;
+			}
 			
 			
 			/* check for errors and redirect to path, if errors found */
 			/* check for errors and redirect to path, if errors found */
 			if(isset($_SESSION['errors']))
 			if(isset($_SESSION['errors']))
@@ -428,11 +439,22 @@ class SettingsController extends Controller
 				else
 				else
 				{
 				{
 					/* validate the user-input */
 					/* validate the user-input */
-					$this->validateInput('plugins', $pluginName, $userInput[$pluginName], $validate);
+					$imageFields = $this->validateInput('plugins', $pluginName, $userInput[$pluginName], $validate);
 
 
 					/* use the input data */
 					/* use the input data */
 					$pluginSettings[$pluginName] = $userInput[$pluginName];
 					$pluginSettings[$pluginName] = $userInput[$pluginName];
 				}
 				}
+
+				# handle images
+				$images = $request->getUploadedFiles();
+
+				if(!isset($_SESSION['errors']) && isset($images[$pluginName]))
+				{
+					$userInput[$pluginName] = $this->saveImages($imageFields, $userInput[$pluginName], $userSettings, $images[$pluginName]);
+
+					# set user input as theme settings
+					$pluginSettings[$pluginName] = $userInput[$pluginName];
+				}
 				
 				
 				/* deactivate the plugin, if there is no active flag */
 				/* deactivate the plugin, if there is no active flag */
 				if(!isset($userInput[$pluginName]['active']))
 				if(!isset($userInput[$pluginName]['active']))
@@ -465,6 +487,9 @@ class SettingsController extends Controller
 		/* fetch the original settings from the folder (plugin or theme) to get the field definitions */
 		/* fetch the original settings from the folder (plugin or theme) to get the field definitions */
 		$originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName);
 		$originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName);
 
 
+		# images get special treatment
+		$imageFieldDefinitions = array();
+
 		if(isset($originalSettings['forms']['fields']))
 		if(isset($originalSettings['forms']['fields']))
 		{
 		{
 			/* flaten the multi-dimensional array with fieldsets to a one-dimensional array */
 			/* flaten the multi-dimensional array with fieldsets to a one-dimensional array */
@@ -509,6 +534,12 @@ class SettingsController extends Controller
 				{
 				{
 					/* validate user input for this field */
 					/* validate user input for this field */
 					$validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition, $skiprequired);
 					$validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition, $skiprequired);
+					
+					if($fieldDefinition['type'] == 'image')
+					{
+						# we want to return all images-fields for further processing
+						$imageFieldDefinitions[$fieldName] = $fieldDefinition;
+					}
 				}
 				}
 				if(!$fieldDefinition && $fieldName != 'active')
 				if(!$fieldDefinition && $fieldName != 'active')
 				{
 				{
@@ -516,6 +547,45 @@ class SettingsController extends Controller
 				}
 				}
 			}
 			}
 		}
 		}
+
+		return $imageFieldDefinitions;
+	}
+
+	protected function saveImages($imageFields, $userInput, $userSettings, $files)
+	{
+
+		# initiate image processor with standard image sizes
+		$processImages = new ProcessImage($userSettings['images']);
+
+		if(!$processImages->checkFolders())
+		{
+			$this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.');
+			return false; 
+		}
+
+		foreach($imageFields as $fieldName => $imageField)
+		{
+			if(isset($userInput[$fieldName]))
+			{
+				# handle single input with single file upload
+    			$image = $files[$fieldName];
+    		
+    			if($image->getError() === UPLOAD_ERR_OK) 
+    			{
+    				# not the most elegant, but createImage expects a base64-encoded string.
+    				$imageContent = $image->getStream()->getContents();
+					$imageData = base64_encode($imageContent);
+					$imageSrc = 'data: ' . $image->getClientMediaType() . ';base64,' . $imageData;
+
+					if($processImages->createImage($imageSrc, $image->getClientFilename(), $userSettings['images'], $overwrite = NULL))
+					{
+						# returns image path to media library
+						$userInput[$fieldName] = $processImages->publishImage();
+					}
+			    }
+			}
+		}
+		return $userInput;
 	}
 	}
 
 
 	/***********************
 	/***********************
@@ -751,13 +821,13 @@ class SettingsController extends Controller
 		
 		
 	private function getLanguages()
 	private function getLanguages()
 	{
 	{
-		return array(
-			'en' => 'English',
-			'ru' => 'Russian',
-			'nl' => 'Dutch, Flemish',
-			'de' => 'German',
-			'it' => 'Italian',
-			'fr' => 'French',
-		);
-	}
-}
+		return array(
+			'en' => 'English',
+			'ru' => 'Russian',
+			'nl' => 'Dutch, Flemish',
+			'de' => 'German',
+			'it' => 'Italian',
+			'fr' => 'French',
+		);
+	}
+}

+ 1 - 1
system/Models/Fields.php

@@ -86,7 +86,7 @@ class Fields
 					}
 					}
 					else
 					else
 					{
 					{
-						$field->unsetAttribute('chhecked');
+						$field->unsetAttribute('checked');
 					}
 					}
 				}
 				}
 				else
 				else

+ 3 - 1
system/Models/Folder.php

@@ -506,7 +506,9 @@ class Folder
 	{
 	{
 		# if it is the first round, create an empty array
 		# if it is the first round, create an empty array
 		if(!$i){ $i = 0; $breadcrumb = array();}
 		if(!$i){ $i = 0; $breadcrumb = array();}
-		
+
+		if(!$searchArray){ return $breadcrumb;}
+
 		while($i < count($searchArray))
 		while($i < count($searchArray))
 		{
 		{
 			if(!isset($content[$searchArray[$i]])){ return false; }
 			if(!isset($content[$searchArray[$i]])){ return false; }

+ 10 - 0
system/Models/Validation.php

@@ -27,6 +27,15 @@ class Validation
 			return false;
 			return false;
 		}, 'invalid values');
 		}, 'invalid values');
 
 
+		Validator::addRule('image_types', function($field, $value, array $params, array $fields) use ($user)
+		{
+    		$allowed 	= ['jpg', 'jpeg', 'png'];
+			$pathinfo	= pathinfo($value);
+			$extension 	= strtolower($pathinfo['extension']);
+			if(array_search($extension, $allowed)){ return true; }
+			return false;
+		}, 'only jpg, jpeg, png allowed');
+
 		Validator::addRule('userAvailable', function($field, $value, array $params, array $fields) use ($user)
 		Validator::addRule('userAvailable', function($field, $value, array $params, array $fields) use ($user)
 		{
 		{
 			$userdata = $user->getUser($value);
 			$userdata = $user->getUser($value);
@@ -434,6 +443,7 @@ class Validation
 			case "image":
 			case "image":
 				$v->rule('noHTML', $fieldName);
 				$v->rule('noHTML', $fieldName);
 				$v->rule('lengthMax', $fieldName, 1000);
 				$v->rule('lengthMax', $fieldName, 1000);
+				$v->rule('image_types', $fieldName);
 				break;
 				break;
 			default:
 			default:
 				$v->rule('lengthMax', $fieldName, 1000);
 				$v->rule('lengthMax', $fieldName, 1000);

+ 3 - 1
system/author/css/style.css

@@ -1114,7 +1114,7 @@ ul.cardInfo{
 	border: 0px;
 	border: 0px;
 }
 }
 .cardFields.open{
 .cardFields.open{
-	max-height: 5000px;
+	max-height: 20000px;
 	transition: max-height 0.5s ease-in;
 	transition: max-height 0.5s ease-in;
 	overflow: hidden;
 	overflow: hidden;
 	border: 1px;
 	border: 1px;
@@ -2490,6 +2490,8 @@ footer a:focus, footer a:hover, footer a:active
 }
 }
 .blox .TOC li:before{ color: #bbb; }
 .blox .TOC li:before{ color: #bbb; }
 
 
+.mbfix{ margin-bottom: 0px!important; }
+
 
 
 @media only screen and (min-width: 600px) {
 @media only screen and (min-width: 600px) {
 	section{
 	section{

+ 4 - 2
system/author/editor/publish-controller.twig

@@ -1,3 +1,5 @@
+{% set itemurl = (item.urlRelWoF == '/') ? '' : item.urlRelWoF %}
+
 <div class="editor buttonset" id="publishController" data-published="{{ item.published }}" data-drafted="{{ item.drafted }}" v-cloak>
 <div class="editor buttonset" id="publishController" data-published="{{ item.published }}" data-drafted="{{ item.drafted }}" v-cloak>
 	<div v-if="errors.message" class="message error">${ errors.message }</div>
 	<div v-if="errors.message" class="message error">${ errors.message }</div>
 	<button v-if="raw" @click.prevent="saveDraft" id="draft" :class="draftResult" :disabled="draftDisabled"><span class="desktop">{{ __('Save') }}&nbsp;</span>{{ __('Draft') }}</button><button @click.prevent="publishDraft" id="publish" :class="publishResult" :disabled="publishDisabled">{{ __('Publish') }}</button>
 	<button v-if="raw" @click.prevent="saveDraft" id="draft" :class="draftResult" :disabled="draftDisabled"><span class="desktop">{{ __('Save') }}&nbsp;</span>{{ __('Draft') }}</button><button @click.prevent="publishDraft" id="publish" :class="publishResult" :disabled="publishDisabled">{{ __('Publish') }}</button>
@@ -5,8 +7,8 @@
 	<div class="secondary">
 	<div class="secondary">
 		<button @click.prevent="depublishArticle" class="button--secondary" :disabled="publishStatus"><span class="desktop">${publishLabel}</span><span class="mobile">${publishLabelMobile}</span></button>
 		<button @click.prevent="depublishArticle" class="button--secondary" :disabled="publishStatus"><span class="desktop">${publishLabel}</span><span class="mobile">${publishLabelMobile}</span></button>
 		<button @click.prevent="showModal('delete')" class="button--secondary danger"><span class="desktop">{{ __('delete') }}</span><span class="mobile">X</span></button>
 		<button @click.prevent="showModal('delete')" class="button--secondary danger"><span class="desktop">{{ __('delete') }}</span><span class="mobile">X</span></button>
-		<a v-if="visual" href="{{ base_url }}/tm/content/raw{{item.urlRelWoF}}" class="button--secondary"><span class="desktop">{{ __('raw mode') }}</span><span class="mobile">{{ __('raw') }}</span></a>
-		<a v-if="raw" href="{{ base_url }}/tm/content/visual{{item.urlRelWoF}}" class="button--secondary"><span class="desktop">{{ __('visual mode') }}</span><span class="mobile">{{ __('visual') }}</span></a>
+		<a v-if="visual" href="{{ base_url }}/tm/content/raw{{ itemurl }}" class="button--secondary"><span class="desktop">{{ __('raw mode') }}</span><span class="mobile">{{ __('raw') }}</span></a>
+		<a v-if="raw" href="{{ base_url }}/tm/content/visual{{ itemurl }}" class="button--secondary"><span class="desktop">{{ __('visual mode') }}</span><span class="mobile">{{ __('visual') }}</span></a>
 		<a target="_blank" class="button--secondary" href="{{ item.urlAbs }}"><svg class="icon baseline icon-external-link"><use xlink:href="#icon-external-link"></use></svg></a>
 		<a target="_blank" class="button--secondary" href="{{ item.urlAbs }}"><svg class="icon baseline icon-external-link"><use xlink:href="#icon-external-link"></use></svg></a>
 	</div>
 	</div>
 	<transition name="fade">
 	<transition name="fade">

+ 61 - 5
system/author/js/typemillutils.js

@@ -5,6 +5,7 @@ let typemillUtilities = {
 	{
 	{
 		this.youtubeItems = document.querySelectorAll( ".youtube" );
 		this.youtubeItems = document.querySelectorAll( ".youtube" );
 	},
 	},
+
 	addYoutubePlayButtons: function(){
 	addYoutubePlayButtons: function(){
 		if(this.youtubeItems)
 		if(this.youtubeItems)
 		{
 		{
@@ -24,12 +25,13 @@ let typemillUtilities = {
 		youtubePlaybutton.classList.add("play-video");
 		youtubePlaybutton.classList.add("play-video");
 		youtubePlaybutton.value = "Play";
 		youtubePlaybutton.value = "Play";
 
 
-		element.parentNode.appendChild(youtubePlaybutton);	
+		element.parentNode.appendChild(youtubePlaybutton);
 	},
 	},
 
 
-	listenToYoutube: function(){
+	listenToClick: function(){
 		document.addEventListener('click', function (event) {
 		document.addEventListener('click', function (event) {
 
 
+			/* listen to youtube */
 			if (event.target.matches('.play-video')) {
 			if (event.target.matches('.play-video')) {
 
 
 				var youtubeID = event.target.parentNode.getElementsByClassName('youtube')[0].id;
 				var youtubeID = event.target.parentNode.getElementsByClassName('youtube')[0].id;
@@ -49,12 +51,66 @@ let typemillUtilities = {
 				videocontainer.innerHTML = "";
 				videocontainer.innerHTML = "";
 				videocontainer.appendChild( iframe );
 				videocontainer.appendChild( iframe );
 			}
 			}
+
+			if (event.target.matches('.function-delete-img')) {
+
+				event.preventDefault();
+				event.stopPropagation();
+
+				var imgUploadField = event.target.closest(".img-upload");
+				var imgSrc = imgUploadField.getElementsByClassName("function-img-src")[0];
+				imgSrc.src = '';
+				var imgUrl = imgUploadField.getElementsByClassName("function-img-url")[0];
+				imgUrl.value = '';
+
+			}
+
+		}, true);	
+	},
+
+	listenToChange: function()
+	{
+		document.addEventListener('change', function (changeevent) {
+
+			/* listen to youtube */
+			if (changeevent.target.matches('.function-img-file')) {
+
+				if(changeevent.target.files.length > 0)
+				{
+					let imageFile = changeevent.target.files[0];
+					let size = imageFile.size / 1024 / 1024;
+				
+					if (!imageFile.type.match('image.*'))
+					{
+						// publishController.errors.message = "Only images are allowed.";
+					}
+					else if (size > this.maxsize)
+					{
+						// publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB";
+					}
+					else
+					{
+						let reader = new FileReader();
+						reader.readAsDataURL(imageFile);
+						reader.onload = function(fileevent) 
+						{
+							var imgUploadField = changeevent.target.closest(".img-upload");
+							var imgSrc = imgUploadField.getElementsByClassName("function-img-src")[0];
+							imgSrc.src = fileevent.target.result;
+							var imgUrl = imgUploadField.getElementsByClassName("function-img-url")[0];
+							imgUrl.value = imageFile.name;
+						}
+					}
+				}
+			}
+
 		}, true);	
 		}, true);	
 	},
 	},
 
 
 	start: function(){
 	start: function(){
 		this.setYoutubeItems();
 		this.setYoutubeItems();
-		this.addYoutubePlayButtons();
-		this.listenToYoutube();
-	},	
+		this.addYoutubePlayButtons();		
+		this.listenToClick();
+		this.listenToChange();
+	},
 };
 };

+ 1 - 0
system/author/languages/de.yaml

@@ -167,6 +167,7 @@ ULIST: ulist
 UNKNOWN: Unbekannt
 UNKNOWN: Unbekannt
 UPDATE_USER: Nutzer aktualisieren
 UPDATE_USER: Nutzer aktualisieren
 UPLOAD_FILE: Datei hochladen
 UPLOAD_FILE: Datei hochladen
+UPLOAD_AN_IMAGE: Bild hochladen
 UPLOAD: hochladen
 UPLOAD: hochladen
 USE_2_TO_20_CHARACTERS: 2 bis 20 Anschläge erlaubt.
 USE_2_TO_20_CHARACTERS: 2 bis 20 Anschläge erlaubt.
 USE_2_TO_40_CHARACTERS: 2 to 40 Anschläge erlaubt.
 USE_2_TO_40_CHARACTERS: 2 to 40 Anschläge erlaubt.

+ 5 - 1
system/author/layouts/layout.twig

@@ -56,7 +56,11 @@
 			</article>
 			</article>
 			<footer></footer>
 			<footer></footer>
 		</div>
 		</div>
-		<script src="{{ base_url }}/system/author/js/color-picker.min.js?20200505"></script>
+<!--		<script src="{{ base_url }}/system/author/js/color-picker.min.js?20200505"></script> -->
 		<script src="{{ base_url }}/system/author/js/author.js?20200505"></script>
 		<script src="{{ base_url }}/system/author/js/author.js?20200505"></script>
+		<script src="{{ base_url }}/system/author/js/typemillutils.js?20200505"></script>
+		<script>
+			typemillUtilities.start()
+		</script>
 	</body>
 	</body>
 </html>
 </html>

+ 73 - 41
system/author/partials/fields.twig

@@ -5,67 +5,99 @@
 		{% if field.help %}<div class="help">?<span class="tooltip">{{__(field.help|slice(0,100))}}</span></div>{% endif %}
 		{% if field.help %}<div class="help">?<span class="tooltip">{{__(field.help|slice(0,100))}}</span></div>{% endif %}
 	</label>
 	</label>
 
 
-	{% if field.type == 'textarea' %}
+	{% if field.type == 'image' %}
+		<div class="img-upload flex flex-wrap item-start">
+			<div class="w-50">
+				<div class="w6 h6 bg-black-40 dtc v-mid bg-chess">
+					<img src="{{ settings[object][itemName][field.name] }}" class="function-img-src mw6 max-h6 dt center">
+				</div>
+			</div>
+			<div class="w-50 ph3 lh-copy f6 relative">
+				<div class="relative dib w-100">
+					<input class="function-img-file absolute o-0 w-100 top-0 z-1 pointer" type="file" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" accept="image/*" />
+					<div class="relative w-100 bn br1 bg-tm-green white pa3 ma0 tc"><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> {{ __('Upload an image') }}</div>
+				</div>
+				<div class="dib w-100 mt3">
+					<label>Image URL</label>
+					<div class="flex">
+						<button class="function-delete-img w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>
+						<input class="function-img-url w-90 mbfix" type="text" name="{{ itemName }}[{{ field.name }}]" value="{{ settings[object][itemName][field.name] }}" readonly>
+					</div>
+				</div>
+				{% if errors[itemName][field.name] %}
+					<div class="error f6">{{ errors[itemName][field.name] | first }}</div>
+				{% endif %}
+
+				{% if field.description %}<div class="description pv3">{{ __(field.description) }}</div>{% endif %}
+
+			</div>
+		</div>
 
 
-		<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
-
-	{% elseif field.type == 'paragraph' %}
-		
-		{{ markdown(field.getContent()) }}	
-
-	{% elseif field.type == 'checkbox' %}
-			
-		<label class="control-group">{{ __( field.getCheckboxLabel() ) }}
-			<input type="checkbox" id="{{ itemName}}[{{ field.name }}]" name="{{ itemName}}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
-			<span class="checkmark"></span>
-		</label>
+	{% else %}
 
 
-	{% elseif field.type == 'checkboxlist' %}
+		{% if field.type == 'textarea' %}
 
 
-		{% set options = field.getOptions() %}
+			<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
 
 
-		{% for value,label in options %}
+		{% elseif field.type == 'paragraph' %}
 			
 			
-			<label class="control-group">{{ __( label ) }}
-				<input type="checkbox" id="{{ itemName }}[{{ field.name }}][{{value}}]" name="{{ itemName }}[{{ field.name }}][{{value}}]" {{ settings[object][itemName][field.name][value] ? ' checked' : '' }}>
+			{{ markdown(field.getContent()) }}	
+
+		{% elseif field.type == 'checkbox' %}
+				
+			<label class="control-group">{{ __( field.getCheckboxLabel() ) }}
+				<input type="checkbox" id="{{ itemName}}[{{ field.name }}]" name="{{ itemName}}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
 				<span class="checkmark"></span>
 				<span class="checkmark"></span>
 			</label>
 			</label>
 
 
-		{% endfor %}
-
-	{% elseif field.type == 'select' %}
+		{% elseif field.type == 'checkboxlist' %}
 
 
-		{% set options = field.getOptions() %}
+			{% set options = field.getOptions() %}
 
 
-		<select id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
 			{% for value,label in options %}
 			{% for value,label in options %}
-				<option value="{{ value }}" {{ (value == field.getAttributeValue('value')) ? ' selected' : '' }}>{{ label }}</option>
+				
+				<label class="control-group">{{ __( label ) }}
+					<input type="checkbox" id="{{ itemName }}[{{ field.name }}][{{value}}]" name="{{ itemName }}[{{ field.name }}][{{value}}]" {{ settings[object][itemName][field.name][value] ? ' checked' : '' }}>
+					<span class="checkmark"></span>
+				</label>
+
 			{% endfor %}
 			{% endfor %}
-		</select>
 
 
-	{% elseif field.type == 'radio' %}
+		{% elseif field.type == 'select' %}
+
+			{% set options = field.getOptions() %}
+
+			<select id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+				{% for value,label in options %}
+					<option value="{{ value }}" {{ (value == field.getAttributeValue('value')) ? ' selected' : '' }}>{{ label }}</option>
+				{% endfor %}
+			</select>
 
 
-		{% set options = field.getOptions() %}
+		{% elseif field.type == 'radio' %}
 
 
-		{% for value,label in options %}
+			{% set options = field.getOptions() %}
+
+			{% for value,label in options %}
+				
+				<label class="control-group">{{ label }} 
+					<input type="radio" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" value="{{ value }}" {{ (value == settings[object][itemName][field.name]) ? ' checked' : '' }}>
+					<span class="radiomark"></span>
+				</label>
+
+			{% endfor %}
 			
 			
-			<label class="control-group">{{ label }} 
-				<input type="radio" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" value="{{ value }}" {{ (value == settings[object][itemName][field.name]) ? ' checked' : '' }}>
-				<span class="radiomark"></span>
-			</label>
+		{% else %}
 
 
-		{% endfor %}
-		
-	{% else %}
+			<input id="{{itemName}}[{{ field.name }}]" name="{{itemName}}[{{ field.name }}]" type="{{ field.type }}"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
 
 
-		<input id="{{itemName}}[{{ field.name }}]" name="{{itemName}}[{{ field.name }}]" type="{{ field.type }}"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+		{% endif %}
 
 
-	{% endif %}
+		{% if field.description %}<div class="description">{{ __(field.description) }}</div>{% endif %}
+		
+		{% if errors[itemName][field.name] %}
+			<span class="error">{{ errors[itemName][field.name] | first }}</span>
+		{% endif %}
 
 
-	{% if field.description %}<div class="description">{{ __(field.description) }}</div>{% endif %}
-	
-	{% if errors[itemName][field.name] %}
-		<span class="error">{{ errors[itemName][field.name] | first }}</span>
 	{% endif %}
 	{% endif %}
 
 
 </div>
 </div>

+ 1 - 1
system/author/partials/form.twig

@@ -3,7 +3,7 @@
 	<script src="https://www.google.com/recaptcha/api.js" async defer></script>
 	<script src="https://www.google.com/recaptcha/api.js" async defer></script>
 
 
 {% endif %}
 {% endif %}
-<form method="POST" action="{{ path_for('form.save') }}">
+<form method="POST" action="{{ path_for('form.save') }}" enctype="multipart/form-data">
 
 
 	<fieldset class="card{{ errors[itemName] ? ' errors' : '' }}">
 	<fieldset class="card{{ errors[itemName] ? ' errors' : '' }}">
 
 

+ 1 - 1
system/author/settings/plugins.twig

@@ -6,7 +6,7 @@
 	
 	
 	<div class="formWrapper">
 	<div class="formWrapper">
 
 
-		<form method="POST" action="{{ path_for('plugins.save') }}" novalidate>
+		<form method="POST" action="{{ path_for('plugins.save') }}" enctype="multipart/form-data" novalidate>
 
 
 			<section id="plugins" class="plugins">
 			<section id="plugins" class="plugins">
 			
 			

+ 1 - 1
system/author/settings/themes.twig

@@ -15,7 +15,7 @@
 				
 				
 				{% for themeName, theme in themes %}
 				{% for themeName, theme in themes %}
 				
 				
-					<form method="POST" id="theme-{{ themeName }}" action="{{ path_for('themes.save') }}">
+					<form method="POST" id="theme-{{ themeName }}" action="{{ path_for('themes.save') }}" enctype="multipart/form-data">
 					
 					
 						<fieldset class="card{{ errors[themeName] ? ' errors' : '' }}">
 						<fieldset class="card{{ errors[themeName] ? ' errors' : '' }}">