Bladeren bron

Version 1.3.4: Media Library

trendschau 5 jaren geleden
bovenliggende
commit
be6a297b83
40 gewijzigde bestanden met toevoegingen van 3258 en 966 verwijderingen
  1. 19 19
      .gitignore
  2. 40 40
      .htaccess
  3. BIN
      media/markdown.png
  4. 4 0
      settings/languages/en.yaml
  5. 1 1
      settings/languages/vuejs-en.yaml
  6. 1 617
      system/Controllers/ArticleApiController.php
  7. 841 0
      system/Controllers/BlockApiController.php
  8. 356 0
      system/Controllers/MediaApiController.php
  9. 17 5
      system/Controllers/MetaApiController.php
  10. 20 2
      system/Controllers/PageController.php
  11. 81 3
      system/Controllers/SettingsController.php
  12. 7 0
      system/Controllers/SetupController.php
  13. 22 0
      system/Extensions/TwigPagelistExtension.php
  14. 21 2
      system/Models/Folder.php
  15. 35 0
      system/Models/Helpers.php
  16. 231 0
      system/Models/ProcessAssets.php
  17. 165 0
      system/Models/ProcessFile.php
  18. 209 164
      system/Models/ProcessImage.php
  19. 32 21
      system/Routes/Api.php
  20. 8 1
      system/Routes/Web.php
  21. 40 20
      system/Settings.php
  22. 167 6
      system/author/css/style.css
  23. 33 2
      system/author/js/author.js
  24. 8 0
      system/author/js/vue-blox-config.js
  25. 766 40
      system/author/js/vue-blox.js
  26. 1 0
      system/author/layouts/layout.twig
  27. 30 4
      system/author/layouts/layoutBlox.twig
  28. 29 2
      system/author/settings/system.twig
  29. 2 1
      system/system.php
  30. 9 1
      themes/typemill/cover.twig
  31. 35 0
      themes/typemill/css/style.css
  32. BIN
      themes/typemill/img/apple-touch-icon-144x144.png
  33. BIN
      themes/typemill/img/apple-touch-icon-152x152.png
  34. BIN
      themes/typemill/img/favicon-16x16.png
  35. BIN
      themes/typemill/img/favicon-32x32.png
  36. BIN
      themes/typemill/img/favicon.ico
  37. BIN
      themes/typemill/img/mstile-144x144.png
  38. 15 8
      themes/typemill/partials/layout.twig
  39. 7 6
      themes/typemill/partials/layoutCover.twig
  40. 6 1
      themes/typemill/typemill.yaml

+ 19 - 19
.gitignore

@@ -1,20 +1,20 @@
-cache/lastCache.txt
-cache/lastSitemap.txt
-cache/metatabs.yaml
-cache/navigation.txt
-cache/sitemap.xml
-cache/structure-draft.txt
-cache/structure-extended.yaml
-cache/structure.txt
-content/index.yaml
-content/00-Welcome/index.yaml
-content/00-Welcome/00-Setup.yaml
-content/00-Welcome/01-Write-Content.yaml
-content/00-Welcome/02-Get-Help.yaml
-content/00-Welcome/03-Markdown-Test.yaml
-settings/settings.yaml
-settings/users
-system/vendor
-plugins/demo
-zips
+cache/lastCache.txt
+cache/lastSitemap.txt
+cache/metatabs.yaml
+cache/navigation.txt
+cache/sitemap.xml
+cache/structure-draft.txt
+cache/structure-extended.yaml
+cache/structure.txt
+content/index.yaml
+content/00-welcome/index.yaml
+content/00-welcome/00-setup.yaml
+content/00-welcome/01-write-content.yaml
+content/00-welcome/02-get-help.yaml
+content/00-welcome/03-markdown-test.yaml
+settings/settings.yaml
+settings/users
+system/vendor
+plugins/demo
+zips
 build.php

+ 40 - 40
.htaccess

@@ -1,41 +1,41 @@
-RewriteEngine On
-
-# If your homepage is http://yourdomain.com/yoursite
-# Set the RewriteBase to:
-# RewriteBase /yoursite
-
-# In some environements, an empty RewriteBase is required:
-# RewriteBase /
-
-# Protect your system files from prying eyes
-RewriteRule ^(system\/author\/) - [L]
-RewriteRule ^(system) - [F,L]
-RewriteRule ^(content) - [F,L]
-RewriteRule ^(settings) - [F,L]
-RewriteRule ^(.*)?\.yml$ - [F,L]
-Rewriterule ^(.*)?\.yaml$ - [F,L]
-RewriteRule ^(.*)?\.txt$ - [F,L]
-RewriteRule ^(.*)?\.example$ - [F,L]
-RewriteRule ^(.*/)?\.git+ - [F,L]
-
-# Use this to redirect HTTP to HTTPS on apache servers
-# RewriteCond %{HTTPS} off
-# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
-
-# Use this to redirect www to non-wwww on apache servers
-# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
-# RewriteRule ^(.*)$ http://%1/$1 [R=301,L]
-
-# Use this to redirect slash/ to no slash urls on apache servers
-# RewriteCond %{REQUEST_FILENAME} !-d
-# RewriteRule ^(.*)/$ /$1 [R=301,L]
-
-# Removes index.php
-RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC]
-RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L]
-
-# Directs all web requests through the site index file
-RewriteCond %{REQUEST_URI} !^/index\.php
-RewriteCond %{REQUEST_FILENAME} !-f
-RewriteCond %{REQUEST_FILENAME} !-d
+RewriteEngine On
+
+# If your homepage is http://yourdomain.com/yoursite
+# Set the RewriteBase to:
+# RewriteBase /yoursite
+
+# In some environements, an empty RewriteBase is required:
+# RewriteBase /
+
+# Protect your system files from prying eyes
+RewriteRule ^(system\/author\/) - [L]
+RewriteRule ^(system) - [F,L]
+RewriteRule ^(content) - [F,L]
+RewriteRule ^(settings) - [F,L]
+RewriteRule ^(.*)?\.yml$ - [F,L]
+Rewriterule ^(.*)?\.yaml$ - [F,L]
+RewriteRule ^(.*)?\.txt$ - [F,L]
+RewriteRule ^(.*)?\.example$ - [F,L]
+RewriteRule ^(.*/)?\.git+ - [F,L]
+
+# Use this to redirect HTTP to HTTPS on apache servers
+# RewriteCond %{HTTPS} off
+# RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
+
+# Use this to redirect www to non-wwww on apache servers
+# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
+# RewriteRule ^(.*)$ http://%1/$1 [R=301,L]
+
+# Use this to redirect slash/ to no slash urls on apache servers
+# RewriteCond %{REQUEST_FILENAME} !-d
+# RewriteRule ^(.*)/$ /$1 [R=301,L]
+
+# Removes index.php
+RewriteCond %{THE_REQUEST} ^GET.*index\.php [NC]
+RewriteRule (.*?)index\.php/*(.*) /$1$2 [R=301,NE,L]
+
+# Directs all web requests through the site index file
+RewriteCond %{REQUEST_URI} !^/index\.php
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule ^ index.php [QSA,L]

BIN
media/markdown.png


+ 4 - 0
settings/languages/en.yaml

@@ -58,8 +58,10 @@ QUOTES: Quote
 TABLE_OF_CONTENTS: Table of Contents
 TABLE: Table
 TEXT_FILE: text-file
+UPLOAD: upload
 VIDEO: Video
 
+# others
 ACCOUNT: Account
 ACTIVE: Active
 ACTUAL_PASSWORD: Actual Password
@@ -70,7 +72,9 @@ ADD_ITEM: add item
 ALL_USERS: All users
 AUTHOR: Author
 BACK_TO_STARTPAGE: back to startpage
+BROWSE: BROWSE
 BY: by
+CHOOSE_FILE: Choose file
 CODE: code
 CONTENT: Content
 COPYRIGHT: Copyright

+ 1 - 1
settings/languages/vuejs-en.yaml

@@ -22,7 +22,7 @@ en:
   delete content-block: delete content-block
   delete row: delete row
   description: description
-  drag a picture or click to select: drag a picture or click to select
+  drag a picture or click to select: upload an image
   Head: Head
   Headline: Headline
   Horizontal Line: Horizontal Line

+ 1 - 617
system/Controllers/ContentApiController.php → system/Controllers/ArticleApiController.php

@@ -7,7 +7,6 @@ use Slim\Http\Response;
 use Typemill\Models\Folder;
 use Typemill\Models\Write;
 use Typemill\Models\WriteYaml;
-use Typemill\Models\ProcessImage;
 use Typemill\Extensions\ParsedownExtension;
 use Typemill\Events\OnPagePublished;
 use Typemill\Events\OnPageUnpublished;
@@ -16,7 +15,7 @@ use Typemill\Events\OnPageSorted;
 use \URLify;
 
 
-class ContentApiController extends ContentController
+class ArticleApiController extends ContentController
 {
 	public function publishArticle(Request $request, Response $response, $args)
 	{
@@ -847,619 +846,4 @@ class ContentApiController extends ContentController
 
 		return $response->withJson(array('data' => $content, 'errors' => false));
 	}
-
-	public function addBlock(Request $request, Response $response, $args)
-	{
-		/* get params from call */
-		$this->params 	= $request->getParams();
-		$this->uri 		= $request->getUri();
-
-		/* validate input */
-		if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); }
-		
-		# set structure
-		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-		
-		/* set item */
-		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
-
-		# set the status for published and drafted
-		$this->setPublishStatus();
-
-		# set path
-		$this->setItemPath($this->item->fileType);
-
-		# read content from file
-		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-
-		# make it more clear which content we have
-		$pageMarkdown = $this->content;
-
-		$blockMarkdown = $this->params['markdown'];
-
-        # standardize line breaks
-        $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown);
-
-        # remove surrounding line breaks
-        $blockMarkdown = trim($blockMarkdown, "\n");		
-		
-		if($pageMarkdown == '')
-		{
-			$pageMarkdown = [];
-		}
-
-		# initialize parsedown extension
-		$parsedown = new ParsedownExtension();
-
-		# if content is not an array, then transform it
-		if(!is_array($pageMarkdown))
-		{
-			# turn markdown into an array of markdown-blocks
-			$pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown);
-		}
-
-		# if it is a new content-block
-		if($this->params['block_id'] == 99999)
-		{
-			# set the id of the markdown-block (it will be one more than the actual array, so count is perfect) 
-			$id = count($pageMarkdown);
-
-			# add the new markdown block to the page content
-			$pageMarkdown[] = $blockMarkdown;			
-		}
-		elseif(($this->params['block_id'] == 0) OR !isset($pageMarkdown[$this->params['block_id']]))
-		{
-			# if the block does not exists, return an error
-			return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404);
-		}
-		else
-		{
-			# insert new markdown block
-			array_splice( $pageMarkdown, $this->params['block_id'], 0, $blockMarkdown );			
-			$id = $this->params['block_id'];
-		}
-	
-		# encode the content into json
-		$pageJson = json_encode($pageMarkdown);
-
-		# set path for the file (or folder)
-		$this->setItemPath('txt');
-	
-		/* update the file */
-		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
-		{
-			# update the internal structure
-			$this->setStructure($draft = true, $cache = false);
-			$this->content = $pageMarkdown;
-		}
-		else
-		{
-			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
-		}
-	
-		/* set safe mode to escape javascript and html in markdown */
-		$parsedown->setSafeMode(true);
-
-		/* parse markdown-file to content-array */
-		$blockArray = $parsedown->text($blockMarkdown);
-		
-		# we assume that toc is not relevant
-		$toc = false;
-
-		# needed for ToC links
-		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
-		
-		if($blockMarkdown == '[TOC]')
-		{
-			# if block is table of content itself, then generate the table of content
-			$tableofcontent = $this->generateToc();
-
-			# and only use the html-markup
-			$blockHTML = $tableofcontent['html'];
-		}
-		else
-		{
-			# parse markdown-content-array to content-string
-			$blockHTML = $parsedown->markup($blockArray, $relurl);
-			
-			# if it is a headline
-			if($blockMarkdown[0] == '#')
-			{
-				# then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup
-				$toc = $this->generateToc();
-			}
-		}
-
-		return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false));
-	}
-
-	protected function generateToc()
-	{
-		# we assume that page has no table of content
-		$toc = false;
-
-		# make sure $this->content is updated
-		$content = $this->content;
-
-		if($content == '')
-		{
-			$content = [];
-		}
-		
-		# initialize parsedown extension
-		$parsedown = new ParsedownExtension();
-		
-		# if content is not an array, then transform it
-		if(!is_array($content))
-		{
-			# turn markdown into an array of markdown-blocks
-			$content = $parsedown->markdownToArrayBlocks($content);
-		}
-		
-		# needed for ToC links
-		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
-		
-		# loop through mardkown-array and create html-blocks
-		foreach($content as $key => $block)
-		{
-			# parse markdown-file to content-array
-			$contentArray 	= $parsedown->text($block);
-			
-			if($block == '[TOC]')
-			{
-				# toc is true and holds the key of the table of content now
-				$toc = $key;
-			}
-
-			# parse markdown-content-array to content-string
-			$content[$key]	= ['id' => $key, 'html' => $parsedown->markup($contentArray, $relurl)];
-		}
-
-		# if page has a table of content
-		if($toc)
-		{
-			# generate the toc markup
-			$tocMarkup = $parsedown->buildTOC($parsedown->headlines);
-
-			# toc holds the id of the table of content and the html-markup now
-			$toc = ['id' => $toc, 'html' => $tocMarkup];
-		}
-
-		return $toc;
-	}
-
-	public function updateBlock(Request $request, Response $response, $args)
-	{
-		/* get params from call */
-		$this->params 	= $request->getParams();
-		$this->uri 		= $request->getUri();
-
-		/* validate input */
-		if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); }
-		
-		# set structure
-		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-		
-		/* set item */
-		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
-
-		# set the status for published and drafted
-		$this->setPublishStatus();
-
-		# set path
-		$this->setItemPath($this->item->fileType);
-
-		# read content from file
-		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-
-		# make it more clear which content we have
-		$pageMarkdown = $this->content;
-
-		$blockMarkdown = $this->params['markdown'];
-
-        # standardize line breaks
-        $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown);
-
-        # remove surrounding line breaks
-        $blockMarkdown = trim($blockMarkdown, "\n");
-
-		if($pageMarkdown == '')
-		{
-			$pageMarkdown = [];
-		}
-
-		# initialize parsedown extension
-		$parsedown = new ParsedownExtension();
-		$parsedown->setVisualMode();
-
-		# if content is not an array, then transform it
-		if(!is_array($pageMarkdown))
-		{
-			# turn markdown into an array of markdown-blocks
-			$pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown);
-		}
-
-		if(!isset($pageMarkdown[$this->params['block_id']]))
-		{
-			# if the block does not exists, return an error
-			return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404);
-		}
-		elseif($this->params['block_id'] == 0)
-		{
-			# if it is the title, then delete the "# " if it exists
-			$blockMarkdown = trim($blockMarkdown, "# ");
-			
-			# store the markdown-headline in a separate variable
-			$blockMarkdownTitle = '# ' . $blockMarkdown;
-			
-			# add the markdown-headline to the page-markdown
-			$pageMarkdown[0] = $blockMarkdownTitle;
-			$id = 0;
-		}
-		else
-		{
-			# update the markdown block in the page content
-			$pageMarkdown[$this->params['block_id']] = $blockMarkdown;
-			$id = $this->params['block_id'];
-		}
-
-		# encode the content into json
-		$pageJson = json_encode($pageMarkdown);
-
-		# set path for the file (or folder)
-		$this->setItemPath('txt');
-	
-		/* update the file */
-		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
-		{
-			# update the internal structure
-			$this->setStructure($draft = true, $cache = false);
-
-			# updated the content variable
-			$this->content = $pageMarkdown;
-		}
-		else
-		{
-			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
-		}
-	
-
-		/* parse markdown-file to content-array, if title parse title. */
-		if($this->params['block_id'] == 0)
-		{
-			$blockArray		= $parsedown->text($blockMarkdownTitle);
-		}
-		else
-		{
-			/* set safe mode to escape javascript and html in markdown */
-			$parsedown->setSafeMode(true);
-
-			$blockArray 	= $parsedown->text($blockMarkdown);
-		}
-		
-		# we assume that toc is not relevant
-		$toc = false;
-
-		# needed for ToC links
-		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
-		
-		if($blockMarkdown == '[TOC]')
-		{
-			# if block is table of content itself, then generate the table of content
-			$tableofcontent = $this->generateToc();
-
-			# and only use the html-markup
-			$blockHTML = $tableofcontent['html'];
-		}
-		else
-		{
-			# parse markdown-content-array to content-string
-			$blockHTML = $parsedown->markup($blockArray, $relurl);
-			
-			# if it is a headline
-			if($blockMarkdown[0] == '#')
-			{
-				# then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup
-				$toc = $this->generateToc();
-			}
-		}
-
-		return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false));
-	}
-	
-	public function moveBlock(Request $request, Response $response, $args)
-	{
-		# get params from call
-		$this->params 	= $request->getParams();
-		$this->uri 		= $request->getUri();
-
-		# validate input 
-		# if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); }
-		
-		# set structure
-		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-		
-		# set item 
-		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
-
-		# set the status for published and drafted
-		$this->setPublishStatus();
-
-		# set path
-		$this->setItemPath($this->item->fileType);
-
-		# read content from file
-		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-
-		# make it more clear which content we have
-		$pageMarkdown = $this->content;
-		
-		if($pageMarkdown == '')
-		{
-			$pageMarkdown = [];
-		}
-
-		# initialize parsedown extension
-		$parsedown = new ParsedownExtension();
-
-		# if content is not an array, then transform it
-		if(!is_array($pageMarkdown))
-		{
-			# turn markdown into an array of markdown-blocks
-			$pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown);
-		}
-
-		$oldIndex = ($this->params['old_index'] + 1);
-		$newIndex = ($this->params['new_index'] + 1);
-		
-		if(!isset($pageMarkdown[$oldIndex]))
-		{
-			# if the block does not exists, return an error
-			return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404);
-		}
-
-		$extract = array_splice($pageMarkdown, $oldIndex, 1);
-		array_splice($pageMarkdown, $newIndex, 0, $extract);
-	
-		# encode the content into json
-		$pageJson = json_encode($pageMarkdown);
-
-		# set path for the file (or folder)
-		$this->setItemPath('txt');
-	
-		/* update the file */
-		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
-		{
-			# update the internal structure
-			$this->setStructure($draft = true, $cache = false);
-
-			# update this content
-			$this->content = $pageMarkdown;
-		}
-		else
-		{
-			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
-		}
-
-		# we assume that toc is not relevant
-		$toc = false;
-
-		# needed for ToC links
-		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
-
-		# if the moved item is a headline
-		if($extract[0][0] == '#')
-		{
-			$toc = $this->generateToc();
-		}
-
-		# if it is the title, then delete the "# " if it exists
-		$pageMarkdown[0] = trim($pageMarkdown[0], "# ");
-
-		return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => false));
-	}
-
-	public function deleteBlock(Request $request, Response $response, $args)
-	{
-		/* get params from call */
-		$this->params 	= $request->getParams();
-		$this->uri 		= $request->getUri();
-		$errors			= false;
-		
-		# set structure
-		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-		
-		# set item
-		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
-
-		# set the status for published and drafted
-		$this->setPublishStatus();
-
-		# set path
-		$this->setItemPath($this->item->fileType);
-
-		# read content from file
-		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
-
-		# get content
-		$this->content;
-
-		if($this->content == '')
-		{
-			$this->content = [];
-		}
-
-		# initialize parsedown extension
-		$parsedown = new ParsedownExtension();
-
-		# if content is not an array, then transform it
-		if(!is_array($this->content))
-		{
-			# turn markdown into an array of markdown-blocks
-			$this->content = $parsedown->markdownToArrayBlocks($this->content);
-		}
-
-		# check if id exists
-		if(!isset($this->content[$this->params['block_id']])){ return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); }
-
-		# check if block is image
-		$contentBlock 		= $this->content[$this->params['block_id']];
-		$contentBlockStart 	= substr($contentBlock, 0, 2);
-		if($contentBlockStart == '[!' OR $contentBlockStart == '![')
-		{
-			# extract image path
-			preg_match("/\((.*?)\)/",$contentBlock,$matches);
-			if(isset($matches[1]))
-			{
-				$imageBaseName	= explode('-', $matches[1]);
-				$imageBaseName	= str_replace('media/live/', '', $imageBaseName[0]);
-				$processImage 	= new ProcessImage();
-				if(!$processImage->deleteImage($imageBaseName))
-				{
-					$errors = 'Could not delete some of the images, please check manually';
-				}
-			}
-		}
-		
-		# delete the block
-		unset($this->content[$this->params['block_id']]);
-		$this->content = array_values($this->content);
-
-		$pageMarkdown = $this->content;
-		
-		# delete markdown from title
-		if(isset($pageMarkdown[0]))
-		{
-			$pageMarkdown[0] = trim($pageMarkdown[0], "# ");
-		}
-		
-		# encode the content into json
-		$pageJson = json_encode($this->content);
-
-		# set path for the file (or folder)
-		$this->setItemPath('txt');		
-	
-		/* update the file */
-		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
-		{
-			# update the internal structure
-			$this->setStructure($draft = true, $cache = false);
-		}
-		else
-		{
-			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
-		}
-		
-		$toc = false;
-
-		if($contentBlock[0] == '#')
-		{
-			$toc = $this->generateToc();
-		}
-
-		return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => $errors));
-	}
-
-	public function createImage(Request $request, Response $response, $args)
-	{
-		/* get params from call */
-		$this->params 	= $request->getParams();
-		$this->uri 		= $request->getUri();
-		
-		$imageProcessor	= new ProcessImage();
-		
-		if($imageProcessor->createImage($this->params['image'], $this->settings['images']))
-		{
-			return $response->withJson(array('errors' => false));		
-		}
-
-		return $response->withJson(array('errors' => 'could not store image to temporary folder'));	
-	}
-	
-	public function publishImage(Request $request, Response $response, $args)
-	{
-		$params 		= $request->getParsedBody();
-
-		$imageProcessor	= new ProcessImage();
-		
-		$imageUrl 		= $imageProcessor->publishImage($this->settings['images'], $name = false);
-		if($imageUrl)
-		{
-			$params['markdown']	= str_replace('imgplchldr', $imageUrl, $params['markdown']);
-						
-			$request 	= $request->withParsedBody($params);
-			
-			return $this->addBlock($request, $response, $args);
-		}
-
-		return $response->withJson(array('errors' => 'could not store image to media folder'));	
-	}
-
-	public function saveVideoImage(Request $request, Response $response, $args)
-	{
-		/* get params from call */
-		$this->params 	= $request->getParams();
-		$this->uri 		= $request->getUri();
-		$class			= false;
-
-		$imageUrl		= $this->params['markdown'];
-		
-		if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false)
-		{
-			$videoID 	= str_replace('https://www.youtube.com/watch?v=', '', $imageUrl);
-			$videoID 	= strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID;
-			$class		= 'youtube';
-		}
-		if(strpos($imageUrl, 'https://youtu.be/') !== false)
-		{
-			$videoID 	= str_replace('https://youtu.be/', '', $imageUrl);
-			$videoID	= strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID;
-			$class		= 'youtube';
-		}
-		
-		if($class == 'youtube')
-		{
-			$videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg';
-			$videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg';
-		}
-
-		$ctx = stream_context_create(array(
-			'https' => array(
-				'timeout' => 1
-				)
-			)
-		);
-		
-		$imageData		= @file_get_contents($videoURLmaxres, 0, $ctx);
-		if($imageData === false)
-		{
-			$imageData	= @file_get_contents($videoURL0, 0, $ctx);
-			if($imageData === false)
-			{
-				return $response->withJson(array('errors' => 'could not get the video image'));
-			}
-		}
-		
-		$imageData64	= 'data:image/jpeg;base64,' . base64_encode($imageData);
-		$desiredSizes	= ['live' => ['width' => 560, 'height' => 315]];
-		$imageProcessor	= new ProcessImage();
-		$tmpImage		= $imageProcessor->createImage($imageData64, $desiredSizes);
-		
-		if(!$tmpImage)
-		{
-			return $response->withJson(array('errors' => 'could not create temporary image'));			
-		}
-		
-		$imageUrl 		= $imageProcessor->publishImage($desiredSizes, $videoID);
-		if($imageUrl)
-		{
-			$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';
-
-			$request 	= $request->withParsedBody($this->params);
-			
-			return $this->addBlock($request, $response, $args);
-		}
-		
-		return $response->withJson(array('errors' => 'could not store the preview image'));	
-	}
 }

