Finish 2.16.0 with media library
12
.gitignore
vendored
@@ -1,17 +1,5 @@
|
||||
cache/sitemap.xml
|
||||
cache/timer.yaml
|
||||
content/index.yaml
|
||||
content/00-welcome/index.yaml
|
||||
content/00-welcome/00-setup-your-website.yaml
|
||||
content/00-welcome/01-write-content.yaml
|
||||
content/00-welcome/02-manage-access.yaml
|
||||
content/00-welcome/03-get-help.yaml
|
||||
content/00-welcome/04-markdown-test.yaml
|
||||
content/01-cyanine-theme/index.yaml
|
||||
content/01-cyanine-theme/00-landingpage.yaml
|
||||
content/01-cyanine-theme/01-colors-and-fonts.yaml
|
||||
content/01-cyanine-theme/02-footer.yaml
|
||||
content/01-cyanine-theme/03-content-elements.yaml
|
||||
system/vendor
|
||||
cypress
|
||||
data/navigation
|
||||
|
2
cache/timer.yaml
vendored
@@ -1 +1 @@
|
||||
licenseupdate: 1743542694
|
||||
licenseupdate: 1744746578
|
||||
|
@@ -1,8 +0,0 @@
|
||||
meta:
|
||||
author: Sebastian
|
||||
created: '2024-09-10'
|
||||
time: 12-43-18
|
||||
navtitle: null
|
||||
modified: '2024-08-01'
|
||||
title: ''
|
||||
description: ''
|
@@ -10,5 +10,8 @@ The content is organized in blocks, and you can move each content block up and d
|
||||
|
||||
You can add all kinds of content like tables, quotes, images, files, an automatic table of contents (TOC), or YouTube videos. There are also plugins to embed media from other platforms or to use selected HTML tags in content.
|
||||
|
||||
{.center loading="lazy" width="820" height="470"}
|
||||
*Just a dummy image*
|
||||
|
||||
If you are a developer, you can write plugins and integrate nearly everything into the editor with `{::]` shortcodes.
|
||||
|
||||
|
11
content/index.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
meta:
|
||||
navtitle: Home
|
||||
title: Typemill
|
||||
description: 'The open-source flat-file cms for text-driven websites. Create handbooks, documentations, manuals, web-novels, traditional websites, and more.'
|
||||
owner: typemill
|
||||
author: typemill
|
||||
modified: '2024-04-25'
|
||||
created: '2024-03-19'
|
||||
time: 17-56-00
|
||||
hide: false
|
||||
noindex: false
|
@@ -8,3 +8,4 @@
|
||||
127.0.0.1;2025-02-27 19:23:07;login: wrong password
|
||||
127.0.0.1;2025-02-27 19:25:24;login: invalid data
|
||||
127.0.0.1;2025-02-27 20:14:02;login: wrong password
|
||||
127.0.0.1;2025-04-13 02:23:28;login: invalid data
|
||||
|
BIN
media/live/chatgpt-typemill-dummy-wide.webp
Normal file
After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB |
BIN
media/original/chatgpt-typemill-dummy-wide.webp
Normal file
After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 32 KiB |
BIN
media/thumbs/chatgpt-typemill-dummy-wide.webp
Normal file
After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.6 KiB |
0
media/tmp/.gitkeep
Normal file
@@ -176,11 +176,12 @@ class ControllerApiFile extends Controller
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
# 20 MB (1 byte * 1024 * 1024 * 20 (for 20 MB))
|
||||
if ($size > 20971520)
|
||||
$maxsizeMB = (isset($this->settings['maxfileuploads']) && is_numeric($this->settings['maxfileuploads'])) ? $this->settings['maxfileuploads'] : 20;
|
||||
$maxsizeBytes = $maxsizeMB * 1024 * 1024;
|
||||
if ($size > $maxsizeBytes)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('File is bigger than 20MB.')
|
||||
'message' => Translations::translate('File is bigger than '. $maxsizeMB . 'MB.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
@@ -226,8 +227,8 @@ class ControllerApiFile extends Controller
|
||||
# if the previous check of the mtype with the base64 string failed, then do it now again with the temporary file
|
||||
if(!$mtype)
|
||||
{
|
||||
$filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']);
|
||||
$filePath = str_replace('/', DIRECTORY_SEPARATOR, $filePath);
|
||||
$filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']);
|
||||
$filePath = str_replace('/', DIRECTORY_SEPARATOR, $filePath);
|
||||
$fullPath = $this->settings['rootPath'] . DIRECTORY_SEPARATOR . $filePath;
|
||||
$finfo = finfo_open( FILEINFO_MIME_TYPE );
|
||||
$mtype = @finfo_file( $finfo, $fullPath );
|
||||
@@ -245,6 +246,30 @@ class ControllerApiFile extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($params['publish']) && $params['publish'] == true)
|
||||
{
|
||||
$storage = new StorageWrapper('\Typemill\Models\Storage');
|
||||
$result = $storage->publishFile($fileinfo['name'] . '.' . $fileinfo['extension']);
|
||||
|
||||
if(!$result)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('We got an error while publishing the file.'),
|
||||
'fullerrors' => $storage->getError()
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('File saved successfully'),
|
||||
'fileinfo' => $fileinfo,
|
||||
'path' => $result,
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
$filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']);
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
@@ -276,7 +301,8 @@ class ControllerApiFile extends Controller
|
||||
if(!$result)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => $storage->getError()
|
||||
'message' => Translations::translate('We got an error while publishing the file.'),
|
||||
'fullerrors' => $storage->getError()
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
|
||||
|
@@ -119,7 +119,6 @@ class ControllerApiImage extends Controller
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
|
||||
public function saveImage(Request $request, Response $response, $args)
|
||||
{
|
||||
$params = $request->getParsedBody();
|
||||
@@ -141,6 +140,18 @@ class ControllerApiImage extends Controller
|
||||
}
|
||||
|
||||
# prepare the image
|
||||
$size = (int) (strlen(rtrim($params['image'], '=')) * 3 / 4);
|
||||
$maxsizeMB = (isset($this->settings['maximageuploads']) && is_numeric($this->settings['maximageuploads'])) ? $this->settings['maximageuploads'] : 20;
|
||||
$maxsizeBytes = $maxsizeMB * 1024 * 1024;
|
||||
if ($size > $maxsizeBytes)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Image is bigger than ' . $maxsizeMB . 'MB.')
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
|
||||
}
|
||||
|
||||
if(!$media->prepareImage($params['image'], $params['name']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
@@ -156,8 +167,14 @@ class ControllerApiImage extends Controller
|
||||
$uniqueImageName = $storage->createUniqueImageName($media->getFilename(), $media->getExtension());
|
||||
$media->setFilename($uniqueImageName);
|
||||
|
||||
# check if images should be transformed to webp
|
||||
if(!isset($params['keepformat']) && $this->settingActive('convertwebp'))
|
||||
{
|
||||
$media->setExtension('webp');
|
||||
$media->convertOriginal();
|
||||
}
|
||||
# store the original image
|
||||
if(!$media->storeOriginalToTmp())
|
||||
elseif(!$media->storeOriginalToTmp())
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => $media->errors[0],
|
||||
@@ -188,12 +205,6 @@ class ControllerApiImage extends Controller
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
|
||||
}
|
||||
|
||||
# for all other image types, check if they should be transformed to webp
|
||||
if(!isset($params['keepformat']) && $this->settingActive('convertwebp'))
|
||||
{
|
||||
$media->setExtension('webp');
|
||||
}
|
||||
|
||||
if(!$media->storeRenditionsToTmp($this->settings['images']))
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
@@ -204,14 +215,35 @@ class ControllerApiImage extends Controller
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
|
||||
}
|
||||
|
||||
if(isset($params['publish']) && $params['publish'] === true)
|
||||
{
|
||||
$result = $storage->publishImage($media->getFullName());
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Image saved successfully'),
|
||||
'name' => 'media/tmp/' . $media->getFullName(),
|
||||
]));
|
||||
if(!$result)
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => $storage->getError()
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Image saved successfully'),
|
||||
'path' => $result,
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
else
|
||||
{
|
||||
$response->getBody()->write(json_encode([
|
||||
'message' => Translations::translate('Image saved successfully'),
|
||||
'name' => 'media/tmp/' . $media->getFullName(),
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
||||
|
||||
public function publishImage(Request $request, Response $response, $args)
|
||||
|
@@ -358,6 +358,7 @@ class ControllerApiKixote extends Controller
|
||||
];
|
||||
|
||||
$apiservice = new ApiCalls();
|
||||
$apiservice->setTimeout(30);
|
||||
$apiResponse = $apiservice->makePostCall($url, $postdata, $authHeader);
|
||||
|
||||
if (!$apiResponse)
|
||||
@@ -423,6 +424,7 @@ class ControllerApiKixote extends Controller
|
||||
];
|
||||
|
||||
$apiservice = new ApiCalls();
|
||||
$apiservice->setTimeout(30);
|
||||
$apiResponse = $apiservice->makePostCall($url, $postdata, $headers);
|
||||
|
||||
if (!$apiResponse) {
|
||||
|
@@ -6,11 +6,18 @@ class ApiCalls
|
||||
{
|
||||
private $error = null;
|
||||
|
||||
private $timeout = 5;
|
||||
|
||||
public function getError()
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
public function setTimeout(int $timeout)
|
||||
{
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
public function makePostCall(string $url, array $data, $authHeader = '')
|
||||
{
|
||||
if (in_array('curl', get_loaded_extensions()))
|
||||
@@ -47,7 +54,7 @@ class ApiCalls
|
||||
curl_setopt($curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
if ($method === 'POST' && $data)
|
||||
{
|
||||
|
@@ -12,6 +12,8 @@ class Media
|
||||
{
|
||||
public $errors = [];
|
||||
|
||||
public $filesize = false;
|
||||
|
||||
protected $basepath = false;
|
||||
|
||||
protected $tmpFolder = false;
|
||||
@@ -30,6 +32,8 @@ class Media
|
||||
|
||||
protected $resizable = true;
|
||||
|
||||
protected $convertorig = false;
|
||||
|
||||
protected $sizes = [];
|
||||
|
||||
public function __construct()
|
||||
@@ -118,6 +122,11 @@ class Media
|
||||
return $this->extension;
|
||||
}
|
||||
|
||||
public function convertOriginal()
|
||||
{
|
||||
$this->convertorig = true;
|
||||
}
|
||||
|
||||
public function getFiletype()
|
||||
{
|
||||
return $this->filetype;
|
||||
@@ -218,7 +227,6 @@ class Media
|
||||
$this->filedata = $sanitized;
|
||||
}
|
||||
|
||||
|
||||
$fullpath = $this->getFullPath();
|
||||
|
||||
if($this->filedata !== false && file_put_contents($fullpath, $this->filedata))
|
||||
@@ -273,7 +281,7 @@ class Media
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
public function storeRenditionsToTmp($sizes)
|
||||
@@ -283,6 +291,12 @@ class Media
|
||||
|
||||
$originalsize = $this->getImageSize($image);
|
||||
|
||||
# if images converted to webp, then also store original as webp
|
||||
if($this->convertorig)
|
||||
{
|
||||
$sizes['original'] = $originalsize;
|
||||
}
|
||||
|
||||
foreach($sizes as $destinationfolder => $desiredsize)
|
||||
{
|
||||
$desiredsize = $this->calculateSize($originalsize, $desiredsize);
|
||||
@@ -432,11 +446,19 @@ class Media
|
||||
public function resizeImage($image, array $desired, array $original)
|
||||
{
|
||||
# resize
|
||||
$ratio = max($desired['width']/$original['width'], $desired['height']/$original['height']);
|
||||
$h = $desired['height'] / $ratio;
|
||||
$x = ($original['width'] - $desired['width'] / $ratio) / 2;
|
||||
$y = ($original['height'] - $desired['height'] / $ratio) / 2;
|
||||
$ratio = max(
|
||||
$desired['width']/$original['width'],
|
||||
$desired['height']/$original['height']
|
||||
);
|
||||
|
||||
# prevent upscaling
|
||||
$ratio = ($ratio > 1) ? 1 : $ratio;
|
||||
|
||||
$w = $desired['width'] / $ratio;
|
||||
$h = $desired['height'] / $ratio;
|
||||
|
||||
$x = ($original['width'] - $w) / 2;
|
||||
$y = ($original['height'] - $h) / 2;
|
||||
|
||||
$resizedImage = imagecreatetruecolor($desired['width'], $desired['height']);
|
||||
|
||||
@@ -456,7 +478,7 @@ class Media
|
||||
public function saveResizedImage($resizedImage, string $destinationfolder, string $extension)
|
||||
{
|
||||
# use method in storage class???
|
||||
$destinationfolder = strtoupper($destinationfolder);
|
||||
$destinationfolder = strtoupper($destinationfolder);
|
||||
|
||||
switch($extension)
|
||||
{
|
||||
@@ -467,11 +489,11 @@ class Media
|
||||
$storedImage = imagegif( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.gif' );
|
||||
break;
|
||||
case "webp":
|
||||
$storedImage = imagewebp( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.webp', 80);
|
||||
$storedImage = imagewebp( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.webp', 95);
|
||||
break;
|
||||
case "jpg":
|
||||
case "jpeg":
|
||||
$storedImage = imagejpeg( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $extension, 80);
|
||||
$storedImage = imagejpeg( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $extension, 90);
|
||||
break;
|
||||
default:
|
||||
$storedImage = false;
|
||||
|
@@ -697,27 +697,29 @@ class Storage
|
||||
if(!$imageinfo && pathinfo($this->liveFolder . $name, PATHINFO_EXTENSION) == 'svg')
|
||||
{
|
||||
$imagedetails = [
|
||||
'name' => $name,
|
||||
'timestamp' => filemtime($this->liveFolder . $name),
|
||||
'bytes' => filesize($this->liveFolder . $name),
|
||||
'width' => '---',
|
||||
'height' => '---',
|
||||
'type' => 'svg',
|
||||
'src_thumb' => 'media/thumbs/' . $name,
|
||||
'src_live' => 'media/live/' . $name,
|
||||
'name' => $name,
|
||||
'timestamp' => filemtime($this->liveFolder . $name),
|
||||
'bytes' => filesize($this->liveFolder . $name),
|
||||
'width' => '---',
|
||||
'height' => '---',
|
||||
'type' => 'svg',
|
||||
'src_thumb' => 'media/thumbs/' . $name,
|
||||
'src_live' => 'media/live/' . $name,
|
||||
'src_original' => 'media/original/' . $name,
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$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,
|
||||
'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,
|
||||
'src_original' => 'media/original/' . $name,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -727,6 +729,40 @@ class Storage
|
||||
return false;
|
||||
}
|
||||
|
||||
# COPIED FROM EBOOK PLUGIN, find a ssolution
|
||||
private function getOriginalImage($basepath, $imageMD)
|
||||
{
|
||||
/* REWRITE THIS: if original is requrested and not present, rewrite it to webp */
|
||||
$tryExtensions = ['webp', 'png', 'jpg', 'jpeg', 'gif'];
|
||||
|
||||
# Extract image URL from markdown
|
||||
if (preg_match('/!\[.*?\]\((media\/live\/(.*?))\)/', $imageMD, $matches))
|
||||
{
|
||||
$origRelativePath = $matches[1]; // media/live/ps-1.webp
|
||||
$filename = $matches[2]; // ps-1.webp
|
||||
|
||||
# Generate the original folder path
|
||||
$originalFolder = 'media/original/';
|
||||
$originalBasePath = $basepath . '/' . $originalFolder . pathinfo($filename, PATHINFO_FILENAME);
|
||||
|
||||
# Check if the image exists in 'media/original/' with any extension
|
||||
foreach ($tryExtensions as $ext)
|
||||
{
|
||||
$newFile = $originalBasePath . '.' . $ext;
|
||||
if (file_exists($newFile))
|
||||
{
|
||||
# Replace 'media/live/' with 'media/original/' and use the correct extension
|
||||
$newRelativePath = preg_replace('/media\/live\//', $originalFolder, $origRelativePath);
|
||||
$newRelativePath = preg_replace('/\.\w+$/', '.' . $ext, $newRelativePath);
|
||||
return str_replace($origRelativePath, $newRelativePath, $imageMD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Return the original markdown if no match found
|
||||
return $imageMD;
|
||||
}
|
||||
|
||||
public function storeCustomImage($image, $extension, $imageName)
|
||||
{
|
||||
switch($extension)
|
||||
|
@@ -112,13 +112,6 @@
|
||||
.fade-enter-from{
|
||||
opacity: 0;
|
||||
}
|
||||
.list-enter-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.list-enter-from{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-item {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
@@ -131,15 +124,17 @@
|
||||
}
|
||||
|
||||
/* enter or fade items in list with transition group */
|
||||
.list-enter-active, .list-leave-active {
|
||||
transition: all 0.2s;
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
position: absolute; /* avoids content shifts */
|
||||
}
|
||||
.list-enter, .list-leave-to {
|
||||
.list-enter,
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.accordion-enter-active, .accordion-leave-active {
|
||||
transition: max-height 0.5s ease, padding 0.5s ease;
|
||||
overflow: hidden;
|
||||
|
@@ -946,6 +946,10 @@ video {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.h-40 {
|
||||
height: 10rem;
|
||||
}
|
||||
@@ -1034,10 +1038,18 @@ video {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.w-3\/5 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.w-4\/5 {
|
||||
width: 80%;
|
||||
}
|
||||
@@ -1074,6 +1086,10 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-half {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.max-w-4xl {
|
||||
max-width: 56rem;
|
||||
}
|
||||
@@ -2427,10 +2443,6 @@ video {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.lg\:w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.lg\:w-3\/4 {
|
||||
width: 75%;
|
||||
}
|
||||
|
@@ -1499,6 +1499,15 @@ bloxeditor.component('image-component', {
|
||||
</div>
|
||||
<div v-if="load" class="loadwrapper"><span class="load"></span></div>
|
||||
<div class="imgmeta p-8" v-if="imgmeta">
|
||||
<div class="flex mb-2">
|
||||
<label class="w-1/5 py-2" for="imgsrc">{{ $filters.translate('Source') }}: </label>
|
||||
<input class="w-3/5 p-2 bg-stone-200 text-stone-900" name="imgsrc" type="text" placeholder="alt" readonly v-model="imgfile" max="100" />
|
||||
<button
|
||||
v-if = "hasSwitchPath()"
|
||||
@click = "switchquality"
|
||||
class = "w-1/5 bg-stone-600 hover:bg-stone-900 text-white px-2 py-3 text-center cursor-pointer transition duration-100"
|
||||
>switch quality</button>
|
||||
</div>
|
||||
<div class="flex mb-2">
|
||||
<label class="w-1/5 py-2" for="imgalt">{{ $filters.translate('Alt-Text') }}: </label>
|
||||
<input class="w-4/5 p-2 bg-stone-200 text-stone-900" name="imgalt" type="text" placeholder="alt" @input="createmarkdown" v-model="imgalt" max="100" />
|
||||
@@ -1528,19 +1537,13 @@ bloxeditor.component('image-component', {
|
||||
<input class="w-2/5 p-2 mr-1 bg-stone-200 text-stone-900" title="imgwidth" type="text" :placeholder="originalwidth" v-model="imgwidth" @input="changewidth" max="6" />
|
||||
<input class="w-2/5 p-2 ml-1 bg-stone-200 text-stone-900" title="imgheight" type="text" :placeholder="originalheight" v-model="imgheight" @input="changeheight" max="6" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label v-if="showresize" for="saveoriginal" class="flex w-full">
|
||||
<span class="w-1/5">{{ $filters.translate('Do not resize') }}:</span>
|
||||
<input type="checkbox" class="w-6 h-6" name="saveoriginal" v-model="noresize" @change="createmarkdown" />
|
||||
</label>
|
||||
</div>
|
||||
<input title="imgid" type="hidden" placeholder="id" v-model="imgid" @input="createmarkdown" max="140" />
|
||||
</div></div>`,
|
||||
data: function(){
|
||||
return {
|
||||
compmarkdown: '',
|
||||
saveimage: false,
|
||||
maxsize: 5, // megabyte
|
||||
maxsize: 10, // megabyte
|
||||
imgpreview: '',
|
||||
showmedialib: false,
|
||||
load: false,
|
||||
@@ -1558,7 +1561,6 @@ bloxeditor.component('image-component', {
|
||||
imgloading: 'lazy',
|
||||
imgattr: '',
|
||||
imgfile: '',
|
||||
showresize: true,
|
||||
noresize: false,
|
||||
newblock: true,
|
||||
}
|
||||
@@ -1567,6 +1569,12 @@ bloxeditor.component('image-component', {
|
||||
|
||||
eventBus.$on('beforeSave', this.beforeSave );
|
||||
|
||||
const maxsize = parseFloat(data?.settings?.maximageuploads);
|
||||
if(!isNaN(maxsize) && maxsize > 0)
|
||||
{
|
||||
this.maxsize = maxsize;
|
||||
}
|
||||
|
||||
this.$refs.markdown.focus();
|
||||
|
||||
if(this.markdown)
|
||||
@@ -1687,6 +1695,29 @@ bloxeditor.component('image-component', {
|
||||
{
|
||||
this.$emit('updateMarkdownEvent', event.target.value);
|
||||
},
|
||||
hasSwitchPath()
|
||||
{
|
||||
if (this.imgfile.startsWith('media/live') || this.imgfile.startsWith('media/original'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
switchquality()
|
||||
{
|
||||
if (this.imgfile.startsWith('media/live'))
|
||||
{
|
||||
this.imgfile = this.imgfile.replace('media/live', 'media/original');
|
||||
}
|
||||
else if (this.imgfile.startsWith('media/original'))
|
||||
{
|
||||
this.imgfile = this.imgfile.replace('media/original', 'media/live');
|
||||
}
|
||||
this.imgpreview = data.urlinfo.baseurl + '/' + this.imgfile;
|
||||
this.imgwidth = 0;
|
||||
this.imgheight = 0;
|
||||
this.createmarkdown();
|
||||
},
|
||||
createmarkdown()
|
||||
{
|
||||
if(this.imgpreview)
|
||||
@@ -2065,6 +2096,12 @@ bloxeditor.component('file-component', {
|
||||
|
||||
eventBus.$on('beforeSave', this.beforeSave );
|
||||
|
||||
const maxsize = parseFloat(data?.settings?.maxfileuploads);
|
||||
if(!isNaN(maxsize) && maxsize > 0)
|
||||
{
|
||||
this.maxsize = maxsize;
|
||||
}
|
||||
|
||||
this.$refs.markdown.focus();
|
||||
|
||||
if(this.markdown)
|
||||
@@ -2332,7 +2369,7 @@ bloxeditor.component('video-component', {
|
||||
<Transition name="initial" appear>
|
||||
<div v-if="showmedialib == 'files'" class="fixed top-0 left-0 right-0 bottom-0 bg-stone-100 z-50">
|
||||
<button class="w-full bg-stone-200 hover:bg-rose-500 hover:text-white p-2 transition duration-100" @click.prevent="showmedialib = false">{{ $filters.translate('close library') }}</button>
|
||||
<medialib parentcomponent="files" @addFromMedialibEvent="addFromMedialibFunction"></medialib>
|
||||
<medialib parentcomponent="videos" @addFromMedialibEvent="addFromMedialibFunction"></medialib>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="initial" appear>
|
||||
@@ -2347,6 +2384,21 @@ bloxeditor.component('video-component', {
|
||||
<use xlink:href="#icon-paperclip"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="bg-chess preview-chess w-full mb-4 flex items-center justify-center">
|
||||
<video
|
||||
v-if = "fileurl"
|
||||
controls = "true"
|
||||
:width = "width"
|
||||
:preload = "preload"
|
||||
:poster = "getPoster()"
|
||||
:key = "preload + fileurl"
|
||||
>
|
||||
<source
|
||||
:src = "baseurl + '/' + fileurl"
|
||||
:type = "getType()"
|
||||
>
|
||||
</video>
|
||||
</div>
|
||||
<div v-if="load" class="loadwrapper"><span class="load"></span></div>
|
||||
<div class="imgmeta p-8" v-if="filemeta">
|
||||
<input
|
||||
@@ -2385,7 +2437,7 @@ bloxeditor.component('video-component', {
|
||||
</div>`,
|
||||
data: function(){
|
||||
return {
|
||||
maxsize: 100, // megabyte
|
||||
maxsize: 20, // megabyte
|
||||
showmedialib: false,
|
||||
load: false,
|
||||
filemeta: false,
|
||||
@@ -2393,6 +2445,7 @@ bloxeditor.component('video-component', {
|
||||
allowedImageExtensions: ['webp', 'png', 'svg', 'jpg', 'jpeg'],
|
||||
allowedExtensions: ['mp4', 'webm', 'ogg'],
|
||||
fileurl: '',
|
||||
baseurl: '',
|
||||
width: '500',
|
||||
fileid: '',
|
||||
imageurl: '',
|
||||
@@ -2404,6 +2457,14 @@ bloxeditor.component('video-component', {
|
||||
mounted: function() {
|
||||
eventBus.$on('beforeSave', this.beforeSave);
|
||||
|
||||
this.baseurl = data.urlinfo.baseurl;
|
||||
|
||||
const maxsize = parseFloat(data?.settings?.maxfileuploads);
|
||||
if(!isNaN(maxsize) && maxsize > 0)
|
||||
{
|
||||
this.maxsize = maxsize;
|
||||
}
|
||||
|
||||
this.$refs.markdown.focus();
|
||||
|
||||
if (this.markdown)
|
||||
@@ -2436,6 +2497,25 @@ bloxeditor.component('video-component', {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getType()
|
||||
{
|
||||
if(this.fileurl)
|
||||
{
|
||||
const parts = this.fileurl.split('.');
|
||||
const extension = parts.pop().toLowerCase();
|
||||
extension.split('?')[0];
|
||||
return 'video/' + extension;
|
||||
}
|
||||
return 'video/';
|
||||
},
|
||||
getPoster()
|
||||
{
|
||||
if(this.imageurl)
|
||||
{
|
||||
return this.baseurl + '/' + this.imageurl;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
addFromMedialibFunction(file)
|
||||
{
|
||||
this.showmedialib = false;
|
||||
@@ -2668,7 +2748,7 @@ bloxeditor.component('audio-component', {
|
||||
<Transition name="initial" appear>
|
||||
<div v-if="showmedialib" class="fixed top-0 left-0 right-0 bottom-0 bg-stone-100 z-50">
|
||||
<button class="w-full bg-stone-200 hover:bg-rose-500 hover:text-white p-2 transition duration-100" @click.prevent="showmedialib = false">{{ $filters.translate('close library') }}</button>
|
||||
<medialib parentcomponent="files" @addFromMedialibEvent="addFromMedialibFunction"></medialib>
|
||||
<medialib parentcomponent="audios" @addFromMedialibEvent="addFromMedialibFunction"></medialib>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -2678,6 +2758,15 @@ bloxeditor.component('audio-component', {
|
||||
</svg>
|
||||
</div>
|
||||
<div v-if="load" class="loadwrapper"><span class="load"></span></div>
|
||||
<div v-if="fileurl" class="bg-yellow-500 w-full py-5 flex items-center justify-center">
|
||||
<audio
|
||||
:src = "baseurl + '/' + fileurl"
|
||||
class = "mx-auto w-3/4"
|
||||
preload = "metadata"
|
||||
controls = "true">
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="imgmeta p-8" v-if="filemeta">
|
||||
<input
|
||||
title = "fileid"
|
||||
@@ -2694,7 +2783,7 @@ bloxeditor.component('audio-component', {
|
||||
<div class="flex mb-2">
|
||||
<label class="w-1/5 py-2" for="width">{{ $filters.translate('Width') }}: </label>
|
||||
<input class="w-4/5 p-2 bg-stone-200 text-stone-900" name="width" type="text" placeholder="500" v-model="width" @input="createmarkdown" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mb-2">
|
||||
<label class="w-1/5 py-2" for="videopreload">{{ $filters.translate('Preload') }}: </label>
|
||||
<select class="w-4/5 p-2 bg-stone-200 text-stone-900" name="videopreload" v-model="preload" @change="createmarkdown">
|
||||
@@ -2707,7 +2796,7 @@ bloxeditor.component('audio-component', {
|
||||
</div>`,
|
||||
data: function(){
|
||||
return {
|
||||
maxsize: 100, // megabyte
|
||||
maxsize: 20, // megabyte
|
||||
showmedialib: false,
|
||||
load: false,
|
||||
filemeta: false,
|
||||
@@ -2718,11 +2807,18 @@ bloxeditor.component('audio-component', {
|
||||
fileid: '',
|
||||
savefile: false,
|
||||
preload: 'none',
|
||||
baseurl: data.urlinfo.baseurl,
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
eventBus.$on('beforeSave', this.beforeSave);
|
||||
|
||||
const maxsize = parseFloat(data?.settings?.maxfileuploads);
|
||||
if(!isNaN(maxsize) && maxsize > 0)
|
||||
{
|
||||
this.maxsize = maxsize;
|
||||
}
|
||||
|
||||
this.$refs.markdown.focus();
|
||||
|
||||
if (this.markdown)
|
||||
|
@@ -1,5 +1,20 @@
|
||||
const bloxeditor = Vue.createApp({
|
||||
template: `<div v-if="editorVisible" class="px-2 lg:px-12 py-8 bg-stone-50 dark:bg-stone-700 dark:text-stone-200 shadow-md mb-16">
|
||||
<div class="absolute top-0 right-0">
|
||||
<button
|
||||
@click.prevent="openmedialib()"
|
||||
class="px-2 py-2 bg-stone-50 border-b-2 border-stone-50 hover:bg-stone-200 dark:text-stone-200 dark:bg-stone-700 dark:border-stone-600 hover:dark:bg-stone-200 hover:dark:text-stone-900 transition duration-100"
|
||||
>
|
||||
<svg class="icon icon-image"><use xlink:href="#icon-image"></use></svg>
|
||||
</button>
|
||||
<Transition name="initial" appear>
|
||||
<div v-if="showmedialib" class="fixed top-0 left-0 right-0 bottom-0 bg-stone-100 z-50">
|
||||
<button class="w-full bg-stone-200 hover:bg-rose-500 hover:text-white p-2 transition duration-100" @click.prevent="showmedialib = false">{{ $filters.translate('close library') }}</button>
|
||||
<medialib parentcomponent="images" @addFromMedialibEvent="addFromMedialibFunction"></medialib>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<draggable
|
||||
v-model="content"
|
||||
@start="onStart"
|
||||
@@ -12,16 +27,20 @@ const bloxeditor = Vue.createApp({
|
||||
<content-block :element="element" :index="index" :class="{dragme: index != 0}"></content-block>
|
||||
</template>
|
||||
</draggable>
|
||||
<new-block :index="999999"></new-block>
|
||||
<new-block ref="newBlock" :index="999999"></new-block>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
showmedialib: false,
|
||||
content: data.content,
|
||||
editorVisible: true,
|
||||
dragDisabled: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
medialib: medialib
|
||||
},
|
||||
computed:
|
||||
{
|
||||
dragOptions()
|
||||
@@ -50,6 +69,50 @@ const bloxeditor = Vue.createApp({
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
openmedialib()
|
||||
{
|
||||
this.showmedialib = true;
|
||||
},
|
||||
addFromMedialibFunction(media)
|
||||
{
|
||||
componentType = false;
|
||||
|
||||
if (typeof media === 'string')
|
||||
{
|
||||
componentType = 'image-component';
|
||||
markdown = '';
|
||||
}
|
||||
else if (media.active === 'videos')
|
||||
{
|
||||
componentType = 'video-component';
|
||||
markdown = '[:video path="'+ media.url +'" width="500" preload="auto" :]';
|
||||
}
|
||||
else if (media.active === 'audios')
|
||||
{
|
||||
componentType = 'audio-component';
|
||||
markdown = '[:audio path="' + media.url + '" width="500px" preload="auto" :]';
|
||||
}
|
||||
else
|
||||
{
|
||||
componentType = 'file-component';
|
||||
markdown = '[' + media.name + '](' + media.url + '){.tm-download file-' + media.extension + '}'
|
||||
}
|
||||
|
||||
if(componentType)
|
||||
{
|
||||
this.showmedialib = false;
|
||||
this.$refs.newBlock.openWithData(componentType, markdown);
|
||||
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: document.body.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100); // small delay, e.g. 100–200ms
|
||||
});
|
||||
}
|
||||
},
|
||||
checkMove(event)
|
||||
{
|
||||
if(event.draggedContext.index == 0 || event.draggedContext.futureIndex == 0)
|
||||
@@ -454,6 +517,15 @@ bloxeditor.component('new-block',{
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
openWithData(componentType, markdown)
|
||||
{
|
||||
this.componentType = false;
|
||||
this.newblockmarkdown = '';
|
||||
this.$nextTick(() => {
|
||||
this.componentType = componentType;
|
||||
this.newblockmarkdown = markdown;
|
||||
});
|
||||
},
|
||||
setComponentType(event, componenttype)
|
||||
{
|
||||
if(this.hasUnsafedContent)
|
||||
|
@@ -792,7 +792,6 @@ app.component('component-customfields', {
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
app.component('component-image', {
|
||||
props: ['id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'css', 'errors', 'keepformat'],
|
||||
components: {
|
||||
@@ -857,6 +856,13 @@ app.component('component-image', {
|
||||
|
||||
</div>`,
|
||||
mounted: function() {
|
||||
|
||||
const maxsize = parseFloat(data?.settings?.maximageuploads);
|
||||
if(!isNaN(maxsize) && maxsize > 0)
|
||||
{
|
||||
this.maxsize = maxsize;
|
||||
}
|
||||
|
||||
if(this.hasValue(this.value))
|
||||
{
|
||||
this.imagepreview = tmaxios.defaults.baseURL + '/' + this.value;
|
||||
@@ -1091,6 +1097,11 @@ app.component('component-file', {
|
||||
|
||||
mounted: function(){
|
||||
this.getrestriction();
|
||||
const maxsize = parseFloat(data?.settings?.maxfileuploads);
|
||||
if(!isNaN(maxsize) && maxsize > 0)
|
||||
{
|
||||
this.maxsize = maxsize;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFromMedialibFunction(file)
|
||||
|
@@ -1,16 +1,18 @@
|
||||
const app = Vue.createApp({
|
||||
template: `<div>
|
||||
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
v-on:click="currentTab = tab"
|
||||
:key="tab"
|
||||
class="px-4 py-2 border-b-2 border-stone-200 hover:border-stone-700 hover:bg-stone-50 dark:text-stone-200 dark:bg-stone-700 dark:border-stone-600 hover:dark:bg-stone-200 hover:dark:text-stone-900 transition duration-100"
|
||||
:class="(tab == currentTab) ? 'bg-stone-50 border-stone-700 dark:bg-stone-200 dark:text-stone-900' : ''"
|
||||
>
|
||||
{{ $filters.translate(tab) }}
|
||||
</button>
|
||||
|
||||
template: `<div class="tabarea">
|
||||
<div class="flex justify-between">
|
||||
<div class="tabitems">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
v-on:click="currentTab = tab"
|
||||
:key="tab"
|
||||
class="px-4 py-2 border-b-2 border-stone-200 hover:border-stone-700 hover:bg-stone-50 dark:text-stone-200 dark:bg-stone-700 dark:border-stone-600 hover:dark:bg-stone-200 hover:dark:text-stone-900 transition duration-100"
|
||||
:class="(tab == currentTab) ? 'bg-stone-50 border-stone-700 dark:bg-stone-200 dark:text-stone-900' : ''"
|
||||
>
|
||||
{{ $filters.translate(tab) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
:class="css"
|
||||
:is="currentTabComponent"
|
||||
@@ -22,8 +24,7 @@ const app = Vue.createApp({
|
||||
:formData="formData[currentTab]"
|
||||
:item="item"
|
||||
v-on:saveform="saveForm">
|
||||
</component>
|
||||
|
||||
</component>
|
||||
</div>`,
|
||||
data: function () {
|
||||
return {
|
||||
@@ -38,6 +39,7 @@ const app = Vue.createApp({
|
||||
messageClass: false,
|
||||
css: "lg:px-16 px-8 lg:py-16 py-8 bg-stone-50 shadow-md mb-16",
|
||||
saved: false,
|
||||
showmedialib: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@@ -6,6 +6,8 @@ language: 'en'
|
||||
langattr: 'en'
|
||||
editor: 'visual'
|
||||
storage: '\Typemill\Models\Storage'
|
||||
maxfileuploads: 20
|
||||
maximageuploads: 10
|
||||
formats:
|
||||
- 'markdown'
|
||||
- 'headline'
|
||||
|