Compare commits

...

18 Commits

Author SHA1 Message Date
lexx 454cece1d8 auth fiux 2026-05-18 20:47:14 +03:00
lexx c49acd74de imporve import export 2026-05-18 19:56:11 +03:00
lexx 965b7e660b command rename 2026-05-18 19:09:55 +03:00
lexx 8c7f65abf5 improve fileservice 2026-05-18 19:07:47 +03:00
lexx 507d643aee sourece phpopyion 2026-05-18 19:04:57 +03:00
lexx f1a0d6a2b1 image filter generator 2026-05-18 18:38:43 +03:00
lexx f74c850e01 import fixed 2026-05-18 18:30:56 +03:00
lexx 9d6d39fe62 fixed import 2026-05-18 18:07:38 +03:00
lexx a9851847fc new dist 2026-05-15 17:07:12 +03:00
lexx d1c896acf4 rich editor 2026-05-14 23:10:07 +03:00
lexx f99aadee83 markdown insert media 2026-05-14 22:49:47 +03:00
lexx 8cd80c016f image fixes 2026-05-14 21:15:33 +03:00
lexx ef29e4d261 wip image templates 2026-05-14 19:24:25 +03:00
lexx 0725366dd5 upload iserts image 2026-05-13 19:33:27 +03:00
lexx a2bcd10607 fixed diff 2026-05-13 19:31:47 +03:00
lexx 37ed966ac3 fixed import 2026-05-13 19:19:35 +03:00
lexx 085c307137 Merge branch 'dev-lunar' of ssh://code.radical-elements.com:2727/lucent/lucent-laravel into dev-lunar 2026-05-13 17:26:42 +03:00
lexx d961d910d8 dropdown overflow 2026-05-13 17:26:35 +03:00
62 changed files with 1844 additions and 901 deletions
+3 -4
View File
@@ -9,10 +9,9 @@
"ext-imagick": "*",
"ext-pdo": "*",
"php": "^8.4",
"intervention/image": "^2.7",
"phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.6",
"staudenmeir/laravel-cte": "^1.0"
"spatie/image-optimizer": "^1.8",
"staudenmeir/laravel-cte": "^1.0",
"intervention/image": "^4.0"
},
"require-dev": {
"phpstan/phpstan": "^1.8",
Generated
+131 -340
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5d4ad76e414375923116e6a9324bd6b6",
"content-hash": "a1bf12f1e2b86bc0da8547f2f6944a86",
"packages": [
{
"name": "brick/math",
@@ -641,123 +641,6 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.9.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884",
"reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
"jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.9.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2026-03-10T16:41:02+00:00"
},
{
"name": "guzzlehttp/uri-template",
"version": "v1.0.5",
@@ -845,50 +728,32 @@
"time": "2025-08-22T14:27:06+00:00"
},
{
"name": "intervention/image",
"version": "2.7.2",
"name": "intervention/gif",
"version": "5.0.1",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "04be355f8d6734c826045d02a1079ad658322dad"
"url": "https://github.com/Intervention/gif.git",
"reference": "bb395af960deffe64d70c976b4df9283f68e762d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad",
"reference": "04be355f8d6734c826045d02a1079ad658322dad",
"url": "https://api.github.com/repos/Intervention/gif/zipball/bb395af960deffe64d70c976b4df9283f68e762d",
"reference": "bb395af960deffe64d70c976b4df9283f68e762d",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0",
"php": ">=5.4.0"
"php": "^8.3"
},
"require-dev": {
"mockery/mockery": "~0.9.2",
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^4"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
},
"providers": [
"Intervention\\Image\\ImageServiceProvider"
]
},
"branch-alias": {
"dev-master": "2.4-dev"
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
"Intervention\\Gif\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -902,19 +767,17 @@
"homepage": "https://intervention.io/"
}
],
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"description": "PHP GIF Encoder/Decoder",
"homepage": "https://github.com/intervention/gif",
"keywords": [
"animation",
"gd",
"image",
"imagick",
"laravel",
"thumbnail",
"watermark"
"gif",
"image"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/2.7.2"
"issues": "https://github.com/Intervention/gif/issues",
"source": "https://github.com/Intervention/gif/tree/5.0.1"
},
"funding": [
{
@@ -924,9 +787,89 @@
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2022-05-21T17:30:32+00:00"
"time": "2026-05-03T06:04:47+00:00"
},
{
"name": "intervention/image",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "fb795553f76afbe55c80d32b6bfe2090a6b1a0af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/fb795553f76afbe55c80d32b6bfe2090a6b1a0af",
"reference": "fb795553f76afbe55c80d32b6bfe2090a6b1a0af",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"intervention/gif": "^5",
"php": "^8.3"
},
"require-dev": {
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.0",
"slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^4"
},
"suggest": {
"ext-exif": "Recommended to be able to read EXIF data properly."
},
"type": "library",
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io"
}
],
"description": "PHP Image Processing",
"homepage": "https://image.intervention.io",
"keywords": [
"gd",
"image",
"imagick",
"resize",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/4.1.0"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
},
{
"url": "https://ko-fi.com/interventionphp",
"type": "ko_fi"
}
],
"time": "2026-05-15T06:52:36+00:00"
},
{
"name": "laravel/framework",
@@ -1446,16 +1389,16 @@
},
{
"name": "league/flysystem",
"version": "3.33.0",
"version": "3.34.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "570b8871e0ce693764434b29154c54b434905350"
"reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350",
"reference": "570b8871e0ce693764434b29154c54b434905350",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
"reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
"shasum": ""
},
"require": {
@@ -1523,9 +1466,9 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/3.33.0"
"source": "https://github.com/thephpleague/flysystem/tree/3.34.0"
},
"time": "2026-03-25T07:59:30+00:00"
"time": "2026-05-14T10:28:08+00:00"
},
{
"name": "league/flysystem-local",
@@ -2311,114 +2254,6 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-factory",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
"php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory"
},
"time": "2024-04-15T12:06:14+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
@@ -2520,50 +2355,6 @@
},
"time": "2021-10-29T13:26:27+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "ramsey/collection",
"version": "2.1.1",
@@ -2836,16 +2627,16 @@
},
{
"name": "symfony/console",
"version": "v6.4.37",
"version": "v6.4.39",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "7bbcaf3fdb1e18fa42a7f0b84a10d091c10548f5"
"reference": "c132f1215fe4aa45b70173cc00ce9a755dd31ec5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/7bbcaf3fdb1e18fa42a7f0b84a10d091c10548f5",
"reference": "7bbcaf3fdb1e18fa42a7f0b84a10d091c10548f5",
"url": "https://api.github.com/repos/symfony/console/zipball/c132f1215fe4aa45b70173cc00ce9a755dd31ec5",
"reference": "c132f1215fe4aa45b70173cc00ce9a755dd31ec5",
"shasum": ""
},
"require": {
@@ -2910,7 +2701,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.4.37"
"source": "https://github.com/symfony/console/tree/v6.4.39"
},
"funding": [
{
@@ -2930,7 +2721,7 @@
"type": "tidelift"
}
],
"time": "2026-04-13T15:27:04+00:00"
"time": "2026-05-12T06:50:03+00:00"
},
{
"name": "symfony/css-selector",
@@ -3467,16 +3258,16 @@
},
{
"name": "symfony/http-kernel",
"version": "v6.4.38",
"version": "v6.4.39",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "769c1ee766d6c327176f4e3bdaad58f521193abd"
"reference": "79329748e3d8a9cd02ec1caedbf92601b269fe39"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/769c1ee766d6c327176f4e3bdaad58f521193abd",
"reference": "769c1ee766d6c327176f4e3bdaad58f521193abd",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/79329748e3d8a9cd02ec1caedbf92601b269fe39",
"reference": "79329748e3d8a9cd02ec1caedbf92601b269fe39",
"shasum": ""
},
"require": {
@@ -3561,7 +3352,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v6.4.38"
"source": "https://github.com/symfony/http-kernel/tree/v6.4.39"
},
"funding": [
{
@@ -3581,7 +3372,7 @@
"type": "tidelift"
}
],
"time": "2026-05-06T13:04:40+00:00"
"time": "2026-05-13T17:49:58+00:00"
},
{
"name": "symfony/mailer",
@@ -4427,16 +4218,16 @@
},
{
"name": "symfony/process",
"version": "v6.4.33",
"version": "v6.4.39",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "c46e854e79b52d07666e43924a20cb6dc546644e"
"reference": "6c93071cb8c91dce5a41960d125e019e64ef6cb5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e",
"reference": "c46e854e79b52d07666e43924a20cb6dc546644e",
"url": "https://api.github.com/repos/symfony/process/zipball/6c93071cb8c91dce5a41960d125e019e64ef6cb5",
"reference": "6c93071cb8c91dce5a41960d125e019e64ef6cb5",
"shasum": ""
},
"require": {
@@ -4468,7 +4259,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v6.4.33"
"source": "https://github.com/symfony/process/tree/v6.4.39"
},
"funding": [
{
@@ -4488,7 +4279,7 @@
"type": "tidelift"
}
],
"time": "2026-01-23T16:02:12+00:00"
"time": "2026-05-11T16:53:15+00:00"
},
{
"name": "symfony/routing",
@@ -4666,16 +4457,16 @@
},
{
"name": "symfony/string",
"version": "v7.4.8",
"version": "v7.4.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "114ac57257d75df748eda23dd003878080b8e688"
"reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688",
"reference": "114ac57257d75df748eda23dd003878080b8e688",
"url": "https://api.github.com/repos/symfony/string/zipball/965f7306a43383d02c6aca1e3f3bd2f0ea5dee15",
"reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15",
"shasum": ""
},
"require": {
@@ -4733,7 +4524,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.4.8"
"source": "https://github.com/symfony/string/tree/v7.4.11"
},
"funding": [
{
@@ -4753,7 +4544,7 @@
"type": "tidelift"
}
],
"time": "2026-03-24T13:12:05+00:00"
"time": "2026-05-13T12:04:42+00:00"
},
{
"name": "symfony/translation",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,11 +1,11 @@
{
"main.js": {
"file": "assets/main-DH0OAeUr.js",
"file": "assets/main-DtbuHUXl.js",
"name": "main",
"src": "main.js",
"isEntry": true,
"css": [
"assets/main-BVNnoznq.css"
"assets/main-BadhVKbO.css"
]
}
}
+5 -10
View File
@@ -88,7 +88,7 @@ export function isEqual(db, ed) {
let isObject = (x) =>
typeof x === "object" && !Array.isArray(x) && x !== null;
let isArray = (x) => x?.constructor === Array;
let isEmpty = (x) => x === null || x === undefined || x == [];
let isEmpty = (x) => x === null || x === undefined;
const db_value = db ?? null;
const ed_value = ed ?? null;
@@ -102,11 +102,14 @@ export function isEqual(db, ed) {
}, true);
}
if (isArray(db_value)) {
if (!isArray(ed_value) || db_value.length !== ed_value.length) {
return false;
}
return db_value.reduce((c, v, i) => {
if (c === false) {
return false;
}
return isEqual(v, ed_value?.[i]);
return isEqual(v, ed_value[i]);
}, true);
}
@@ -119,14 +122,6 @@ export function isEqual(db, ed) {
}
return false;
// const ok = Object.keys,
// tx = typeof x,
// ty = typeof y;
// return x && y && tx === "object" && tx === ty
// ? ok(x).length === ok(y).length &&
// ok(x).every((key) => isEqual(x[key], y[key]))
// : x === y;
}
export function debounce(fn, delay) {
+26 -25
View File
@@ -56,23 +56,23 @@
path: '<path d="M416 288h-95.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H416c17.67 0 32-14.33 32-32S433.7 288 416 288zM544 32h-223.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H544c17.67 0 32-14.33 32-32S561.7 32 544 32zM352 416h-32c-17.67 0-32 14.33-32 32s14.33 32 32 32h32c17.67 0 31.1-14.33 31.1-32S369.7 416 352 416zM480 160h-159.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H480c17.67 0 32-14.33 32-32S497.7 160 480 160zM192.4 330.7L160 366.1V64.03C160 46.33 145.7 32 128 32S96 46.33 96 64.03v302L63.6 330.7c-6.312-6.883-14.94-10.38-23.61-10.38c-7.719 0-15.47 2.781-21.61 8.414c-13.03 11.95-13.9 32.22-1.969 45.27l87.1 96.09c12.12 13.26 35.06 13.26 47.19 0l87.1-96.09c11.94-13.05 11.06-33.31-1.969-45.27C224.6 316.8 204.4 317.7 192.4 330.7z"/>',
viewBox: "0 0 576 512",
},
"filter": {
filter: {
path: '<path d="M3.853 54.87C10.47 40.9 24.54 32 40 32H472C487.5 32 501.5 40.9 508.1 54.87C514.8 68.84 512.7 85.37 502.1 97.33L320 320.9V448C320 460.1 313.2 471.2 302.3 476.6C291.5 482 278.5 480.9 268.8 473.6L204.8 425.6C196.7 419.6 192 410.1 192 400V320.9L9.042 97.33C-.745 85.37-2.765 68.84 3.854 54.87L3.853 54.87z"/>',
viewBox: "0 0 512 512",
},
"calendar": {
calendar: {
path: '<path d="M96 32C96 14.33 110.3 0 128 0C145.7 0 160 14.33 160 32V64H288V32C288 14.33 302.3 0 320 0C337.7 0 352 14.33 352 32V64H400C426.5 64 448 85.49 448 112V160H0V112C0 85.49 21.49 64 48 64H96V32zM448 464C448 490.5 426.5 512 400 512H48C21.49 512 0 490.5 0 464V192H448V464z"/>',
viewBox: "0 0 448 512",
},
"pencil": {
pencil: {
path: '<path d="M421.7 220.3L188.5 453.4L154.6 419.5L158.1 416H112C103.2 416 96 408.8 96 400V353.9L92.51 357.4C87.78 362.2 84.31 368 82.42 374.4L59.44 452.6L137.6 429.6C143.1 427.7 149.8 424.2 154.6 419.5L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3zM492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75z"/>',
viewBox: "0 0 512 512",
},
"database": {
database: {
path: '<path d="M448 80V128C448 172.2 347.7 208 224 208C100.3 208 0 172.2 0 128V80C0 35.82 100.3 0 224 0C347.7 0 448 35.82 448 80zM393.2 214.7C413.1 207.3 433.1 197.8 448 186.1V288C448 332.2 347.7 368 224 368C100.3 368 0 332.2 0 288V186.1C14.93 197.8 34.02 207.3 54.85 214.7C99.66 230.7 159.5 240 224 240C288.5 240 348.3 230.7 393.2 214.7V214.7zM54.85 374.7C99.66 390.7 159.5 400 224 400C288.5 400 348.3 390.7 393.2 374.7C413.1 367.3 433.1 357.8 448 346.1V432C448 476.2 347.7 512 224 512C100.3 512 0 476.2 0 432V346.1C14.93 357.8 34.02 367.3 54.85 374.7z"/>',
viewBox: "0 0 448 512",
},
"dice": {
dice: {
path: '<path d="M447.1 224c0-12.56-4.781-25.13-14.35-34.76l-174.9-174.9C249.1 4.786 236.5 0 223.1 0C211.4 0 198.9 4.786 189.2 14.35L14.35 189.2C4.783 198.9-.0011 211.4-.0011 223.1c0 12.56 4.785 25.17 14.35 34.8l174.9 174.9c9.625 9.562 22.19 14.35 34.75 14.35s25.13-4.783 34.75-14.35l174.9-174.9C443.2 249.1 447.1 236.6 447.1 224zM96 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1S120 210.8 120 224S109.3 248 96 248zM224 376c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S237.3 376 224 376zM224 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1S248 210.8 248 224S237.3 248 224 248zM224 120c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S237.3 120 224 120zM352 248c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S365.3 248 352 248zM591.1 192l-118.7 0c4.418 10.27 6.604 21.25 6.604 32.23c0 20.7-7.865 41.38-23.63 57.14l-136.2 136.2v46.37C320 490.5 341.5 512 368 512h223.1c26.5 0 47.1-21.5 47.1-47.1V240C639.1 213.5 618.5 192 591.1 192zM479.1 376c-13.25 0-23.1-10.75-23.1-23.1s10.75-23.1 23.1-23.1s23.1 10.75 23.1 23.1S493.2 376 479.1 376z"/>',
viewBox: "0 0 640 512",
},
@@ -81,7 +81,7 @@
path: '<path d="M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z"/>',
viewBox: "0 0 512 512",
},
"eye": {
eye: {
path: '<path d="M279.6 160.4C282.4 160.1 285.2 160 288 160C341 160 384 202.1 384 256C384 309 341 352 288 352C234.1 352 192 309 192 256C192 253.2 192.1 250.4 192.4 247.6C201.7 252.1 212.5 256 224 256C259.3 256 288 227.3 288 192C288 180.5 284.1 169.7 279.6 160.4zM480.6 112.6C527.4 156 558.7 207.1 573.5 243.7C576.8 251.6 576.8 260.4 573.5 268.3C558.7 304 527.4 355.1 480.6 399.4C433.5 443.2 368.8 480 288 480C207.2 480 142.5 443.2 95.42 399.4C48.62 355.1 17.34 304 2.461 268.3C-.8205 260.4-.8205 251.6 2.461 243.7C17.34 207.1 48.62 156 95.42 112.6C142.5 68.84 207.2 32 288 32C368.8 32 433.5 68.84 480.6 112.6V112.6zM288 112C208.5 112 144 176.5 144 256C144 335.5 208.5 400 288 400C367.5 400 432 335.5 432 256C432 176.5 367.5 112 288 112z"/>',
viewBox: "0 0 576 512",
},
@@ -93,19 +93,19 @@
path: '<path d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/>',
viewBox: "0 0 512 512",
},
"expand": {
expand: {
path: '<path d="M128 32H32C14.31 32 0 46.31 0 64v96c0 17.69 14.31 32 32 32s32-14.31 32-32V96h64c17.69 0 32-14.31 32-32S145.7 32 128 32zM416 32h-96c-17.69 0-32 14.31-32 32s14.31 32 32 32h64v64c0 17.69 14.31 32 32 32s32-14.31 32-32V64C448 46.31 433.7 32 416 32zM128 416H64v-64c0-17.69-14.31-32-32-32s-32 14.31-32 32v96c0 17.69 14.31 32 32 32h96c17.69 0 32-14.31 32-32S145.7 416 128 416zM416 320c-17.69 0-32 14.31-32 32v64h-64c-17.69 0-32 14.31-32 32s14.31 32 32 32h96c17.69 0 32-14.31 32-32v-96C448 334.3 433.7 320 416 320z"/>',
viewBox: "0 0 448 512",
},
"compress": {
compress: {
path: '<path d="M128 320H32c-17.69 0-32 14.31-32 32s14.31 32 32 32h64v64c0 17.69 14.31 32 32 32s32-14.31 32-32v-96C160 334.3 145.7 320 128 320zM416 320h-96c-17.69 0-32 14.31-32 32v96c0 17.69 14.31 32 32 32s32-14.31 32-32v-64h64c17.69 0 32-14.31 32-32S433.7 320 416 320zM320 192h96c17.69 0 32-14.31 32-32s-14.31-32-32-32h-64V64c0-17.69-14.31-32-32-32s-32 14.31-32 32v96C288 177.7 302.3 192 320 192zM128 32C110.3 32 96 46.31 96 64v64H32C14.31 128 0 142.3 0 160s14.31 32 32 32h96c17.69 0 32-14.31 32-32V64C160 46.31 145.7 32 128 32z"/>',
viewBox: "0 0 448 512",
},
"check": {
check: {
path: '<path d="M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z"/>',
viewBox: "0 0 448 512",
},
"close": {
close: {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/>',
viewBox: "0 0 24 24",
},
@@ -113,7 +113,7 @@
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12l4-4m-4 4 4 4"/>',
viewBox: "0 0 24 24",
},
"list": {
list: {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 8h10M9 12h10M9 16h10M4.99 8H5m-.02 4h.01m0 4H5"/>',
viewBox: "0 0 24 24",
},
@@ -121,13 +121,16 @@
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6h8m-8 6h8m-8 6h8M4 16a2 2 0 1 1 3.321 1.5L4 20h5M4 5l2-1v6m-2 0h4"/>',
viewBox: "0 0 24 24",
},
"italic": {
italic: {
path: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m8.874 19 6.143-14M6 19h6.33m-.66-14H18"/>',
viewBox: "0 0 24 24",
}
},
upload: {
path: '<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/> <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z"/>',
viewBox: "0 0 16 16",
},
};
export let width = 16;
export let height = 16;
export let icon = "";
@@ -138,16 +141,15 @@
</script>
<svg
class="bi"
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox={selectedIcon.viewBox}
aria-labelledby={icon}
role="presentation"
{stroke}
{fill}
class="bi"
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox={selectedIcon.viewBox}
aria-labelledby={icon}
role="presentation"
{stroke}
{fill}
>
{@html selectedIcon.path}
</svg>
@@ -155,6 +157,5 @@
<style>
svg {
vertical-align: text-top;
}
</style>
+39 -24
View File
@@ -2,13 +2,15 @@
import { createEventDispatcher, getContext } from "svelte";
import Icon from "../common/Icon.svelte";
import FileIndex from "./FileIndex.svelte";
import Dropdown from "../common/Dropdown.svelte";
let dialogEl;
export let presetMode = false;
const dispatch = createEventDispatcher();
const channel = getContext("channel");
$: files = [];
$: selectedRecords = [];
$: selectedFiles = [];
// onMount(() => {
// load();
// });
@@ -19,7 +21,7 @@
}
dialogEl.close();
selectedRecords = [];
selectedFiles = [];
}
function load(recordId) {
@@ -31,14 +33,14 @@
.catch((error) => console.log(error));
}
function insert(e) {
function insert(e, preset) {
e.preventDefault();
dispatch("insert_files", selectedRecords);
dispatch("insert_files", { files: selectedFiles, preset: preset });
}
function replace(e) {
e.preventDefault();
dispatch("replace_files", selectedRecords);
dispatch("replace_files", selectedFiles);
}
export function open(recordId) {
@@ -49,25 +51,38 @@
<dialog bind:this={dialogEl}>
<div class="dialog-header">
<button
type="button"
class="button"
on:click={insert}
disabled={selectedRecords.length === 0}
>
Insert
</button>
<button
type="button"
class="button"
on:click={replace}
disabled={selectedRecords.length === 0}
>
Replace
</button>
{#if selectedRecords.length > 0}
{#if presetMode}
<Dropdown>
<div slot="button">Insert Preset</div>
{#each channel.imagePresets as preset}
<button
class=" dropdown-item button"
on:click={(e) => insert(e, preset)}
>{preset.name}</button
>
{/each}
</Dropdown>
{:else}
<button
type="button"
class="button"
on:click={insert}
disabled={selectedFiles.length === 0}
>
Insert
</button>
<button
type="button"
class="button"
on:click={replace}
disabled={selectedFiles.length === 0}
>
Replace
</button>
{/if}
{#if selectedFiles.length > 0}
<span class="">
{selectedRecords.length} records selected
{selectedFiles.length} records selected
</span>
{/if}
@@ -82,6 +97,6 @@
</div>
<div class="dialog-body">
<FileIndex {files} bind:selected={selectedRecords}></FileIndex>
<FileIndex {files} bind:selected={selectedFiles}></FileIndex>
</div>
</dialog>
+3 -3
View File
@@ -1,6 +1,6 @@
<script>
import Icon from "../common/Icon.svelte";
import { imgurl } from "./imageserver.js";
import { fileurl, imgurl } from "./imageserver.js";
import { getContext } from "svelte";
export let file;
@@ -11,7 +11,6 @@
let fileSide;
let fontSize;
console.log({ channel });
if (size == "large") {
imageSide = 256;
fileSide = 32;
@@ -36,7 +35,8 @@
{#if file.mime.startsWith("image")}
<!-- href={imgurl(record)} -->
<a
href="{channel.lucentUrl}/files/{file.id}"
target="_blank"
href={fileurl(channel, file)}
title={file.filename}
style="width:{imageSide}px;height:{imageSide}px"
>
+2
View File
@@ -1,5 +1,6 @@
<script>
import { createEventDispatcher, getContext } from "svelte";
import Icon from "../common/Icon.svelte";
const dispatch = createEventDispatcher();
@@ -45,6 +46,7 @@
<fieldset class="upload-button" disabled={isLoading}>
<label class="button primary btn-spinner">
<Icon icon="upload"></Icon>
<span
class="spinner-border spinner-border-sm"
role="status"
+13 -3
View File
@@ -2,7 +2,16 @@ export function imgurl(channel, file) {
if (file.mime === "image/svg+xml") {
return fileurl(channel, file);
}
return channel.filesUrl + `/thumbs/${file.path}`;
const webpPath = file.path.slice(0, file.path.lastIndexOf(".")) + ".webp";
return channel.filesUrl + `/thumbs/${webpPath}`;
}
export function presetUrl(channel, file, preset) {
if (file.mime === "image/svg+xml") {
return fileurl(channel, file);
}
const webpPath = file.path.slice(0, file.path.lastIndexOf(".")) + ".webp";
return channel.filesUrl + `/templates/${preset}/${webpPath}`;
}
export function fileurl(channel, file) {
@@ -16,13 +25,14 @@ export function htmlurl(channel, file, preset) {
if (file.width > 0) {
let presetUrl = url;
if (preset) {
presetUrl = channel.filesUrl + `/templates/${preset}/${file.path}`;
const webpPath = file.path.slice(0, file.path.lastIndexOf(".")) + ".webp";
presetUrl = channel.filesUrl + `/templates/${preset}/${webpPath}`;
}
html = `<img src="${presetUrl}" alt="${file.path}" />`;
} else if (file.mime === "image/svg+xml") {
html = `<img src="${url}" alt="${file.path}"/>`;
} else {
html = `<a href="${url}">${file.originalName}</a>`;
html = `<a href="${url}">${file.filename}</a>`;
}
return html;
+2
View File
@@ -10,6 +10,8 @@
<div class="top-nav">
{#if channel.auth == "lucent"}
<a class="top-nav-item" href="{channel.lucentUrl}/members">Members</a>
{:else}
<a href="/lunar">Store admin</a>
{/if}
{#if channel.commands.length > 0}
<Dropdown>
+13 -7
View File
@@ -8,19 +8,25 @@
import { indentWithTab } from "@codemirror/commands";
import { markdown } from "@codemirror/lang-markdown";
import { lintKeymap } from "@codemirror/lint";
import { fileurl, presetUrl } from "../files/imageserver";
let parentElement;
let codeMirrorView;
export let value;
export let editable = true;
export function insertMedia(info) {
let insertText = "";
if (info.file.width > 0) {
insertText = `![${info.file.filename}](${info.url})`;
} else {
insertText = `[${info.file.filename}](${info.originalUrl})`;
}
export function insertMedia(channel, files, presetPath) {
const insertText = files.reduce((text, aFile) => {
const url =
aFile.width > 0
? presetUrl(channel, aFile, presetPath)
: fileurl(channel, aFile);
let addTest = `![${aFile.filename}](${url})`;
return text + "\n" + addTest;
}, "");
const cursor = codeMirrorView.state.selection.main.head;
const transaction = codeMirrorView.state.update({
changes: {
+10 -9
View File
@@ -11,15 +11,16 @@
value = e.target.value;
}
export function insertMedia(info) {
if (info.file.width > 0) {
var attachment = new Trix.Attachment({ content: info.html });
editor.editor.insertAttachment(attachment);
} else {
editor.editor.insertHTML(
`<a href="${info.originalUrl}">${info.file.filename}</a>`,
);
}
export function insertMedia(html) {
console.log({ html });
var attachment = new Trix.Attachment({ content: html });
editor.editor.insertAttachment(attachment);
// if (info.file.width > 0) {
// var attachment = new Trix.Attachment({ content: html });
// editor.editor.insertAttachment(attachment);
// } else {
// editor.editor.insertHTML(html);
// }
}
onMount(() => {
+1
View File
@@ -69,6 +69,7 @@
if (isCreateMode) {
return false;
}
return !isEqual(originalContent, {
data: record.data,
status: record.status,
-3
View File
@@ -86,11 +86,8 @@
{:else if field.info.name === "rich"}
<RichEditor
bind:value={data[field.name]}
{schema}
{field}
{validationErrors}
{isCreateMode}
bind:graph
{record}
/>
{:else if field.info.name === "markdown"}
+11 -7
View File
@@ -4,6 +4,7 @@
import PreviewFile from "../previews/PreviewFile.svelte";
import FileDialog from "../../dialog/FileDialog.svelte";
import Uploader from "../../files/Uploader.svelte";
import Icon from "../../common/Icon.svelte";
export let field;
export let record;
@@ -22,7 +23,7 @@
function insertFiles(e) {
e.preventDefault();
browseModal.close();
value = [...(value ?? []), ...(e.detail ?? [])];
value = [...(value ?? []), ...(e.detail.files ?? [])];
}
function replaceFiles(e) {
@@ -32,7 +33,7 @@
}
function uploadComplete(e) {
// value = [...value, e.detail];
value = [...value, ...e.detail];
}
function openBrowseModal(e) {
@@ -41,12 +42,15 @@
}
</script>
<div class="mb-0">
<button class="button" on:click={openBrowseModal}> Browse </button>
<div
class="mb-0"
style="display: flex; align-items: start; justify-content: start; gap:6px"
>
<button class="button" on:click={openBrowseModal}>
<Icon icon="photo-film"></Icon> Browse
</button>
<div>
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
</div>
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
</div>
{#if value.length > 0}
<Sortable sortableClass="mt-3" on:update={reorder}>
@@ -1,12 +1,13 @@
<script>
import { getContext } from "svelte";
import { htmlurl, presetUrl } from "../../files/imageserver";
import Codemirror from "../../libs/CodemirrorMarkdown.svelte";
import { getErrorMessage } from "./errorMessage";
import RichEditorFiles from "./RichEditorFiles.svelte";
const channel = getContext("channel");
export let value;
export let field;
export let graph;
export let record;
export let isCreateMode;
// export let id;
@@ -14,24 +15,21 @@
$: errorMessage = getErrorMessage(validationErrors, field.name);
let editor;
function insertMedia(e){
editor.insertMedia(e.detail)
function onFilesInserted(e) {
const presetPath = e.detail.preset.path;
editor.insertMedia(channel, e.detail.files, presetPath);
}
</script>
<div class="mb-3">
<Codemirror bind:this={editor} bind:value editable={!field.readonly || isCreateMode} />
{#if field.collections.length > 0}
<RichEditorFiles
bind:graph
{record}
{field}
{validationErrors}
on:editor-insert={insertMedia}
>
</RichEditorFiles>
{/if}
<RichEditorFiles {record} {field} {validationErrors} {onFilesInserted}
></RichEditorFiles>
<Codemirror
bind:this={editor}
bind:value
editable={!field.readonly || isCreateMode}
/>
{#if errorMessage}
<div class="invalid-feedback d-block">
{errorMessage}
@@ -2,38 +2,31 @@
import RichEditorFiles from "./RichEditorFiles.svelte";
import { getErrorMessage } from "./errorMessage";
import Trix from "../../libs/Trix.svelte";
import { htmlurl } from "../../files/imageserver";
import { getContext } from "svelte";
const channel = getContext("channel");
export let value;
export let field;
export let isCreateMode;
export let graph;
export let record;
export let validationErrors;
let editor;
$: errorMessage = getErrorMessage(validationErrors, field.name);
let additionalConfig = {
readonly: field.readonly && !isCreateMode,
};
function insertMedia(e) {
editor.insertMedia(e.detail);
function onFilesInserted(e) {
const presetPath = e.detail.preset.path;
e.detail.files.map((aFile) => {
const html = htmlurl(channel, aFile, presetPath);
editor.insertMedia(html);
});
}
</script>
<div class="mb-0">
<Trix {field} bind:this={editor} bind:value></Trix>
<!-- <Tinymce bind:this={editor} bind:value {additionalConfig}/>-->
{#if field.collections.length > 0}
<RichEditorFiles
bind:graph
{record}
{field}
{validationErrors}
on:editor-insert={insertMedia}
></RichEditorFiles>
{/if}
<RichEditorFiles {record} {field} {onFilesInserted}></RichEditorFiles>
{#if errorMessage}
<div class="invalid-feedback d-block">
@@ -1,76 +1,48 @@
<script>
import PreviewFile from "../previews/PreviewFile.svelte";
import Dropdown from "../../common/Dropdown.svelte";
import Dialog from "../../dialog/Dialog.svelte";
import {insertEdges} from "./reference.js";
import {getContext} from "svelte";
import { getContext } from "svelte";
import FileDialog from "../../dialog/FileDialog.svelte";
import Uploader from "../../files/Uploader.svelte";
import Icon from "../../common/Icon.svelte";
const channel = getContext("channel");
export let field;
export let record;
export let graph
export let onFilesInserted;
let browseModal;
$: references = graph?.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) {
function openBrowseModal(e) {
e.preventDefault();
graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name)
);
browseModal.open(record.id);
}
function openBrowseModal(e, schema) {
function insertFiles(e) {
e.preventDefault();
browseModal.open(schema);
onFilesInserted(e);
browseModal.close();
}
function insert(e) {
function replaceFiles(e) {
e.preventDefault();
browseModal.close();
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action);
}
function uploadComplete(e) {}
</script>
<div class="mb-3">
<label class="mt-4 mb-3">Rich editor files</label>
{#if field.collections.length === 1}
<button
class="button"
on:click={(e) => openBrowseModal(e, collections[0].name)}
>
Browse
</button>
{:else}
<Dropdown>
<div slot="button">
Browse
</div>
{#each collections as collection}
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) => openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
{/each}
</Dropdown>
{/if}
<div
class="mb-0"
style="display: flex; align-items: start; justify-content: start; gap:6px"
>
<button class="button" on:click={openBrowseModal}>
<Icon icon="photo-film"></Icon> Browse
</button>
<Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
</div>
{#if references.length > 0}
{#each references as reference (reference.id)}
<!--This div helps the sorting thing-->
<div>
<PreviewFile record={reference} hasDelete={true} hasInsert={true} on:remove={removeReference}
on:editor-insert></PreviewFile>
</div>
{/each}
{/if}
<Dialog bind:this={browseModal} on:insert={insert}></Dialog>
<FileDialog
bind:this={browseModal}
on:insert_files={insertFiles}
on:replace_files={replaceFiles}
presetMode={true}
></FileDialog>
@@ -1,36 +1,18 @@
<script>
import Icon from "../../common/Icon.svelte";
import { createEventDispatcher, getContext } from "svelte";
import { createEventDispatcher } from "svelte";
import Preview from "../../files/Preview.svelte";
import Dropdown from "../../common/Dropdown.svelte";
const dispatch = createEventDispatcher();
const channel = getContext("channel");
export let file;
export let hasDelete = false;
export let hasInsert = false;
let imagePresets = Object.keys(channel.imageFilters);
function remove(e) {
e.preventDefault();
dispatch("remove_file", file.id);
}
function insert(e, preset) {
e.preventDefault();
// let html = htmlurl(channel, record, preset);
// let url = !preset
// ? `/${record._file.path}`
// : `/templates/${preset}/${record._file.path}`;
// dispatch("editor-insert", {
// html: html,
// url: channel.filesUrl + url,
// originalUrl: channel.filesUrl + "/" + record._file.path,
// record: record,
// });
}
</script>
<div class="preview-file">
@@ -47,25 +29,6 @@
<div
style="display: flex;gap:4px; align-items: center; margin-right: 10px;"
>
{#if hasInsert}
<div class="reference-action">
<Dropdown>
<div slot="button">
<Icon icon="photo-film" />
</div>
<button
class="dropdown-item button"
on:click={(e) => insert(e, null)}>original</button
>
{#each imagePresets as preset}
<button
class="dropdown-item button"
on:click={(e) => insert(e, preset)}>{preset}</button
>
{/each}
</Dropdown>
</div>
{/if}
{#if hasDelete}
<div class="reference-action">
<button class="button" on:click={remove}>
+4
View File
@@ -1038,6 +1038,7 @@
"integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
"debug": "^4.3.4",
@@ -1614,6 +1615,7 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dev": true,
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -1654,6 +1656,7 @@
"integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
@@ -1716,6 +1719,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
"integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.36",
+40 -43
View File
@@ -1,51 +1,48 @@
.autocomplete {
position: relative;
z-index: 1000;
overflow: visible;
.autocomplete-option {
cursor: pointer;
font-size: 14px;
padding: 3px 10px;
&:hover {
background: var(--p40);
border-radius: 12px;
z-index: 1000;
overflow: visible;
.autocomplete-option {
cursor: pointer;
font-size: 14px;
padding: 3px 10px;
&:hover {
background: var(--p40);
border-radius: 12px;
}
}
}
&:focus-within {
.autocomplete-results{
display: flex;
&:focus-within {
.autocomplete-results {
display: flex;
}
}
}
}
.autocomplete-selected-value {
font-size: 13px;
margin-top: 10px;
border-radius: 12px;
background: var(--p30);
padding: 3px 10px;
display: inline-flex;
justify-content: center;
gap: 4px;
line-height: 22px;
&:hover {
background: var(--p40);
}
font-size: 13px;
margin-top: 10px;
border-radius: 12px;
background: var(--p30);
padding: 3px 10px;
display: inline-flex;
justify-content: center;
gap: 4px;
line-height: 22px;
&:hover {
background: var(--p40);
}
}
.autocomplete-results {
display: none;
flex-direction: column;
padding: 10px;
overflow: visible;
position: absolute;
border-radius: 12px;
z-index: 20;
background: var(--p30);
//border: 1px solid var(--p40);
transition: 600ms;
flex-grow: 1;
top: 45px;
width: 100%;
}
display: none;
flex-direction: column;
padding: 10px;
overflow: visible;
position: absolute;
border-radius: 12px;
z-index: 20;
background: var(--p30);
//border: 1px solid var(--p40);
transition: 600ms;
flex-grow: 1;
top: 45px;
width: 100%;
}
+80 -83
View File
@@ -1,93 +1,90 @@
.button{
border-radius: 12px;
background: var(--p20);
padding: 3px 10px;
cursor: pointer;
border: 0px solid var(--p30);
font-size: 14px;
min-height: 27px;
display: flex;
align-items: center;
gap: 4px;
color: var(--text);
&:focus {
}
&:hover {
background: var(--p30);
}
&:active {
background: var(--p50)!important;
box-shadow: none;
}
&.active {
background: var(--p30);
}
&.secondary{
background: var(--p30);
&:hover {
background: var(--p40);
}
}
&.primary{
background: var(--p70);
color: var(--p10);
&:hover {
background: var(--p90);
}
}
&[disabled] {
pointer-events: none;
opacity: .7;
color: var(--text);
}
}
.upload-button{
padding: 0;
border: none;
label{
.button {
border-radius: 12px;
background: var(--p20);
padding: 3px 10px;
cursor: pointer;
border: 0px solid var(--p30);
font-size: 14px;
line-height: 14px;
font-weight: normal;
background: var(--p80)!important;
color: var(--p10);
}
min-height: 27px;
display: flex;
align-items: center;
gap: 4px;
color: var(--text);
&:focus {
}
&:hover {
background: var(--p30);
}
&:active {
background: var(--p50) !important;
box-shadow: none;
}
&.active {
background: var(--p30);
}
&.secondary {
background: var(--p30);
&:hover {
background: var(--p40);
}
}
&.primary {
background: var(--p70);
color: var(--p10);
&:hover {
background: var(--p90);
}
}
&[disabled] {
pointer-events: none;
opacity: 0.7;
color: var(--text);
}
}
.button-text{
border: none;
padding: 0;
background: transparent;
cursor: pointer;
.upload-button {
padding: 0;
border: none;
label {
margin: 0;
font-size: 14px;
line-height: 14px;
font-weight: normal;
background: var(--p80) !important;
color: var(--p10);
}
}
.button-text {
border: none;
padding: 0;
background: transparent;
cursor: pointer;
}
.spinner-border {
width: 12px;
height: 12px;
border: 2px solid var(--p10);
border-bottom-color: var(--p30);
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
width: 12px;
height: 12px;
border: 2px solid var(--p10);
border-bottom-color: var(--p30);
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "lucent-laravel",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+2
View File
@@ -50,4 +50,6 @@ interface AuthService
public function registerAdmin(string $name, string $email): User;
public function validateRoles(array $roles): array;
public function isExternal(): bool;
public function redirectHome(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse;
}
+9
View File
@@ -220,4 +220,13 @@ readonly class AuthServiceLucent implements AuthService
->values()
->toArray();
}
public function isExternal(): bool
{
return false;
}
public function redirectHome(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
{
return redirect("/home");
}
}
+9
View File
@@ -102,4 +102,13 @@ readonly class AuthServiceLunar implements AuthService
->values()
->toArray();
}
public function isExternal(): bool
{
return true;
}
public function redirectHome(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
{
return redirect("/lunar");
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Lucent\Account;
use Lucent\Primitive\Collection;
use PhpOption\Option;
use Lucent\Option\Option;
interface UserRepo
{
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Lucent\Account;
use Carbon\Carbon;
use Lucent\Database\Database;
use Lucent\Primitive\Collection;
use PhpOption\Option;
use Lucent\Option\Option;
class UserRepoLucent implements UserRepo
{
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Lucent\Account;
use Carbon\Carbon;
use Lucent\Database\Database;
use Lucent\Primitive\Collection;
use PhpOption\Option;
use Lucent\Option\Option;
class UserRepoLunar implements UserRepo
{
+13
View File
@@ -3,6 +3,7 @@
namespace Lucent\Channel;
use Lucent\Channel\Data\UserCommand;
use Lucent\Data\ImagePreset;
use Lucent\Primitive\Collection;
use Lucent\Data\Schema;
use Lucent\Data\ChannelAuth;
@@ -12,6 +13,10 @@ final class Channel
public string $lucentUrl;
public string $filesUrl;
public string $previewTargetUrl;
/**
* @param array<ImagePreset> $imagePresets
*/
public array $imagePresets;
/**
* @param Collection<Schema> $schemas
@@ -30,6 +35,14 @@ final class Channel
$this->lucentUrl = $url . "/lucent";
$this->filesUrl = $this->makeFilesUrl();
$this->previewTargetUrl = $url . "/" . $previewTarget;
$this->imagePresets = array_map(function ($i) {
$preset = new $i();
return new ImagePreset(
id: $i,
name: $preset->getName(),
path: $preset->getPath(),
);
}, $this->imageFilters);
}
private function makeFilesUrl(): string
+24 -2
View File
@@ -8,7 +8,7 @@ use Lucent\File\FileService;
use Lucent\Primitive\Collection;
use Lucent\Data\Schema;
use Lucent\Schema\SchemaService;
use PhpOption\Option;
use Lucent\Option\Option;
final class ChannelService
{
@@ -45,7 +45,7 @@ final class ChannelService
previewTarget: rtrim(config("lucent.previewTarget") ?? "", "/"),
commands: Collection::make($userCommands),
schemas: $schemasCollection,
imageFilters: config("lucent.imageFilters") ?? [],
imageFilters: self::loadImageFilters(),
roles: $schemasArray["roles"] ?? [],
);
@@ -54,6 +54,28 @@ final class ChannelService
return $channelService;
}
private static function loadImageFilters(): array
{
$relativePath = config("lucent.image_filter_path");
if (!$relativePath) {
return [];
}
$dir = base_path($relativePath);
if (!is_dir($dir)) {
return [];
}
$namespace = str_replace("/", "\\", ucwords($relativePath, "/"));
return array_values(
array_map(
fn(string $file) => $namespace . "\\" . basename($file, ".php"),
glob("{$dir}/*.php") ?: [],
),
);
}
/**
* @param string $name
* @return Option<Schema>
@@ -8,7 +8,7 @@ use Lucent\Data\Schema;
use Lucent\Schema\SchemaService;
use Lucent\File\FileService;
class CompileSchemas extends Command
class CompileSchemasCommand extends Command
{
protected $signature = "lucent:schemas";
@@ -4,12 +4,14 @@ namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\File\FileService;
use Lucent\ResultType\Error;
use Lucent\ResultType\Result;
use Lucent\ResultType\Success;
use ZipArchive;
class Export extends Command
class ExportCommand extends Command
{
protected $signature = "lucent:export";
protected $prefix = "lucent_";
protected $description = "Export data and files";
@@ -32,8 +34,30 @@ class Export extends Command
$stamp = now()->format("Y_m_d_His");
$sqlFile = $exportDir . "/dump_{$stamp}.sql";
$zipFile = $exportDir . "/export_{$stamp}.zip";
$filesDir = $fileService->loadPublicDisk()->path("lucent/files");
// Dump database
$result = $this->dumpDatabase($db, $tables, $sqlFile)->flatMap(
fn($sql) => $this->buildZip($sql, $filesDir, $zipFile),
);
if (file_exists($sqlFile)) {
unlink($sqlFile);
}
if ($result->error()->isDefined()) {
$this->error($result->error()->get());
return;
}
$this->info("Exported to {$zipFile}");
}
/** @return Result<string, string> */
private function dumpDatabase(
array $db,
array $tables,
string $sqlFile,
): Result {
$tableArgs = collect($tables)->map(fn($t) => "-t {$t}")->join(" ");
$command = sprintf(
"PGPASSWORD=%s pg_dump -h %s -p %s -U %s -d %s %s --no-owner --no-acl > %s",
@@ -49,27 +73,29 @@ class Export extends Command
exec($command, result_code: $code);
if ($code !== 0) {
$this->error("pg_dump failed");
return;
return Error::create("pg_dump failed.");
}
$this->info("Database dumped.");
// Zip SQL + files
$publicDisk = $fileService->loadPublicDisk();
$filesDir = $publicDisk->path("lucent/files");
return Success::create($sqlFile);
}
/** @return Result<string, string> */
private function buildZip(
string $sqlFile,
string $filesDir,
string $zipFile,
): Result {
$zip = new ZipArchive();
if (
$zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !==
true
) {
$this->error("Could not create zip archive.");
return;
return Error::create("Could not create zip archive.");
}
$zip->addFile($sqlFile, "dump_{$stamp}.sql");
$zip->addFile($sqlFile, basename($sqlFile));
if (is_dir($filesDir)) {
$iterator = new \RecursiveIteratorIterator(
@@ -91,9 +117,6 @@ class Export extends Command
$zip->close();
// Clean up originals
unlink($sqlFile);
$this->info("Exported to {$zipFile}");
return Success::create($zipFile);
}
}
@@ -6,7 +6,7 @@ use Illuminate\Console\Command;
use Lucent\Primitive\Collection;
use Lucent\Schema\CollectionSchema;
class GenerateCollectionSchema extends Command
class GenerateCollectionSchemaCommand extends Command
{
protected $signature = 'lucent:generate:collection {name}';
@@ -0,0 +1,64 @@
<?php
namespace Lucent\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
class GenerateImageFilterCommand extends Command
{
protected $signature = "lucent:generate:image_filter {name}";
protected $description = "Generate an image filter";
public function handle()
{
$name = $this->argument("name");
$relativePath = config("lucent.image_filter_path");
$dir = base_path($relativePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$className = Str::of($name)->camel()->ucfirst() . "ImageFilter";
$pathName = Str::of($name)->snake()->lower();
$filePath = "{$dir}/{$className}.php";
if (file_exists($filePath)) {
$this->error("Filter {$name} already exists at {$filePath}");
return Command::FAILURE;
}
$namespace = str_replace("/", "\\", ucwords($relativePath, "/"));
$stub = <<<PHP
<?php namespace {$namespace};
use Lucent\ImageFilterInterface;
use Intervention\Image\Interfaces\ImageInterface;
class {$className} implements ImageFilterInterface
{
public function apply(ImageInterface \$image): ImageInterface
{
return \$image;
}
public function getName(): string {
return "{$name}";
}
public function getPath(): string {
return "{$pathName}";
}
}
PHP;
file_put_contents($filePath, $stub);
$this->info("Created {$filePath}");
return Command::SUCCESS;
}
}
@@ -4,9 +4,12 @@ namespace Lucent\Commands;
use Illuminate\Console\Command;
use Lucent\File\FileService;
use Lucent\ResultType\Error;
use Lucent\ResultType\Result;
use Lucent\ResultType\Success;
use ZipArchive;
class Import extends Command
class ImportCommand extends Command
{
protected $signature = "lucent:import";
@@ -44,26 +47,44 @@ class Import extends Command
return;
}
// Extract to temp directory
$tempDir = storage_path("exports/.import_tmp_" . uniqid());
$result = $this->extractZip($zipFile, $tempDir)
->flatMap(fn($dir) => $this->restoreDatabase($dir))
->flatMap(fn($dir) => $this->restoreFiles($dir, $fileService));
$this->cleanup($tempDir);
if ($result->error()->isDefined()) {
$this->error($result->error()->get());
return;
}
$this->info("Import complete.");
}
/** @return Result<string, string> */
private function extractZip(string $zipFile, string $tempDir): Result
{
mkdir($tempDir, 0755, true);
$zip = new ZipArchive();
if ($zip->open($zipFile) !== true) {
$this->error("Could not open zip archive.");
$this->cleanup($tempDir);
return;
return Error::create("Could not open zip archive.");
}
$zip->extractTo($tempDir);
$zip->close();
// Restore database
return Success::create($tempDir);
}
/** @return Result<string, string> */
private function restoreDatabase(string $tempDir): Result
{
$sqlFiles = glob($tempDir . "/*.sql");
if (empty($sqlFiles)) {
$this->error("No SQL dump found inside the archive.");
$this->cleanup($tempDir);
return;
return Error::create("No SQL dump found inside the archive.");
}
$db = config("database.connections.pgsql");
@@ -74,7 +95,6 @@ class Import extends Command
"lucent_edges",
];
// Truncate existing tables before restore
$truncate = collect($tables)
->map(fn($t) => "TRUNCATE TABLE {$t} CASCADE;")
->join(" ");
@@ -91,12 +111,6 @@ class Import extends Command
exec($truncateCmd, result_code: $truncateCode);
if ($truncateCode !== 0) {
$this->error("Failed to truncate existing tables.");
$this->cleanup($tempDir);
return;
}
$restoreCmd = sprintf(
"PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -f %s",
$db["password"],
@@ -108,54 +122,53 @@ class Import extends Command
);
exec($restoreCmd, result_code: $restoreCode);
if ($restoreCode !== 0) {
$this->error("Database restore failed.");
$this->cleanup($tempDir);
return;
return Error::create("Database restore failed.");
}
$this->info("Database restored.");
// Replace files
return Success::create($tempDir);
}
/** @return Result<null, string> */
private function restoreFiles(
string $tempDir,
FileService $fileService,
): Result {
$srcFilesDir = $tempDir . "/files";
if (is_dir($srcFilesDir)) {
$publicDisk = $fileService->loadPublicDisk();
$filesDir = $publicDisk->path("lucent");
$destFilesDir = $publicDisk->path("lucent");
// Remove existing files directory or create it if missing
if (is_dir($destFilesDir)) {
exec("rm -rf " . escapeshellarg($destFilesDir));
} else {
mkdir($destFilesDir, 0755, true);
}
exec(
sprintf(
"cp -R %s %s",
escapeshellarg($srcFilesDir),
escapeshellarg($destFilesDir),
),
result_code: $copyCode,
);
if ($copyCode !== 0) {
$this->error("Failed to restore files.");
$this->cleanup($tempDir);
return;
}
$this->info("Files restored.");
} else {
if (!is_dir($srcFilesDir)) {
$this->warn(
"No files directory found in archive — skipping file restore.",
);
return Success::create(null);
}
$this->cleanup($tempDir);
$this->info("Import complete.");
$publicDisk = $fileService->loadPublicDisk();
$destFilesDir = $publicDisk->path("lucent/files");
if (is_dir($destFilesDir)) {
exec("rm -rf " . escapeshellarg($destFilesDir));
}
mkdir($destFilesDir, 0755, true);
exec(
sprintf(
"cp -R %s/* %s",
escapeshellarg($srcFilesDir),
escapeshellarg($destFilesDir),
),
result_code: $copyCode,
);
if ($copyCode !== 0) {
return Error::create("Failed to restore files.");
}
$this->info("Files restored.");
return Success::create(null);
}
private function cleanup(string $dir): void
@@ -5,7 +5,7 @@ namespace Lucent\Commands;
use DirectoryIterator;
use Illuminate\Console\Command;
class LiveLink extends Command
class LiveLinkCommand extends Command
{
protected $signature = 'lucent:livelink';
@@ -8,7 +8,7 @@ use Lucent\File\FileRepo;
use Lucent\File\FileService;
use Lucent\Query\Query;
class RebuildThumbnails extends Command
class RebuildThumbnailsCommand extends Command
{
protected $signature = "lucent:rebuild:thumbnails";
@@ -30,7 +30,9 @@ class RebuildThumbnails extends Command
try {
$this->fileService->createTemplates($disk, $file->path);
} catch (Exception $e) {
echo "File " . $file->filename . " could not be rebuilt \n";
$this->error(
"File " . $file->filename . " could not be rebuilt \n",
);
}
}
}
@@ -6,7 +6,7 @@ use Illuminate\Console\Command;
use Lucent\Edge\EdgeService;
use Lucent\Query\Query;
class RemoveOrphanEdges extends Command
class RemoveOrphanEdgesCommand extends Command
{
protected $signature = 'lucent:removeOrphanEdges';
@@ -11,7 +11,7 @@ use Lucent\Setup\Step\LucentConfigStep;
use Lucent\Setup\Step\StorageLinkSetupStep;
use Lucent\Setup\Step\StorageSetupStep;
class Setup extends Command
class SetupCommand extends Command
{
protected $signature = "lucent:setup";
@@ -7,7 +7,7 @@ use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Lucent\Database\Database;
class SetupDatabase extends Command
class SetupDatabaseCommand extends Command
{
protected $signature = "lucent:setup-db";
protected $prefix = "lucent_";
+1 -11
View File
@@ -7,6 +7,7 @@ return [
"private_disk" => env("LUCENT_PRIVATE_DISK", "local"),
"public_disk" => env("LUCENT_PUBLIC_DISK", "public"),
"schemas_path" => env("LUCENT_SCHEMAS_PATH", "resources/lucent/schemas"),
"image_filter_path" => "app/Lucent/ImageFilters",
"database" => env("LUCENT_DB_CONNECTION", env("DB_CONNECTION", "sqlite")),
"name" => env("LUCENT_NAME", "Lucent"),
"url" => env("LUCENT_URL", env("APP_URL")),
@@ -21,17 +22,6 @@ return [
*
* */
"commands" => [],
/*
* Image filter will be available both for rich editor fields
* and throughout your application
*
* example:
* [
* "filterName" => Filter::class
* ]
*
* */
"imageFilters" => [],
"canInvite" => ["admin"],
"canBuild" => ["admin"],
"systemUserId" => "",
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Lucent\Data;
class ImagePreset
{
function __construct(
public string $id,
public string $name,
public string $path,
) {}
}
+65 -13
View File
@@ -4,13 +4,18 @@ namespace Lucent\File;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Format;
use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService;
use Lucent\Id\Id;
use Lucent\LucentException;
use Lucent\Data\File as DataFile;
use Lucent\ResultType\Error;
use Lucent\ResultType\Result;
use Lucent\ResultType\Success;
use Spatie\ImageOptimizer\OptimizerChainFactory;
class FileService
@@ -18,6 +23,7 @@ class FileService
public function __construct(
public ChannelService $channelService,
public ImageManager $imageManager,
public Logger $logger,
) {}
public function createFromUrl(
@@ -67,7 +73,10 @@ class FileService
}
if ($this->isImage($mimetype)) {
$this->createTemplates($disk, $path);
$result = $this->createTemplates($disk, $path);
if ($result->error()->isDefined()) {
throw new LucentException($result->error()->get());
}
}
[$width, $height] = $this->isImage($mimetype)
@@ -123,22 +132,65 @@ class FileService
return Storage::disk(config("lucent.private_disk"));
}
public function createTemplates(Filesystem $disk, string $path): void
/** @return Result<null, string> */
public function createTemplates(Filesystem $disk, string $path): Result
{
$originalImage = $this->imageManager->make(
$this->loadPublicDisk()->get("lucent/" . $path),
);
foreach (config("lucent.imageFilters") as $preset => $filterClass) {
$imageClone = clone $originalImage;
$image = $imageClone->filter(new $filterClass());
$templateUri = "lucent/templates/" . $preset . "/" . $path;
$disk->put($templateUri, $image->encode("webp", 75));
$filePath = "lucent/" . $path;
if (!$this->loadPublicDisk()->exists($filePath)) {
return Error::create("File not found: {$filePath}");
}
$thumbDir = "lucent/thumbs/" . $path;
$originalImage = $this->imageManager->decode(
$this->loadPublicDisk()->get($filePath),
);
$image = $originalImage->fit(300, 300);
$disk->put($thumbDir, $image->encode("webp", 75));
foreach ($this->channelService->channel->imageFilters as $filterClass) {
$filterClassInstance = new $filterClass();
$templateUri =
"lucent/templates/" .
$filterClassInstance->getPath() .
"/" .
substr($path, 0, strrpos($path, ".")) .
".webp";
$ok = $disk->put(
$templateUri,
(clone $originalImage)
->modify($filterClassInstance)
->encodeUsingFormat(
Format::WEBP,
progressive: true,
quality: 80,
),
);
if (!$ok) {
return Error::create(
"Failed to write template: {$templateUri}",
);
}
}
$thumbUri =
"lucent/thumbs/" . substr($path, 0, strrpos($path, ".")) . ".webp";
$ok = $disk->put(
$thumbUri,
$originalImage
->cover(300, 300)
->encodeUsingFormat(
Format::WEBP,
progressive: true,
quality: 80,
),
);
if (!$ok) {
return Error::create("Failed to write thumbnail: {$thumbUri}");
}
return Success::create(null);
}
/**
+26
View File
@@ -28,6 +28,10 @@ class AuthController
public function register(Request $request): View|RedirectResponse
{
if ($this->authService->isExternal()) {
return $this->authService->redirectHome();
}
if ($this->accountService->countUsers() > 0) {
return redirect(
$this->channelService->channel->lucentUrl . "/login",
@@ -43,6 +47,10 @@ class AuthController
public function postRegister(Request $request): Response
{
if ($this->authService->isExternal()) {
abort(400);
}
if ($this->accountService->countUsers() > 0) {
abort(400);
}
@@ -61,6 +69,9 @@ class AuthController
public function login()
{
if ($this->authService->isExternal()) {
return $this->authService->redirectHome();
}
if ($this->accountService->countUsers() == 0) {
return redirect(
$this->channelService->channel->lucentUrl . "/register",
@@ -76,6 +87,9 @@ class AuthController
public function postLogin(Request $request)
{
if ($this->authService->isExternal()) {
abort(400);
}
$this->authService->sendLoginEmail($request->input("email"));
return [];
}
@@ -87,6 +101,10 @@ class AuthController
// "token" => $request->input("token"),
// ]);
if ($this->authService->isExternal()) {
abort(400);
}
return $this->svelte->render(
layout: "account",
view: "verify",
@@ -100,6 +118,10 @@ class AuthController
public function postVerify(Request $request)
{
if ($this->authService->isExternal()) {
abort(400);
}
try {
$this->authService->login(
$request->input("email"),
@@ -113,6 +135,10 @@ class AuthController
public function logout(): RedirectResponse
{
if ($this->authService->isExternal()) {
abort(400);
}
$this->session->flush();
return redirect($this->channelService->channel->lucentUrl . "/login");
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Lucent;
use Intervention\Image\Interfaces\ModifierInterface;
interface ImageFilterInterface extends ModifierInterface
{
public function getName(): string;
public function getPath(): string;
}
+22 -19
View File
@@ -14,19 +14,21 @@ use Lucent\Account\UserRepo;
use Lucent\Account\UserRepoLucent;
use Lucent\Account\UserRepoLunar;
use Lucent\Channel\ChannelService;
use Lucent\Commands\CompileSchemas;
use Lucent\Commands\GenerateCollectionSchema;
use Lucent\Commands\LiveLink;
use Lucent\Commands\RebuildThumbnails;
use Lucent\Commands\RemoveOrphanEdges;
use Lucent\Commands\SetupDatabase;
use Lucent\Commands\Export;
use Lucent\Commands\Import;
use Lucent\Commands\Setup;
use Lucent\Commands\CompileSchemasCommand;
use Lucent\Commands\ExportCommand;
use Lucent\Commands\GenerateCollectionSchemaCommand;
use Lucent\Commands\GenerateImageFilterCommand;
use Lucent\Commands\ImportCommand;
use Lucent\Commands\LiveLinkCommand;
use Lucent\Commands\RebuildThumbnailsCommand;
use Lucent\Commands\RemoveOrphanEdgesCommand;
use Lucent\Commands\SetupCommand;
use Lucent\Commands\SetupDatabaseCommand;
use Lucent\Data\ChannelAuth;
use Lucent\File\FileService;
use Lucent\Query\DatabaseGraph\DatabaseGraph;
use Lucent\Query\DatabaseGraph\PgsqlDatabaseGraph;
use Intervention\Image\Drivers\Imagick\Driver;
class LucentServiceProvider extends ServiceProvider
{
@@ -40,7 +42,7 @@ class LucentServiceProvider extends ServiceProvider
});
$this->app->bind(ImageManager::class, function () {
return new ImageManager(["driver" => "imagick"]);
return ImageManager::usingDriver(new Driver());
});
$this->app->bind(DatabaseGraph::class, function () {
@@ -93,15 +95,16 @@ class LucentServiceProvider extends ServiceProvider
if ($this->app->runningInConsole()) {
$this->commands([
CompileSchemas::class,
RebuildThumbnails::class,
LiveLink::class,
RemoveOrphanEdges::class,
SetupDatabase::class,
GenerateCollectionSchema::class,
Export::class,
Import::class,
Setup::class,
CompileSchemasCommand::class,
RebuildThumbnailsCommand::class,
LiveLinkCommand::class,
RemoveOrphanEdgesCommand::class,
SetupDatabaseCommand::class,
GenerateCollectionSchemaCommand::class,
GenerateImageFilterCommand::class,
ExportCommand::class,
ImportCommand::class,
SetupCommand::class,
]);
}
+5
View File
@@ -0,0 +1,5 @@
<?php
namespace Lucent\Modules;
class ImageModule {}
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Lucent\Option;
use Traversable;
/**
* @template T
*
* @extends Option<T>
*/
final class LazyOption extends Option
{
/** @var callable(mixed...):(Option<T>) */
private $callback;
/** @var array<int, mixed> */
private $arguments;
/** @var Option<T>|null */
private $option;
/**
* @template S
* @param callable(mixed...):(Option<S>) $callback
* @param array<int, mixed> $arguments
*
* @return LazyOption<S>
*/
public static function create($callback, array $arguments = []): self
{
return new self($callback, $arguments);
}
/**
* @param callable(mixed...):(Option<T>) $callback
* @param array<int, mixed> $arguments
*/
public function __construct($callback, array $arguments = [])
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Invalid callback given');
}
$this->callback = $callback;
$this->arguments = $arguments;
}
public function isDefined(): bool
{
return $this->option()->isDefined();
}
public function isEmpty(): bool
{
return $this->option()->isEmpty();
}
public function get()
{
return $this->option()->get();
}
public function getOrElse($default)
{
return $this->option()->getOrElse($default);
}
public function getOrCall($callable)
{
return $this->option()->getOrCall($callable);
}
public function getOrThrow(\Exception $ex)
{
return $this->option()->getOrThrow($ex);
}
public function orElse(Option $else)
{
return $this->option()->orElse($else);
}
public function ifDefined($callable)
{
$this->option()->forAll($callable);
}
public function forAll($callable)
{
return $this->option()->forAll($callable);
}
public function map($callable)
{
return $this->option()->map($callable);
}
public function flatMap($callable)
{
return $this->option()->flatMap($callable);
}
public function filter($callable)
{
return $this->option()->filter($callable);
}
public function filterNot($callable)
{
return $this->option()->filterNot($callable);
}
public function select($value)
{
return $this->option()->select($value);
}
public function reject($value)
{
return $this->option()->reject($value);
}
/** @return Traversable<T> */
public function getIterator(): Traversable
{
return $this->option()->getIterator();
}
public function foldLeft($initialValue, $callable)
{
return $this->option()->foldLeft($initialValue, $callable);
}
public function foldRight($initialValue, $callable)
{
return $this->option()->foldRight($initialValue, $callable);
}
/** @return Option<T> */
private function option(): Option
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) {
$this->option = $option;
} else {
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
}
}
return $this->option;
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
namespace Lucent\Option;
use EmptyIterator;
/**
* @extends Option<mixed>
*/
final class None extends Option
{
/** @var None|null */
private static $instance;
/** @return None */
public static function create(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function get()
{
throw new \RuntimeException('None has no value.');
}
public function getOrCall($callable)
{
return $callable();
}
public function getOrElse($default)
{
return $default;
}
public function getOrThrow(\Exception $ex)
{
throw $ex;
}
public function isEmpty(): bool
{
return true;
}
public function isDefined(): bool
{
return false;
}
public function orElse(Option $else)
{
return $else;
}
public function ifDefined($callable)
{
// no-op
}
public function forAll($callable)
{
return $this;
}
public function map($callable)
{
return $this;
}
public function flatMap($callable)
{
return $this;
}
public function filter($callable)
{
return $this;
}
public function filterNot($callable)
{
return $this;
}
public function select($value)
{
return $this;
}
public function reject($value)
{
return $this;
}
public function getIterator(): EmptyIterator
{
return new EmptyIterator();
}
public function foldLeft($initialValue, $callable)
{
return $initialValue;
}
public function foldRight($initialValue, $callable)
{
return $initialValue;
}
private function __construct()
{
}
}
+230
View File
@@ -0,0 +1,230 @@
<?php
namespace Lucent\Option;
use ArrayAccess;
use IteratorAggregate;
/**
* @template T
*
* @implements IteratorAggregate<T>
*/
abstract class Option implements IteratorAggregate
{
/**
* @template S
*
* @param S $value
* @param S $noneValue
*
* @return Option<S>
*/
public static function fromValue($value, $noneValue = null)
{
if ($value === $noneValue) {
return None::create();
}
return new Some($value);
}
/**
* @template S
*
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array
* @param string|int|null $key
*
* @return Option<S>
*/
public static function fromArraysValue($array, $key)
{
if ($key === null || !(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
return None::create();
}
return new Some($array[$key]);
}
/**
* @template S
*
* @param callable $callback
* @param array $arguments
* @param S $noneValue
*
* @return LazyOption<S>
*/
public static function fromReturn($callback, array $arguments = [], $noneValue = null)
{
return new LazyOption(static function () use ($callback, $arguments, $noneValue) {
/** @var mixed */
$return = call_user_func_array($callback, $arguments);
if ($return === $noneValue) {
return None::create();
}
return new Some($return);
});
}
/**
* @template S
*
* @param Option<S>|callable|S $value
* @param S $noneValue
*
* @return Option<S>|LazyOption<S>
*/
public static function ensure($value, $noneValue = null)
{
if ($value instanceof self) {
return $value;
} elseif (is_callable($value)) {
return new LazyOption(static function () use ($value, $noneValue) {
/** @var mixed */
$return = $value();
if ($return instanceof self) {
return $return;
} else {
return self::fromValue($return, $noneValue);
}
});
} else {
return self::fromValue($value, $noneValue);
}
}
/**
* @template S
*
* @param callable $callback
* @param mixed $noneValue
*
* @return callable
*/
public static function lift($callback, $noneValue = null)
{
return static function () use ($callback, $noneValue) {
/** @var array<int, mixed> */
$args = func_get_args();
$reduced_args = array_reduce(
$args,
/** @param bool $status */
static function ($status, self $o) {
return $o->isEmpty() ? true : $status;
},
false
);
if ($reduced_args) {
return None::create();
}
$args = array_map(
static function (self $o) {
return $o->get();
},
$args
);
return self::ensure(call_user_func_array($callback, $args), $noneValue);
};
}
/** @return T */
abstract public function get();
/**
* @template S
* @param S $default
* @return T|S
*/
abstract public function getOrElse($default);
/**
* @template S
* @param callable():S $callable
* @return T|S
*/
abstract public function getOrCall($callable);
/** @return T */
abstract public function getOrThrow(\Exception $ex);
abstract public function isEmpty(): bool;
abstract public function isDefined(): bool;
/**
* @param Option<T> $else
* @return Option<T>
*/
abstract public function orElse(self $else);
/** @deprecated Use forAll() instead. */
abstract public function ifDefined($callable);
/**
* @param callable(T):mixed $callable
* @return Option<T>
*/
abstract public function forAll($callable);
/**
* @template S
* @param callable(T):S $callable
* @return Option<S>
*/
abstract public function map($callable);
/**
* @template S
* @param callable(T):Option<S> $callable
* @return Option<S>
*/
abstract public function flatMap($callable);
/**
* @param callable(T):bool $callable
* @return Option<T>
*/
abstract public function filter($callable);
/**
* @param callable(T):bool $callable
* @return Option<T>
*/
abstract public function filterNot($callable);
/**
* @param T $value
* @return Option<T>
*/
abstract public function select($value);
/**
* @param T $value
* @return Option<T>
*/
abstract public function reject($value);
/**
* @template S
* @param S $initialValue
* @param callable(S, T):S $callable
* @return S
*/
abstract public function foldLeft($initialValue, $callable);
/**
* @template S
* @param S $initialValue
* @param callable(T, S):S $callable
* @return S
*/
abstract public function foldRight($initialValue, $callable);
}
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace Lucent\Option;
use ArrayIterator;
/**
* @template T
*
* @extends Option<T>
*/
final class Some extends Option
{
/** @var T */
private $value;
/** @param T $value */
public function __construct($value)
{
$this->value = $value;
}
/**
* @template U
* @param U $value
* @return Some<U>
*/
public static function create($value): self
{
return new self($value);
}
public function isDefined(): bool
{
return true;
}
public function isEmpty(): bool
{
return false;
}
public function get()
{
return $this->value;
}
public function getOrElse($default)
{
return $this->value;
}
public function getOrCall($callable)
{
return $this->value;
}
public function getOrThrow(\Exception $ex)
{
return $this->value;
}
public function orElse(Option $else)
{
return $this;
}
public function ifDefined($callable)
{
$this->forAll($callable);
}
public function forAll($callable)
{
$callable($this->value);
return $this;
}
public function map($callable)
{
return new self($callable($this->value));
}
public function flatMap($callable)
{
/** @var mixed */
$rs = $callable($this->value);
if (!$rs instanceof Option) {
throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?');
}
return $rs;
}
public function filter($callable)
{
if (true === $callable($this->value)) {
return $this;
}
return None::create();
}
public function filterNot($callable)
{
if (false === $callable($this->value)) {
return $this;
}
return None::create();
}
public function select($value)
{
if ($this->value === $value) {
return $this;
}
return None::create();
}
public function reject($value)
{
if ($this->value === $value) {
return None::create();
}
return $this;
}
/** @return ArrayIterator<int, T> */
public function getIterator(): ArrayIterator
{
return new ArrayIterator([$this->value]);
}
public function foldLeft($initialValue, $callable)
{
return $callable($initialValue, $this->value);
}
public function foldRight($initialValue, $callable)
{
return $callable($this->value, $initialValue);
}
}
+1 -5
View File
@@ -6,12 +6,10 @@ use Lucent\ArrayContainer;
class RecordData extends ArrayContainer
{
public function merge(RecordData $data): RecordData
{
$this->data = array_merge($this->data, $data->toArray());
return $this;
}
@@ -19,6 +17,4 @@ class RecordData extends ArrayContainer
{
return $this->data;
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Lucent\ResultType;
use Lucent\Option\None;
use Lucent\Option\Some;
/**
* @template T
* @template E
*
* @extends \Lucent\ResultType\Result<T,E>
*/
final class Error extends Result
{
/**
* @var E
*/
private $value;
/**
* Internal constructor for an error value.
*
* @param E $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template F
*
* @param F $value
*
* @return \Lucent\ResultType\Result<T,F>
*/
public static function create($value): Error
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \Lucent\Option\Option<T>
*/
public function success()
{
return None::create();
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \Lucent\ResultType\Result<S,E>
*/
public function map(callable $f): Result
{
return self::create($this->value);
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\Lucent\ResultType\Result<S,F> $f
*
* @return \Lucent\ResultType\Result<S,F>
*/
public function flatMap(callable $f): Result
{
/** @var \Lucent\ResultType\Result<S,F> */
return self::create($this->value);
}
/**
* Get the error option value.
*
* @return \Lucent\Option\Option<E>
*/
public function error(): Some
{
return Some::create($this->value);
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \Lucent\ResultType\Result<T,F>
*/
public function mapError(callable $f): Result
{
return self::create($f($this->value));
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Lucent\ResultType;
/**
* @template T
* @template E
*/
abstract class Result
{
/**
* Get the success option value.
*
* @return \Lucent\Option\Option<T>
*/
abstract public function success();
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \Lucent\ResultType\Result<S,E>
*/
abstract public function map(callable $f);
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\Lucent\ResultType\Result<S,F> $f
*
* @return \Lucent\ResultType\Result<S,F>
*/
abstract public function flatMap(callable $f);
/**
* Get the error option value.
*
* @return \Lucent\Option\Option<E>
*/
abstract public function error();
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \Lucent\ResultType\Result<T,F>
*/
abstract public function mapError(callable $f);
}
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Lucent\ResultType;
use Lucent\Option\None;
use Lucent\Option\Some;
/**
* @template T
* @template E
*
* @extends \Lucent\ResultType\Result<T,E>
*/
final class Success extends Result
{
/**
* @var T
*/
private $value;
/**
* Internal constructor for a success value.
*
* @param T $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template S
*
* @param S $value
*
* @return \Lucent\ResultType\Result<S,E>
*/
public static function create($value): Success
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \Lucent\Option\Option<T>
*/
public function success(): Some
{
return Some::create($this->value);
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \Lucent\ResultType\Result<S,E>
*/
public function map(callable $f): Result
{
return self::create($f($this->value));
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\Lucent\ResultType\Result<S,F> $f
*
* @return \Lucent\ResultType\Result<S,F>
*/
public function flatMap(callable $f)
{
return $f($this->value);
}
/**
* Get the error option value.
*
* @return \Lucent\Option\Option<E>
*/
public function error()
{
return None::create();
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \Lucent\ResultType\Result<T,F>
*/
public function mapError(callable $f): Result
{
return self::create($this->value);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ use Lucent\Database\Database;
use Lucent\Edge\Edge;
use Lucent\Primitive\Collection;
use Lucent\Record\RecordData;
use PhpOption\Option;
use Lucent\Option\Option;
use stdClass;
class RevisionRepo
+1 -1
View File
@@ -6,7 +6,7 @@ use Lucent\Channel\ChannelService;
use Lucent\Edge\Edge;
use Lucent\Primitive\Collection;
use Lucent\Record\Record;
use PhpOption\Option;
use Lucent\Option\Option;
readonly class RevisionService
{
+10 -7
View File
@@ -1,7 +1,7 @@
<?php
use PhpOption\None;
use PhpOption\Some;
use Lucent\Option\None;
use Lucent\Option\Some;
if (!function_exists("some")) {
/**
@@ -44,9 +44,9 @@ if (!function_exists("schemas_path")) {
}
if (!function_exists("lucent_file")) {
function lucent_file(\Lucent\Data\File $file): string
function lucent_file(array $file): string
{
$path = $file->path;
$path = $file["path"];
return app()->make(\Lucent\Channel\ChannelService::class)->channel
->filesUrl .
"/" .
@@ -55,10 +55,13 @@ if (!function_exists("lucent_file")) {
}
if (!function_exists("lucent_image")) {
function lucent_image(\Lucent\Data\File $file, string $template): string
function lucent_image(array $file, string $template): string
{
$path = $file->path;
$path = $file["path"];
return app()->make(\Lucent\Channel\ChannelService::class)->channel
->filesUrl . "/templates/$template/$path";
->filesUrl .
"/templates/$template/" .
substr($path, 0, strrpos($path, ".")) .
".webp";
}
}