+ 841 - 0
system/Controllers/BlockApiController.php

@@ -0,0 +1,841 @@
+<?php
+
+namespace Typemill\Controllers;
+
+use Slim\Http\Request;
+use Slim\Http\Response;
+use Typemill\Models\Folder;
+use Typemill\Models\Write;
+use Typemill\Models\WriteYaml;
+use Typemill\Models\ProcessImage;
+use Typemill\Models\ProcessFile;
+use Typemill\Extensions\ParsedownExtension;
+use \URLify;
+
+class BlockApiController extends ContentController
+{
+
+	public function addBlock(Request $request, Response $response, $args)
+	{
+		/* get params from call */
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		/* validate input */
+		if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); }
+		
+		# set structure
+		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+		
+		/* set item */
+		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
+
+		# set the status for published and drafted
+		$this->setPublishStatus();
+
+		# set path
+		$this->setItemPath($this->item->fileType);
+
+		# read content from file
+		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+
+		# make it more clear which content we have
+		$pageMarkdown = $this->content;
+
+		$blockMarkdown = $this->params['markdown'];
+
+        # standardize line breaks
+        $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown);
+
+        # remove surrounding line breaks
+        $blockMarkdown = trim($blockMarkdown, "\n");		
+		
+		if($pageMarkdown == '')
+		{
+			$pageMarkdown = [];
+		}
+
+		# initialize parsedown extension
+		$parsedown = new ParsedownExtension();
+
+		# if content is not an array, then transform it
+		if(!is_array($pageMarkdown))
+		{
+			# turn markdown into an array of markdown-blocks
+			$pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown);
+		}
+
+		# if it is a new content-block
+		if($this->params['block_id'] == 99999)
+		{
+			# set the id of the markdown-block (it will be one more than the actual array, so count is perfect) 
+			$id = count($pageMarkdown);
+
+			# add the new markdown block to the page content
+			$pageMarkdown[] = $blockMarkdown;			
+		}
+		elseif(($this->params['block_id'] == 0) OR !isset($pageMarkdown[$this->params['block_id']]))
+		{
+			# if the block does not exists, return an error
+			return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404);
+		}
+		else
+		{
+			# insert new markdown block
+			array_splice( $pageMarkdown, $this->params['block_id'], 0, $blockMarkdown );			
+			$id = $this->params['block_id'];
+		}
+	
+		# encode the content into json
+		$pageJson = json_encode($pageMarkdown);
+
+		# set path for the file (or folder)
+		$this->setItemPath('txt');
+	
+		/* update the file */
+		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
+		{
+			# update the internal structure
+			$this->setStructure($draft = true, $cache = false);
+			$this->content = $pageMarkdown;
+		}
+		else
+		{
+			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
+		}
+	
+		/* set safe mode to escape javascript and html in markdown */
+		$parsedown->setSafeMode(true);
+
+		/* parse markdown-file to content-array */
+		$blockArray = $parsedown->text($blockMarkdown);
+		
+		# we assume that toc is not relevant
+		$toc = false;
+
+		# needed for ToC links
+		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
+		
+		if($blockMarkdown == '[TOC]')
+		{
+			# if block is table of content itself, then generate the table of content
+			$tableofcontent = $this->generateToc();
+
+			# and only use the html-markup
+			$blockHTML = $tableofcontent['html'];
+		}
+		else
+		{
+			# parse markdown-content-array to content-string
+			$blockHTML = $parsedown->markup($blockArray, $relurl);
+			
+			# if it is a headline
+			if($blockMarkdown[0] == '#')
+			{
+				# then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup
+				$toc = $this->generateToc();
+			}
+		}
+
+		return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false));
+	}
+
+	protected function generateToc()
+	{
+		# we assume that page has no table of content
+		$toc = false;
+
+		# make sure $this->content is updated
+		$content = $this->content;
+
+		if($content == '')
+		{
+			$content = [];
+		}
+		
+		# initialize parsedown extension
+		$parsedown = new ParsedownExtension();
+		
+		# if content is not an array, then transform it
+		if(!is_array($content))
+		{
+			# turn markdown into an array of markdown-blocks
+			$content = $parsedown->markdownToArrayBlocks($content);
+		}
+		
+		# needed for ToC links
+		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
+		
+		# loop through mardkown-array and create html-blocks
+		foreach($content as $key => $block)
+		{
+			# parse markdown-file to content-array
+			$contentArray 	= $parsedown->text($block);
+			
+			if($block == '[TOC]')
+			{
+				# toc is true and holds the key of the table of content now
+				$toc = $key;
+			}
+
+			# parse markdown-content-array to content-string
+			$content[$key]	= ['id' => $key, 'html' => $parsedown->markup($contentArray, $relurl)];
+		}
+
+		# if page has a table of content
+		if($toc)
+		{
+			# generate the toc markup
+			$tocMarkup = $parsedown->buildTOC($parsedown->headlines);
+
+			# toc holds the id of the table of content and the html-markup now
+			$toc = ['id' => $toc, 'html' => $tocMarkup];
+		}
+
+		return $toc;
+	}
+
+	public function updateBlock(Request $request, Response $response, $args)
+	{
+		/* get params from call */
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		/* validate input */
+		if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); }
+		
+		# set structure
+		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+		
+		/* set item */
+		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
+
+		# set the status for published and drafted
+		$this->setPublishStatus();
+
+		# set path
+		$this->setItemPath($this->item->fileType);
+
+		# read content from file
+		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+
+		# make it more clear which content we have
+		$pageMarkdown = $this->content;
+
+		$blockMarkdown = $this->params['markdown'];
+
+        # standardize line breaks
+        $blockMarkdown = str_replace(array("\r\n", "\r"), "\n", $blockMarkdown);
+
+        # remove surrounding line breaks
+        $blockMarkdown = trim($blockMarkdown, "\n");
+
+		if($pageMarkdown == '')
+		{
+			$pageMarkdown = [];
+		}
+
+		# initialize parsedown extension
+		$parsedown = new ParsedownExtension();
+		$parsedown->setVisualMode();
+
+		# if content is not an array, then transform it
+		if(!is_array($pageMarkdown))
+		{
+			# turn markdown into an array of markdown-blocks
+			$pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown);
+		}
+
+		if(!isset($pageMarkdown[$this->params['block_id']]))
+		{
+			# if the block does not exists, return an error
+			return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404);
+		}
+		elseif($this->params['block_id'] == 0)
+		{
+			# if it is the title, then delete the "# " if it exists
+			$blockMarkdown = trim($blockMarkdown, "# ");
+			
+			# store the markdown-headline in a separate variable
+			$blockMarkdownTitle = '# ' . $blockMarkdown;
+			
+			# add the markdown-headline to the page-markdown
+			$pageMarkdown[0] = $blockMarkdownTitle;
+			$id = 0;
+		}
+		else
+		{
+			# update the markdown block in the page content
+			$pageMarkdown[$this->params['block_id']] = $blockMarkdown;
+			$id = $this->params['block_id'];
+		}
+
+		# encode the content into json
+		$pageJson = json_encode($pageMarkdown);
+
+		# set path for the file (or folder)
+		$this->setItemPath('txt');
+	
+		/* update the file */
+		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
+		{
+			# update the internal structure
+			$this->setStructure($draft = true, $cache = false);
+
+			# updated the content variable
+			$this->content = $pageMarkdown;
+		}
+		else
+		{
+			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
+		}
+	
+
+		/* parse markdown-file to content-array, if title parse title. */
+		if($this->params['block_id'] == 0)
+		{
+			$blockArray		= $parsedown->text($blockMarkdownTitle);
+		}
+		else
+		{
+			/* set safe mode to escape javascript and html in markdown */
+			$parsedown->setSafeMode(true);
+
+			$blockArray 	= $parsedown->text($blockMarkdown);
+		}
+		
+		# we assume that toc is not relevant
+		$toc = false;
+
+		# needed for ToC links
+		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
+		
+		if($blockMarkdown == '[TOC]')
+		{
+			# if block is table of content itself, then generate the table of content
+			$tableofcontent = $this->generateToc();
+
+			# and only use the html-markup
+			$blockHTML = $tableofcontent['html'];
+		}
+		else
+		{
+			# parse markdown-content-array to content-string
+			$blockHTML = $parsedown->markup($blockArray, $relurl);
+			
+			# if it is a headline
+			if($blockMarkdown[0] == '#')
+			{
+				# then the TOC holds either false (if no toc used in the page) or it holds an object with the id and toc-markup
+				$toc = $this->generateToc();
+			}
+		}
+
+		return $response->withJson(array('content' => [ 'id' => $id, 'html' => $blockHTML ] , 'markdown' => $blockMarkdown, 'id' => $id, 'toc' => $toc, 'errors' => false));
+	}
+	
+	public function moveBlock(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		# validate input 
+		# if(!$this->validateBlockInput()){ return $response->withJson($this->errors,422); }
+		
+		# set structure
+		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+		
+		# set item 
+		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
+
+		# set the status for published and drafted
+		$this->setPublishStatus();
+
+		# set path
+		$this->setItemPath($this->item->fileType);
+
+		# read content from file
+		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+
+		# make it more clear which content we have
+		$pageMarkdown = $this->content;
+		
+		if($pageMarkdown == '')
+		{
+			$pageMarkdown = [];
+		}
+
+		# initialize parsedown extension
+		$parsedown = new ParsedownExtension();
+
+		# if content is not an array, then transform it
+		if(!is_array($pageMarkdown))
+		{
+			# turn markdown into an array of markdown-blocks
+			$pageMarkdown = $parsedown->markdownToArrayBlocks($pageMarkdown);
+		}
+
+		$oldIndex = ($this->params['old_index'] + 1);
+		$newIndex = ($this->params['new_index'] + 1);
+		
+		if(!isset($pageMarkdown[$oldIndex]))
+		{
+			# if the block does not exists, return an error
+			return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404);
+		}
+
+		$extract = array_splice($pageMarkdown, $oldIndex, 1);
+		array_splice($pageMarkdown, $newIndex, 0, $extract);
+	
+		# encode the content into json
+		$pageJson = json_encode($pageMarkdown);
+
+		# set path for the file (or folder)
+		$this->setItemPath('txt');
+	
+		/* update the file */
+		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
+		{
+			# update the internal structure
+			$this->setStructure($draft = true, $cache = false);
+
+			# update this content
+			$this->content = $pageMarkdown;
+		}
+		else
+		{
+			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
+		}
+
+		# we assume that toc is not relevant
+		$toc = false;
+
+		# needed for ToC links
+		$relurl = '/tm/content/' . $this->settings['editor'] . '/' . $this->item->urlRel;
+
+		# if the moved item is a headline
+		if($extract[0][0] == '#')
+		{
+			$toc = $this->generateToc();
+		}
+
+		# if it is the title, then delete the "# " if it exists
+		$pageMarkdown[0] = trim($pageMarkdown[0], "# ");
+
+		return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => false));
+	}
+
+	public function deleteBlock(Request $request, Response $response, $args)
+	{
+		/* get params from call */
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+		$errors			= false;
+		
+		# set structure
+		if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+		
+		# set item
+		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
+
+		# set the status for published and drafted
+		$this->setPublishStatus();
+
+		# set path
+		$this->setItemPath($this->item->fileType);
+
+		# read content from file
+		if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
+
+		# get content
+		$this->content;
+
+		if($this->content == '')
+		{
+			$this->content = [];
+		}
+
+		# initialize parsedown extension
+		$parsedown = new ParsedownExtension();
+
+		# if content is not an array, then transform it
+		if(!is_array($this->content))
+		{
+			# turn markdown into an array of markdown-blocks
+			$this->content = $parsedown->markdownToArrayBlocks($this->content);
+		}
+
+		# check if id exists
+		if(!isset($this->content[$this->params['block_id']])){ return $response->withJson(array('data' => false, 'errors' => 'The ID of the content-block is wrong.'), 404); }
+
+		$contentBlock = $this->content[$this->params['block_id']];
+
+		# delete the block
+		unset($this->content[$this->params['block_id']]);
+		$this->content = array_values($this->content);
+
+		$pageMarkdown = $this->content;
+		
+		# delete markdown from title
+		if(isset($pageMarkdown[0]))
+		{
+			$pageMarkdown[0] = trim($pageMarkdown[0], "# ");
+		}
+		
+		# encode the content into json
+		$pageJson = json_encode($this->content);
+
+		# set path for the file (or folder)
+		$this->setItemPath('txt');		
+	
+		/* update the file */
+		if($this->write->writeFile($this->settings['contentFolder'], $this->path, $pageJson))
+		{
+			# update the internal structure
+			$this->setStructure($draft = true, $cache = false);
+		}
+		else
+		{
+			return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if the file is writable']], 404);
+		}
+		
+		$toc = false;
+
+		if($contentBlock[0] == '#')
+		{
+			$toc = $this->generateToc();
+		}
+
+		return $response->withJson(array('markdown' => $pageMarkdown, 'toc' => $toc, 'errors' => $errors));
+	}
+
+	public function getMediaLibImages(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders('images'))
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$imagelist 		= $imageProcessor->scanMediaFlat();
+
+		return $response->withJson(array('images' => $imagelist));
+	}
+
+	public function getMediaLibFiles(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$fileProcessor	= new ProcessFile();
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$filelist 		= $fileProcessor->scanFilesFlat();
+
+		return $response->withJson(array('files' => $filelist));
+	}
+
+	public function getImage(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$this->setStructure($draft = true, $cache = false);
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders('images'))
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$imageDetails 	= $imageProcessor->getImageDetails($this->params['name'], $this->structure);
+
+		if($imageDetails)
+		{
+			return $response->withJson(array('image' => $imageDetails));
+		}
+		
+		#	return $response->withJson(array('image' => false, 'errors' => 'image name invalid or not found'));
+		return $response->withJson(['errors' => ['message' => 'Image name invalid or not found.']], 404);
+	}
+
+	public function getFile(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$this->setStructure($draft = true, $cache = false);
+
+		$fileProcessor	= new ProcessFile();
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$fileDetails 	= $fileProcessor->getFileDetails($this->params['name'], $this->structure);
+
+		if($fileDetails)
+		{
+			return $response->withJson(['file' => $fileDetails]);
+		}
+
+		return $response->withJson(['errors' => ['message' => 'file name invalid or not found']],404);
+	}
+
+	public function createImage(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+		
+		# do this shit in the model ...
+		$imagename = explode('.', $this->params['name']);
+		array_pop($imagename);
+		$imagename = implode('-', $imagename);
+		$name = URLify::filter(iconv(mb_detect_encoding($imagename, mb_detect_order(), true), "UTF-8", $imagename));
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders('images'))
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+		
+		if($imageProcessor->createImage($this->params['image'], $name, $this->settings['images']))
+		{
+			return $response->withJson(array('errors' => false));	
+		}
+
+		return $response->withJson(array('errors' => 'could not store image to temporary folder'));	
+	}
+
+	public function createFile(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$finfo = finfo_open( FILEINFO_MIME_TYPE );
+		$mtype = finfo_file( $finfo, $this->params['file'] );
+		finfo_close( $finfo );
+
+		$allowedMimes = $this->getAllowedMtypes();
+		if(!in_array($mtype, $allowedMimes))
+		{
+			return $response->withJson(array('errors' => 'File-type is not allowed'));
+		}
+
+		# sanitize file name
+		$filename = basename($this->params['name']);
+		$filename = explode('.', $this->params['name']);
+		array_pop($filename);
+		$filename = implode('-', $filename);
+		$name = URLify::filter(iconv(mb_detect_encoding($filename, mb_detect_order(), true), "UTF-8", $filename));
+
+		$fileProcessor	= new ProcessFile();
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+		
+		if($fileProcessor->createFile($this->params['file'], $name))
+		{
+			return $response->withJson(array('errors' => false, 'name' => $name));
+		}
+
+		return $response->withJson(array('errors' => 'could not store file to temporary folder'));
+	}
+	
+	public function publishImage(Request $request, Response $response, $args)
+	{
+		$params 		= $request->getParsedBody();
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+		
+		$imageUrl 		= $imageProcessor->publishImage();
+		if($imageUrl)
+		{
+			# replace the image placeholder in markdown with the image url
+			$params['markdown']	= str_replace('imgplchldr', $imageUrl, $params['markdown']);
+						
+			$request 	= $request->withParsedBody($params);
+		
+			if($params['new'])
+			{
+				return $this->addBlock($request, $response, $args);
+			}
+			return $this->updateBlock($request, $response, $args);
+		}
+
+		return $response->withJson(array('errors' => 'could not store image to media folder'));	
+	}
+
+	public function deleteImage(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		if(!isset($this->params['name']))
+		{
+			return $response->withJson(array('errors' => 'image name is missing'));	
+		}
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$errors 		= $imageProcessor->deleteImage($this->params['name']);
+
+		return $response->withJson(array('errors' => $errors));
+	}
+
+	public function deleteFile(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		if(!isset($this->params['name']))
+		{
+			return $response->withJson(array('errors' => 'file name is missing'));	
+		}
+
+		$fileProcessor	= new ProcessFile();
+
+		$errors = false;
+		if($fileProcessor->deleteFile($this->params['name']))
+		{
+			return $response->withJson(array('errors' => false));
+		}
+
+		return $response->withJson(array('errors' => 'could not delete the file'));
+	}
+
+	public function saveVideoImage(Request $request, Response $response, $args)
+	{
+		/* get params from call */
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+		$class			= false;
+
+		$imageUrl		= $this->params['markdown'];
+		
+		if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false)
+		{
+			$videoID 	= str_replace('https://www.youtube.com/watch?v=', '', $imageUrl);
+			$videoID 	= strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID;
+			$class		= 'youtube';
+		}
+		if(strpos($imageUrl, 'https://youtu.be/') !== false)
+		{
+			$videoID 	= str_replace('https://youtu.be/', '', $imageUrl);
+			$videoID	= strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID;
+			$class		= 'youtube';
+		}
+		
+		if($class == 'youtube')
+		{
+			$videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg';
+			$videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg';
+		}
+
+		$ctx = stream_context_create(array(
+			'https' => array(
+				'timeout' => 1
+				)
+			)
+		);
+		
+		$imageData		= @file_get_contents($videoURLmaxres, 0, $ctx);
+		if($imageData === false)
+		{
+			$imageData	= @file_get_contents($videoURL0, 0, $ctx);
+			if($imageData === false)
+			{
+				return $response->withJson(array('errors' => 'could not get the video image'));
+			}
+		}
+		
+		$imageData64	= 'data:image/jpeg;base64,' . base64_encode($imageData);
+		$desiredSizes	= ['live' => ['width' => 560, 'height' => 315]];
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$tmpImage		= $imageProcessor->createImage($imageData64, $desiredSizes);
+		
+		if(!$tmpImage)
+		{
+			return $response->withJson(array('errors' => 'could not create temporary image'));			
+		}
+		
+		$imageUrl 		= $imageProcessor->publishImage($desiredSizes, $videoID);
+		if($imageUrl)
+		{
+			$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';
+
+			$request 	= $request->withParsedBody($this->params);
+			
+			if($this->params['new'])
+			{
+				return $this->addBlock($request, $response, $args);
+			}
+			return $this->updateBlock($request, $response, $args);
+		}
+		
+		return $response->withJson(array('errors' => 'could not store the preview image'));	
+	}
+
+	private function getAllowedMtypes()
+	{
+		return array(
+		   	'application/zip',
+		   	'application/gzip',
+		   	'application/vnd.rar',
+			'application/vnd.visio',
+			'application/vnd.ms-excel',
+			'application/vnd.ms-powerpoint',
+			'application/vnd.ms-word.document.macroEnabled.12',
+			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+			'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+			'application/vnd.apple.keynote',
+			'application/vnd.apple.mpegurl',
+			'application/vnd.apple.numbers',
+			'application/vnd.apple.pages',
+			'application/vnd.amazon.mobi8-ebook',
+			'application/epub+zip',
+			'application/pdf',
+		   	'image/png',
+		   	'image/jpeg',
+		   	'image/gif',
+		   	'image/svg+xml',
+		   	'font/*',
+		   	'audio/mpeg',
+		   	'audio/mp4',
+		   	'audio/ogg',
+		   	'video/mpeg',
+		   	'video/mp4',
+		   	'video/ogg',
+		);
+	}	
+}

