Initial Commit

This commit is contained in:
Gogs
2026-06-08 23:26:12 +02:00
commit 92c237f83d
16 changed files with 4545 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<info>
<id>kicanvasviewer</id>
<name>KiCanvas Viewer</name>
<summary>View KiCad files with KiCanvas</summary>
<description>Opens KiCad schematic and PCB files using KiCanvas.</description>
<version>0.1.0</version>
<licence>agpl</licence>
<author>Herwig Birke</author>
<namespace>KiCanvasViewer</namespace>
<category>files</category>
<dependencies>
<nextcloud min-version="33" max-version="33"/>
</dependencies>
</info>
+21
View File
@@ -0,0 +1,21 @@
<?php
return [
'routes' => [
[
'name' => 'viewer#view',
'url' => '/view/{fileId}',
'verb' => 'GET',
],
[
'name' => 'viewer#file',
'url' => '/file/{fileId}/{name}',
'verb' => 'GET',
],
[
'name' => 'viewer#resolve',
'url' => '/resolve/{etag}',
'verb' => 'GET',
],
],
];
+60
View File
@@ -0,0 +1,60 @@
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0");
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap");
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html,
body {
height: 100vh;
font-family:
"Nunito", ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa,
Manjari, "Arial Rounded MT Bold", Calibri, source-sans-pro, sans-serif;
line-height: 1.5;
width: 100%;
margin: 0 auto;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
main {
max-width: 1024px;
margin: 1rem auto;
}
p {
margin-bottom: 1rem;
margin-top: 1rem;
}
/* Styles for KiCanvas */
kicanvas-embed {
display: block;
width: 100%;
max-height: 100%;
aspect-ratio: 1.414;
background-color: aqua;
margin-bottom: 1rem;
margin-top: 1rem;
}
kc-kicanvas-shell {
width: 100%;
height: 100vh;
overflow: hidden;
}
+5
View File
@@ -0,0 +1,5 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="#fff" />
</svg>

After

Width:  |  Height:  |  Size: 984 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
/*! For license information please see viewerhandler.js.LICENSE.txt */
(()=>{"use strict";var e;(function(e){const{id:i,mimes:r,mimesAliases:a,component:n}=e;if(!i||""===i.trim()||"string"!=typeof i)throw new Error("The handler does not have a valid id");if(!(r&&Array.isArray(r)||a))throw new Error("Handler needs a valid mime array or mimesAliases");if(!n||"object"!=typeof n&&"function"!=typeof n)throw new Error("The handler does not have a valid component")})(e={id:"kicanvasviewer",group:"kicanvasviewer",mimes:["application/x-kicad-schematic","application/x-kicad-pcb"],component:{name:"KiCanvasInlineViewer",data:()=>({src:null,error:null,ready:!1,started:!1}),async mounted(){if(this.started)return;this.started=!0;const e=this.$attrs?.etag;if(e)try{const i=await fetch(OC.generateUrl("/apps/kicanvasviewer/resolve/"+encodeURIComponent(e)));if(!i.ok)return void(this.error="KiCanvas: could not resolve file");const r=await i.json();if(!r.fileId||!r.fileName)return void(this.error="KiCanvas: resolve returned no fileId/fileName");this.src=window.location.origin+OC.generateUrl("/apps/kicanvasviewer/file/"+encodeURIComponent(r.fileId)+"/"+encodeURIComponent(r.fileName)),await new Promise((e,i)=>{if(customElements.get("kc-kicanvas-shell"))return void e();const r=document.createElement("script");r.type="module",r.src=OC.linkTo("kicanvasviewer","js/kicanvas/kicanvas.js"),r.onload=e,r.onerror=i,document.head.appendChild(r)}),this.ready=!0,this.$forceUpdate()}catch(e){console.error(e),this.error="KiCanvas error"}else this.error="KiCanvas: no etag found"},render(e){return this.error?e("div",{style:{padding:"40px",fontSize:"20px"}},this.error):this.ready&&this.src?e("div",{style:{position:"absolute",inset:"0",zIndex:100,background:"var(--color-main-background)"}},[e("kc-kicanvas-shell",{key:this.src,attrs:{src:this.src},style:{display:"block",width:"100%",height:"100%"}})]):e("div",{style:{padding:"40px",fontSize:"20px"}},"Loading KiCanvas...")}}}),window._oca_viewer_handlers??(window._oca_viewer_handlers=new Map),window._oca_viewer_handlers.has(e.id)?console.warn(`Handler with id ${e.id} is already registered.`):window._oca_viewer_handlers.set(e.id,e)})();
+4
View File
@@ -0,0 +1,4 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace OCA\KiCanvasViewer\AppInfo;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\Util;
class Application extends App implements IBootstrap {
public const APP_ID = 'kicanvasviewer';
public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
}
public function register(IRegistrationContext $context): void {
$context->registerEventListener(
LoadAdditionalScriptsEvent::class,
LoadAdditionalScriptsListener::class
);
}
public function boot(IBootContext $context): void {
}
}
@@ -0,0 +1,18 @@
<?php
namespace OCA\KiCanvasViewer\AppInfo;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;
class LoadAdditionalScriptsListener implements IEventListener {
public function handle(Event $event): void {
if (!$event instanceof LoadAdditionalScriptsEvent) {
return;
}
Util::addScript('kicanvasviewer', 'viewerhandler');
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace OCA\KiCanvasViewer\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\JSONResponse;
class ViewerController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IRootFolder $rootFolder,
private IUserSession $userSession,
) {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function view(int $fileId): TemplateResponse {
$file = $this->getFileById($fileId);
$name = $file->getName();
return new TemplateResponse('kicanvasviewer', 'viewer', [
'fileId' => $fileId,
'fileName' => $name,
]);
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function file(int $fileId, string $name): DataDisplayResponse {
$file = $this->getFileById($fileId);
if (!preg_match('/\.(kicad_sch|kicad_pcb)$/i', $file->getName())) {
return new DataDisplayResponse('Unsupported file type', 400);
}
$response = new DataDisplayResponse($file->getContent());
$response->addHeader('Content-Type', 'text/plain; charset=utf-8');
$response->addHeader('Content-Disposition', 'inline; filename="' . basename($file->getName()) . '"');
return $response;
}
private function getFileById(int $fileId) {
$user = $this->userSession->getUser();
if ($user === null) {
throw new \Exception('Not logged in');
}
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$nodes = $userFolder->getById($fileId);
if (empty($nodes)) {
throw new \Exception('File not found');
}
return $nodes[0];
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function resolve(string $etag): JSONResponse {
$user = $this->userSession->getUser();
if ($user === null) {
return new JSONResponse(['error' => 'not logged in'], 401);
}
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$nodes = array_merge(
$userFolder->searchByMime('application/x-kicad-schematic'),
$userFolder->searchByMime('application/x-kicad-pcb')
);
foreach ($nodes as $node) {
if ($node->getEtag() === $etag) {
return new JSONResponse([
'fileId' => $node->getId(),
'fileName' => $node->getName(),
'mime' => $node->getMimeType(),
]);
}
}
return new JSONResponse(['error' => 'not found'], 404);
}
}
+2125
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"scripts": {
"build": "webpack --mode production"
},
"dependencies": {
"@nextcloud/files": "^4.0.0",
"@nextcloud/viewer": "^1.0.0"
},
"devDependencies": {
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
}
}
+127
View File
@@ -0,0 +1,127 @@
import { registerHandler } from '@nextcloud/viewer'
function loadKiCanvasScript() {
return new Promise((resolve, reject) => {
if (customElements.get('kc-kicanvas-shell')) {
resolve()
return
}
const script = document.createElement('script')
script.type = 'module'
script.src = OC.linkTo('kicanvasviewer', 'js/kicanvas/kicanvas.js')
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}
const KiCanvasInlineViewer = {
name: 'KiCanvasInlineViewer',
data() {
return {
src: null,
error: null,
ready: false,
started: false,
}
},
async mounted() {
if (this.started) {
return
}
this.started = true
const etag = this.$attrs?.etag
if (!etag) {
this.error = 'KiCanvas: no etag found'
return
}
try {
const res = await fetch(
OC.generateUrl('/apps/kicanvasviewer/resolve/' + encodeURIComponent(etag))
)
if (!res.ok) {
this.error = 'KiCanvas: could not resolve file'
return
}
const data = await res.json()
if (!data.fileId || !data.fileName) {
this.error = 'KiCanvas: resolve returned no fileId/fileName'
return
}
this.src = window.location.origin + OC.generateUrl(
'/apps/kicanvasviewer/file/' +
encodeURIComponent(data.fileId) +
'/' +
encodeURIComponent(data.fileName)
)
await loadKiCanvasScript()
this.ready = true
this.$forceUpdate()
} catch (e) {
console.error(e)
this.error = 'KiCanvas error'
}
},
render(h) {
if (this.error) {
return h('div', {
style: {
padding: '40px',
fontSize: '20px',
},
}, this.error)
}
if (!this.ready || !this.src) {
return h('div', {
style: {
padding: '40px',
fontSize: '20px',
},
}, 'Loading KiCanvas...')
}
return h('div', {
style: {
position: 'absolute',
inset: '0',
zIndex: 100,
background: 'var(--color-main-background)',
},
}, [
h('kc-kicanvas-shell', {
key: this.src,
attrs: {
src: this.src,
},
style: {
display: 'block',
width: '100%',
height: '100%',
},
}),
])
},
}
registerHandler({
id: 'kicanvasviewer',
group: 'kicanvasviewer',
mimes: [
'application/x-kicad-schematic',
'application/x-kicad-pcb',
],
component: KiCanvasInlineViewer,
})
+22
View File
@@ -0,0 +1,22 @@
<?php
script('kicanvasviewer', 'kicanvas/kicanvas');
style('kicanvasviewer', 'viewer');
$fileId = $_['fileId'];
$fileName = $_['fileName'];
$src = \OC::$server->getURLGenerator()->linkToRoute(
'kicanvasviewer.viewer.file',
[
'fileId' => $fileId,
'name' => $fileName,
]
);
?>
<div id="kicanvasviewer" style="width:100%; height:calc(100vh - 50px); min-height:800px;">
<kc-kicanvas-shell
id="kicanvas"
style="display:block; width:100%; height:100%; min-height:800px;"
src="<?= htmlspecialchars($src, ENT_QUOTES) ?>"></kc-kicanvas-shell>
</div>
+17
View File
@@ -0,0 +1,17 @@
const path = require('path')
module.exports = {
entry: {
viewerhandler: './src/viewerhandler.js',
},
output: {
path: path.resolve(__dirname, 'js'),
filename: '[name].js',
clean: false,
},
resolve: {
fallback: {
stream: false,
},
},
}