1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-06 14:16:46 +02:00

Finish 2.16.0 with media library

This commit is contained in:
trendschau
2025-04-15 22:16:23 +02:00
parent 86b5fd432a
commit 18ef42e62a
42 changed files with 1020 additions and 488 deletions

12
.gitignore vendored
View File

@@ -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
View File

@@ -1 +1 @@
licenseupdate: 1743542694
licenseupdate: 1744746578

View File

@@ -1,8 +0,0 @@
meta:
author: Sebastian
created: '2024-09-10'
time: 12-43-18
navtitle: null
modified: '2024-08-01'
title: ''
description: ''

View File

@@ -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.
![](media/live/chatgpt-typemill-dummy-wide.webp){.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
View 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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

0
media/tmp/.gitkeep Normal file
View File

View 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);

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -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)

View File

@@ -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 = '![](' + media + ')';
}
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. 100200ms
});
}
},
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)

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View 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: {

View File

@@ -6,6 +6,8 @@ language: 'en'
langattr: 'en'
editor: 'visual'
storage: '\Typemill\Models\Storage'
maxfileuploads: 20
maximageuploads: 10
formats:
- 'markdown'
- 'headline'