+ 356 - 0
system/Controllers/MediaApiController.php

@@ -0,0 +1,356 @@
+<?php
+
+namespace Typemill\Controllers;
+
+use Slim\Http\Request;
+use Slim\Http\Response;
+use Typemill\Models\ProcessImage;
+use Typemill\Models\ProcessFile;
+use Typemill\Controllers\BlockApiController;
+use \URLify;
+
+class MediaApiController extends ContentController
+{
+	public function getMediaLibImages(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders('images'))
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+		
+		$imagelist 		= $imageProcessor->scanMediaFlat();
+
+		return $response->withJson(['images' => $imagelist]);
+	}
+
+	public function getMediaLibFiles(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$fileProcessor	= new ProcessFile();
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+		
+		$filelist 		= $fileProcessor->scanFilesFlat();
+
+		return $response->withJson(['files' => $filelist]);
+	}
+
+	public function getImage(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$this->setStructure($draft = true, $cache = false);
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders('images'))
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+
+		$imageDetails 	= $imageProcessor->getImageDetails($this->params['name'], $this->structure);
+		
+		if($imageDetails)
+		{
+			return $response->withJson(['image' => $imageDetails]);
+		}
+		
+		return $response->withJson(['errors' => 'Image not found or image name not valid.'], 404);
+	}
+
+	public function getFile(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		$this->setStructure($draft = true, $cache = false);
+
+		$fileProcessor	= new ProcessFile();
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+
+		$fileDetails 	= $fileProcessor->getFileDetails($this->params['name'], $this->structure);
+
+		if($fileDetails)
+		{
+			return $response->withJson(['file' => $fileDetails]);
+		}
+
+		return $response->withJson(['errors' => 'file not found or file name invalid'],404);
+	}
+
+	public function createImage(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+		
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		
+		if(!$imageProcessor->checkFolders('images'))
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}	
+
+		if($imageProcessor->createImage($this->params['image'], $this->params['name'], $this->settings['images']))
+		{
+			return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]);	
+		}
+
+		return $response->withJson(['errors' => 'could not store image to temporary folder']);	
+	}
+
+	public function uploadFile(Request $request, Response $response, $args)
+	{
+		# get params from call
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		# make sure only allowed filetypes are uploaded
+		$finfo = finfo_open( FILEINFO_MIME_TYPE );
+		$mtype = finfo_file( $finfo, $this->params['file'] );
+		finfo_close( $finfo );
+		$allowedMimes = $this->getAllowedMtypes();
+		if(!in_array($mtype, $allowedMimes))
+		{
+			return $response->withJson(array('errors' => 'File-type is not allowed'));
+		}
+
+		$fileProcessor	= new ProcessFile();
+
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+	
+		$fileinfo = $fileProcessor->storeFile($this->params['file'], $this->params['name']);
+		if($fileinfo)
+		{
+			return $response->withJson(['errors' => false, 'info' => $fileinfo]);
+		}
+
+		return $response->withJson(['errors' => 'could not store file to temporary folder'],500);
+	}
+	
+	public function publishImage(Request $request, Response $response, $args)
+	{
+		$params 		= $request->getParsedBody();
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+		
+		if($imageProcessor->publishImage())
+		{
+			$request 	= $request->withParsedBody($params);
+		
+			$block = new BlockApiController($this->c);
+			if($params['new'])
+			{
+				return $block->addBlock($request, $response, $args);
+			}
+			return $block->updateBlock($request, $response, $args);
+		}
+
+		return $response->withJson(['errors' => 'could not store image to media folder'],500);	
+	}
+
+	public function publishFile(Request $request, Response $response, $args)
+	{
+		$params 		= $request->getParsedBody();
+
+		$fileProcessor	= new ProcessFile();
+		if(!$fileProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+		
+		if($fileProcessor->publishFile())
+		{
+			$request 	= $request->withParsedBody($params);
+		
+			$block = new BlockApiController($this->c);
+			if($params['new'])
+			{
+				return $block->addBlock($request, $response, $args);
+			}
+			return $block->updateBlock($request, $response, $args);
+		}
+
+		return $response->withJson(['errors' => 'could not store file to media folder'],500);	
+	}
+
+	public function deleteImage(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		if(!isset($this->params['name']))
+		{
+			return $response->withJson(['errors' => 'image name is missing'],500);
+		}
+
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
+		}
+
+		if($imageProcessor->deleteImage($this->params['name']))
+		{
+			return $response->withJson(['errors' => false]);
+		}
+
+		return $response->withJson(['errors' => 'Oops, looks like we could not delete all sizes of that image.'], 500);
+	}
+
+	public function deleteFile(Request $request, Response $response, $args)
+	{
+		# get params from call 
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+
+		if(!isset($this->params['name']))
+		{
+			return $response->withJson(['errors' => 'file name is missing'],500);	
+		}
+
+		$fileProcessor	= new ProcessFile();
+
+		if($fileProcessor->deleteFile($this->params['name']))
+		{
+			return $response->withJson(['errors' => false]);
+		}
+
+		return $response->withJson(['errors' => 'could not delete the file'],500);
+	}
+
+	public function saveVideoImage(Request $request, Response $response, $args)
+	{
+		/* get params from call */
+		$this->params 	= $request->getParams();
+		$this->uri 		= $request->getUri();
+		$class			= false;
+
+		$imageUrl		= $this->params['markdown'];
+		
+		if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false)
+		{
+			$videoID 	= str_replace('https://www.youtube.com/watch?v=', '', $imageUrl);
+			$videoID 	= strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID;
+			$class		= 'youtube';
+		}
+		if(strpos($imageUrl, 'https://youtu.be/') !== false)
+		{
+			$videoID 	= str_replace('https://youtu.be/', '', $imageUrl);
+			$videoID	= strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID;
+			$class		= 'youtube';
+		}
+		
+		if($class == 'youtube')
+		{
+			$videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg';
+			$videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg';
+		}
+
+		$ctx = stream_context_create(array(
+			'https' => array(
+				'timeout' => 1
+				)
+			)
+		);
+		
+		$imageData		= @file_get_contents($videoURLmaxres, 0, $ctx);
+		if($imageData === false)
+		{
+			$imageData	= @file_get_contents($videoURL0, 0, $ctx);
+			if($imageData === false)
+			{
+				return $response->withJson(array('errors' => 'could not get the video image'));
+			}
+		}
+		
+		$imageData64	= 'data:image/jpeg;base64,' . base64_encode($imageData);
+		$desiredSizes	= ['live' => ['width' => 560, 'height' => 315]];
+		$imageProcessor	= new ProcessImage($this->settings['images']);
+		if(!$imageProcessor->checkFolders())
+		{
+			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
+		}
+
+		$tmpImage		= $imageProcessor->createImage($imageData64, $desiredSizes);
+		
+		if(!$tmpImage)
+		{
+			return $response->withJson(array('errors' => 'could not create temporary image'));			
+		}
+		
+		$imageUrl 		= $imageProcessor->publishImage($desiredSizes, $videoID);
+		if($imageUrl)
+		{
+			$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';
+
+			$request 	= $request->withParsedBody($this->params);
+			
+			$block = new BlockApiController($this->c);
+			if($params['new'])
+			{
+				return $block->addBlock($request, $response, $args);
+			}
+			return $block->updateBlock($request, $response, $args);
+		}
+		
+		return $response->withJson(array('errors' => 'could not store the preview image'));	
+	}
+
+	private function getAllowedMtypes()
+	{
+		return array(
+		   	'application/zip',
+		   	'application/gzip',
+		   	'application/vnd.rar',
+			'application/vnd.visio',
+			'application/vnd.ms-excel',
+			'application/vnd.ms-powerpoint',
+			'application/vnd.ms-word.document.macroEnabled.12',
+			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+			'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+			'application/vnd.apple.keynote',
+			'application/vnd.apple.mpegurl',
+			'application/vnd.apple.numbers',
+			'application/vnd.apple.pages',
+			'application/vnd.amazon.mobi8-ebook',
+			'application/epub+zip',
+			'application/pdf',
+		   	'image/png',
+		   	'image/jpeg',
+		   	'image/gif',
+		   	'image/svg+xml',
+		   	'font/*',
+		   	'audio/mpeg',
+		   	'audio/mp4',
+		   	'audio/ogg',
+		   	'video/mpeg',
+		   	'video/mp4',
+		   	'video/ogg',
+		);
+	}	
+}

+ 17 - 5
system/Controllers/MetaApiController.php

@@ -200,9 +200,8 @@ class MetaApiController extends ContentController
 
 		if($tab == 'meta')
 		{
-
 			# if manual date has been modified
-			if(isset($metaInput['manualdate']) && !isset($metaPage['meta']['manualdate']) OR ($metaInput['manualdate'] != $metaPage['meta']['manualdate']))
+			if( $this->hasChanged($metaInput, $metaPage['meta'], 'manualdate'))
 			{
 				# update the time
 				$metaInput['time'] = date('H-i-s', time());
@@ -230,7 +229,7 @@ class MetaApiController extends ContentController
 			}
 
 			# if folder has changed and contains pages instead of posts or posts instead of pages
-			if($this->item->elementType == "folder" && ($metaPage['meta']['contains'] !== $metaInput['contains']))
+			if($this->item->elementType == "folder" && isset($metaInput['contains']) && $this->hasChanged($metaInput, $metaPage['meta'], 'contains'))
 			{
 				$structure = true;
 
@@ -258,9 +257,9 @@ class MetaApiController extends ContentController
 			}
 			elseif(
 				# check if navtitle or hide-value has been changed
-				($metaPage['meta']['navtitle'] != $metaInput['navtitle']) 
+				($this->hasChanged($metaInput, $metaPage['meta'], 'navtitle'))
 				OR 
-				($metaPage['meta']['hide'] != $metaInput['hide'])
+				($this->hasChanged($metaInput, $metaPage['meta'], 'hide'))
 			)
 			{
 				# add new file data. Also makes sure that the value is set.
@@ -297,6 +296,19 @@ class MetaApiController extends ContentController
 		# return with the new metadata
 		return $response->withJson(array('metadata' => $metaInput, 'structure' => $structure, 'item' => $this->item, 'errors' => false));
 	}
+
+	protected function hasChanged($input, $page, $field)
+	{
+		if(isset($input[$field]) && isset($page[$field]) && $input[$field] == $page[$field])
+		{
+			return false;
+		}
+		if(!isset($input[$field]) && !isset($input[$field]))
+		{
+			return false;
+		}
+		return true;
+	}
 }
 
 # check models -> writeYaml for getPageMeta and getPageMetaDefaults.

+ 20 - 2
system/Controllers/PageController.php

@@ -108,13 +108,16 @@ class PageController extends Controller
 			/* get breadcrumb for page */
 			$breadcrumb = Folder::getBreadcrumb($structure, $item->keyPathArray);
 			$breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
+
+			# set pages active for navigation again
+			Folder::getBreadcrumb($navigation, $item->keyPathArray);
 			
 			/* add the paging to the item */
 			$item = Folder::getPagingForItem($structure, $item);
 		}
 
 		# dispatch the item
-		$item 			= $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();	
+		$item 			= $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
 
 		# set the filepath
 		$filePath 	= $pathToContent . $item->path;
@@ -211,6 +214,18 @@ class PageController extends Controller
 			$this->c->assets->addCSS($base_url . '/cache/' . $theme . '-custom.css');
 		}
 
+		$logo = false;
+		if(isset($settings['logo']) && $settings['logo'] != '')
+		{
+			$logo = 'media/files/' . $settings['logo'];
+		}
+
+		$favicon = false;
+		if(isset($settings['favicon']) && $settings['favicon'] != '')
+		{
+			$favicon = true;
+		}
+
 		return $this->render($response, $route, [
 			'home'			=> $home,
 			'navigation' 	=> $navigation, 
@@ -221,7 +236,10 @@ class PageController extends Controller
 			'settings' 		=> $settings,
 			'metatabs'		=> $metatabs,
 			'base_url' 		=> $base_url, 
-			'image' 		=> $firstImage ]);
+			'image' 		=> $firstImage,
+			'logo'			=> $logo,
+			'favicon'		=> $favicon
+		]);
 	}
 
 	protected function getCachedStructure($cache)

+ 81 - 3
system/Controllers/SettingsController.php

@@ -7,6 +7,8 @@ use Typemill\Models\Write;
 use Typemill\Models\Fields;
 use Typemill\Models\Validation;
 use Typemill\Models\User;
+use Typemill\Models\ProcessFile;
+use Typemill\Models\ProcessImage;
 
 class SettingsController extends Controller
 {	
@@ -15,7 +17,7 @@ class SettingsController extends Controller
 	*********************/
 	
 	public function showSettings($request, $response, $args)
-	{
+	{		
 		$user				= new User();
 		$settings 			= $this->c->get('settings');
 		$defaultSettings	= \Typemill\Settings::getDefaultSettings();
@@ -46,8 +48,10 @@ class SettingsController extends Controller
 			$settings 			= \Typemill\Settings::getUserSettings();
 			$defaultSettings	= \Typemill\Settings::getDefaultSettings();
 			$params 			= $request->getParams();
+			$files 				= $request->getUploadedFiles();
 			$newSettings		= isset($params['settings']) ? $params['settings'] : false;
 			$validate			= new Validation();
+			$processFiles		= new ProcessFile();
 
 			if($newSettings)
 			{
@@ -62,6 +66,8 @@ class SettingsController extends Controller
 					'formats'		=> $newSettings['formats'],
 				);
 				
+				# https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; 
+
 				$copyright 			= $this->getCopyright();
 
 				$validate->settings($newSettings, $copyright, $defaultSettings['formats'], 'settings');
@@ -77,8 +83,80 @@ class SettingsController extends Controller
 				$this->c->flash->addMessage('error', 'Please correct the errors');
 				return $response->withRedirect($this->c->router->pathFor('settings.show'));
 			}
-			
-			/* store updated settings */
+
+			if(!$processFiles->checkFolders())
+			{
+					$this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.');
+					return $response->withRedirect($this->c->router->pathFor('settings.show'));
+			}
+
+			# handle single input with single file upload
+    		$logo = $files['settings']['logo'];
+    		if($logo->getError() === UPLOAD_ERR_OK) 
+    		{
+    			$allowed = ['jpg', 'jpeg', 'png', 'svg'];
+    			$extension = pathinfo($logo->getClientFilename(), PATHINFO_EXTENSION);
+    			if(!in_array(strtolower($extension), $allowed))
+    			{
+					$_SESSION['errors']['settings']['logo'] = array('Only jpg, jpeg, png and svg allowed');
+					$this->c->flash->addMessage('error', 'Please correct the errors');
+					return $response->withRedirect($this->c->router->pathFor('settings.show'));
+    			}
+
+    			$processFiles->deleteFileWithName('logo');
+		        $newSettings['logo'] = $processFiles->moveUploadedFile($logo, $overwrite = true, $name = 'logo');
+		    }
+		    elseif(isset($params['settings']['deletelogo']) && $params['settings']['deletelogo'] == 'delete')
+		    {
+		    	$processFiles->deleteFileWithName('logo');
+		    	$newSettings['logo'] = '';
+		    }
+		    else
+		    {
+		    	$newSettings['logo'] = 	isset($settings['logo']) ? $settings['logo'] : ''; 
+		    }
+
+			# handle single input with single file upload
+    		$favicon = $files['settings']['favicon'];
+    		if ($favicon->getError() === UPLOAD_ERR_OK) 
+    		{
+    			$extension = pathinfo($favicon->getClientFilename(), PATHINFO_EXTENSION);
+    			if(strtolower($extension) != 'png')
+    			{
+					$_SESSION['errors']['settings']['favicon'] = array('Only .png-files allowed');
+					$this->c->flash->addMessage('error', 'Please correct the errors');
+					return $response->withRedirect($this->c->router->pathFor('settings.show'));
+    			}
+
+    			$processImage = new ProcessImage([
+    				'16' => ['width' => 16, 'height' => 16], 
+    				'32' => ['width' => 32, 'height' => 32],
+    				'72' => ['width' => 72, 'height' => 72],
+    				'114' => ['width' => 114, 'height' => 114],
+    				'144' => ['width' => 144, 'height' => 144],
+    				'180' => ['width' => 180, 'height' => 180],
+    			]);
+    			$favicons = $processImage->generateSizesFromImageFile('favicon.png', $favicon->file);
+
+    			foreach($favicons as $key => $favicon)
+    			{
+    				imagepng( $favicon, $processFiles->fileFolder . 'favicon-' . $key . '.png' );
+					# $processFiles->moveUploadedFile($favicon, $overwrite = true, $name = 'favicon-' . $key);
+    			}
+
+		        $newSettings['favicon'] = 'favicon';
+		    }
+		    elseif(isset($params['settings']['deletefav']) && $params['settings']['deletefav'] == 'delete')
+		    {
+		    	$processFiles->deleteFileWithName('favicon');
+		    	$newSettings['favicon'] = '';
+		    }
+		    else
+		    {
+		    	$newSettings['favicon'] = isset($settings['favicon']) ? $settings['favicon'] : ''; 
+		    }
+
+			# store updated settings
 			\Typemill\Settings::updateSettings(array_merge($settings, $newSettings));
 			
 			$this->c->flash->addMessage('info', 'Settings are stored');

+ 7 - 0
system/Controllers/SetupController.php

@@ -9,6 +9,13 @@ use Typemill\Models\Write;
 
 class SetupController extends Controller
 {
+
+	# redirect if visit /setup route
+	public function redirect($request, $response)
+	{
+		return $response->withRedirect($this->c->router->pathFor('setup.show'));
+	}
+
 	public function show($request, $response, $args)
 	{
 		/* make some checks befor you install */

+ 22 - 0
system/Extensions/TwigPagelistExtension.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace Typemill\Extensions;
+
+use Typemill\Models\Folder;
+
+class TwigPagelistExtension extends \Twig_Extension
+{
+	public function getFunctions()
+	{
+		return [
+			new \Twig_SimpleFunction('getPageList', array($this, 'getList' ))
+		];
+	}
+
+	public function getList($folderContentDetails, $url)
+	{
+		$pagelist = Folder::getItemForUrlFrontend($folderContentDetails, $url);
+
+		return $pagelist;
+	}
+}

+ 21 - 2
system/Models/Folder.php

@@ -30,10 +30,9 @@ class Folder
 				}
 			}
 		}
-		return $folderContent;		
+		return $folderContent;
 	}
 	
