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('"','"').replace("'", ''').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
+}