Move JS client to a separate repo
This commit is contained in:
parent
8b32a745a4
commit
d9986c5434
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
.gitignore
|
||||
data/
|
||||
www/webdav.css
|
||||
www/webdav.js
|
||||
config.local.php
|
||||
error.log
|
||||
debug.log
|
||||
lib/KD2/
|
6
Makefile
6
Makefile
|
@ -1,4 +1,8 @@
|
|||
deps:
|
||||
js-deps:
|
||||
wget -O www/webdav.js https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/webdav.js
|
||||
wget -O www/webdav.css https://raw.githubusercontent.com/kd2org/webdav-manager.js/main/webdav.css
|
||||
|
||||
php-deps:
|
||||
wget -O lib/KD2/WebDAV.php 'https://fossil.kd2.org/kd2fw/doc/tip/src/lib/KD2/WebDAV.php'
|
||||
wget -O lib/KD2/WebDAV_NextCloud.php 'https://fossil.kd2.org/kd2fw/doc/tip/src/lib/KD2/WebDAV_NextCloud.php'
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ Although this is a demo, this can be used as a simple but powerful file sharing
|
|||
|
||||
This server features:
|
||||
|
||||
* User-friendly directory listings for file browsing with a web browser:
|
||||
* Upload directly from browser
|
||||
* User-friendly directory listings for file browsing with a web browser, using [WebDAV Manager.js](https://github.com/kd2org/webdav-manager.js)
|
||||
* Upload directly from browser, using paste, or drag and drop
|
||||
* Rename
|
||||
* Delete
|
||||
* Create and edit text file
|
||||
|
|
|
@ -17,6 +17,15 @@ class Server
|
|||
|
||||
public function route(?string $uri = null): bool
|
||||
{
|
||||
header('Access-Control-Allow-Origin: *', true);
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? null;
|
||||
|
||||
// Always say YES to OPTIONS
|
||||
if ($method == 'OPTIONS') {
|
||||
$this->dav->http_options();
|
||||
return true;
|
||||
}
|
||||
|
||||
$nc = new NextCloud($this->dav, $this->users);
|
||||
|
||||
if ($r = $nc->route($uri)) {
|
||||
|
|
|
@ -6,15 +6,24 @@ use KD2\WebDAV\Server as WebDAV_Server;
|
|||
|
||||
class WebDAV extends WebDAV_Server
|
||||
{
|
||||
protected function html_directory(string $uri, iterable $list, array $strings = self::LANGUAGE_STRINGS): ?string
|
||||
protected function html_directory(string $uri, iterable $list): ?string
|
||||
{
|
||||
$out = parent::html_directory($uri, $list, $strings);
|
||||
$out = parent::html_directory($uri, $list);
|
||||
|
||||
if (null !== $out) {
|
||||
$out = str_replace('</head>', sprintf('<link rel="stylesheet" type="text/css" href="%sfiles.css" /></head>', WWW_URL), $out);
|
||||
$out = str_replace('</body>', sprintf('<script type="text/javascript" src="%sfiles.js"></script></body>', WWW_URL), $out);
|
||||
$out = str_replace('<body>', sprintf('<body style="opacity: 0"><script type="text/javascript" src="%swebdav.js"></script>', WWW_URL), $out);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function http_options(): void
|
||||
{
|
||||
parent::http_options();
|
||||
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
header('Access-Control-Allow-Headers: Authorization, *');
|
||||
header('Access-Control-Allow-Methods: GET,HEAD,PUT,DELETE,COPY,MOVE,PROPFIND,MKCOL,LOCK,UNLOCK');
|
||||
}
|
||||
}
|
||||
|
|
173
www/files.css
173
www/files.css
|
@ -1,173 +0,0 @@
|
|||
body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
input[type=button], input[type=submit] {
|
||||
font-size: 1.2em;
|
||||
padding: .3em .5em;
|
||||
margin: 0 .3em;
|
||||
border: none;
|
||||
background: #ccc;
|
||||
border-radius: .2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
td input[type=button], td input[type=submit] {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
input[type=text], textarea {
|
||||
font-size: 1.2em;
|
||||
padding: .3em .5em;
|
||||
border: none;
|
||||
background: #fff;
|
||||
border-radius: .2em;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
box-shadow: 0px 0px 5px blue;
|
||||
outline: 1px solid #999;
|
||||
}
|
||||
|
||||
input[type=button]:hover, input[type=submit]:hover {
|
||||
color: darkred;
|
||||
text-decoration: underline;
|
||||
background: #fff;
|
||||
box-shadow: 0px 0px 5px #000;
|
||||
}
|
||||
|
||||
.close {
|
||||
text-align: right;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close input {
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
dialog {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
bottom: 1em;
|
||||
left: 1em;
|
||||
box-shadow: 0px 0px 5px #000;
|
||||
background: #eee;
|
||||
border: none;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
dialog form div {
|
||||
clear: both;
|
||||
margin: 2em 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.a {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#mdp div, #mdp textarea {
|
||||
width: calc(100% - 1em);
|
||||
padding: .5em;
|
||||
font-size: 1em;
|
||||
height: calc(100% - 1em);
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#md {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#mdp {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: .2em;
|
||||
background: #ddd;
|
||||
height: 82vh;
|
||||
}
|
||||
|
||||
dialog.preview {
|
||||
height: calc(100%);
|
||||
width: calc(100%);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: #ddd;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
iframe, .md_preview {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: 0;
|
||||
width: calc(100vw);
|
||||
height: calc(100% - 2em);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview form {
|
||||
height: calc(100% - 2em);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview form > div {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.md_preview {
|
||||
width: calc(100vw - 2em);
|
||||
height: calc(100vh - 2em);
|
||||
padding: 1em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preview .close {
|
||||
height: 2em;
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
color: #000;
|
||||
box-shadow: 0px 0px 5px #000;
|
||||
}
|
||||
|
||||
.preview img {
|
||||
max-width: 95%;
|
||||
max-height: 95%;
|
||||
}
|
||||
|
||||
input[name=rename] {
|
||||
width: 30em;
|
||||
}
|
218
www/files.js
218
www/files.js
|
@ -1,218 +0,0 @@
|
|||
// Microdown
|
||||
// https://github.com/commit-intl/micro-down
|
||||
const microdown=function(){function l(n,e,r){return"<"+n+(r?" "+Object.keys(r).map(function(n){return r[n]?n+'="'+(a(r[n])||"")+'"':""}).join(" "):"")+">"+e+"</"+n+">"}function c(n,e){return e=n.match(/^[+-]/m)?"ul":"ol",n?"<"+e+">"+n.replace(/(?:[+-]|\d+\.) +(.*)\n?(([ \t].*\n?)*)/g,function(n,e,r){return"<li>"+g(e+"\n"+(t=r||"").replace(new RegExp("^"+(t.match(/^\s+/)||"")[0],"gm"),"").replace(o,c))+"</li>";var t})+"</"+e+">":""}function e(r,t,u,c){return function(n,e){return n=n.replace(t,u),l(r,c?c(n):n)}}function t(n,u){return f(n,[/<!--((.|\n)*?)-->/g,"\x3c!--$1--\x3e",/^("""|```)(.*)\n((.*\n)*?)\1/gm,function(n,e,r,t){return'"""'===e?l("div",p(t,u),{class:r}):u&&u.preCode?l("pre",l("code",a(t),{class:r})):l("pre",a(t),{class:r})},/(^>.*\n?)+/gm,e("blockquote",/^> ?(.*)$/gm,"$1",r),/((^|\n)\|.+)+/g,e("table",/^.*(\n\|---.*?)?$/gm,function(n,t){return e("tr",/\|(-?)([^|]*)\1(\|$)?/gm,function(n,e,r){return l(e||t?"th":"td",g(r))})(n.slice(0,n.length-(t||"").length))}),o,c,/#\[([^\]]+?)]/g,'<a name="$1"></a>',/^(#+) +(.*)(?:$)/gm,function(n,e,r){return l("h"+e.length,g(r))},/^(===+|---+)(?=\s*$)/gm,"<hr>"],p,u)}var i=this,a=function(n){return n?n.replace(/"/g,""").replace(/</g,"<").replace(/>/g,">"):""},o=/(?:(^|\n)([+-]|\d+\.) +(.*(\n[ \t]+.*)*))+/g,g=function c(n,i){var o=[];return n=(n||"").trim().replace(/`([^`]*)`/g,function(n,e){return"\\"+o.push(l("code",a(e)))}).replace(/[!&]?\[([!&]?\[.*?\)|[^\]]*?)]\((.*?)( .*?)?\)|(\w+:\/\/[$\-.+!*'()/,\w]+)/g,function(n,e,r,t,u){return u?i?n:"\\"+o.push(l("a",u,{href:u})):"&"==n[0]?(e=e.match(/^(.+),(.+),([^ \]]+)( ?.+?)?$/),"\\"+o.push(l("iframe","",{width:e[1],height:e[2],frameborder:e[3],class:e[4],src:r,title:t}))):"\\"+o.push("!"==n[0]?l("img","",{src:r,alt:e,title:t}):l("a",c(e,1),{href:r,title:t}))}),n=function r(n){return n.replace(/\\(\d+)/g,function(n,e){return r(o[Number.parseInt(e)-1])})}(i?n:r(n))},r=function t(n){return f(n,[/([*_]{1,3})((.|\n)+?)\1/g,function(n,e,r){return e=e.length,r=t(r),1<e&&(r=l("strong",r)),e%2&&(r=l("em",r)),r},/(~{1,3})((.|\n)+?)\1/g,function(n,e,r){return l([,"u","s","del"][e.length],t(r))},/ \n|\n /g,"<br>"],t)},f=function(n,e,r,t){for(var u,c=0;c<e.length;){if(u=e[c++].exec(n))return r(n.slice(0,u.index),t)+("string"==typeof e[c]?e[c].replace(/\$(\d)/g,function(n,e){return u[e]}):e[c].apply(i,u))+r(n.slice(u.index+u[0].length),t);c++}return n},p=function(n,e){n=n.replace(/[\r\v\b\f]/g,"").replace(/\\./g,function(n){return"&#"+n.charCodeAt(1)+";"});var r=t(n,e);return r!==n||r.match(/^[\s\n]*$/i)||(r=g(r).replace(/((.|\n)+?)(\n\n+|$)/g,function(n,e){return l("p",e)})),r.replace(/&#(\d+);/g,function(n,e){return String.fromCharCode(parseInt(e))})};return{parse:p,block:t,inline:r,inlineBlock:g}}();
|
||||
|
||||
function html(unsafe)
|
||||
{
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
const url = location.pathname;
|
||||
|
||||
const PREVIEW_TYPES = /^image\/(png|webp|svg|jpeg|jpg|gif|png)|^application\/pdf|^text\/|^audio\/|^video\//;
|
||||
|
||||
const upload_form = `<div class="a">
|
||||
<input class="mkdir" type="button" value="New directory" />
|
||||
<input type="file" style="display: none;" />
|
||||
<input class="mkfile" type="button" value="New text file" />
|
||||
<input class="upload" type="button" value="Upload file" />
|
||||
</div>`;
|
||||
|
||||
const file_buttons = `<td><input class="rename" type="button" value="Rename" />
|
||||
<input class="delete" type="button" value="Delete" /></td>`;
|
||||
|
||||
const edit_button = `<input class="edit" type="button" value="Edit" />`;
|
||||
|
||||
const mkdir_dialog = `<input type="text" name="mkdir" placeholder="Directory name" />`;
|
||||
const mkfile_dialog = `<input type="text" name="mkfile" placeholder="File name" />`;
|
||||
const rename_dialog = `<input type="text" name="rename" placeholder="New file name" />`;
|
||||
const edit_dialog = `<textarea name="edit" cols="70" rows="30"></textarea>`;
|
||||
const markdown_dialog = `<div id="mdp"><textarea name="edit" cols="70" rows="30"></textarea><div id="md"></div></div>`;
|
||||
const delete_dialog = `<h3>Confirm delete ?</h3>`;
|
||||
|
||||
const dialog_tpl = `<dialog open><p class="close"><input type="button" value="✖ Close" class="close" /></p><form><div>%s</div>%b</form></dialog>`;
|
||||
|
||||
const req = (method, url, body, headers) => {
|
||||
fetch(url, {method, body, headers}).then((r) => location.reload());
|
||||
return false;
|
||||
};
|
||||
|
||||
var evt, evt2;
|
||||
|
||||
const dialog = (hash, html, ok_btn = true) => {
|
||||
location.hash = '#' + hash;
|
||||
var tpl = dialog_tpl.replace(/%b/, ok_btn ? '<p><input type="submit" value="OK" /></p>' : '');
|
||||
$('body').insertAdjacentHTML('beforeend', tpl.replace(/%s/, html));
|
||||
$('.close input').onclick = close;
|
||||
evt = window.addEventListener('keyup', (e) => {
|
||||
if (e.key != 'Escape') return;
|
||||
e.preventDefault();
|
||||
close();
|
||||
return false;
|
||||
});
|
||||
evt2 = window.addEventListener('hashchange', (e) => {
|
||||
if (location.hash == '') {
|
||||
close();
|
||||
}
|
||||
});
|
||||
if (a = $('dialog form input, dialog form textarea')) a.focus();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (!$('dialog')) return;
|
||||
$('dialog').remove();
|
||||
window.removeEventListener('keyup', evt);
|
||||
evt = null;
|
||||
window.removeEventListener('hashchange', evt2);
|
||||
evt2 = null;
|
||||
location.hash = '';
|
||||
};
|
||||
|
||||
const $ = (a) => document.querySelector(a);
|
||||
|
||||
$('h1').insertAdjacentHTML('afterend', upload_form);
|
||||
|
||||
Array.from($('table').rows).forEach((tr) => {
|
||||
var $$ = (a) => tr.querySelector(a);
|
||||
var file_url = $$('a').href;
|
||||
var file_name = $$('a').innerText;
|
||||
var dir = $$('[colspan]');
|
||||
var type = !dir ? $$('td:nth-child(3)').innerText : 'dir';
|
||||
|
||||
// For back link
|
||||
if (dir && $$('a').getAttribute('href').indexOf('..') != -1) {
|
||||
dir.setAttribute('colspan', 4);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add rename/delete buttons
|
||||
tr.insertAdjacentHTML('beforeend', file_buttons);
|
||||
|
||||
if (type.match(PREVIEW_TYPES)) {
|
||||
$$('a').onclick = () => {
|
||||
if (file_url.match(/\.md$/)) {
|
||||
dialog(file_name, '<div class="md_preview"></div>', false);
|
||||
fetch(file_url).then((r) => r.text().then((t) => {
|
||||
$('.md_preview').innerHTML = microdown.parse(html(t));
|
||||
}));
|
||||
}
|
||||
else if (type.match(/^image\//)) {
|
||||
dialog(file_name, `<img src="${file_url}" />`, false);
|
||||
}
|
||||
else if (type.match(/^audio\//)) {
|
||||
dialog(file_name, `<audio controls="true" autoplay="true" src="${file_url}" />`, false);
|
||||
}
|
||||
else if (type.match(/^video\//)) {
|
||||
dialog(file_name, `<video controls="true" autoplay="true" src="${file_url}" />`, false);
|
||||
}
|
||||
else {
|
||||
dialog(file_name, `<iframe src="${file_url}" />`, false);
|
||||
}
|
||||
$('dialog').className = 'preview';
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
if (type.match(/^text\/|application\/x-empty/)) {
|
||||
$$('td:nth-child(5)').insertAdjacentHTML('beforeend', edit_button);
|
||||
|
||||
$$('.edit').onclick = (e) => {
|
||||
fetch(file_url).then((r) => r.text().then((t) => {
|
||||
let md = file_url.match(/\.md$/);
|
||||
dialog(file_name, md ? markdown_dialog : edit_dialog);
|
||||
var txt = $('textarea[name=edit]');
|
||||
txt.value = t;
|
||||
|
||||
// Markdown editor
|
||||
if (md) {
|
||||
let pre = $('#md');
|
||||
|
||||
txt.oninput = () => {
|
||||
pre.innerHTML = microdown.parse(html(txt.value));
|
||||
};
|
||||
|
||||
txt.oninput();
|
||||
|
||||
// Sync scroll, not perfect but better than nothing
|
||||
txt.onscroll = (e) => {
|
||||
var p = e.target.scrollTop / (e.target.scrollHeight - e.target.offsetHeight);
|
||||
var target = e.target == pre ? txt : pre;
|
||||
target.scrollTop = p * (target.scrollHeight - target.offsetHeight);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
document.forms[0].onsubmit = () => {
|
||||
var content = txt.value;
|
||||
|
||||
return req('PUT', file_url, content);
|
||||
};
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
$$('.delete').onclick = (e) => {
|
||||
dialog('delete', delete_dialog);
|
||||
document.forms[0].onsubmit = () => {
|
||||
return req('DELETE', file_url, '');
|
||||
};
|
||||
};
|
||||
|
||||
$$('.rename').onclick = () => {
|
||||
dialog('rename', rename_dialog);
|
||||
let t = $('input[name=rename]');
|
||||
t.value = file_name;
|
||||
t.focus();
|
||||
t.selectionStart = 0;
|
||||
t.selectionEnd = file_name.lastIndexOf('.');
|
||||
document.forms[0].onsubmit = () => {
|
||||
var name = t.value;
|
||||
|
||||
if (!name) return false;
|
||||
|
||||
return req('MOVE', file_url, '', {'Destination': location.pathname + name});
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
$('.mkdir').onclick = () => {
|
||||
dialog('mkdir', mkdir_dialog);
|
||||
document.forms[0].onsubmit = () => {
|
||||
var name = $('input[name=mkdir]').value;
|
||||
|
||||
if (!name) return false;
|
||||
|
||||
return req('MKCOL', url + name);
|
||||
};
|
||||
};
|
||||
|
||||
$('.mkfile').onclick = () => {
|
||||
dialog('mkfile', mkfile_dialog);
|
||||
document.forms[0].onsubmit = () => {
|
||||
var name = $('input[name=mkfile]').value;
|
||||
|
||||
if (!name) return false;
|
||||
|
||||
return req('PUT', url + name, '');
|
||||
};
|
||||
};
|
||||
|
||||
var fi = $('input[type=file]');
|
||||
|
||||
$('.upload').onclick = () => fi.click();
|
||||
|
||||
fi.onchange = () => {
|
||||
if (!fi.files.length) return;
|
||||
|
||||
var body = new Blob(fi.files);
|
||||
var name = fi.files[0].name;
|
||||
|
||||
return req('PUT', url + name, body);
|
||||
};
|
Loading…
Reference in a new issue