-	
 	/*
 	* scans content of a folder recursively
 	* vars: folder path as string
@@ -301,6 +300,25 @@ class Folder
 		return $result;
 	}
 
+	public static function getItemForUrlFrontend($folderContentDetails, $url, $result = NULL)
+	{
+		foreach($folderContentDetails as $key => $item)
+		{
+			# set item active, needed to move item in navigation
+			if($item->urlRelWoF === $url)
+			{
+				$item->active = true;
+				$result = $item;
+			}
+			elseif($item->elementType === "folder")
+			{
+				$result = self::getItemForUrlFrontend($item->folderContent, $url, $result);
+			}
+		}
+
+		return $result;
+	}	
+
 	public static function getPagingForItem($content, $item)
 	{
 		$keyPos 			= count($item->keyPathArray)-1;
@@ -475,6 +493,7 @@ class Folder
 		
 		while($i < count($searchArray))
 		{
+			if(!isset($content[$searchArray[$i]])){ return false; }
 			$item = $content[$searchArray[$i]];
 
 			if($i == count($searchArray)-1)

+ 35 - 0
system/Models/Helpers.php

@@ -24,4 +24,39 @@ class Helpers{
 		$table .= '</table></body></html>';
 		echo $table;
 	}
+
+	public static function array_sort($array, $on, $order=SORT_ASC)
+	{
+	    $new_array = array();
+	    $sortable_array = array();
+
+	    if (count($array) > 0) {
+	        foreach ($array as $k => $v) {
+	            if (is_array($v)) {
+	                foreach ($v as $k2 => $v2) {
+	                    if ($k2 == $on) {
+	                        $sortable_array[$k] = $v2;
+	                    }
+	                }
+	            } else {
+	                $sortable_array[$k] = $v;
+	            }
+	        }
+
+	        switch ($order) {
+	            case SORT_ASC:
+	                asort($sortable_array);
+	            break;
+	            case SORT_DESC:
+	                arsort($sortable_array);
+	            break;
+	        }
+
+	        foreach ($sortable_array as $k => $v) {
+	            $new_array[] = $array[$k];
+			}
+		}
+
+		return $new_array;
+	}
 }

+ 231 - 0
system/Models/ProcessAssets.php

@@ -0,0 +1,231 @@
+<?php
+namespace Typemill\Models;
+
+use \URLify;
+
+class ProcessAssets
+{
+	# holds the path to the baseFolder
+	protected $baseFolder;
+
+	# holds the path to the mediaFolder
+	protected $mediaFolder;
+
+	# holds the path to the temporary image folder
+	protected $tmpFolder;
+
+	# holds the path where original images are stored
+	protected $originalFolder;
+
+	# holds the path where images for frontend use are stored
+	protected $liveFolder;
+
+	# holds the folder where the thumbs for the media library are stored
+	protected $thumbFolder;
+
+	# holds the folder where the thumbs for the media library are stored
+	public $fileFolder;
+
+	# holds the desired sizes for image resizing
+	protected $desiredSizes;
+
+	public function __construct($desiredSizes = NULL)
+	{
+		$this->baseFolder		= getcwd() . DIRECTORY_SEPARATOR;
+
+		$this->mediaFolder		= $this->baseFolder . 'media' . DIRECTORY_SEPARATOR;
+
+		$this->tmpFolder		= $this->mediaFolder . 'tmp' . DIRECTORY_SEPARATOR;
+
+		$this->originalFolder	= $this->mediaFolder . 'original' . DIRECTORY_SEPARATOR;
+
+		$this->liveFolder 		= $this->mediaFolder . 'live' . DIRECTORY_SEPARATOR;
+
+		$this->thumbFolder 		= $this->mediaFolder . 'thumbs' . DIRECTORY_SEPARATOR;
+
+		$this->fileFolder 		= $this->mediaFolder . 'files' . DIRECTORY_SEPARATOR;
+
+		$this->desiredSizes 	= $desiredSizes;
+	}
+
+	public function checkFolders($forassets = null)
+	{
+
+		$folders = [$this->mediaFolder, $this->tmpFolder, $this->fileFolder];
+		
+		if($forassets == 'images')
+		{
+			$folders = [$this->mediaFolder, $this->tmpFolder, $this->originalFolder, $this->liveFolder, $this->thumbFolder];
+		}
+
+		foreach($folders as $folder)
+		{
+			if(!file_exists($folder))
+			{
+				if(!mkdir($folder, 0774, true))
+				{
+					return false;
+				}
+				if($folder == $this->thumbFolder)
+				{
+					# cleanup old systems
+					$this->cleanupLiveFolder();
+
+					# generate thumbnails from live folder
+					$this->generateThumbs();
+				}
+			}
+			elseif(!is_writeable($folder))
+			{
+				return false;
+			} 
+		}
+		return true;
+	}
+
+	public function setFileName($originalname, $type, $overwrite = null)
+	{
+		$pathinfo			= pathinfo($originalname);
+		
+		$this->extension 	= strtolower($pathinfo['extension']);
+		$this->filename 	= URLify::filter(iconv(mb_detect_encoding($pathinfo['filename'], mb_detect_order(), true), "UTF-8", $pathinfo['filename']));
+
+		$filename = $this->filename;
+
+		# check if file name is 
+		if(!$overwrite)
+		{
+			$suffix = 1;
+
+			$destination = $this->liveFolder;
+			if($type == 'file')
+			{
+				$destination = $this->fileFolder;
+			}
+
+			while(file_exists($destination . $filename . '.' . $this->extension))
+			{
+				$filename = $this->filename . '-' . $suffix;
+				$suffix++;
+			}
+		}
+
+		$this->filename = $filename;
+
+		return true;
+	}
+
+	public function getName()
+	{
+		return $this->filename;
+	}
+
+	public function getExtension()
+	{
+		return $this->extension;
+	}
+
+	public function getFullName()
+	{
+		return $this->filename . '.' . $this->extension;
+	}
+
+	public function clearTempFolder()
+	{
+		$files 		= scandir($this->tmpFolder);
+		$result		= true;
+		
+		foreach($files as $file)
+		{
+			if (!in_array($file, array(".","..")))
+			{			
+				$filelink = $this->tmpFolder . $file;
+				if(!unlink($filelink))
+				{
+					$success = false;
+				}	
+			}
+		}
+		
+		return $result;
+	}
+
+	public function cleanupLiveFolder()
+	{		
+		# delete all old thumbs mlibrary in live folder
+		foreach(glob($this->liveFolder . '*mlibrary*') as $filename)
+		{
+			unlink($filename);
+		}
+
+		return true;
+	}	
+	
+	public function findPagesWithUrl($structure, $url, $result)
+	{
+		foreach ($structure as $key => $item)
+		{
+			if($item->elementType == 'folder')
+			{
+				$result = $this->findPagesWithUrl($item->folderContent, $url, $result);
+			}
+			else
+			{
+				$live = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.md';
+				$draft = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.txt';
+
+				# check live first
+				if(file_exists($live))
+				{
+					$content = file_get_contents($live);
+					
+					if (stripos($content, $url) !== false)
+					{
+						$result[] = $item->urlRelWoF;
+					}
+					# if not in live, check in draft
+					elseif(file_exists($draft))
+					{
+						$content = file_get_contents($draft);
+						
+						if (stripos($content, $url) !== false)
+						{
+							$result[] = $item->urlRelWoF;
+						}
+					}
+				}
+			}
+		}
+		return $result;
+	}
+
+    public function formatSizeUnits($bytes)
+    {
+        if ($bytes >= 1073741824)
+        {
+            $bytes = number_format($bytes / 1073741824, 2) . ' GB';
+        }
+        elseif ($bytes >= 1048576)
+        {
+            $bytes = number_format($bytes / 1048576, 2) . ' MB';
+        }
+        elseif ($bytes >= 1024)
+        {
+            $bytes = number_format($bytes / 1024, 2) . ' KB';
+        }
+        elseif ($bytes > 1)
+        {
+            $bytes = $bytes . ' bytes';
+        }
+        elseif ($bytes == 1)
+        {
+            $bytes = $bytes . ' byte';
+        }
+        else
+        {
+            $bytes = '0 bytes';
+        }
+
+        return $bytes;
+	}	
+}

+ 165 - 0
system/Models/ProcessFile.php

@@ -0,0 +1,165 @@
+<?php
+namespace Typemill\Models;
+
+use Slim\Http\UploadedFile;
+use Typemill\Models\Helpers;
+use \URLify;
+
+class ProcessFile extends ProcessAssets
+{
+	/**
+	 * Moves the uploaded file to the upload directory. Only used for settings / NON VUE.JS uploads
+	 *
+	 * @param string $directory directory to which the file is moved
+	 * @param UploadedFile $uploadedFile file uploaded file to move
+	 * @return string filename of moved file
+	 */
+	public function moveUploadedFile(UploadedFile $uploadedFile, $overwrite = false, $name = false)
+	{
+		$this->setFileName($uploadedFile->getClientFilename(), 'file');
+		
+		if($name)
+		{
+			$this->setFileName($name . '.' . $this->extension, 'file', $overwrite);
+		}
+		
+	    $uploadedFile->moveTo($this->fileFolder . $this->getFullName());
+
+	    return $this->getFullName();
+	}
+
+	public function storeFile($file, $name)
+	{
+		$this->setFileName($name, 'file');
+
+		$this->clearTempFolder();
+
+		$file = $this->decodeFile($file);
+
+		$path = $this->tmpFolder . $this->getFullName();
+
+		if(file_put_contents($path, $file))
+		{
+			$size = filesize($path);
+			$size = $this->formatSizeUnits($size);
+
+			$title = str_replace('-', ' ', $this->filename);
+			$title = $title . ' (' . strtoupper($this->extension) . ', ' . $size .')';
+
+			return ['title' => $title, 'name' => $this->filename, 'extension' => $this->extension, 'size' => $size, 'url' => 'media/files/' . $this->getFullName()];
+		}
+
+		return false;
+	}
+
+	public function publishFile()
+	{
+		$files 			= scandir($this->tmpFolder);
+		$success		= true;
+		
+		foreach($files as $file)
+		{
+			if (!in_array($file, array(".","..")))
+			{
+				$success = rename($this->tmpFolder . $file, $this->fileFolder . $file);
+			}
+		}
+		
+		return $success;
+	}
+
+	public function decodeFile(string $file)
+	{
+        $fileParts 		= explode(";base64,", $file);
+        $fileType		= explode("/", $fileParts[0]);
+		$fileData		= base64_decode($fileParts[1]);
+	
+		if ($fileData !== false)
+		{
+			return array("file" => $fileData, "type" => $fileType[1]);
+		}
+		
+		return false;
+	}
+
+
+	public function deleteFile($name)
+	{
+		# validate name 
+		$name = basename($name);
+
+		if(file_exists($this->fileFolder . $name) && unlink($this->fileFolder . $name))
+		{
+			return true;
+		}
+
+		return false;
+	}
+
+
+	public function deleteFileWithName($name)
+	{
+		# e.g. delete $name = 'logo';
+
+		$name = basename($name);
+
+		if($name != '' && !in_array($name, array(".","..")))
+		{
+			foreach(glob($this->fileFolder . $name . '.*') as $file)
+			{
+				unlink($file);
+			}
+		}
+	}
+
+
+	/*
+	* scans content of a folder (without recursion)
+	* vars: folder path as string
+	* returns: one-dimensional array with names of folders and files
+	*/
+	public function scanFilesFlat()
+	{
+		$files 		= scandir($this->fileFolder);
+		$filelist	= array();
+
+		foreach ($files as $key => $name)
+		{
+			if (!in_array($name, array(".","..")) && file_exists($this->fileFolder . $name))
+			{
+				$filelist[] = [
+					'name' 		=> $name,
+					'timestamp'	=> filemtime($this->fileFolder . $name),
+					'info'		=> pathinfo($this->fileFolder . $name),
+					'url'		=> 'media/files/' . $name,
+				];
+			}
+		}
+
+		$filelist = Helpers::array_sort($filelist, 'timestamp', SORT_DESC);
+
+		return $filelist;
+	}
+
+
+	public function getFileDetails($name, $structure)
+	{		
+		$name = basename($name);
+
+		if (!in_array($name, array(".","..")) && file_exists($this->fileFolder . $name))
+		{
+			$filedetails = [
+				'name' 		=> $name,
+				'timestamp'	=> filemtime($this->fileFolder . $name),
+				'bytes' 	=> filesize($this->fileFolder . $name),
+				'info'		=> pathinfo($this->fileFolder . $name),
+				'url'		=> 'media/files/' . $name,
+				'pages'		=> $this->findPagesWithUrl($structure, $name, $result = [])
+			];
+
+			return $filedetails;
+		}
+
+		return false;
+	}
+}

+ 209 - 164
system/Models/ProcessImage.php

@@ -1,9 +1,11 @@
 <?php
 namespace Typemill\Models;
 
-class ProcessImage
+use Typemill\Models\Helpers;
+
+class ProcessImage extends ProcessAssets
 {
-	public function createImage(string $image, array $desiredSizes)
+	public function createImage(string $image, string $name, array $desiredSizes)
 	{
 		# fix error from jpeg-library
 		ini_set ('gd.jpeg_ignore_warning', 1);
@@ -11,7 +13,10 @@ class ProcessImage
 		
 		# clear temporary folder
 		$this->clearTempFolder();
-		
+
+		# set the name of the image 
+		$this->setFileName($name, 'image');
+
 		# decode the image from base64-string
 		$imageDecoded	= $this->decodeImage($image);
 		$imageData		= $imageDecoded["image"];
@@ -28,25 +33,24 @@ class ProcessImage
 		
 		# resize the images
 		$resizedImages	= $this->imageResize($image, $imageSize, $desiredSizes, $imageType);
-		
-		$basePath		= getcwd() . DIRECTORY_SEPARATOR . 'media';
-		$tmpFolder		= $basePath . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
 
-		$this->saveOriginal($tmpFolder, $imageData, 'original', $imageType);
-	
-		if($imageType == "gif" && $this->detectAnimatedGif($imageData))
-		{
-			$this->saveOriginal($tmpFolder, $imageData, 'live', $imageType);
+		# store the original name as txt-file
+		$tmpname = fopen($this->tmpFolder . $this->getName() . '.' . $imageType .  ".txt", "w");
+
+		$this->saveOriginal($this->tmpFolder, $imageData, $name = 'original', $imageType);
 			
-			return true;
-		}
-		
 		# temporary store resized images
 		foreach($resizedImages as $key => $resizedImage)
 		{
-			$this->saveImage($tmpFolder, $resizedImage, $key, $imageType);
+			$this->saveImage($this->tmpFolder, $resizedImage, $key, $imageType);
 		}
-		
+	
+		# if the image is an animated gif, then overwrite the resized version for live use with the original version
+		if($imageType == "gif" && $this->detectAnimatedGif($imageData))
+		{
+			$this->saveOriginal($this->tmpFolder, $imageData, $name = 'live', $imageType);			
+		}
+	
 		return true;
 	}
 	
@@ -60,42 +64,54 @@ class ProcessImage
 		return false;
 	}
 	
