preact

Massimo Melina committed Dec 8, 2021 at 19:35 UTC d8bb1ad56f5b872882378467f8815b9ea39dc51c
9 files changed +159 -303
NOTES.md new
+19
@@ -0,0 +1,19 @@
1 +# File browser
2 +* Consider packing toolkit
3 + * if it's a "no", download CDN stuff
4 +
5 +## What GUI lib?
6 +
7 +### Material
8 +* css+js
9 + * https://materializecss.com/
10 +* preact wrapper for [MCW](https://github.com/material-components/material-components-web)
11 + * https://github.com/developit/preact-material-components
12 +
13 +### Others
14 +* https://preactjs.com/about/libraries-addons/
15 + * [fluid](https://unpkg.com/preact-fluid@0.9.1/lib/index.js)
16 + * no module?
17 +
18 +# Study
19 +https://www.tutorialspoint.com/koajs/koajs_quick_guide.htm
\ No newline at end of file
frontend/api.js new
+22
@@ -0,0 +1,22 @@
1 +import { useEffect, useState } from "./main.js";
2 +
3 +export function apiCall(cmd, params) {
4 + return fetch('/~/api/'+cmd, {
5 + method: 'POST',
6 + body:params && JSON.stringify(params)
7 + }).then(res => {
8 + if (res.ok)
9 + return res.json()
10 + const msg = 'Failed API ' + cmd + (params ? ' ' + JSON.stringify(params) : '')
11 + console.warn(msg)
12 + throw Error(msg)
13 + })
14 +}
15 +
16 +export function useApi(cmd, params) {
17 + const [x, setX] = useState()
18 + useEffect(()=>{
19 + apiCall(cmd, params).then(setX, setX)
20 + }, [cmd])
21 + return x
22 +}
\ No newline at end of file
frontend/index.html
+6 -26
@@ -1,33 +1,13 @@
1 <!DOCTYPE html>
2 -<html>
2 +<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 - <title>HFS 3 front end</title>
5 + <title>HFS</title>
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7 - <link rel="stylesheet" type="text/css" href="/~/frontend/main.css" />
8 -
9 - <!--[if lt IE 9 ]> <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]-->
10 - <script src='/socket.io/socket.io.js'></script>
11 - <script src='/~/jquery-1.11.3.js'></script>
12 - <script src='/~/frontend/main.js'></script>
13 - </head>
7 + <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
8 + <link rel="stylesheet" type="text/css" href="/~/main.css" />
9 + <script type="module" src='/~/main.js'></script>
10 +</head>
11 <body>
15 -<div id="control-tiles">
16 - <div id='folder'></div>
17 - <div id='folder-info'></div>
18 - <div>
19 - <label>View mode</label>
20 - <select id='mode'><option>tiles<option>list</select>
21 - </div>
22 - <div>
23 - <label>Order</label>
24 - <select id='order'><option value=''>none<option value='label' selected>name<option>type<option>size</select>
25 - </div>
26 - <div>
27 - <label>Pagination</label>
28 - <select id='pagination'><option value=''>none<option>50<option selected>200<option>500</select>
29 - </div>
30 -</div>
31 -<ul id='items'></ul>
12 </body>
13 </html>
frontend/main.css
-54
@@ -1,54 +0,0 @@
1 -body { font:10pt verdana, arial, sans-serif; }
2 -a img { border:0; } /* conform IE */
3 -
4 -#items { list-style:none; padding:0; margin-top:1em; }
5 -#control-tiles { display: inline; }
6 -#control-tiles > div {
7 - display:inline-block;
8 - border-left: 5px solid #bbb;
9 - background-color: #eee;
10 - padding: 0.3em 1em;
11 - margin: 0.1em 0;
12 - color: #777;
13 - line-height: 2em;
14 -}
15 -#folder { font-size:1.6em; font-weight:bold; word-break:break-all; display:block !important; padding-left:0.5em !important; }
16 -#folder img { height:1em; padding: 0 0.1em; }
17 -#folder a { text-decoration:none; }
18 -#folder a:hover { text-decoration:underline; }
19 -
20 -#num-items { min-width:3em; display: inline-block; font-weight:bold; }
21 -
22 -.full-label { position:absolute; padding-right:1em; text-align:left; background-color:#eee; }
23 -
24 -/* tiles */
25 -[mode=tiles] #items li { display:inline-block; } /* without this, the LI has height:0 */
26 -[mode=tiles] #items li.forced-br { display:block; } /* used for easeing the CPU */
27 -[mode=tiles] .item-link { float:left; overflow:hidden; width:8.2em; height:9.1em; /* this 0.1 more ensure the content doesn't go over for very little causing necessary effects */
28 - text-align:center; text-decoration:none;
29 - margin:1px; /* reserve space for the border on hover */
30 -}
31 -[mode=tiles].hover .item-link:hover {
32 - background:#ddd;
33 - border:1px solid #bbb; margin:0; /* replace margin with border */
34 -}
35 -[mode=tiles].hover .item-link:hover + .item-details { display:block; float:left; }
36 -
37 -[mode=tiles] .item-label { display:block; padding:0 0.5em; margin:3px; word-wrap:break-word; }
38 -[mode=tiles] .item-details { display:none; }
39 -[mode=tiles] .item-details-wrapper { position:absolute; background:#ffe; border:1px solid #eec; padding:0.2em 0.4em; }
40 -[mode=tiles] .full-label { max-width:17em; border:1px solid #bbb; padding:0.2em 0.5em; }
41 -
42 -/* list */
43 -[mode=list] li { border-bottom:1px solid #eee; height:1.5em; }
44 -[mode=list] li:nth-of-type(odd) { background:#eee; } /* stripes */
45 -[mode=list] .item-link { clear:left; float:left; text-decoration:none;
46 - width:80%; max-width:30em; height:1.5em; overflow:hidden;
47 - margin-right:1em; /* leave some space to breathe */
48 - white-space:nowrap; /* show as much as you can, even half-words, otherwise it will wrap under */
49 -}
50 -[mode=list] .full-label { white-space:normal; } /* re-enable wrapping */
51 -[mode=list] .item-link img { height:1.5em; vertical-align:middle; float:left; }
52 -[mode=list] .item-label { margin-left:0.5em; margin-right:1em; }
53 -[mode=list] .item-type { display:none; }
54 -[mode=list] .item-size { display:inline; min-width:7em; }
frontend/main.js
+21 -209
@@ -1,216 +1,28 @@
1 -/**
2 - * @author Massimo Melina <a@rejetto.com>
3 - */
1 +import { h, render, Fragment } from 'https://unpkg.com/preact@latest?module';
2 +import { useState, useEffect } from 'https://unpkg.com/preact@latest/hooks/dist/hooks.module.js?module';
3 +export { h, render, Fragment, useState, useEffect }
4
5 -function loadJS(libs) {
6 - libs = libs.split(',');
7 - for (var i=libs.length; i--;) {
8 - document.write("<script src='/~/"+libs[i]+".js'></script>");
9 - }
10 -}
11 -
12 -loadJS('extending,cs-shared,misc');
13 -
14 -
15 -var socket, currentFolder, listFromServer, currentMode, foldersBefore=1, currentPage= 0;
16 -
17 -// this object will hold all the customizable stuff, that is added in file "tpl.js"
18 -var TPL = function(event) {
19 - var fun = TPL[currentMode+'_'+event] || TPL[event];
20 - if (!fun) return;
21 - var newArgs = [];
22 - for (var i=1,a=arguments,l=a.length; i<l; ++i)
23 - newArgs.push(a[i]);
24 - fun.apply(this, newArgs);
25 -};
26 -
27 -loadJS('frontend/tpl,frontend/vfs');
28 -
29 -// understand the requested folder from the URL
30 -function getURLfolder() {
31 - var sub = location.hash.substr(1);
32 - return sub.startsWith('/') ? sub : location.pathname+sub;
33 -} // getURLfolder
34 -
35 -$(function onJQ(){ // dom ready
36 -
37 - (function speedSetup() {
38 -
39 - function chooseSpeed(q, map, def){
40 - for (var k in map) {
41 - if (q <= k) return map[k];
42 - }
43 - return def;
44 - } // chooseSpeed
45 -
46 - $('#pagination').val( chooseSpeed(benchmark(), {
47 - 5: 500,
48 - 50: 200
49 - }, 50) );
50 - })();
51 -
52 - socket = io.connect(window.location.origin);
53 -
54 - socket.on('connect', function onIO(){ // socket ready
55 - log('connected');
56 -
57 - var saved = JSON.parse(getCookie('settings')) || {};
58 - ['Order','Mode','Pagination'].forEach(function(v){
59 - var lc = v.low();
60 - var update = window['update'+v];
61 - update(saved[lc]); // try to restore last options
62 - // change things at user will
63 - $('#'+lc).change(function(){
64 - update();
65 - redrawItems();
66 - });
67 - });
68 -
69 - loadFolder(getURLfolder(), function onFolder(){ // folder ready
70 -
71 - /* support for the BACK button: when the user clicks the BACK button, the address bar changes going out of
72 - sync with the view. We fix it ASAP. I don't know an event for the address bar, so i'm polling. */
73 - repeat(300, function(){
74 - var shouldBe = getURLfolder();
75 - if (currentFolder != shouldBe) {
76 - loadFolder(shouldBe);
77 - }
78 - });
5 +import { useApi } from './api.js'
6 +//import * as Fluid from 'https://unpkg.com/preact-fluid@latest?module'; console.log(Fluid)
7
80 - }); // don't redraw
81 - });//socket connect
82 -
83 - socket.on('vfs.changed', function(data){
84 - log('vfs.changed',data);
85 - // it would be nicer to update only the changed item, but for now easily reload the whole dir
86 - var folder = data.uri.endsWith('/') ? data.uri : dirname(data.uri);
87 - if (folder === currentFolder) {
88 - loadFolder();
89 - }
90 - });
8 +document.addEventListener('DOMContentLoaded', ()=>
9 + render( h(App), document.body) )
10
92 -});
93 -
94 -// ask the server for the items list of the specified folder, then sort and display
95 -function loadFolder(path /** optional */, cb /** optional */) {
96 - if (path) currentFolder = path;
97 - // breadcrumbs
98 - $('#folder').html(
99 - '<a href="/#"><img src="'+getPicURI('home')+'"/></a>'+
100 - // build pairs text/link
101 - decodeURI(currentFolder).split('/').filter('A').map('["/"+A, "/#"+C.slice(0,B+1).join("/")+"/"]')
102 - // then to html
103 - .map('$("<a>").text(A[0]).attr({href:A[1]})[0].outerHTML').join('')
104 - );
105 -
106 - socket.emit('get list', { path:currentFolder }, function onGetList(reply){
107 - if (showError(reply)) return;
108 - listFromServer = reply; // hold it in a global variable, to not loose it
109 - convertList(reply);
110 - $('#folder-info').html("Items: <span id='num-items'>{0}</span>".x(reply.items.length));
111 - sortItems();
112 - redrawItems();
113 - if (typeof cb == 'function') {
114 - cb();
115 - }
116 - });
117 -} // loadFolder
118 -
119 -function showError(reply) {
120 - if (reply.ok) return false;
121 - if (reply.error) alert('Error: '+reply.error);
122 - return true;
123 -} // showError
124 -
125 -// convert file list format: expands some field-names
126 -function convertList(serverReply) {
127 - if (!serverReply) return;
128 - var a = serverReply.items;
129 - if (!a) return;
130 - for (var i=a.length; i--;) {
131 - var o = a[i]._remapKeys({ n:'label', t:'type', s:'size'});
132 - switch (o.type) {
133 - case undefined: // no type is default type: file
134 - o.type = 'file'; // now continue with the case 'file'
135 - case 'file': // for files, calculate specific type
136 - var t = nameToType(o.label);
137 - if (t) o.type = t;
138 - break;
139 - case 'link':
140 - o.url = o.resource;
141 - break;
142 - }
143 - o.url = o.url || escapeLink(currentFolder+o.label)+(o.type == 'folder' ? '/' : '');
144 - }
145 -} // convertList
146 -
147 -function escapeLink(s) {
148 - //return encodeURI(s)
149 - return s.replace('"','&quot;').replace("'", '&#39;').replace('%','%25')
11 +function App() {
12 + return h(BrowseFiles)
13 }
14
152 -function updateSettingsCookie(settings) {
153 - var v = getCookie('settings');
154 - try { v = JSON.parse(v) }
155 - catch(e) {}
156 - if (!v || typeof v != 'object') v = {};
157 - v._expand(settings);
158 - setCookie('settings', JSON.stringify(v));
159 -} // updateSettingsCookie
160 -
161 -function updateMode(v){
162 - // if no value is passed, then read it from the DOM, otherwise write it in the DOM
163 - if (!v || !v.length) v = $('#mode').val();
164 - else $('#mode').val(v);
165 -
166 - currentMode = v; // global
167 - updateSettingsCookie({ mode: v }); // remember
168 - $('html').attr('mode', v);
169 -} // updateMode
170 -
171 -function updatePagination(v){
172 - // if no value is passed, then read it from the DOM, otherwise write it in the DOM
173 - if (!v || !v.length) v = $('#pagination').val();
174 - else $('#pagination').val(v);
175 -
176 - currentPagination = v; // global
177 - updateSettingsCookie({ pagination: v }); // remember
178 - redrawItems();
179 -} // updatePagination
180 -
181 -function updateOrder(v) {
182 - // if no value is passed, then read it from the DOM, otherwise write it in the DOM
183 - if (!v || !v.length) v = $('#order').val();
184 - else $('#order').val(v);
185 -
186 - currentOrder = v; // global
187 - updateSettingsCookie({ order: v }); // remember
188 - if (v) {
189 - $('#order option[value=""]').remove(); // after we have sorted the items there's no way to return to the original order, so let's hide this option
190 - }
191 - sortItems();
192 -} // updateOrder
15 +function BrowseFiles() {
16 + return h(Fragment, {},
17 + h(Head),
18 + h(FilesList) )
19 +}
20
194 -function sortItems() {
195 - if (!currentOrder || !listFromServer) return; // no job
21 +function Head() {
22 + return null
23 +}
24
197 - listFromServer.items.sort(function cb(a,b,field){
198 - field = field || currentOrder;
199 - if (field != 'type' && foldersBefore) { // if field is 'type', then the folders are always put at the top
200 - var res = -cmp(a['type'] == 'folder', b['type'] == 'folder');
201 - if (res) return res;
202 - }
203 - var va = a[field];
204 - var vb = b[field];
205 - switch (field) {
206 - case 'label':
207 - va=va.low(), vb=vb.low();
208 - break;
209 - case 'type':
210 - if (va == 'folder') va=''; // trick to get folders at the top
211 - if (vb == 'folder') vb='';
212 - break;
213 - }
214 - return cmp(va,vb) || (field=='label' ? 0 : cb(a,b,'label'));
215 - });
216 -} // sortItems
25 +function FilesList() {
26 + const list = useApi('files_list', { path:'/' })
27 + return h('pre', {}, list ? JSON.stringify(list) : 'list')
28 +}
\ No newline at end of file
package-lock.json
+41
@@ -6,10 +6,12 @@
6 "": {
7 "dependencies": {
8 "koa": "^2.13.4",
9 + "koa-mount": "^4.0.0",
10 "koa-static": "^5.0.0"
11 },
12 "devDependencies": {
13 "@types/koa": "^2.13.4",
14 + "@types/koa-mount": "^4.0.1",
15 "@types/koa-static": "^4.0.2",
16 "@types/node": "^16.11.12",
17 "nodemon": "^2.0.15",
@@ -195,6 +197,15 @@
197 "@types/koa": "*"
198 }
199 },
200 + "node_modules/@types/koa-mount": {
201 + "version": "4.0.1",
202 + "resolved": "https://registry.npmjs.org/@types/koa-mount/-/koa-mount-4.0.1.tgz",
203 + "integrity": "sha512-HNeg80CVS9Dfq8dGYqCZZCAUm7g6jPCNJ1ydqVLEJxLrjmeburpvq+lOZkE4rxBZ6O38dr3tj9IA3IfbdoI05w==",
204 + "dev": true,
205 + "dependencies": {
206 + "@types/koa": "*"
207 + }
208 + },
209 "node_modules/@types/koa-send": {
210 "version": "4.1.3",
211 "resolved": "https://registry.npmjs.org/@types/koa-send/-/koa-send-4.1.3.tgz",
@@ -1214,6 +1225,18 @@
1225 "node": ">= 10"
1226 }
1227 },
1228 + "node_modules/koa-mount": {
1229 + "version": "4.0.0",
1230 + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz",
1231 + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==",
1232 + "dependencies": {
1233 + "debug": "^4.0.1",
1234 + "koa-compose": "^4.1.0"
1235 + },
1236 + "engines": {
1237 + "node": ">= 7.6.0"
1238 + }
1239 + },
1240 "node_modules/koa-send": {
1241 "version": "5.0.1",
1242 "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz",
@@ -2246,6 +2269,15 @@
2269 "@types/koa": "*"
2270 }
2271 },
2272 + "@types/koa-mount": {
2273 + "version": "4.0.1",
2274 + "resolved": "https://registry.npmjs.org/@types/koa-mount/-/koa-mount-4.0.1.tgz",
2275 + "integrity": "sha512-HNeg80CVS9Dfq8dGYqCZZCAUm7g6jPCNJ1ydqVLEJxLrjmeburpvq+lOZkE4rxBZ6O38dr3tj9IA3IfbdoI05w==",
2276 + "dev": true,
2277 + "requires": {
2278 + "@types/koa": "*"
2279 + }
2280 + },
2281 "@types/koa-send": {
2282 "version": "4.1.3",
2283 "resolved": "https://registry.npmjs.org/@types/koa-send/-/koa-send-4.1.3.tgz",
@@ -3025,6 +3057,15 @@
3057 "koa-compose": "^4.1.0"
3058 }
3059 },
3060 + "koa-mount": {
3061 + "version": "4.0.0",
3062 + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz",
3063 + "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==",
3064 + "requires": {
3065 + "debug": "^4.0.1",
3066 + "koa-compose": "^4.1.0"
3067 + }
3068 + },
3069 "koa-send": {
3070 "version": "5.0.1",
3071 "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz",
package.json
+2
@@ -5,10 +5,12 @@
5 },
6 "dependencies": {
7 "koa": "^2.13.4",
8 + "koa-mount": "^4.0.0",
9 "koa-static": "^5.0.0"
10 },
11 "devDependencies": {
12 "@types/koa": "^2.13.4",
13 + "@types/koa-mount": "^4.0.1",
14 "@types/koa-static": "^4.0.2",
15 "@types/node": "^16.11.12",
16 "nodemon": "^2.0.15",
src/apis.ts new
+7
@@ -0,0 +1,7 @@
1 +export default {
2 + files_list(){
3 + return {
4 + list: ['a','b']
5 + }
6 + }
7 +}
\ No newline at end of file
src/index.ts
+41 -14
@@ -1,19 +1,46 @@
1 import Koa from 'koa'
2 import serve from 'koa-static'
3 +import send from 'koa-send'
4 +import mount from 'koa-mount'
5 +import apis from './apis'
6
7 const srv = new Koa()
5 -const frontend = serve('frontend')
6 -srv.use((ctx,next) => {
7 - const rq = ctx.request
8 - if (rq.method === 'GET' && rq.originalUrl === '/c') {
9 - ctx.body = 'ciao3'
10 - }
11 - else
12 - return frontend(ctx,next)
13 -})
8 +srv.use(preventCrossDir())
9 +srv.use(serveRoot())
10 +srv.use(mount('/~/api/', api(apis)))
11 +srv.use(mount('/~/', serve('frontend')))
12 +
13 srv.on('error', err => console.error('server error', err))
15 -srv.listen(3000, ()=> console.log('running'))
16 -/*
17 -body
18 -set(k,v)
19 -* */
\ No newline at end of file
14 +srv.listen(80, ()=> console.log('running',new Date().toLocaleString()))
15 +
16 +function preventCrossDir() : Koa.Middleware {
17 + return async (ctx, next) => {
18 + // @ts-ignore
19 + ctx.assert(!ctx.originalUrl.includes('..'), 400, 'cross-dir');
20 + await next()
21 + }
22 +}
23 +
24 +function serveRoot() : Koa.Middleware {
25 + return async (ctx, next) => {
26 + if (ctx.method === 'GET' && ctx.path === '/')
27 + await send(ctx, 'frontend/index.html')
28 + await next()
29 + }
30 +}
31 +
32 +type ApiHandler = (ctx:any) => any
33 +type ApiHandlers = Record<string, ApiHandler>
34 +
35 +function api(apis: ApiHandlers) : Koa.Middleware {
36 + return async (ctx, next) => {
37 + console.log('API',ctx.method, ctx.path)
38 + // @ts-ignore
39 + ctx.assert(ctx.path in apis, 404, 'invalid api')
40 + const cb = (apis as any)[ctx.path]
41 + const res = await cb(ctx)
42 + if (res)
43 + ctx.body = res
44 + await next()
45 + }
46 +}