-	public function publishImage(array $desiredSizes, $name = false)
+	public function publishImage()
 	{
-		/* get images from tmp folder */
-		$basePath		= getcwd() . DIRECTORY_SEPARATOR . 'media';
-		$tmpFolder		= $basePath . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
-		$originalFolder	= $basePath . DIRECTORY_SEPARATOR . 'original' . DIRECTORY_SEPARATOR;
-		$liveFolder		= $basePath . DIRECTORY_SEPARATOR . 'live' . DIRECTORY_SEPARATOR;
+		# name is stored in temporary folder as name of the .txt-file
+		foreach(glob($this->tmpFolder . '*.txt') as $imagename)
+		{
+			$tmpname = str_replace('.txt', '', basename($imagename));
 
-		if(!file_exists($originalFolder)){ mkdir($originalFolder, 0774, true); }
-		if(!file_exists($liveFolder)){ mkdir($liveFolder, 0774, true); }
+			# set extension and sanitize name
+			$this->setFileName($tmpname, 'image');
 
-		$name 			= $name ? $name : uniqid();
-		
-		$files 			= scandir($tmpFolder);
+			unlink($imagename);
+		}
+
+		$name 			=  uniqid();
+
+		if($this->filename && $this->extension)
+		{			
+			$name 		= $this->filename;
+		}
+
+		$files 			= scandir($this->tmpFolder);
 		$success		= true;
 		
 		foreach($files as $file)
 		{
 			if (!in_array($file, array(".","..")))
-			{			
+			{
 				$tmpfilename 	= explode(".", $file);
 				
 				if($tmpfilename[0] == 'original')
 				{
-					$success = rename($tmpFolder . $file, $originalFolder . $name . '-' . $file);
+					$success = rename($this->tmpFolder . $file, $this->originalFolder . $name . '.' . $tmpfilename[1]);
+				}
+				if($tmpfilename[0] == 'live')
+				{
+					$success = rename($this->tmpFolder . $file, $this->liveFolder . $name . '.' . $tmpfilename[1]);
 				}
-				else
+				if($tmpfilename[0] == 'thumbs')
 				{
-					$success = rename($tmpFolder . $file, $liveFolder . $name . '-' . $file);
+					$success = rename($this->tmpFolder . $file, $this->thumbFolder . $name . '.' . $tmpfilename[1]);
 				}
 			}
 		}
 		
 		if($success)
 		{
-			return 'media/live/' . $name . '-live.' . $tmpfilename[1];
+			return true;
+			return 'media/live/' . $name . '.' . $tmpfilename[1];
 		}
 		
 		return false;
@@ -126,6 +142,7 @@ class ProcessImage
 	{
 		foreach($desiredSizes as $key => $desiredSize)
 		{
+			# if desired size is bigger than the actual image, then drop the desired sizes and use the actual image size instead
 			if($desiredSize['width'] > $imageSize['width'])
 			{
 				$desiredSizes[$key] = $imageSize;
@@ -141,129 +158,61 @@ class ProcessImage
 		return $desiredSizes;
 	}
 
-	public function imageResize($imageData, array $imageSize, array $desiredSizes, $imageType)
+	public function imageResize($imageData, array $source, array $desiredSizes, $imageType)
 	{
-		$copiedImages			= array();
-		$source_aspect_ratio 	= $imageSize['width'] / $imageSize['height'];
-				
-		foreach($desiredSizes as $key => $desiredSize)
-		{
-			$desired_aspect_ratio 	= $desiredSize['width'] / $desiredSize['height'];
 
-			if ( $source_aspect_ratio > $desired_aspect_ratio )
-			{
-				# when source image is wider
-				$temp_height 	= $desiredSize['height'];
-				$temp_width 	= ( int ) ($desiredSize['height'] * $source_aspect_ratio);
-				$temp_width 	= round($temp_width, 0);
-			}
-			else
-			{
-				# when source image is similar or taller
-				$temp_width 	= $desiredSize['width'];
-				$temp_height 	= ( int ) ($desiredSize['width'] / $source_aspect_ratio);
-				$temp_height 	= round($temp_height, 0);
-			}
+		$copiedImages			= array();
 
-			# Create a temporary GD image with desired size
-			$temp_gdim = imagecreatetruecolor( $temp_width, $temp_height );
+		foreach($desiredSizes as $key => $desired)
+		{
+			// resize
+		    $ratio = max($desired['width']/$source['width'], $desired['height']/$source['height']);
+		    $h = $desired['height'] / $ratio;
+		    $x = ($source['width'] - $desired['width'] / $ratio) / 2;
+		    $y = ($source['height'] - $desired['height'] / $ratio) / 2;
+		    $w = $desired['width'] / $ratio;
 
-			if ($imageType == "gif")
-			{
-				$transparent_index 	= imagecolortransparent($imageData);
-				imagepalettecopy($imageData, $temp_gdim);
-				imagefill($temp_gdim, 0, 0, $transparent_index);
-				imagecolortransparent($temp_gdim, $transparent_index);
-				imagetruecolortopalette($temp_gdim, true, 256);
-			}
-			elseif($imageType == "png")
-			{ 
-				imagealphablending($temp_gdim, false);
-				imagesavealpha($temp_gdim, true);
-				$transparent = imagecolorallocatealpha($temp_gdim, 255, 255, 255, 127);
-				imagefilledrectangle($temp_gdim, 0, 0, $temp_width, $temp_height, $transparent);
-			}
-			
-			# resize image
-			imagecopyresampled(
-				$temp_gdim,
-				$imageData,
-				0, 0,
-				0, 0,
-				$temp_width, $temp_height,
-				$imageSize['width'], $imageSize['height']
-			);
-
-			$copiedImages[$key]		= $temp_gdim;
-			
-			/*
-			
-			# Copy cropped region from temporary image into the desired GD image
-			$x0 = ( $temp_width - $desiredSize['width'] ) / 2;
-			$y0 = ( $temp_height - $desiredSize['height'] ) / 2;
+			$new = imagecreatetruecolor($desired['width'], $desired['height']);
 
-			$desired_gdim = imagecreatetruecolor( $desiredSize['width'], $desiredSize['height'] );
+		  	// preserve transparency
+		  	if($imageType == "gif" or $imageType == "png")
+		  	{
+		    	imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127));
+		    	imagealphablending($new, false);
+		    	imagesavealpha($new, true);
+		  	}
 
-			if ($imageType == "gif")
-			{
-				imagepalettecopy($temp_gdim, $desired_gdim);
-				imagefill($desired_gdim, 0, 0, $transparent_index);
-				imagecolortransparent($desired_gdim, $transparent_index);
-				imagetruecolortopalette($desired_gdim, true, 256);
-			}
-			elseif($imageType == "png")
-			{
-				imagealphablending($desired_gdim, false);
-				imagesavealpha($desired_gdim,true);
-				$transparent = imagecolorallocatealpha($desired_gdim, 255, 255, 255, 127);
-				imagefilledrectangle($desired_gdim, 0, 0, $desired_size['with'], $desired_size['height'], $transparent);
-			}
+		  	imagecopyresampled($new, $imageData, 0, 0, $x, $y, $desired['width'], $desired['height'], $w, $h);
 
-			imagecopyresampled(
-				$desired_gdim,
-				$temp_gdim,
-				0, 0,
-				0, 0,
-				$x0, $y0,
-				$desiredSize['width'], $desiredSize['height']				
-			);
-			$copiedImages[$key]		= $desired_gdim;
-			
-			*/
+			$copiedImages[$key]		= $new;
 		}
+
 		return $copiedImages;
 	}
 	
+	# save original in temporary folder
 	public function saveOriginal($folder, $image, $name, $type)
-	{
-		if(!file_exists($folder))
-		{
-			mkdir($folder, 0774, true);
-		}
-		
+	{		
 		$path = $folder . $name . '.' . $type;
 		
 		file_put_contents($path, $image);
 	}
 
+
+	# save resized images in temporary folder
 	public function saveImage($folder, $image, $name, $type)
-	{
-		if(!file_exists($folder))
-		{
-			mkdir($folder, 0774, true);
-		}
-		
+	{		
 		if($type == "png")
 		{
-			$result = imagepng( $image, $folder . '/' . $name . '.png' );
+			$result = imagepng( $image, $folder . $name . '.png' );
 		}
 		elseif($type == "gif")
 		{
-			$result = imagegif( $image, $folder . '/' . $name . '.gif' );
+			$result = imagegif( $image, $folder . $name . '.gif' );
 		}
 		else
 		{
-			$result = imagejpeg( $image, $folder . '/' . $name . '.jpeg' );
+			$result = imagejpeg( $image, $folder . $name . '.jpeg' );
 			$type = 'jpeg';
 		}
 		
@@ -277,60 +226,156 @@ class ProcessImage
 		return false;
 	}
 	
-	public function clearTempFolder()
+	public function deleteImage($name)
 	{
-		$folder		= getcwd() . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
 
-		if(!file_exists($folder))
+		# validate name 
+		$name = basename($name);
+
+		$result = true;
+
+		if(!file_exists($this->originalFolder . $name) OR !unlink($this->originalFolder . $name))
 		{
-			mkdir($folder, 0774, true);
-			return true;
-		}		
-		
-		$files 		= scandir($folder);
-		$result		= true;
-		
-		foreach($files as $file)
+			$result = false;
+		}
+
+		if(!file_exists($this->liveFolder . $name) OR !unlink($this->liveFolder . $name))
 		{
-			if (!in_array($file, array(".","..")))
-			{			
-				$filelink = $folder . $file;
-				if(!unlink($filelink))
-				{
-					$success = false;
-				}	
+			$result = false;
+		}
+
+		if(!file_exists($this->thumbFolder . $name) OR !unlink($this->thumbFolder . $name))
+		{
+			$result = false;
+		}
+
+		# you should not use glob but exact name with ending 
+		/*
+		foreach(glob($this->originalFolder . $name) as $image)
+		{
+			if(!unlink($image))
+			{
+				$success = false;
 			}
 		}
+		*/
 		
+		# array_map('unlink', glob("some/dir/*.txt"));
+
 		return $result;
 	}
-	
-	public function deleteImage($name)
+
+	/*
+	* scans content of a folder (without recursion)
+	* vars: folder path as string
+	* returns: one-dimensional array with names of folders and files
+	*/
+	public function scanMediaFlat()
 	{
-		$baseFolder		= getcwd() . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR;
-		$original		= $baseFolder . 'original' . DIRECTORY_SEPARATOR . $name . '*';
-		$live			= $baseFolder . 'live' . DIRECTORY_SEPARATOR . $name . '*';
-		$success 		= true;
-		
-		foreach(glob($original) as $image)
+		$thumbs 		= array_diff(scandir($this->thumbFolder), array('..', '.'));
+		$imagelist		= array();
+
+		foreach ($thumbs as $key => $name)
 		{
-			if(!unlink($image))
+			if (file_exists($this->liveFolder . $name))
 			{
-				$success = false;
+				$imagelist[] = [
+					'name' 		=> $name,
+					'timestamp'	=> filemtime($this->liveFolder . $name),
+					'src_thumb'	=> 'media/thumbs/' . $name,
+					'src_live'	=> 'media/live/' . $name,
+				];
 			}
 		}
-		
-		foreach(glob($live) as $image)
+
+		$imagelist = Helpers::array_sort($imagelist, 'timestamp', SORT_DESC);
+
+		return $imagelist;
+	}
+
+
+	public function getImageDetails($name, $structure)
+	{		
+		$name = basename($name);
+
+		if (!in_array($name, array(".","..")) && file_exists($this->liveFolder . $name))
 		{
-			if(!unlink($image))
+			$imageinfo 		= getimagesize($this->liveFolder . $name);
+
+			$imagedetails = [
+				'name' 		=> $name,
+				'timestamp'	=> filemtime($this->liveFolder . $name),
+				'bytes' 	=> filesize($this->liveFolder . $name),
+				'width'		=> $imageinfo[0],
+				'height'	=> $imageinfo[1],
+				'type'		=> $imageinfo['mime'],
+				'src_thumb'	=> 'media/thumbs/' . $name,
+				'src_live'	=> 'media/live/' . $name,
+				'pages'		=> $this->findPagesWithUrl($structure, $name, $result = [])
+			];
+
+			return $imagedetails;
+		}
+
+		return false;
+	}
+
+	public function generateThumbs()
+	{
+		# generate images from live folder to 'tmthumbs'
+		$liveImages 	= scandir($this->liveFolder);
+
+		foreach ($liveImages as $key => $name)
+		{
+			if (!in_array($name, array(".","..")))
 			{
-				$success = false;
+				$this->generateThumbFromImageFile($name);
 			}
 		}
+	}
+
+	public function generateThumbFromImageFile($filename)
+	{
+		$this->setFileName($filename, 'image', $overwrite = true);
+
+		if($this->extension == 'jpeg') $this->extension = 'jpg';
 		
-		return $success;
+		switch($this->extension)
+		{
+			case 'gif': $image = imagecreatefromgif($this->liveFolder . $filename); break;
+			case 'jpg': $image = imagecreatefromjpeg($this->liveFolder . $filename); break;
+			case 'png': $image = imagecreatefrompng($this->liveFolder . $filename); break;
+			default: return 'image type not supported';
+		}
+
+		$originalSize 	= $this->getImageSize($image);
+
+		$thumbSize		= $this->desiredSizes['thumbs'];
+
+		$thumb 			= $this->imageResize($image, $originalSize, ['thumbs' => $thumbSize ], $this->extension);
+
+		$this->saveImage($this->thumbFolder, $thumb['thumbs'], $this->filename, $this->extension);
 	}
-}
 
+	public function generateSizesFromImageFile($filename, $image)
+	{
+		$this->setFileName($filename, 'image');
+
+		if($this->extension == 'jpeg') $this->extension = 'jpg';
+		
+		switch($this->extension)
+		{
+			case 'gif': $image = imagecreatefromgif($image); break;
+			case 'jpg': $image = imagecreatefromjpeg($image); break;
+			case 'png': $image = imagecreatefrompng($image); break;
+			default: return 'image type not supported';
+		}
+
+		$originalSize 	= $this->getImageSize($image);
+
+		$resizedImages 	= $this->imageResize($image, $originalSize, $this->desiredSizes, $this->extension);
+
+		return $resizedImages;
+	}
 
-?>
+}

+ 32 - 21
system/Routes/Api.php

@@ -3,34 +3,45 @@
 use Typemill\Controllers\SettingsController;
 use Typemill\Controllers\ContentController;
 use Typemill\Controllers\ContentApiController;
+use Typemill\Controllers\ArticleApiController;
+use Typemill\Controllers\BlockApiController;
+use Typemill\Controllers\MediaApiController;
 use Typemill\Controllers\MetaApiController;
 use Typemill\Middleware\RestrictApiAccess;
 
 $app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router']));
 
-$app->post('/api/v1/article/markdown', ContentApiController::class . ':getArticleMarkdown')->setName('api.article.markdown')->add(new RestrictApiAccess($container['router']));
-$app->post('/api/v1/article/html', ContentApiController::class . ':getArticleHtml')->setName('api.article.html')->add(new RestrictApiAccess($container['router']));
-$app->post('/api/v1/article/publish', ContentApiController::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router']));
-$app->delete('/api/v1/article/unpublish', ContentApiController::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router']));
-$app->delete('/api/v1/article/discard', ContentApiController::class . ':discardArticleChanges')->setName('api.article.discard')->add(new RestrictApiAccess($container['router']));
-$app->post('/api/v1/post', ContentApiController::class . ':createPost')->setName('api.post.create')->add(new RestrictApiAccess($container['router']));
-$app->post('/api/v1/article', ContentApiController::class . ':createArticle')->setName('api.article.create')->add(new RestrictApiAccess($container['router']));
-$app->put('/api/v1/article', ContentApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router']));
-$app->delete('/api/v1/article', ContentApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router']));
-$app->post('/api/v1/article/sort', ContentApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/article/markdown', ArticleApiController::class . ':getArticleMarkdown')->setName('api.article.markdown')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/article/html', ArticleApiController::class . ':getArticleHtml')->setName('api.article.html')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/article/publish', ArticleApiController::class . ':publishArticle')->setName('api.article.publish')->add(new RestrictApiAccess($container['router']));
+$app->delete('/api/v1/article/unpublish', ArticleApiController::class . ':unpublishArticle')->setName('api.article.unpublish')->add(new RestrictApiAccess($container['router']));
+$app->delete('/api/v1/article/discard', ArticleApiController::class . ':discardArticleChanges')->setName('api.article.discard')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/article/sort', ArticleApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/article', ArticleApiController::class . ':createArticle')->setName('api.article.create')->add(new RestrictApiAccess($container['router']));
+$app->put('/api/v1/article', ArticleApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router']));
+$app->delete('/api/v1/article', ArticleApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/baseitem', ArticleApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router']));
+$app->get('/api/v1/navigation', ArticleApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/post', ArticleApiController::class . ':createPost')->setName('api.post.create')->add(new RestrictApiAccess($container['router']));
+
+$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router']));
 $app->get('/api/v1/article/metaobject', MetaApiController::class . ':getArticleMetaobject')->setName('api.articlemetaobject.get')->add(new RestrictApiAccess($container['router']));
 $app->get('/api/v1/article/metadata', MetaApiController::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new RestrictApiAccess($container['router']));
 $app->post('/api/v1/article/metadata', MetaApiController::class . ':updateArticleMeta')->setName('api.articlemeta.update')->add(new RestrictApiAccess($container['router']));
-$app->post('/api/v1/baseitem', ContentApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router']));
-$app->get('/api/v1/navigation', ContentApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router']));
-$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router']));
-
-$app->post('/api/v1/block', ContentApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router']));
-$app->put('/api/v1/block', ContentApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router']));
-$app->delete('/api/v1/block', ContentApiController::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router']));
-$app->put('/api/v1/moveblock', ContentApiController::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router']));
 
-$app->post('/api/v1/image', ContentApiController::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router']));
-$app->put('/api/v1/image', ContentApiController::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/block', BlockApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router']));
+$app->put('/api/v1/block', BlockApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router']));
+$app->delete('/api/v1/block', BlockApiController::class . ':deleteBlock')->setName('api.block.delete')->add(new RestrictApiAccess($container['router']));
+$app->put('/api/v1/moveblock', BlockApiController::class . ':moveBlock')->setName('api.block.move')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/video', BlockApiController::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router']));
 
-$app->post('/api/v1/video', ContentApiController::class . ':saveVideoImage')->setName('api.video.save')->add(new RestrictApiAccess($container['router']));
+$app->get('/api/v1/medialib/images', MediaApiController::class . ':getMediaLibImages')->setName('api.medialibimg.get')->add(new RestrictApiAccess($container['router']));
+$app->get('/api/v1/medialib/files', MediaApiController::class . ':getMediaLibFiles')->setName('api.medialibfiles.get')->add(new RestrictApiAccess($container['router']));
+$app->get('/api/v1/image', MediaApiController::class . ':getImage')->setName('api.image.get')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/image', MediaApiController::class . ':createImage')->setName('api.image.create')->add(new RestrictApiAccess($container['router']));
+$app->put('/api/v1/image', MediaApiController::class . ':publishImage')->setName('api.image.publish')->add(new RestrictApiAccess($container['router']));
+$app->delete('/api/v1/image', MediaApiController::class . ':deleteImage')->setName('api.image.delete')->add(new RestrictApiAccess($container['router']));
+$app->get('/api/v1/file', MediaApiController::class . ':getFile')->setName('api.file.get')->add(new RestrictApiAccess($container['router']));
+$app->post('/api/v1/file', MediaApiController::class . ':uploadFile')->setName('api.file.upload')->add(new RestrictApiAccess($container['router']));
+$app->put('/api/v1/file', MediaApiController::class . ':publishFile')->setName('api.file.publish')->add(new RestrictApiAccess($container['router']));
+$app->delete('/api/v1/file', MediaApiController::class . ':deleteFile')->setName('api.file.delete')->add(new RestrictApiAccess($container['router']));

+ 8 - 1
system/Routes/Web.php

@@ -69,4 +69,11 @@ foreach($routes as $pluginRoute)
 	}
 }
 
-$app->get('/[{params:.*}]', PageController::class . ':index')->setName('home');
+if($settings['settings']['setup'])
+{
+	$app->get('/[{params:.*}]', SetupController::class . ':redirect');	
+}
+else
+{
+	$app->get('/[{params:.*}]', PageController::class . ':index')->setName('home');
+}

+ 40 - 20
system/Settings.php

@@ -16,6 +16,25 @@ class Settings
 			$settings 			= array_merge($defaultSettings, $userSettings);
 		}
 
+		# if there is no theme set
+		if(!isset($settings['theme']))
+		{
+			# scan theme folder and get the first theme
+			$themefolder = $settings['rootPath'] . $settings['themeFolder'] . DIRECTORY_SEPARATOR;
+			$themes = array_diff(scandir($themefolder), array('..', '.'));
+			$firsttheme = reset($themes);
+
+			# if there is a theme with valid theme settings-file
+			if($firsttheme && self::getObjectSettings('themes', $firsttheme))
+			{
+				$settings['theme'] = $firsttheme;
+			}
+			else
+			{
+				die('There is no theme in the theme-folder. Please add a theme from https://themes.typemill.net');
+			}
+		}
+
 	    # i18n
 	    # load the strings of the set language
 	    $language = $settings['language'];
@@ -30,7 +49,7 @@ class Settings
 		{
 			$themeSettings = self::getObjectSettings('themes', $settings['theme']);
 			$settings['themes'][$settings['theme']] = isset($themeSettings['settings']) ? $themeSettings['settings'] : false;
-		}		
+		}
 
 		return array('settings' => $settings);
 	}
@@ -48,7 +67,6 @@ class Settings
 			'language'								=> 'en',
 			'startpage'								=> true,
 			'rootPath'								=> $rootPath,
-			'theme'									=> 'typemill',
 			'themeFolder'							=> 'themes',
 			'themeBasePath'							=> $rootPath,
 			'themePath'								=> '',
@@ -56,14 +74,14 @@ class Settings
 			'userPath'								=> $rootPath . 'settings' . DIRECTORY_SEPARATOR . 'users',
 			'authorPath'							=> __DIR__ . DIRECTORY_SEPARATOR . 'author' . DIRECTORY_SEPARATOR,
 			'editor'								=> 'visual',
-			'formats'								=> ['markdown', 'headline', 'ulist', 'olist', 'table', 'quote', 'image', 'video', 'toc', 'hr', 'definition', 'code'],
+			'formats'								=> ['markdown', 'headline', 'ulist', 'olist', 'table', 'quote', 'image', 'video', 'file', 'toc', 'hr', 'definition', 'code'],
 			'contentFolder'							=> 'content',
 			'cache'									=> true,
 			'cachePath'								=> $rootPath . 'cache',
 			'version'								=> '1.3.3',
 			'setup'									=> true,
 			'welcome'								=> true,
-			'images'								=> ['live' => ['width' => 820], 'mlibrary' => ['width' => 50, 'height' => 50]],
+			'images'								=> ['live' => ['width' => 820], 'thumbs' => ['width' => 250, 'height' => 150]],
 		];
 	}
 	
@@ -137,23 +155,25 @@ class Settings
 		
 		if($userSettings)
 		{
-			# whitelist settings that can be stored in usersettings (values are not relevant here, only keys)			
+			# whitelist settings that can be stored in usersettings (values are not relevant here, only keys)
 			$allowedUserSettings = ['displayErrorDetails' => true,
-									'title' => false,
-									'copyright' => false,
-									'language' => false,
-									'startpage' => false,
-									'author' => false,
-									'year' => false,
-									'theme' => false,
-									'editor' => false,
-									'formats' => false,
-									'setup' => false,
-									'welcome' => false,
-									'images' => false,
-									'plugins' => false,
-									'themes' => false,
-									'latestVersion' => false 
+									'title' => true,
+									'copyright' => true,
+									'language' => true,
+									'startpage' => true,
+									'author' => true,
+									'year' => true,
+									'theme' => true,
+									'editor' => true,
+									'formats' => true,
+									'setup' => true,
+									'welcome' => true,
+									'images' => true,
+									'plugins' => true,
+									'themes' => true,
+									'latestVersion' => true,
+									'logo' => true,
+									'favicon' => true, 
 								];
 
 			# cleanup the existing usersettings

+ 167 - 6
system/author/css/style.css

@@ -2,7 +2,7 @@
 *  		TRANSITION	  *
 **********************/
 
-a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{
+a, a:link, a:visited, a:focus, a:hover, a:active, .link, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{
 	-webkit-transition: color 0.2s ease;
 	-moz-transition: color 0.2s ease;
 	-o-transition: color 0.2s ease;
@@ -30,6 +30,130 @@ a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, .tab-button,
 	-ms-transition: all 0.1s ease;
 	transition: all 0.1s ease;	
 }
+/********************
+*  	TACHYONS 	    *
+********************/
+
+.tm-red{
+	color:#e0474c;
+}
+.bg-tm-red{
+	background:#e0474c;
+}
+.b--tm-red{
+	border-color: #e0474c;
+}
+.hover-tm-red:hover{
+	color:#e0474c;
+}
+.hover-bg-tm-red:hover{
+	background:#e0474c;
+}
+.hover-b--tm-red:hover{
+	border-color:#e0474c;
+}
+.tm-green{
+	color:#66b0a3;
+}
+.bg-tm-green{
+	background:#66b0a3;
+}
+.b--tm-green{
+	border-color:#66b0a3;
+}
+.hover-tm-green:hover{
+	color:#66b0a3;
+}
+.hover-bg-tm-green:hover{
+	background:#66b0a3;
+}
+.hover-b--tm-green:hover{
+	border-color:#66b0a3;
+}
+.w-100{
+	width:100%!important;
+}
+.w-15{
+	width: 15%;
+}
+.w-29{
+	width: 29%;
+}
+.w-95{
+	width:95%;
+}
+.w6{
+	width: 22rem;
+}
+.h0{
+	height: 0;
+}
+.h6{
+	height: 22rem;
+}
+.mw6{
+	max-width:22rem;
+}
+.max-h6{
+	max-height: 22rem;
+}
+.shadow-tm{
+	box-shadow: 0 0 2px 1px rgba(0,0,0,.1);
+}
+.bg-chess{
+	background-image: linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%);
+ 	background-size: 20px 20px;
+	background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
+}
+input.upload{
+	position: absolute;
+	top: 0;
+	right: 0;
+	margin: 0;
+	padding: 0;
+	font-size: 20px;
+	cursor: pointer;
+	opacity: 0;
+}
+.top-3{
+	top: 3rem;
+}
+.list-enter-active, .list-leave-active {
+  transition: all .3s;
+}
+.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
+  opacity: 0;
+}
+
+/****************************
+*  	download-commponent		*
+****************************/
+
+a.tm-download
+{
+	line-height: 35px;
+	margin-left: 40px;
+}
+a.tm-download::before{
+	content: '\2193';
+	position: absolute;
+	margin-left: -40px;
+	width: 30px;
+	height: 30px;
+	line-height: 30px;
+	font-family: "Comic Sans MS",cursive,sans-serif;	
+	border: 2px solid #e0474c;
+	border-radius: 50%;
+	text-align: center;
+	text-decoration: underline;
+}
+a.tm-download:hover::before{
+	text-decoration:underline;
+	color: #fff;
+	background: #e0474c;
+}
+
+
 /********************
 *  	COMMONS 	    *
 ********************/
@@ -453,8 +577,7 @@ footer{
 	bottom: 0;
 	left: 0;
 	right: 0;
-	background: #FFF;
-	background: rgba(255,255,255,0.9);
+	background: rgba(0,0,0,0.4);
 }
 .modal{
 	display: none;
@@ -1526,14 +1649,14 @@ label .help, .label .help{
 }
 .blox-buttons button.edit{
 	background: #70c1b3;
-	color: #eee;
+	color: #fff;
 }
 .blox-buttons button.edit:hover{
 	background: #4D978A;
 }
 .blox-buttons button.cancel:hover{
 	background: #e0474c;
-	color: #eee;
+	color: #fff;
 }
 .blox-buttons button.edit:disabled, .blox-buttons button.cancel:disabled{
 	background: #eee;
@@ -1750,6 +1873,9 @@ button.post-button:hover{
   width: 0.7142857142857142em;
   margin-right: 10px;
 }
+.icon-upload {
+  width: 0.9285714285714285em;
+}
 .format-bar .hidden{
 	display: none;
 }
@@ -1764,7 +1890,7 @@ button.post-button:hover{
 .format-bar .editactive{
 	position: relative;
 	margin-left: -20px;
-	margin-right: 20px;
+	margin-right: -20px;
 }
 .format-bar.blox{
 	width: auto;
@@ -2121,6 +2247,25 @@ button.delDL i:hover{
 .blox blockquote p{
 	margin-left: 50px;
 }
+.imageupload{
+	width: 50%;
+	position: relative;
+	display: inline-block;
+	border-right: 1px dotted grey;
+	box-sizing:border-box;
+}
+.imageselect{
+	width: 50%;
+	position: relative;
+	display: inline-block;
+	box-sizing:border-box;
+	border:0px;
+	padding: 0 0 0 0;
+	margin: 0 0 0 0;
+	min-height: 70px;
+	background: #f9f8f6;
+	font-family: Helvetica, Calibri, Arial, sans-serif;
+}
 .dropbox{
 	min-height: 70px;
 	background: #f9f8f6;
@@ -2212,6 +2357,22 @@ button.delDL i:hover{
   border-color: transparent transparent transparent rgba(255, 255, 255, 0.75);
   content: ' ';
 }
+.medialib{
+	margin: auto;
+	width: 100%;
+	height: 80%;
+	overflow: auto;
+	background: #f9f8f6;
+	max-width: 1200px;
+}
+.imagecard{
+	margin: 10px;
+	box-shadow:0 2px 5px rgba(22,23,26,.05);
+	display: inline-block;
+	vertical-align: top;
+	background: #fff;
+}
+
 sup{}
 cite{}
 abbr{}

+ 33 - 2
system/author/js/author.js

@@ -125,7 +125,7 @@
 	/**********************************
 	** 		START VERSION CHECK	 	 **
 	**********************************/
-		
+
 	if(document.getElementById("system"))
 	{
 		getVersions('system', document.getElementsByClassName("fc-system-version"));
@@ -243,7 +243,6 @@
 		}
 		return segmentsA.length - segmentsB.length;
 	}
-
 	
 	/*************************************
 	** 		CARDS: ACTIVATE/OPEN CLOSE	**
@@ -269,6 +268,38 @@
 		}
 	}
 	
+	/*************************************
+	**			Input Type File			**
+	*************************************/
+
+	var fileinputs = document.querySelectorAll( ".fileinput" );
+						
+	for (i = 0; i < fileinputs.length; ++i)
+	{
+		(function () {
+			
+			thisfileinput = fileinputs[i];
+
+			var deletefilebutton	= thisfileinput.getElementsByClassName("deletefilebutton")[0];
+			var deletefileinput		= thisfileinput.getElementsByClassName("deletefileinput")[0];
+			var visiblefilename		= thisfileinput.getElementsByClassName("visiblefilename")[0];
+			var hiddenfile			= thisfileinput.getElementsByClassName("hiddenfile")[0];
+								
+			hiddenfile.onchange = function()
+			{
+				visiblefilename.value = this.files[0].name;
+			}
+
+			deletefilebutton.onclick = function(event)
+			{
+				event.preventDefault();
+				deletefileinput.value = 'delete';
+				visiblefilename.value = '';
+			}
+
+		}());	
+	}
+	
 	/*************************************
 	**			COLOR PICKER			**
 	*************************************/

+ 8 - 0
system/author/js/vue-blox-config.js

@@ -42,6 +42,13 @@ let determiner = {
 		}
 		return false;
 	},
+	file: function(block,lines,firstChar,secondChar,thirdChar){
+		if( (firstChar == '[' && lines[0].indexOf('{.tm-download') != -1) )
+		{
+			return "file-component";
+		}
+		return false;
+	},
 	code: function(block,lines,firstChar,secondChar,thirdChar){
 		if( firstChar == '`' && secondChar == '`' && thirdChar == '`')
 		{
@@ -67,6 +74,7 @@ let bloxFormats = {
 			quote: { label: '<svg class="icon icon-quotes-left"><use xlink:href="#icon-quotes-left"></use></svg>', title: 'Quote', component: 'quote-component' },
 			image: { label: '<svg class="icon icon-image"><use xlink:href="#icon-image"></use></svg>', title: 'Image', component: 'image-component' },
 			video: { label: '<svg class="icon icon-play"><use xlink:href="#icon-play"></use></svg>', title: 'Video', component: 'video-component' },
+			file: { label: '<svg class="icon icon-paperclip"><use xlink:href="#icon-paperclip"></use></svg>', title: 'File', component: 'file-component' },
 			toc: { label: '<svg class="icon icon-list-alt"><use xlink:href="#icon-list-alt"></use></svg>', title: 'Table of Contents', component: 'toc-component' },
 			hr: { label: '<svg class="icon icon-pagebreak"><use xlink:href="#icon-pagebreak"></use></svg>', title: 'Horizontal Line', component: 'hr-component' },
 			definition: { label: '<svg class="icon icon-dots-two-vertical"><use xlink:href="#icon-dots-two-vertical"></use></svg>', title: 'Definition List', component: 'definition-component' },

+ 766 - 40
system/author/js/vue-blox.js

@@ -5,9 +5,9 @@ const contentComponent = Vue.component('content-block', {
 	template: '<div ref="bloxcomponent" class="blox-editor" :class="newblock">' +
 				'<div v-if="newblock" class="newblock-info">Choose a content-type <button class="newblock-close" @click.prevent="closeNewBlock($event)">close</button></div>' +	
 				'<div class="blox-wrapper" :class="{ editactive: edit }">' +
-				 '<div class="sideaction" v-if="body">' + 
-				  '<button class="add" :disabled="disabled" :title="$t(\'add content-block\')" @click.prevent="addNewBlock($event)"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></button>' +
-				  '<button class="delete" :disabled="disabled" :title="$t(\'delete content-block\')" @click.prevent="deleteBlock($event)"><svg class="icon icon-close"><use xlink:href="#icon-close"></use></svg></button>' +
+				 '<div class="sideaction" slot="header" v-if="body">' + 
+				  '<button class="add" :disabled="disabled" :title="$t(\'add content-block\')" @mousedown.prevent="disableSort()" @click.prevent="addNewBlock($event)"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></button>' +
+				  '<button class="delete" :disabled="disabled" :title="$t(\'delete content-block\')" @mousedown.prevent="disableSort()" @click.prevent="deleteBlock($event)"><svg class="icon icon-close"><use xlink:href="#icon-close"></use></svg></button>' +
 				 '</div>' + 
 				 '<div class="background-helper" @keyup.enter="submitBlock" @click="getData">' +
 				  '<div class="component" ref="component">' +
@@ -40,6 +40,10 @@ const contentComponent = Vue.component('content-block', {
 		eventBus.$on('closeComponents', this.closeComponents);
 	},
 	methods: {
+		disableSort: function(event)
+		{
+			this.$root.$data.sortdisabled = true; 
+		},
 		addNewBlock: function(event)
 		{
 			/* we have to get from dom because block-data might not be set when user clicked on add button before opened the component */
@@ -76,27 +80,17 @@ const contentComponent = Vue.component('content-block', {
 		updateMarkdown: function($event)
 		{
 			this.compmarkdown = $event;
-			this.$nextTick(function () {
-				this.$refs.preview.style.minHeight = this.$refs.component.offsetHeight + 'px';
-			});
+			this.setComponentSize();
 		},
-		switchToEditMode: function()
+		setComponentSize: function()
 		{
-			if(this.edit){ return; }
-			eventBus.$emit('closeComponents');
-			self = this;
-			self.$root.$data.freeze = true; 						/* freeze the data */
-		  	self.$root.$data.sortdisabled = true;			/* disable sorting */
-			this.preview = 'hidden'; 								/* hide the html-preview */
-			this.edit = true;										/* show the edit-mode */
-			this.compmarkdown = self.$root.$data.blockMarkdown;		/* get markdown data */
-			this.componentType = self.$root.$data.blockType;		/* get block-type of element */
 			if(this.componentType == 'image-component')
 			{
+				myself = this;
 				setTimeout(function(){ 
-					self.$nextTick(function () 
+					myself.$nextTick(function () 
 					{
-						self.$refs.preview.style.minHeight = self.$refs.component.offsetHeight + 'px';
+						myself.$refs.preview.style.minHeight = myself.$refs.component.offsetHeight + 'px';
 					});
 				}, 200);
 			}
@@ -104,10 +98,23 @@ const contentComponent = Vue.component('content-block', {
 			{
 				this.$nextTick(function () 
 				{
-					this.$refs.preview.style.minHeight = self.$refs.component.offsetHeight + 'px';
+					this.$refs.preview.style.minHeight = this.$refs.component.offsetHeight + 'px';
 				});				
 			}
 		},
+		switchToEditMode: function()
+		{
+			if(this.edit){ return; }
+			eventBus.$emit('closeComponents');
+			self = this;
+			self.$root.$data.freeze = true; 						/* freeze the data */
+		  	self.$root.$data.sortdisabled = true;			/* disable sorting */
+			this.preview = 'hidden'; 								/* hide the html-preview */
+			this.edit = true;										/* show the edit-mode */
+			this.compmarkdown = self.$root.$data.blockMarkdown;		/* get markdown data */
+			this.componentType = self.$root.$data.blockType;		/* get block-type of element */
+			this.setComponentSize();
+		},
 		closeComponents: function()
 		{
 			this.preview = 'visible';
@@ -216,7 +223,8 @@ const contentComponent = Vue.component('content-block', {
 				{
 					var compmarkdown = this.compmarkdown.split('\n\n').join('\n');
 				}
-*/				var compmarkdown = this.compmarkdown;
+*/				
+				var compmarkdown = this.compmarkdown;
 
 				var params = {
 					'url':				document.getElementById("path").value,
@@ -228,13 +236,33 @@ const contentComponent = Vue.component('content-block', {
 
 				if(this.componentType == 'image-component' && self.$root.$data.file)
 				{
-					var url = self.$root.$data.root + '/api/v1/image';
+					var url 	= self.$root.$data.root + '/api/v1/image';
 					var method 	= 'PUT';
+					params.new 	= false;
+					if(self.$root.$data.newblock || self.$root.$data.blockId == 99999)
+					{
+						params.new = true;
+					}
 				}
 				else if(this.componentType == 'video-component')
 				{
-					var url = self.$root.$data.root + '/api/v1/video';
-					var method = 'POST';
+					var url 	= self.$root.$data.root + '/api/v1/video';
+					var method 	= 'POST';
+					params.new 	= false;
+					if(self.$root.$data.newblock || self.$root.$data.blockId == 99999)
+					{
+						params.new = true;
+					}
+				}
+				else if(this.componentType == 'file-component')
+				{
+					var url 	= self.$root.$data.root + '/api/v1/file';
+					var method 	= 'PUT';
+					params.new 	= false;
+					if(self.$root.$data.newblock || self.$root.$data.blockId == 99999)
+					{
+						params.new = true;
+					}
 				}
 				else if(self.$root.$data.newblock || self.$root.$data.blockId == 99999)
 				{
@@ -246,7 +274,7 @@ const contentComponent = Vue.component('content-block', {
 					var url = self.$root.$data.root + '/api/v1/block';
 					var method 	= 'PUT';
 				}
-				
+
 				sendJson(function(response, httpStatus)
 				{
 					if(httpStatus == 400)
@@ -1180,12 +1208,21 @@ const videoComponent = Vue.component('video-component', {
 	},
 })
 
+
 const imageComponent = Vue.component('image-component', {
 	props: ['compmarkdown', 'disabled'],
 	template: '<div class="dropbox">' +
 				'<input type="hidden" ref="markdown" :value="compmarkdown" :disabled="disabled" @input="updatemarkdown" />' +
-				'<input type="file" name="image" accept="image/*" class="input-file" @change="onFileChange( $event )" /> ' +
-				'<p>{{ $t(\'drag a picture or click to select\') }}</p>' +
+				'<div class="imageupload">' + 
+					'<input type="file" name="image" accept="image/*" class="input-file" @change="onFileChange( $event )" /> ' +
+					'<p><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> {{ $t(\'drag a picture or click to select\') }}</p>' +
+				'</div>' +
+				'<button class="imageselect" @click.prevent="openmedialib()"><svg class="icon icon-image baseline"><use xlink:href="#icon-image"></use></svg> select from medialib</button>' +
+				'<transition name="fade-editor">' +
+					'<div v-if="showmedialib" class="modalWindow">' +
+						'<medialib parentcomponent="images"></medialib>' + 
+					'</div>' +
+				'</transition>' +
 				'<div class="contenttype"><svg class="icon icon-image"><use xlink:href="#icon-image"></use></svg></div>' +	
 				'<img class="uploadPreview" :src="imgpreview" />' +
 				'<div v-if="load" class="loadwrapper"><span class="load"></span></div>' +
@@ -1201,6 +1238,7 @@ const imageComponent = Vue.component('image-component', {
 		return {
 			maxsize: 5, // megabyte
 			imgpreview: false,
+			showmedialib: false,
 			load: false,
 			imgmeta: false,
 			imgalt: '',
@@ -1209,7 +1247,7 @@ const imageComponent = Vue.component('image-component', {
 			imglink: '',
 			imgclass: 'center',
 			imgid: '',
-			imgfile: 'imgplchldr',
+			imgfile: '',
 		}
 	},
 	mounted: function(){
@@ -1280,6 +1318,10 @@ const imageComponent = Vue.component('image-component', {
 		}
 	},
 	methods: {
+		openmedialib: function()
+		{
+			this.showmedialib = true;
+		},
 		isChecked: function(classname)
 		{
 			if(this.imgclass == classname)
@@ -1372,7 +1414,7 @@ const imageComponent = Vue.component('image-component', {
 					errors = 'Maximum size of image caption is 140 characters';
 				}
 			}
-						
+
 			if(errors)
 			{
 				this.$parent.freezePage();
@@ -1402,17 +1444,17 @@ const imageComponent = Vue.component('image-component', {
 				}
 				else
 				{
-					self = this;					
-					this.$parent.freezePage();
-					this.$root.$data.file = true;
-					this.load = true;
+					self = this;
 					
+					self.$parent.freezePage();
+					self.$root.$data.file = true;
+					self.load = true;
+
 					let reader = new FileReader();
 					reader.readAsDataURL(imageFile);
 					reader.onload = function(e) {
+
 						self.imgpreview = e.target.result;
-						self.$emit('updatedMarkdown', '![](imgplchldr)');
-						
 						
 						/* load image to server */
 						var url = self.$root.$data.root + '/api/v1/image';
@@ -1420,22 +1462,23 @@ const imageComponent = Vue.component('image-component', {
 						var params = {
 							'url':				document.getElementById("path").value,
 							'image':			e.target.result,
+							'name': 			imageFile.name, 
 							'csrf_name': 		document.getElementById("csrf_name").value,
 							'csrf_value':		document.getElementById("csrf_value").value,
 						};
-									
+
 						var method 	= 'POST';
-						
+
 						sendJson(function(response, httpStatus)
-						{							
+						{
 							if(httpStatus == 400)
 							{
-								self.$parent.activatePage();
+								self.activatePage();
 							}
 							if(response)
 							{
-								self.$parent.activatePage();
 								self.load = false;
+								self.$parent.activatePage();
 								
 								var result = JSON.parse(response);
 
@@ -1446,6 +1489,200 @@ const imageComponent = Vue.component('image-component', {
 								else
 								{
 									self.imgmeta = true;
+									self.imgfile = result.name;
+									self.$emit('updatedMarkdown', '![]('+ result.name +')');
+								}
+							}
+						}, method, url, params);
+					}
+				}
+			}
+		}
+	}
+})
+
+const fileComponent = Vue.component('file-component', {
+	props: ['compmarkdown', 'disabled'],
+	template: '<div class="dropbox">' +
+				'<input type="hidden" ref="markdown" :value="compmarkdown" :disabled="disabled" @input="updatemarkdown" />' +
+				'<div class="imageupload">' + 
+					'<input type="file" accept="*" name="file" class="input-file" @change="onFileChange( $event )" /> ' +
+					'<p><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> {{ $t(\'upload file\') }}</p>' +
+				'</div>' +
+				'<button class="imageselect" @click.prevent="openmedialib()"><svg class="icon icon-paperclip baseline"><use xlink:href="#icon-paperclip"></use></svg> select from medialib</button>' +
+				'<transition name="fade-editor">' +
+					'<div v-if="showmedialib" class="modalWindow">' +
+						'<medialib parentcomponent="files"></medialib>' + 
+					'</div>' +
+				'</transition>' +
+				'<div class="contenttype"><svg class="icon icon-paperclip"><use xlink:href="#icon-paperclip"></use></svg></div>' +
+				'<div v-if="load" class="loadwrapper"><span class="load"></span></div>' +
+				'<div class="imgmeta relative" v-if="filemeta">' +
+				  '<label for="filetitle">{{ $t(\'Title\') }}: </label>' + 
+				  '<input name="filetitle" type="text" placeholder="Add a title for the download-link" v-model="filetitle" @input="createmarkdown" max="64" />' + 
+				  '<input title="fileid" type="hidden" placeholder="id" v-model="fileid" @input="createmarkdown" max="140" />' +
+				'</div></div>',
+	data: function(){
+		return {
+			maxsize: 5, // megabyte
+			showmedialib: false,
+			load: false,
+			filemeta: false,
+			filetitle: '',
+			fileextension: '',
+			fileurl: '',
+			fileid: ''
+		}
+	},
+	mounted: function(){
+		
+		this.$refs.markdown.focus();
+
+		if(this.compmarkdown)
+		{
+			this.filemeta = true;
+			
+			var filemarkdown = this.compmarkdown;
+			
+			var filetitle = filemarkdown.match(/\[.*?\]/);
+			if(filetitle)
+			{
+				filemarkdown = filemarkdown.replace(filetitle[0],'');
+				this.filetitle = filetitle[0].slice(1,-1);
+			}
+
+			var fileattr = filemarkdown.match(/\{.*?\}/);
+			var fileextension = filemarkdown.match(/file-(.*)?\}/);
+			if(fileattr && fileextension)
+			{
+				filemarkdown = filemarkdown.replace(fileattr[0],'');
+				this.fileextension = fileextension[1].trim();
+			}
+
+			var fileurl = filemarkdown.match(/\(.*?\)/g);
+			if(fileurl)
+			{
+				filemarkdown = filemarkdown.replace(fileurl[0],'');
+				this.fileurl = fileurl[0].slice(1,-1);
+			}
+		}
+	},
+	methods: {
+		openmedialib: function()
+		{
+			this.showmedialib = true;
+		},
+		isChecked: function(classname)
+		{
+			if(this.fileclass == classname)
+			{
+				return ' checked';
+			}
+		},
+		updatemarkdown: function(event)
+		{
+			this.$emit('updatedMarkdown', event.target.value);
+		},
+		createmarkdown: function()
+		{
+			var errors = false;
+			
+			if(this.filetitle.length < 101)
+			{
+				filemarkdown = '[' + this.filetitle + ']';
+			}
+			else
+			{
+				errors = 'Maximum size of file-text is 100 characters';
+				filemarkdown = '[]';
+			}
+			if(this.fileurl != '')
+			{
+				if(this.fileurl.length < 101)
+				{
+					filemarkdown = '[' + this.filetitle + '](' + this.fileurl + ')';
+				}
+				else
+				{
+					errors = 'Maximum size of file link is 100 characters';
+				}
+			}
+			if(this.fileextension != '')
+			{
+				filemarkdown = filemarkdown + '{.tm-download file-' + this.fileextension + '}';
+			}
+			if(errors)
+			{
+				this.$parent.freezePage();
+				publishController.errors.message = errors;
+			}
+			else
+			{
+				publishController.errors.message = false;
+				this.$parent.activatePage();
+				this.$emit('updatedMarkdown', filemarkdown);
+			}
+		},
+		onFileChange: function( e )
+		{
+			if(e.target.files.length > 0)
+			{
+				let uploadedFile = e.target.files[0];
+				let size = uploadedFile.size / 1024 / 1024;
+				
+				if (size > this.maxsize)
+				{
+					publishController.errors.message = "The maximal size of a file is " + this.maxsize + " MB";
+				}
+				else
+				{
+					self = this;
+					
+					self.$parent.freezePage();
+					self.$root.$data.file = true;
+					self.load = true;
+
+					let reader = new FileReader();
+					reader.readAsDataURL(uploadedFile);
+					reader.onload = function(e) {
+						
+						/* load file to server */
+						var url = self.$root.$data.root + '/api/v1/file';
+						
+						var params = {
+							'url':				document.getElementById("path").value,
+							'file':				e.target.result,
+							'name': 			uploadedFile.name, 
+							'csrf_name': 		document.getElementById("csrf_name").value,
+							'csrf_value':		document.getElementById("csrf_value").value,
+						};
+
+						var method 	= 'POST';
+
+						sendJson(function(response, httpStatus)
+						{
+							if(httpStatus == 400)
+							{
+								self.activatePage();
+							}
+							if(response)
+							{
+								self.load = false;
+								self.$parent.activatePage();
+								
+								var result = JSON.parse(response);
+
+								if(result.errors)
+								{
+									publishController.errors.message = result.errors;
+								}
+								else
+								{
+									self.filemeta = true;
+									self.filetitle = result.info.title;
+									self.fileextension = result.info.extension;
+									self.fileurl = result.info.url;
+									self.createmarkdown();
 								}
 							}
 						}, method, url, params);						
@@ -1456,6 +1693,493 @@ const imageComponent = Vue.component('image-component', {
 	}
 })
 
+const medialib = Vue.component('medialib', {
+	props: ['parentcomponent'],
+	template: '<div class="medialib">' +
+				'<div class="mt3">' +
+					'<div class="w-30 dib v-top ph4 pv3">' +
+						'<button class="f6 link br0 ba ph3 pv2 mb2 w-100 dim white bn bg-tm-red" @click.prevent="closemedialib()">{{ $t(\'close library\') }}</button>' +
+	                    '<div class="w-100 relative">' + 
+	                    	'<div><input v-model="search" class="w-100 border-box pa2 mb3 br0 ba b--light-silver"><svg class="icon icon-search absolute top-1 right-1 pa1 gray"><use xlink:href="#icon-search"></use></svg></div>' +
+	                    '</div>' + 
+						'<button @click.prevent="showImages()" class="link br0 ba ph3 pv2 mv2 mr1" :class="isImagesActive()">Images</button>' +
+						'<button @click.prevent="showFiles()" class="link br0 ba ph3 pv2 mv2 ml1" :class="isFilesActive()">Files</button>' +
+					'</div>' +
+					'<div class="w-70 dib v-top center">' +
+						'<div v-if="errors" class="w-95 mv3 white bg-tm-red tc f5 lh-copy pa3">{{errors}}</div>' +
+						'<transition-group name="list">' +
+							'<div class="w-29 ma3 dib v-top bg-white shadow-tm overflow-hidden" v-for="(image, index) in filteredImages" :key="image.name" v-if="showimages">' +
+								'<a href="#" @click.prevent="selectImage(image)" :style="getBackgroundImage(image)" class="link mw5 dt hide-child cover bg-center">' +
+	  								'<span class="white dtc v-mid center w-100 h-100 child bg-black-80 pa5"><svg class="icon icon-check baseline"><use xlink:href="#icon-check"></use></svg> click to select</span>' +
+								'</a>' + 
+								'<div>' + 
+									'<div class="w-70 dib v-top pl3 pv3 f6 truncate"><strong>{{ image.name }}</strong></div>' + 
+									'<button @click.prevent="showImageDetails(image,index)" class="w-15 center dib link bn v-mid pv3 bg-white hover-bg-tm-green hover-white"><svg class="icon icon-info baseline"><use xlink:href="#icon-info"></use></svg></button>' +
+									'<button @click.prevent="deleteImage(image,index)" class="w-15 center dib link bn v-mid pv3 bg-white hover-bg-tm-red hover-white"><svg class="icon icon-trash-o baseline"><use xlink:href="#icon-trash-o"></use></svg></button>' +
+								'</div>' +
+							'</div>' +
+						'</transition-group>' +
+						'<div class="w-95 dib v-top bg-white mv3 relative" v-if="showimagedetails">' +
+							'<div class="flex flex-wrap item-start">' +
+								'<div class="w-50">' +
+									'<div class="w6 h6 bg-black-40 dtc v-mid bg-chess">' +
+										'<img :src="imagedetaildata.src_live" class="mw6 max-h6 dt center">' +
+									'</div>' +
+								'</div>' +
+								'<div class="w-50 pa3 lh-copy f7 relative">' +
+									'<div class="black-30 mt3 mb1">Name</div><div class="b">{{ imagedetaildata.name}}</div>' +
+									'<div class="black-30 mt3 mb1">URL</div><div class="b">{{ imagedetaildata.src_live}}</div>' +
+									'<div class="flex flex-wrap item-start">' + 
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Size</div><div class="b">{{ getSize(imagedetaildata.bytes) }}</div>' +
+										'</div>' +
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Dimensions</div><div class="b">{{ imagedetaildata.width }}x{{ imagedetaildata.height }} px</div>' +
+										'</div>' +
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Type</div><div class="b">{{ imagedetaildata.type }}</div>' +
+										'</div>' +
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Date</div><div class="b">{{ getDate(imagedetaildata.timestamp) }}</div>' +
+										'</div>' +
+									'</div>' +
+									'<div class="absolute w-90 bottom-0 flex justify-between">' +
+										'<button @click.prevent="selectImage(imagedetaildata)" class="w-50 mr1 pa2 link bn bg-light-gray hover-bg-tm-green hover-white"><svg class="icon icon-check baseline"><use xlink:href="#icon-check"></use></svg> select</button>' +
+										'<button @click.prevent="deleteImage(imagedetaildata, detailindex)" class="w-50 ml1 pa2 link bn bg-light-gray hover-bg-tm-red hover-white"><svg class="icon icon-trash-o baseline"><use xlink:href="#icon-trash-o"></use></svg> delete</button>' +									 
+									'</div>' +
+								'</div>' +
+							'</div>' +
+							'<button class="f7 link br0 ba ph3 pv2 dim white bn bg-tm-red absolute top-0 right-0" @click.prevent="showImages()">close details</button>' +
+							'<div class="pa3">' +
+								'<h4>Image used in:</h4>' +
+								'<ul class="ma0 pa0" v-if="imagedetaildata.pages && imagedetaildata.pages.length > 0">' + 
+									'<li class="list pa1" v-for="page in imagedetaildata.pages">' + 
+										'<a class="link tm-red" :href="baseurl + page">{{ page }}</a>' + 
+									'</li>' + 
+								'</ul>' +
+								'<div v-else>No pages found.</div>'+
+							'</div>' +
+						'</div>' +
+						'<transition-group name="list">' +
+							'<div class="w-29 ma3 dib v-top bg-white shadow-tm overflow-hidden" v-for="(file, index) in filteredFiles" :key="file.name" v-if="showfiles">' +
+								'<a href="#" @click.prevent="selectFile(file)" class="w-100 link cover bg-tm-green bg-center relative dt">' +
+	  								'<div class="absolute w-100 tc white f1 top-3 h0 ttu" v-html="file.info.extension"></div>' +
+	  								'<div class="link dt hide-child w-100">' +
+	  									'<span class="white dtc v-top center w-100 h-100 child pt6 pb3 tc"><svg class="icon icon-check baseline"><use xlink:href="#icon-check"></use></svg> click to select</span>' +
+									'</div>' +
+								'</a>' + 
+								'<div>' + 
+									'<div class="w-70 dib v-top pl3 pv3 f6 truncate"><strong>{{ file.name }}</strong></div>' + 
+									'<button @click.prevent="showFileDetails(file,index)" class="w-15 center dib link bn v-mid pv3 bg-white hover-bg-tm-green hover-white"><svg class="icon icon-info baseline"><use xlink:href="#icon-info"></use></svg></button>' +
+									'<button @click.prevent="deleteFile(file,index)" class="w-15 center dib link bn v-mid pv3 bg-white hover-bg-tm-red hover-white"><svg class="icon icon-trash-o baseline"><use xlink:href="#icon-trash-o"></use></svg></button>' +
+								'</div>' +
+							'</div>' +
+						'</transition-group>' +
+						'<div class="w-95 dib v-top bg-white mv3 relative" v-if="showfiledetails">' +
+							'<div class="flex flex-wrap item-start">' +
+								'<div class="w-50">' +
+									'<div class="w6 h6 bg-black-40 dtc v-mid bg-tm-green tc">' +
+										'<div class="w-100 dt center white f1 ttu">{{ filedetaildata.info.extension }}</div>' +
+									'</div>' +
+								'</div>' +
+								'<div class="w-50 pa3 lh-copy f7 relative">' +
+									'<div class="black-30 mt3 mb1">Name</div><div class="b">{{ filedetaildata.name}}</div>' +
+									'<div class="black-30 mt3 mb1">URL</div><div class="b">{{ filedetaildata.url}}</div>' +
+									'<div class="flex flex-wrap item-start">' + 
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Size</div><div class="b">{{ getSize(filedetaildata.bytes) }}</div>' +
+										'</div>' +
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Type</div><div class="b">{{ filedetaildata.info.extension }}</div>' +
+										'</div>' +
+										'<div class="w-50">' +
+											'<div class="black-30 mt3 mb1">Date</div><div class="b">{{ getDate(filedetaildata.timestamp) }}</div>' +
+										'</div>' +
+									'</div>' +
+									'<div class="absolute w-90 bottom-0 flex justify-between">' +
+										'<button @click.prevent="selectFile(filedetaildata)" class="w-50 mr1 pa2 link bn bg-light-gray hover-bg-tm-green hover-white"><svg class="icon icon-check baseline"><use xlink:href="#icon-check"></use></svg> select</button>' +
+										'<button @click.prevent="deleteFile(filedetaildata, detailindex)" class="w-50 ml1 pa2 link bn bg-light-gray hover-bg-tm-red hover-white"><svg class="icon icon-trash-o baseline"><use xlink:href="#icon-trash-o"></use></svg> delete</button>' +
+									'</div>' +
+								'</div>' +
+							'</div>' +
+							'<button class="f7 link br0 ba ph3 pv2 dim white bn bg-tm-red absolute top-0 right-0" @click.prevent="showFiles()">close details</button>' +
+							'<div class="pa3">' +
+								'<h4>File used in:</h4>' +
+								'<ul class="ma0 pa0" v-if="filedetaildata.pages && filedetaildata.pages.length > 0">' + 
+									'<li class="list pa1" v-for="page in filedetaildata.pages">' + 
+										'<a class="link tm-red" :href="baseurl + page">{{ page }}</a>' + 
+									'</li>' + 
+								'</ul>' +
+								'<div v-else>No pages found.</div>'+
+							'</div>' +
+						'</div>' +
+					'</div>' +
+				  '</div>' +
+			  '</div>',
+	data: function(){
+		return {
+			imagedata: false,
+			showimages: true,
+			imagedetaildata: false,
+			showimagedetails: false,
+			filedata: false,
+			showfiles: false,
+			filedetaildata: false,
+			showfiledetails: false,
+			detailindex: false,
+			load: false,
+			baseurl: false,
+			search: '',
+			errors: false,
+		}
+	},
+	mounted: function(){
+		
+		if(this.parentcomponent == 'files')
+		{
+			this.showFiles();
+		}
+
+		this.errors = false;
+		var self = this;
+
+        myaxios.get('/api/v1/medialib/images',{
+        	params: {
+				'url':			document.getElementById("path").value,
+				'csrf_name': 	document.getElementById("csrf_name").value,
+				'csrf_value':	document.getElementById("csrf_value").value,
+        	}
+		})
+        .then(function (response)
+        {
+       		self.imagedata = response.data.images;
+        })
+        .catch(function (error)
+        {
+           	if(error.response)
+            {
+            	self.errors = error.response.data.errors;
+            }
+        });
+	},
+    computed: {
+        filteredImages() {
+
+			var searchimages = this.search;
+            var filteredImages = {};
+            var images = this.imagedata;
+            Object.keys(images).forEach(function(key) {
+                var searchindex = key + ' ' + images[key].name;
+                if(searchindex.toLowerCase().indexOf(searchimages.toLowerCase()) !== -1)
+                {
+                    filteredImages[key] = images[key];
+                }
+            });
+            return filteredImages;
+        },
+        filteredFiles() {
+
+			var searchfiles = this.search;
+            var filteredFiles = {};
+            var files = this.filedata;
+            Object.keys(files).forEach(function(key) {
+                var searchindex = key + ' ' + files[key].name;
+                if(searchindex.toLowerCase().indexOf(searchfiles.toLowerCase()) !== -1)
+                {
+                    filteredFiles[key] = files[key];
+                }
+            });
+            return filteredFiles;
+        }        
+    },	
+	methods: {
+		isImagesActive: function()
+		{
+			if(this.showimages)
+			{
+				return 'bg-tm-green white';
+			}
+			return 'bg-light-gray black';
+		},
+		isFilesActive: function()
+		{
+			if(this.showfiles)
+			{
+				return 'bg-tm-green white';
+			}
+			return 'bg-light-gray black';
+		},
+		closemedialib: function()
+		{
+			this.$parent.showmedialib = false;
+		},
+		getBackgroundImage: function(image)
+		{
+			return 'background-image: url(' + image.src_thumb + ');width:250px';
+		},
+		showImages: function()
+		{
+			this.errors = false;
+			this.showimages = true;
+			this.showfiles = false;
+			this.showimagedetails = false;
+			this.showfiledetails = false;
+			this.imagedetaildata = false;
+			this.detailindex = false;
+		},
+		showFiles: function()
+		{
+			this.showimages = false;
+			this.showfiles = true;
+			this.showimagedetails = false;
+			this.showfiledetails = false;
+			this.imagedetaildata = false;
+			this.filedetaildata = false;
+			this.detailindex = false;
+
+			if(!this.files)
+			{
+				this.errors = false;
+				var filesself = this;
+
+		        myaxios.get('/api/v1/medialib/files',{
+		        	params: {
+						'url':			document.getElementById("path").value,
+						'csrf_name': 	document.getElementById("csrf_name").value,
+						'csrf_value':	document.getElementById("csrf_value").value,
+		        	}
+				})
+		        .then(function (response)
+		        {
+		       		filesself.filedata = response.data.files;
+		        })
+		        .catch(function (error)
+		        {
+		           	if(error.response)
+		            {
+		            	filesself.errors = error.response.data.errors;
+		            }
+		        });
+			}
+		},
+		showImageDetails: function(image,index)
+		{
+			this.errors = false;
+			this.showimages = false;
+			this.showfiles = false;
+			this.showimagedetails = true;
+			this.detailindex = index;
+			this.baseurl = myaxios.defaults.baseURL + '/tm/content/visual';
+
+			var imageself = this;
+
+	        myaxios.get('/api/v1/image',{
+	        	params: {
+					'url':			document.getElementById("path").value,
+					'name': 		image.name,
+					'csrf_name': 	document.getElementById("csrf_name").value,
+					'csrf_value':	document.getElementById("csrf_value").value,
+	        	}
+			})
+	        .then(function (response)
+	        {
+	       		imageself.imagedetaildata = response.data.image;
+	        })
+	        .catch(function (error)
+	        {
+	           	if(error.response)
+	            {
+		            imageself.errors = error.response.data.errors;
+	            }
+	        });
+		},
+		showFileDetails: function(file,index)
+		{
+			this.errors = false;
+			this.showimages = false;
+			this.showfiles = false;
+			this.showimagedetails = false;
+			this.showfiledetails = true;
+			this.detailindex = index;
+			
+			this.baseurl = myaxios.defaults.baseURL + '/tm/content/visual';
+
+			var fileself = this;
+
+	        myaxios.get('/api/v1/file',{
+	        	params: {
+					'url':			document.getElementById("path").value,
+					'name': 		file.name,
+					'csrf_name': 	document.getElementById("csrf_name").value,
+					'csrf_value':	document.getElementById("csrf_value").value,
+	        	}
+			})
+	        .then(function (response)
+	        {
+	       		fileself.filedetaildata = response.data.file;
+	        })
+	        .catch(function (error)
+	        {
+	           	if(error.response)
+	            {
+		            fileself.errors = error.response.data.errors;
+	            }
+	        });
+		},
+		selectImage: function(image)
+		{
+			this.showImages();
+
+			if(this.parentcomponent == 'images')
+			{
+				var imgmarkdown = {target: {value: '![alt]('+ image.src_live +')' }};
+
+				this.$parent.imgfile = image.src_live;
+				this.$parent.imgpreview = image.src_live;
+				this.$parent.imgmeta = true;
+
+				this.$parent.showmedialib = false;
+
+				this.$parent.updatemarkdown(imgmarkdown);
+			}
+			if(this.parentcomponent == 'files')
+			{
+				var filemarkdown = {target: {value: '[' + image.name + '](' + image.src_live +'){.tm-download}' }};
+
+				this.$parent.filemeta = true;
+				this.$parent.filetitle = image.name;
+
+				this.$parent.showmedialib = false;
+
+				this.$parent.updatemarkdown(filemarkdown);
+			}
+		},
+		selectFile: function(file)
+		{
+			/* if image component is open */
+			if(this.parentcomponent == 'images')
+			{
+				var imgextensions = ['png','jpg', 'jpeg', 'gif', 'svg'];
+				if(imgextensions.indexOf(file.info.extension) == -1)
+				{
+					this.errors = "you cannot insert a file into an image component";
+					return;
+				}
+				var imgmarkdown = {target: {value: '![alt]('+ file.url +')' }};
+
+				this.$parent.imgfile = file.url;
+				this.$parent.imgpreview = file.url;
+				this.$parent.imgmeta = true;
+
+				this.$parent.showmedialib = false;
+
+				this.$parent.updatemarkdown(imgmarkdown);				
+			}
+			if(this.parentcomponent == 'files')
+			{
+				var filemarkdown = {target: {value: '['+ file.name +']('+ file.url +'){.tm-download file-' + file.info.extension + '}' }};
+
+				this.$parent.showmedialib = false;
+
+				this.$parent.filemeta = true;
+				this.$parent.filetitle = file.info.filename + ' (' + file.info.extension.toUpperCase() + ')';
+
+				this.$parent.updatemarkdown(filemarkdown);
+			}
+			this.showFiles();
+		},		
+		removeImage: function(index)
+		{
+			this.imagedata.splice(index,1);
+		},
+		removeFile: function(index)
+		{
+			this.filedata.splice(index,1);
+		},
+		deleteImage: function(image, index)
+		{
+			imageself = this;
+
+	        myaxios.delete('/api/v1/image',{
+	        	data: {
+					'url':			document.getElementById("path").value,
+					'name': 		image.name,
+					'index': 		index,
+					'csrf_name': 	document.getElementById("csrf_name").value,
+					'csrf_value':	document.getElementById("csrf_value").value,
+	        	}
+			})
+	        .then(function (response)
+	        {
+				imageself.showImages();
+	        	imageself.removeImage(index);
+	        })
+	        .catch(function (error)
+	        {
+	           	if(error.response)
+	            {
+		            imageself.errors = error.response.data.errors;
+	            }
+	        });
+		},
+		deleteFile: function(file, index)
+		{
+			fileself = this;
+
+	        myaxios.delete('/api/v1/file',{
+	        	data: {
+					'url':			document.getElementById("path").value,
+					'name': 		file.name,
+					'index': 		index,
+					'csrf_name': 	document.getElementById("csrf_name").value,
+					'csrf_value':	document.getElementById("csrf_value").value,
+	        	}
+			})
+	        .then(function (response)
+	        {
+		       	fileself.showFiles();
+	        	fileself.removeFile(index);
+	        })
+	        .catch(function (error)
+	        {
+	           	if(error.response)
+	            {
+		            fileself.errors = error.response.data.errors;
+	            }
+	        });
+		},
+		getDate(timestamp)
+		{
+			date = new Date(timestamp * 1000);
+			
+			datevalues = {
+			   'year': date.getFullYear(),
+			   'month': date.getMonth()+1,
+			   'day': date.getDate(),
+			   'hour': date.getHours(),
+			   'minute': date.getMinutes(),
+			   'second': date.getSeconds(),
+			};
+			return datevalues.year + '-' + datevalues.month + '-' + datevalues.day; 
+		},
+		getSize(bytes)
+		{
+		    var i = Math.floor(Math.log(bytes) / Math.log(1024)),
+		    sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+		    return (bytes / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + sizes[i];
+		},
+		isChecked: function(classname)
+		{
+			if(this.imgclass == classname)
+			{
+				return ' checked';
+			}
+		},
+	},
+})
+
+
 let activeFormats = [];
 
 for(var i = 0; i < formatConfig.length; i++)
@@ -1576,11 +2300,13 @@ let editor = new Vue({
 		}, method, url, params);
 	},
 	methods: {
-		onStart: function(evt)
+		onStart: function()
 		{
+
 		},
 		moveBlock: function(evt)
 		{
+
 			var params = {
 				'url':			document.getElementById("path").value,
 				'old_index': 	evt.oldIndex,

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

@@ -17,6 +17,7 @@
 		<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ base_url }}/system/author/img/apple-touch-icon-152x152.png" />
 		
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/normalize.css" />
+		<link rel="stylesheet" href="{{ base_url }}/system/author/css/tachyons.min.css?20200226" />
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/style.css?20200226" />
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/color-picker.min.css" />
 	</head>

+ 30 - 4
system/author/layouts/layoutBlox.twig

@@ -17,6 +17,7 @@
 		<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ base_url }}/system/author/img/apple-touch-icon-152x152.png" />
 		
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/normalize.css" />
+		<link rel="stylesheet" href="{{ base_url }}/system/author/css/tachyons.min.css" />
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/style.css?20200226" />
 		<link rel="stylesheet" href="{{ base_url }}/system/author/css/color-picker.min.css" />
 
@@ -68,14 +69,20 @@
 					<title>{{ __('FOLDER') }}</title>
 					<path d="M24 20.5v-11c0-0.828-0.672-1.5-1.5-1.5h-11c-0.828 0-1.5-0.672-1.5-1.5v-1c0-0.828-0.672-1.5-1.5-1.5h-5c-0.828 0-1.5 0.672-1.5 1.5v15c0 0.828 0.672 1.5 1.5 1.5h19c0.828 0 1.5-0.672 1.5-1.5zM26 9.5v11c0 1.922-1.578 3.5-3.5 3.5h-19c-1.922 0-3.5-1.578-3.5-3.5v-15c0-1.922 1.578-3.5 3.5-3.5h5c1.922 0 3.5 1.578 3.5 3.5v0.5h10.5c1.922 0 3.5 1.578 3.5 3.5z"></path>
 				</symbol>
-
-				
+				<symbol id="icon-upload" viewBox="0 0 26 28">
+					<title>{{ __('UPLOAD') }}</title>
+					<path d="M20 23c0-0.547-0.453-1-1-1s-1 0.453-1 1 0.453 1 1 1 1-0.453 1-1zM24 23c0-0.547-0.453-1-1-1s-1 0.453-1 1 0.453 1 1 1 1-0.453 1-1zM26 19.5v5c0 0.828-0.672 1.5-1.5 1.5h-23c-0.828 0-1.5-0.672-1.5-1.5v-5c0-0.828 0.672-1.5 1.5-1.5h6.672c0.422 1.156 1.531 2 2.828 2h4c1.297 0 2.406-0.844 2.828-2h6.672c0.828 0 1.5 0.672 1.5 1.5zM20.922 9.375c-0.156 0.375-0.516 0.625-0.922 0.625h-4v7c0 0.547-0.453 1-1 1h-4c-0.547 0-1-0.453-1-1v-7h-4c-0.406 0-0.766-0.25-0.922-0.625-0.156-0.359-0.078-0.797 0.219-1.078l7-7c0.187-0.203 0.453-0.297 0.703-0.297s0.516 0.094 0.703 0.297l7 7c0.297 0.281 0.375 0.719 0.219 1.078z"></path>
+				</symbol>				
 				<symbol id="icon-image" viewBox="0 0 32 32">
 					<title>{{ __('IMAGE') }}</title>
 					<path d="M29.996 4c0.001 0.001 0.003 0.002 0.004 0.004v23.993c-0.001 0.001-0.002 0.003-0.004 0.004h-27.993c-0.001-0.001-0.003-0.002-0.004-0.004v-23.993c0.001-0.001 0.002-0.003 0.004-0.004h27.993zM30 2h-28c-1.1 0-2 0.9-2 2v24c0 1.1 0.9 2 2 2h28c1.1 0 2-0.9 2-2v-24c0-1.1-0.9-2-2-2v0z"></path>
 					<path d="M26 9c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z"></path>
 					<path d="M28 26h-24v-4l7-12 8 10h2l7-6z"></path>
 				</symbol>
+				<symbol id="icon-paperclip" viewBox="0 0 22 28">
+					<title>{{ __('PAPERCLIP') }}</title>
+					<path d="M21.938 21.641c0 2.438-1.859 4.297-4.297 4.297-1.375 0-2.703-0.594-3.672-1.563l-12.141-12.125c-1.109-1.125-1.766-2.656-1.766-4.234 0-3.313 2.609-5.953 5.922-5.953 1.594 0 3.125 0.641 4.266 1.766l9.453 9.469c0.094 0.094 0.156 0.219 0.156 0.344 0 0.328-0.875 1.203-1.203 1.203-0.141 0-0.266-0.063-0.359-0.156l-9.469-9.484c-0.75-0.734-1.766-1.203-2.828-1.203-2.219 0-3.938 1.797-3.938 4 0 1.062 0.438 2.078 1.188 2.828l12.125 12.141c0.594 0.594 1.422 0.984 2.266 0.984 1.328 0 2.312-0.984 2.312-2.312 0-0.859-0.391-1.672-0.984-2.266l-9.078-9.078c-0.25-0.234-0.594-0.375-0.938-0.375-0.594 0-1.047 0.438-1.047 1.047 0 0.344 0.156 0.672 0.391 0.922l6.406 6.406c0.094 0.094 0.156 0.219 0.156 0.344 0 0.328-0.891 1.219-1.219 1.219-0.125 0-0.25-0.063-0.344-0.156l-6.406-6.406c-0.625-0.609-0.984-1.469-0.984-2.328 0-1.719 1.344-3.062 3.063-3.062 0.875 0 1.719 0.359 2.328 0.984l9.078 9.078c0.984 0.969 1.563 2.297 1.563 3.672z"></path>
+				</symbol>
 				<symbol id="icon-play" viewBox="0 0 32 32">
 					<title>{{ __('VIDEO') }}</title>
 					<path d="M30.662 5.003c-4.488-0.645-9.448-1.003-14.662-1.003s-10.174 0.358-14.662 1.003c-0.86 3.366-1.338 7.086-1.338 10.997s0.477 7.63 1.338 10.997c4.489 0.645 9.448 1.003 14.662 1.003s10.174-0.358 14.662-1.003c0.86-3.366 1.338-7.086 1.338-10.997s-0.477-7.63-1.338-10.997zM12 22v-12l10 6-10 6z"></path>
@@ -142,12 +149,31 @@
 					<title>{{ __('CROSS') }}</title>
 					<path d="M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"></path>
 				</symbol>
+				<symbol id="icon-trash-o" viewBox="0 0 22 28">
+					<title>{{ __('TRASH') }}</title>
+					<path d="M8 11.5v9c0 0.281-0.219 0.5-0.5 0.5h-1c-0.281 0-0.5-0.219-0.5-0.5v-9c0-0.281 0.219-0.5 0.5-0.5h1c0.281 0 0.5 0.219 0.5 0.5zM12 11.5v9c0 0.281-0.219 0.5-0.5 0.5h-1c-0.281 0-0.5-0.219-0.5-0.5v-9c0-0.281 0.219-0.5 0.5-0.5h1c0.281 0 0.5 0.219 0.5 0.5zM16 11.5v9c0 0.281-0.219 0.5-0.5 0.5h-1c-0.281 0-0.5-0.219-0.5-0.5v-9c0-0.281 0.219-0.5 0.5-0.5h1c0.281 0 0.5 0.219 0.5 0.5zM18 22.813v-14.812h-14v14.812c0 0.75 0.422 1.188 0.5 1.188h13c0.078 0 0.5-0.438 0.5-1.188zM7.5 6h7l-0.75-1.828c-0.047-0.063-0.187-0.156-0.266-0.172h-4.953c-0.094 0.016-0.219 0.109-0.266 0.172zM22 6.5v1c0 0.281-0.219 0.5-0.5 0.5h-1.5v14.812c0 1.719-1.125 3.187-2.5 3.187h-13c-1.375 0-2.5-1.406-2.5-3.125v-14.875h-1.5c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h4.828l1.094-2.609c0.313-0.766 1.25-1.391 2.078-1.391h5c0.828 0 1.766 0.625 2.078 1.391l1.094 2.609h4.828c0.281 0 0.5 0.219 0.5 0.5z"></path>
+				</symbol>
+				<symbol id="icon-info" viewBox="0 0 32 32">
+					<title>{{ __('INFO') }}</title>
+					<path d="M14 9.5c0-0.825 0.675-1.5 1.5-1.5h1c0.825 0 1.5 0.675 1.5 1.5v1c0 0.825-0.675 1.5-1.5 1.5h-1c-0.825 0-1.5-0.675-1.5-1.5v-1z"></path>
+					<path d="M20 24h-8v-2h2v-6h-2v-2h6v8h2z"></path>
+					<path d="M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 29c-7.18 0-13-5.82-13-13s5.82-13 13-13 13 5.82 13 13-5.82 13-13 13z"></path>
+				</symbol>				
 				<symbol id="icon-eye-blocked" viewBox="0 0 32 32">
-					<title>eye-blocked</title>
+					<title>{{ __('EYE_BLOCKED') }}</title>
 					<path d="M29.561 0.439c-0.586-0.586-1.535-0.586-2.121 0l-6.318 6.318c-1.623-0.492-3.342-0.757-5.122-0.757-6.979 0-13.028 4.064-16 10 1.285 2.566 3.145 4.782 5.407 6.472l-4.968 4.968c-0.586 0.586-0.586 1.535 0 2.121 0.293 0.293 0.677 0.439 1.061 0.439s0.768-0.146 1.061-0.439l27-27c0.586-0.586 0.586-1.536 0-2.121zM13 10c1.32 0 2.44 0.853 2.841 2.037l-3.804 3.804c-1.184-0.401-2.037-1.521-2.037-2.841 0-1.657 1.343-3 3-3zM3.441 16c1.197-1.891 2.79-3.498 4.67-4.697 0.122-0.078 0.246-0.154 0.371-0.228-0.311 0.854-0.482 1.776-0.482 2.737 0 1.715 0.54 3.304 1.459 4.607l-1.904 1.904c-1.639-1.151-3.038-2.621-4.114-4.323z"></path>
 					<path d="M24 13.813c0-0.849-0.133-1.667-0.378-2.434l-10.056 10.056c0.768 0.245 1.586 0.378 2.435 0.378 4.418 0 8-3.582 8-8z"></path>
 					<path d="M25.938 9.062l-2.168 2.168c0.040 0.025 0.079 0.049 0.118 0.074 1.88 1.199 3.473 2.805 4.67 4.697-1.197 1.891-2.79 3.498-4.67 4.697-2.362 1.507-5.090 2.303-7.889 2.303-1.208 0-2.403-0.149-3.561-0.439l-2.403 2.403c1.866 0.671 3.873 1.036 5.964 1.036 6.978 0 13.027-4.064 16-10-1.407-2.81-3.504-5.2-6.062-6.938z"></path>
-				</symbol>				
+				</symbol>
+				<symbol id="icon-search" viewBox="0 0 26 28">
+					<title>{{ __('SEARCH') }}</title>
+					<path d="M18 13c0-3.859-3.141-7-7-7s-7 3.141-7 7 3.141 7 7 7 7-3.141 7-7zM26 26c0 1.094-0.906 2-2 2-0.531 0-1.047-0.219-1.406-0.594l-5.359-5.344c-1.828 1.266-4.016 1.937-6.234 1.937-6.078 0-11-4.922-11-11s4.922-11 11-11 11 4.922 11 11c0 2.219-0.672 4.406-1.937 6.234l5.359 5.359c0.359 0.359 0.578 0.875 0.578 1.406z"></path>
+				</symbol>
+				<symbol id="icon-cancel-circle" viewBox="0 0 32 32">
+					<title>{{ __('CANCEL') }}</title>
+					<path d="M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 29c-7.18 0-13-5.82-13-13s5.82-13 13-13 13 5.82 13 13-5.82 13-13 13z"></path>
+					<path d="M21 8l-5 5-5-5-3 3 5 5-5 5 3 3 5-5 5 5 3-3-5-5 5-5z"></path>
+				</symbol>			
 				{{ assets.renderSvg() }}
 			</defs>
 		</svg>

+ 29 - 2
system/author/settings/system.twig

@@ -10,7 +10,7 @@
 	
 	<div class="formWrapper">
 
-		<form method="POST" action="{{ path_for('settings.save') }}">
+		<form method="POST" action="{{ path_for('settings.save') }}" enctype="multipart/form-data">
 		
 			<section id="system" class="settings">
 
@@ -63,6 +63,34 @@
 					</div><div class="medium">
 						<label for="settings[sitemap]">Google Sitemap <small>({{ __('Readonly') }})</small></label>
 						<input type="text" name="settings[sitemap]" id="sitemap" readonly value="{{ base_url }}/cache/sitemap.xml" />
+					</div><div class="medium{{ errors.settings.logo ? ' error' : '' }}">
+						<label for="settings[logo]">Logo <small>(jpg,jpeg,png,svg)</small></label>
+						<div class="flex fileinput">
+							<button class="deletefilebutton w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>
+							<input class="deletefileinput" name="settings[deletelogo]" type="hidden" value="NULL" />
+							<input class="visiblefilename w-60" type="text" disabled="disabled" value="{{ old.settings.logo ? old.settings.logo : settings.logo }}" placeholder="{{ __('CHOOSE_FILE') }}" />
+							<div class="relative w-30 pv3 tc bg-tm-green white dim pointer">
+	    						<span class="f7">{{ __('BROWSE') }}</span>
+								<input class="upload hiddenfile" type="file" name="settings[logo]" value="{{ old.settings.logo ? old.settings.logo : settings.logo }}" />
+							</div>
+						</div>
+						{% if errors.settings.logo %}
+							<span class="error">{{ errors.settings.logo | first }}</span>
+						{% endif %}
+					</div><div class="medium{{ errors.settings.favicon ? ' error' : '' }}">
+						<label for="settings[favicon]">Favicon <small>(png)</small></label>
+						<div class="flex fileinput">
+							<button class="deletefilebutton w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>
+							<input class="deletefileinput" name="settings[deletefav]" type="hidden" value="NULL" />
+							<input class="visiblefilename w-60" type="text" disabled="disabled" value="{{ old.settings.favicon ? old.settings.favicon : settings.favicon }}" placeholder="{{ __('CHOOSE_FILE') }}" />
+							<div class="relative w-30 pv3 tc bg-tm-green white dim pointer">
+	    						<span class="f7">{{ __('BROWSE') }}</span>
+								<input class="upload hiddenfile" type="file" name="settings[favicon]" value="{{ old.settings.favicon ? old.settings.favicon : settings.favicon }}" />
+							</div>
+						</div>
+						{% if errors.settings.favicon %}
+							<span class="error">{{ errors.settings.favicon | first }}</span>
+						{% endif %}
 					</div>
 					<hr>
 					<header class="headline">
@@ -102,5 +130,4 @@
 		</form>
 	
 	</div>
-	
 {% endblock %}

+ 2 - 1
system/system.php

@@ -199,8 +199,9 @@ $container['view'] = function ($container)
     $view->addExtension(new Slim\Views\TwigExtension($container['router'], $basePath));
 	$view->addExtension(new Twig_Extension_Debug());
     $view->addExtension(new Typemill\Extensions\TwigUserExtension());
-	$view->addExtension(new Typemill\Extensions\TwigMarkdownExtension());	
+	$view->addExtension(new Typemill\Extensions\TwigMarkdownExtension());
 	$view->addExtension(new Typemill\Extensions\TwigMetaExtension());	
+	$view->addExtension(new Typemill\Extensions\TwigPagelistExtension());	
 
         // i18n
 	$view->addExtension(new Typemill\Extensions\TwigLanguageExtension( $container->get('settings')['labels'] ));

+ 9 - 1
themes/typemill/cover.twig

@@ -2,7 +2,15 @@
 
 {% block content %}
 
-	<h1>{{ title }}</h1>
+	{% if logo and settings.themes.typemill.coverlogo %}
+
+		<img src="{{ logo }}" alt="logo {{title}}" class="coverlogo"/>
+	
+	{% else %}
+
+		<h1 class="coverheadline">{{ title }}</h1>
+
+	{% endif %}
 
 	<div class="lead">
 

+ 35 - 0
themes/typemill/css/style.css

@@ -578,6 +578,41 @@ cite{}
 abbr{}
 hr{}
 
+img.logo{
+	width: 100%;
+}
+img.coverlogo{
+	margin-top: 4em;
+}
+/****************************
+*  	download-commponent		*
+****************************/
+
+a.tm-download
+{
+	line-height: 35px;
+	margin-left: 40px;
+}
+a.tm-download::before{
+	content: '\2193';
+	position: absolute;
+	margin-left: -40px;
+	width: 30px;
+	height: 30px;
+	line-height: 30px;
+	font-family: "Comic Sans MS",cursive,sans-serif;	
+	border: 2px solid #e0474c;
+	border-radius: 50%;
+	text-align: center;
+	text-decoration: underline;
+}
+a.tm-download:hover::before{
+	text-decoration:underline;
+	color: #fff;
+	background: #e0474c;
+}
+
+
 /************************
 *  	TABLE OF CONTENTS 	*
 ************************/

BIN
themes/typemill/img/apple-touch-icon-144x144.png


BIN
themes/typemill/img/apple-touch-icon-152x152.png


BIN
themes/typemill/img/favicon-16x16.png


BIN
themes/typemill/img/favicon-32x32.png


BIN
themes/typemill/img/favicon.ico


BIN
themes/typemill/img/mstile-144x144.png


+ 15 - 8
themes/typemill/partials/layout.twig

@@ -10,14 +10,15 @@
 		<meta name="description" content="{{ metatabs.meta.description }}" />
 		<meta name="author" content="{{ settings.author }}" />
 		<meta name="generator" content="TYPEMILL" />
-		<meta name="msapplication-TileColor" content="#F9F8F6" />
-		<meta name="msapplication-TileImage" content="{{ base_url }}/themes/typemill/img/mstile-144x144.png" />
 
-		<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ base_url }}/themes/typemill/img/apple-touch-icon-144x144.png" />
-		<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ base_url }}/themes/typemill/img/apple-touch-icon-152x152.png" />
-		<link rel="icon" type="image/png" href="{{ base_url }}/themes/typemill/img/favicon-32x32.png" sizes="32x32" />
-		<link rel="icon" type="image/png" href="{{ base_url }}/themes/typemill/img/favicon-16x16.png" sizes="16x16" />
-		
+		{% if favicon %}
+			<meta name="msapplication-TileColor" content="#F9F8F6" />
+			<meta name="msapplication-TileImage" content="{{ base_url }}/media/files/favicon-144.png" />
+			<link rel="icon" type="image/png" href="{{ base_url }}/media/files/favicon-16.png" sizes="16x16" />
+			<link rel="icon" type="image/png" href="{{ base_url }}/media/files/favicon-32.png" sizes="32x32" />
+			<link rel="apple-touch-icon" sizes="180x180" href="{{ base_url }}/media/files/favicon-180.png" />
+		{% endif %}
+	
 		<link rel="canonical" href="{{ item.urlAbs }}" />
 		
 		<meta property="og:site_name" content="{{ settings.title }}">
@@ -41,7 +42,13 @@
 	<body>
 		<div class="main">
 			<header>
-				<p><a href="{{ base_url }}">{{ settings.title }}</a></p>
+				<p><a href="{{ base_url }}">
+					{% if logo %}
+						<img src="{{ logo }}" class="logo"/>
+					{% else %}
+						{{ settings.title }}
+					{% endif %}
+				</a></p>
 			</header>
 			
 			<article class="{{ item.elementType == 'file' ? 'page' : 'chapter' }}">

+ 7 - 6
themes/typemill/partials/layoutCover.twig

@@ -10,13 +10,14 @@
 		<meta name="description" content="{{ metatabs.meta.description }}" />
 		<meta name="author" content="{{ settings.author }}" />
 		<meta name="generator" content="TYPEMILL" />
-		<meta name="msapplication-TileColor" content="#F9F8F6" />
-		<meta name="msapplication-TileImage" content="{{ base_url }}/themes/typemill/img/mstile-144x144.png" />
 
-		<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ base_url }}/themes/typemill/img/apple-touch-icon-144x144.png" />
-		<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ base_url }}/themes/typemill/img/apple-touch-icon-152x152.png" />
-		<link rel="icon" type="image/png" href="{{ base_url }}/themes/typemill/img/favicon-32x32.png" sizes="32x32" />
-		<link rel="icon" type="image/png" href="{{ base_url }}/themes/typemill/img/favicon-16x16.png" sizes="16x16" />
+		{% if favicon %}
+			<meta name="msapplication-TileColor" content="#F9F8F6" />
+			<meta name="msapplication-TileImage" content="{{ base_url }}/media/files/favicon-144.png" />
+			<link rel="icon" type="image/png" href="{{ base_url }}/media/files/favicon-16.png" sizes="16x16" />
+			<link rel="icon" type="image/png" href="{{ base_url }}/media/files/favicon-32.png" sizes="32x32" />
+			<link rel="apple-touch-icon" sizes="180x180" href="{{ base_url }}/media/files/favicon-180.png" />
+		{% endif %}
 		
 		<link rel="canonical" href="{{ item.urlAbs }}" />
 		

+ 6 - 1
themes/typemill/typemill.yaml

@@ -1,5 +1,5 @@
 name: Typemill Theme
-version: 1.2.2
+version: 1.2.3
 description: The standard theme for Typemill. Responsive, minimal and without any dependencies. It uses the system fonts Calibri and Helvetica. No JavaScript is used. 
 author: Sebastian Schürmanns
 homepage: https://typemill.net
@@ -27,6 +27,11 @@ forms:
       label: Different Design for Startpage
       checkboxlabel: Activate Special Startpage-Design
 
+    coverlogo:
+      type: checkbox
+      label: Logo on startpage
+      checkboxlabel: Show logo instead of title on startpage
+
     start:
       type: text
       label: Label for Start Button