lfss 0.12.3__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/changelog.md +17 -0
- frontend/api.js +90 -9
- frontend/base.css +33 -0
- frontend/edit.css +102 -0
- frontend/edit.html +29 -0
- frontend/edit.js +130 -0
- frontend/login.css +1 -0
- frontend/scripts.js +50 -3
- frontend/styles.css +10 -34
- lfss/api/__init__.py +9 -200
- lfss/api/bundle.py +201 -0
- lfss/api/connector.py +56 -52
- lfss/cli/__init__.py +8 -1
- lfss/cli/cli.py +57 -8
- lfss/cli/cli_lib.py +2 -4
- lfss/eng/database.py +125 -64
- lfss/eng/datatype.py +19 -0
- lfss/eng/error.py +14 -2
- lfss/svc/app.py +2 -0
- lfss/svc/app_base.py +6 -2
- lfss/svc/app_native.py +35 -21
- lfss/svc/app_native_user.py +28 -0
- lfss/svc/common_impl.py +30 -9
- {lfss-0.12.3.dist-info → lfss-0.13.0.dist-info}/METADATA +1 -1
- {lfss-0.12.3.dist-info → lfss-0.13.0.dist-info}/RECORD +27 -21
- {lfss-0.12.3.dist-info → lfss-0.13.0.dist-info}/WHEEL +0 -0
- {lfss-0.12.3.dist-info → lfss-0.13.0.dist-info}/entry_points.txt +0 -0
docs/changelog.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
|
2
|
+
## 0.13
|
3
|
+
|
4
|
+
### 0.13.0
|
5
|
+
- Break: Py client GET API not handle 404 as None.
|
6
|
+
- Break & Fix: Move will transfer ownership.
|
7
|
+
- Delete user transfer ownership of files outside their directory.
|
8
|
+
- In favor of `Client` instead of `Connector` for API client class.
|
9
|
+
- Add `/move` and `/set-perm` api for moving files and setting file permission.
|
10
|
+
- Add `/user` prefix for user related api.
|
11
|
+
- Add CLI command `mv`, `cp`, and `perm`.
|
12
|
+
- Add `name` method for record classes.
|
13
|
+
- Improve assertion error handling.
|
14
|
+
- Add user storage query api and query exists api.
|
15
|
+
- Non-exist user path will return 404 instead of 400.
|
16
|
+
- Frontend add editor page for text files.
|
17
|
+
|
1
18
|
## 0.12
|
2
19
|
|
3
20
|
### 0.12.3
|
frontend/api.js
CHANGED
@@ -59,13 +59,51 @@ async function fmtFailedResponse(res){
|
|
59
59
|
export default class Connector {
|
60
60
|
|
61
61
|
constructor(){
|
62
|
+
// get default endpoint from url
|
63
|
+
const searchParams = (new URL(window.location.href)).searchParams;
|
64
|
+
const defaultToken = searchParams.get('lfss-token') || '';
|
65
|
+
const defaultEndpoint = searchParams.get('lfss-endpoint') ||
|
66
|
+
(searchParams.origin == 'null' ? 'http://localhost:8000' : searchParams.origin);
|
67
|
+
|
62
68
|
/** @type {Config} */
|
63
69
|
this.config = {
|
64
|
-
endpoint:
|
65
|
-
token:
|
70
|
+
endpoint: defaultEndpoint,
|
71
|
+
token: defaultToken
|
66
72
|
}
|
67
73
|
}
|
68
74
|
|
75
|
+
async getText(path){
|
76
|
+
if (path.startsWith('/')){ path = path.slice(1); }
|
77
|
+
const res = await fetch(this.config.endpoint + '/' + path, {
|
78
|
+
method: 'GET',
|
79
|
+
headers: {
|
80
|
+
"Authorization": 'Bearer ' + this.config.token
|
81
|
+
},
|
82
|
+
});
|
83
|
+
if (res.status != 200){
|
84
|
+
throw new Error(`Failed to get file, status code: ${res.status}, message: ${await fmtFailedResponse(res)}`);
|
85
|
+
}
|
86
|
+
return await res.text();
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* @param {string} path - the path to the file (url)
|
91
|
+
* @param {string} text - the text content to upload
|
92
|
+
* @param {Object} [options] - Optional upload configuration.
|
93
|
+
* @param {'abort' | 'overwrite' | 'skip'} [options.conflict='abort'] - Conflict resolution strategy:
|
94
|
+
* @param {string} [options.type='text/plain'] - The MIME type of the text file.
|
95
|
+
* NOTE: type is only a hint, the backend may ignore it...
|
96
|
+
*/
|
97
|
+
async putText(path, text, {
|
98
|
+
conflict = 'abort',
|
99
|
+
type = 'text/plain'
|
100
|
+
}){
|
101
|
+
const file = new Blob([text], { type: type });
|
102
|
+
return await this.put(path, file, {
|
103
|
+
conflict: conflict
|
104
|
+
});
|
105
|
+
}
|
106
|
+
|
69
107
|
/**
|
70
108
|
* @param {string} path - the path to the file (url)
|
71
109
|
* @param {File} file - the file to upload
|
@@ -393,7 +431,7 @@ export default class Connector {
|
|
393
431
|
* @returns {Promise<UserRecord>} - the promise of the request
|
394
432
|
*/
|
395
433
|
async whoami(){
|
396
|
-
const res = await fetch(this.config.endpoint + '/_api/whoami', {
|
434
|
+
const res = await fetch(this.config.endpoint + '/_api/user/whoami', {
|
397
435
|
method: 'GET',
|
398
436
|
headers: {
|
399
437
|
'Authorization': 'Bearer ' + this.config.token
|
@@ -405,13 +443,36 @@ export default class Connector {
|
|
405
443
|
return await res.json();
|
406
444
|
};
|
407
445
|
|
446
|
+
/**
|
447
|
+
* List peer users
|
448
|
+
* @param {Object} [options] - Optional configuration.
|
449
|
+
* @param {number} [options.level=1] - The level of users to list. 1 for at leaset read permission
|
450
|
+
* @param {boolean} [options.incoming=false] - Whether to list incoming users (can access me) or outgoing users (I can access)
|
451
|
+
* @returns {Promise<UserRecord[]>} - the promise of the request
|
452
|
+
*/
|
453
|
+
async listPeers({ level = 1, incoming = false } = {}){
|
454
|
+
const dst = new URL(this.config.endpoint + '/_api/user/list-peers');
|
455
|
+
dst.searchParams.append('level', level);
|
456
|
+
dst.searchParams.append('incoming', incoming);
|
457
|
+
const res = await fetch(dst.toString(), {
|
458
|
+
method: 'GET',
|
459
|
+
headers: {
|
460
|
+
'Authorization': 'Bearer ' + this.config.token
|
461
|
+
},
|
462
|
+
});
|
463
|
+
if (res.status != 200){
|
464
|
+
throw new Error('Failed to list peer users, status code: ' + res.status);
|
465
|
+
}
|
466
|
+
return await res.json();
|
467
|
+
}
|
468
|
+
|
408
469
|
/**
|
409
470
|
* @param {string} path - file path(url)
|
410
471
|
* @param {number} permission - please refer to the permMap
|
411
472
|
*/
|
412
473
|
async setFilePermission(path, permission){
|
413
474
|
if (path.startsWith('/')){ path = path.slice(1); }
|
414
|
-
const dst = new URL(this.config.endpoint + '/_api/
|
475
|
+
const dst = new URL(this.config.endpoint + '/_api/set-perm');
|
415
476
|
dst.searchParams.append('path', path);
|
416
477
|
dst.searchParams.append('perm', permission);
|
417
478
|
const res = await fetch(dst.toString(), {
|
@@ -432,9 +493,9 @@ export default class Connector {
|
|
432
493
|
async move(path, newPath){
|
433
494
|
if (path.startsWith('/')){ path = path.slice(1); }
|
434
495
|
if (newPath.startsWith('/')){ newPath = newPath.slice(1); }
|
435
|
-
const dst = new URL(this.config.endpoint + '/_api/
|
436
|
-
dst.searchParams.append('
|
437
|
-
dst.searchParams.append('
|
496
|
+
const dst = new URL(this.config.endpoint + '/_api/move');
|
497
|
+
dst.searchParams.append('src', path);
|
498
|
+
dst.searchParams.append('dst', newPath);
|
438
499
|
const res = await fetch(dst.toString(), {
|
439
500
|
method: 'POST',
|
440
501
|
headers: {
|
@@ -496,8 +557,28 @@ export async function listPath(conn, path, {
|
|
496
557
|
} = {}){
|
497
558
|
|
498
559
|
if (path === '/' || path === ''){
|
499
|
-
// this handles separate case for the root directory
|
500
|
-
|
560
|
+
// this handles separate case for the root directory
|
561
|
+
const dirnames = [
|
562
|
+
(await conn.whoami()).username + '/'
|
563
|
+
].concat(
|
564
|
+
(await conn.listPeers({ level: 1, incoming: false })).map(u => u.username + '/')
|
565
|
+
)
|
566
|
+
return [
|
567
|
+
{
|
568
|
+
dirs: dirnames.map(dirname => ({
|
569
|
+
url: dirname,
|
570
|
+
size: -1,
|
571
|
+
create_time: '',
|
572
|
+
update_time: '',
|
573
|
+
access_time: '',
|
574
|
+
n_files: -1
|
575
|
+
})),
|
576
|
+
files: []
|
577
|
+
}, {
|
578
|
+
dirs: dirnames.length,
|
579
|
+
files: 0
|
580
|
+
}
|
581
|
+
]
|
501
582
|
}
|
502
583
|
|
503
584
|
orderBy = orderBy == 'none' ? '' : orderBy;
|
frontend/base.css
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
html {
|
4
|
+
overflow: -moz-scrollbars-vertical;
|
5
|
+
overflow-y: scroll;
|
6
|
+
}
|
7
|
+
|
8
|
+
body{
|
9
|
+
font-family: Arial, sans-serif;
|
10
|
+
background-color: #f1f1f1;
|
11
|
+
display: flex;
|
12
|
+
justify-content: center;
|
13
|
+
align-items: center;
|
14
|
+
}
|
15
|
+
|
16
|
+
input[type=button], button{
|
17
|
+
background-color: #195f8b;
|
18
|
+
color: white;
|
19
|
+
padding: 0.8rem;
|
20
|
+
margin: 0;
|
21
|
+
border: none;
|
22
|
+
border-radius: 0.25rem;
|
23
|
+
cursor: pointer;
|
24
|
+
}
|
25
|
+
|
26
|
+
input[type=text], input[type=password]
|
27
|
+
{
|
28
|
+
width: 100%;
|
29
|
+
padding: 0.75rem;
|
30
|
+
border: 1px solid #ccc;
|
31
|
+
border-radius: 0.25rem;
|
32
|
+
height: 1rem;
|
33
|
+
}
|
frontend/edit.css
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
@import url('./base.css');
|
2
|
+
|
3
|
+
body{
|
4
|
+
font-family: Arial, sans-serif;
|
5
|
+
background-color: #f1f1f1;
|
6
|
+
display: flex;
|
7
|
+
flex-direction: column;
|
8
|
+
align-items: center;
|
9
|
+
margin: 0;
|
10
|
+
padding: 0;
|
11
|
+
}
|
12
|
+
|
13
|
+
div#header {
|
14
|
+
top: 0;
|
15
|
+
left: 0;
|
16
|
+
right: 0;
|
17
|
+
width: 100%;
|
18
|
+
|
19
|
+
|
20
|
+
display: flex;
|
21
|
+
flex-direction: row;
|
22
|
+
align-items: center;
|
23
|
+
gap: 0.5rem;
|
24
|
+
|
25
|
+
background-color: white;
|
26
|
+
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
27
|
+
padding-block: 0.5rem;
|
28
|
+
padding-inline: 1rem;
|
29
|
+
z-index: 1000;
|
30
|
+
}
|
31
|
+
|
32
|
+
div#header .left {
|
33
|
+
display: flex;
|
34
|
+
flex-direction: row;
|
35
|
+
align-items: center;
|
36
|
+
gap: 0.5rem;
|
37
|
+
flex-grow: 1;
|
38
|
+
padding-inline: 0.5rem;
|
39
|
+
}
|
40
|
+
|
41
|
+
div#header .right {
|
42
|
+
display: flex;
|
43
|
+
flex-direction: row;
|
44
|
+
align-items: center;
|
45
|
+
gap: 0.5rem;
|
46
|
+
padding-inline: 0.5rem;
|
47
|
+
}
|
48
|
+
|
49
|
+
span.unicode-btn{
|
50
|
+
display: flex;
|
51
|
+
justify-content: center;
|
52
|
+
align-items: center;
|
53
|
+
left: 1.5rem;
|
54
|
+
width: 1rem;
|
55
|
+
padding: 0.25rem 0.5rem;
|
56
|
+
border-radius: 0.5rem;
|
57
|
+
background-color: rgb(189, 203, 211);
|
58
|
+
cursor: pointer;
|
59
|
+
}
|
60
|
+
|
61
|
+
#file-path-label {
|
62
|
+
font-weight: bold;
|
63
|
+
font-size: 1.2rem;
|
64
|
+
display: flex;
|
65
|
+
align-items: center;
|
66
|
+
gap: 0.25rem;
|
67
|
+
}
|
68
|
+
#new-hint{
|
69
|
+
font-size: small;
|
70
|
+
font-weight: bold;
|
71
|
+
color: green;
|
72
|
+
margin-left: 0.25rem;
|
73
|
+
background-color: #e0ffe0;
|
74
|
+
border-radius: 0.25rem;
|
75
|
+
padding: 0.1rem 0.25rem;
|
76
|
+
user-select: none;
|
77
|
+
pointer-events: none;
|
78
|
+
}
|
79
|
+
|
80
|
+
textarea#editor{
|
81
|
+
width: max(90vw, 500px);
|
82
|
+
height: 80vh;
|
83
|
+
margin-top: 1rem;
|
84
|
+
padding: 1rem;
|
85
|
+
font-family: 'Courier New', Courier, monospace;
|
86
|
+
font-size: 1rem;
|
87
|
+
border: 1px solid #ccc;
|
88
|
+
border-radius: 0.25rem;
|
89
|
+
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
90
|
+
resize: none;
|
91
|
+
outline: none;
|
92
|
+
}
|
93
|
+
|
94
|
+
#save-hint{
|
95
|
+
font-size: 0.9rem;
|
96
|
+
color: white;
|
97
|
+
opacity: 0;
|
98
|
+
transition: opacity 0.3s ease-in-out;
|
99
|
+
background-color: green;
|
100
|
+
padding: 0.25rem 0.5rem;
|
101
|
+
border-radius: 0.5rem;
|
102
|
+
}
|
frontend/edit.html
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>LFSS-Editor</title>
|
7
|
+
<link rel="stylesheet" href="./edit.css">
|
8
|
+
<link rel="stylesheet" href="./login.css">
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
<div id="header">
|
13
|
+
<div class="left">
|
14
|
+
<span id='home-btn' class="unicode-btn">⬅️</span>
|
15
|
+
<label id="file-path-label"></label>
|
16
|
+
</div>
|
17
|
+
<div class="right">
|
18
|
+
<span id='save-hint' class="hint">Saved</span>
|
19
|
+
<span id='save-btn' class="unicode-btn">💾</span>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="container content" id="view">
|
24
|
+
<textarea id="editor"></textarea>
|
25
|
+
</div>
|
26
|
+
|
27
|
+
<script src="./edit.js" type="module"></script>
|
28
|
+
</body>
|
29
|
+
</html>
|
frontend/edit.js
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
import { store } from './state.js';
|
2
|
+
import { maybeShowLoginPanel } from './login.js';
|
3
|
+
|
4
|
+
const MAX_FILE_SIZE_MB = 5;
|
5
|
+
|
6
|
+
{
|
7
|
+
// initialization
|
8
|
+
store.init();
|
9
|
+
maybeShowLoginPanel(store, ).then(
|
10
|
+
(user) => {
|
11
|
+
console.log("User record", user);
|
12
|
+
}
|
13
|
+
)
|
14
|
+
}
|
15
|
+
|
16
|
+
const filePathLabel = document.getElementById('file-path-label');
|
17
|
+
const textArea = document.querySelector('#editor');
|
18
|
+
const backhomeBtn = document.getElementById('home-btn');
|
19
|
+
const saveBtn = document.getElementById('save-btn');
|
20
|
+
const saveHint = document.getElementById('save-hint');
|
21
|
+
|
22
|
+
// disable until file is loaded
|
23
|
+
saveBtn.disabled = true;
|
24
|
+
textArea.disabled = true;
|
25
|
+
|
26
|
+
backhomeBtn.addEventListener('click', () => {
|
27
|
+
const urlFrom = new URL(window.location.href);
|
28
|
+
if (urlFrom.searchParams.has('from')) {
|
29
|
+
const from = urlFrom.searchParams.get('from');
|
30
|
+
window.location.href = from;
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
window.location.href = './index.html';
|
34
|
+
});
|
35
|
+
|
36
|
+
// make textarea tab insert spaces
|
37
|
+
textArea.addEventListener('keydown', (e) => {
|
38
|
+
const TAB_SIZE = 4;
|
39
|
+
if (e.key === 'Tab') {
|
40
|
+
e.preventDefault();
|
41
|
+
const start = textArea.selectionStart;
|
42
|
+
const end = textArea.selectionEnd;
|
43
|
+
textArea.value = textArea.value.substring(0, start) + ' '.repeat(TAB_SIZE) + textArea.value.substring(end);
|
44
|
+
textArea.selectionStart = textArea.selectionEnd = start + TAB_SIZE;
|
45
|
+
}
|
46
|
+
});
|
47
|
+
|
48
|
+
const urlParams = new URLSearchParams(window.location.search);
|
49
|
+
const filePath = urlParams.get('p') || urlParams.get('path');
|
50
|
+
|
51
|
+
function raiseError(msg) {
|
52
|
+
filePathLabel.textContent = `Error: ${msg}`;
|
53
|
+
filePathLabel.style.color = 'darkred';
|
54
|
+
textArea.disabled = true;
|
55
|
+
throw new Error(msg);
|
56
|
+
}
|
57
|
+
|
58
|
+
|
59
|
+
if (!filePath) {
|
60
|
+
raiseError('No file specified');
|
61
|
+
}
|
62
|
+
if (filePath.endsWith('/')) {
|
63
|
+
raiseError('Path cannot be a directory');
|
64
|
+
}
|
65
|
+
|
66
|
+
async function loadContent() {
|
67
|
+
content = await store.conn.getText(filePath).catch((e) => {
|
68
|
+
raiseError(`Failed to read file "${filePath}": ${e.message}`);
|
69
|
+
});
|
70
|
+
textArea.value = content;
|
71
|
+
return content;
|
72
|
+
}
|
73
|
+
|
74
|
+
// check existence
|
75
|
+
let ftype='';
|
76
|
+
let content = '';
|
77
|
+
|
78
|
+
const fmeta = await store.conn.getMetadata(filePath).catch((e) => {
|
79
|
+
raiseError(`File "${filePath}" does not exist or cannot be accessed.`);
|
80
|
+
});
|
81
|
+
filePathLabel.textContent = filePath;
|
82
|
+
if (fmeta != null) {
|
83
|
+
ftype = fmeta.mime_type || '';
|
84
|
+
saveHint.style.opacity = 1;
|
85
|
+
if (fmeta.file_size && fmeta.file_size > MAX_FILE_SIZE_MB * 1024 * 1024) {
|
86
|
+
raiseError(`File too large (${(fmeta.file_size / (1024 * 1024)).toFixed(2)} MB). Max allowed size is ${MAX_FILE_SIZE_MB} MB.`);
|
87
|
+
}
|
88
|
+
content = await loadContent();
|
89
|
+
}
|
90
|
+
else {
|
91
|
+
const newHint = document.createElement('span');
|
92
|
+
newHint.id = 'new-hint';
|
93
|
+
newHint.textContent = 'new';
|
94
|
+
filePathLabel.appendChild(newHint);
|
95
|
+
textArea.focus();
|
96
|
+
}
|
97
|
+
|
98
|
+
async function saveFile() {
|
99
|
+
const content = textArea.value;
|
100
|
+
try {
|
101
|
+
await store.conn.putText(filePath, content, {conflict: 'overwrite', type: ftype? ftype : 'text/plain'});
|
102
|
+
saveHint.style.opacity = 1;
|
103
|
+
// remove new file hint if exists
|
104
|
+
const newHint = document.getElementById('new-hint');
|
105
|
+
if (newHint) { newHint.remove(); }
|
106
|
+
}
|
107
|
+
catch (e) {
|
108
|
+
raiseError(`Failed to save file "${filePath}": ${e.message}`);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
// unfreeze elements
|
113
|
+
saveBtn.disabled = false;
|
114
|
+
textArea.disabled = false;
|
115
|
+
|
116
|
+
saveBtn.addEventListener('click', saveFile);
|
117
|
+
textArea.addEventListener('input', () => {
|
118
|
+
saveHint.style.opacity = 0;
|
119
|
+
if (content == textArea.value){
|
120
|
+
saveHint.style.opacity = 1;
|
121
|
+
}
|
122
|
+
});
|
123
|
+
|
124
|
+
// bind Ctrl+S to save
|
125
|
+
document.addEventListener('keydown', (e) => {
|
126
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
127
|
+
e.preventDefault();
|
128
|
+
saveBtn.click();
|
129
|
+
}
|
130
|
+
});
|
frontend/login.css
CHANGED
frontend/scripts.js
CHANGED
@@ -84,10 +84,11 @@ pathBackButton.addEventListener('click', () => {
|
|
84
84
|
function onFileNameInpuChange(){
|
85
85
|
const fileName = uploadFileNameInput.value;
|
86
86
|
if (fileName.endsWith('/')){
|
87
|
-
uploadFileNameInput.classList.add('
|
87
|
+
uploadFileNameInput.classList.add('red-bg');
|
88
88
|
return;
|
89
89
|
}
|
90
90
|
if (fileName.length === 0){
|
91
|
+
uploadFileNameInput.classList.remove('red-bg');
|
91
92
|
uploadFileNameInput.classList.remove('duplicate');
|
92
93
|
}
|
93
94
|
else {
|
@@ -95,9 +96,15 @@ function onFileNameInpuChange(){
|
|
95
96
|
conn.getMetadata(p).then(
|
96
97
|
(data) => {
|
97
98
|
console.log("Got file meta", data);
|
98
|
-
if (data===null)
|
99
|
-
|
99
|
+
if (data===null) {
|
100
|
+
uploadFileNameInput.classList.remove('red-bg');
|
101
|
+
uploadFileNameInput.classList.remove('duplicate');
|
102
|
+
}
|
103
|
+
else if (data.url){
|
104
|
+
uploadFileNameInput.classList.add('duplicate');
|
105
|
+
}
|
100
106
|
else throw new Error('Invalid response');
|
107
|
+
updateFileUploadButton();
|
101
108
|
}
|
102
109
|
);
|
103
110
|
}
|
@@ -117,11 +124,48 @@ randomizeFnameButton.addEventListener('click', () => {
|
|
117
124
|
uploadFileNameInput.value = newName;
|
118
125
|
onFileNameInpuChange();
|
119
126
|
});
|
127
|
+
function updateFileUploadButton(){
|
128
|
+
if (uploadFileSelector.files.length === 0){
|
129
|
+
// change background color to indicate new/edit
|
130
|
+
if (!uploadButton.classList.contains('toedit')){
|
131
|
+
uploadButton.classList.add('toedit');
|
132
|
+
}
|
133
|
+
if (uploadFileNameInput.classList.contains('duplicate') && !uploadFileNameInput.value.endsWith('/')){
|
134
|
+
uploadButton.innerHTML = 'Edit';
|
135
|
+
uploadButton.title = 'Edit the existing file with the specified name';
|
136
|
+
}
|
137
|
+
else {
|
138
|
+
uploadButton.innerHTML = 'New';
|
139
|
+
uploadButton.title = 'Create a new empty file with the specified name';
|
140
|
+
}
|
141
|
+
}
|
142
|
+
else {
|
143
|
+
// change background color to indicate upload
|
144
|
+
if (uploadButton.classList.contains('toedit')){
|
145
|
+
uploadButton.classList.remove('toedit');
|
146
|
+
}
|
147
|
+
uploadButton.innerHTML = 'Upload';
|
148
|
+
uploadButton.title = 'Upload the selected file';
|
149
|
+
}
|
150
|
+
}
|
151
|
+
updateFileUploadButton();
|
120
152
|
uploadFileSelector.addEventListener('change', () => {
|
153
|
+
updateFileUploadButton();
|
121
154
|
uploadFileNameInput.value = uploadFileSelector.files[0].name;
|
122
155
|
onFileNameInpuChange();
|
123
156
|
});
|
124
157
|
uploadButton.addEventListener('click', () => {
|
158
|
+
// create new empty file or edit existing file
|
159
|
+
if (uploadFileSelector.files.length === 0){
|
160
|
+
const newUrl = ensurePathURI(store.dirpath + uploadFileNameInput.value);
|
161
|
+
const thisUrl = new URL(window.location.href);
|
162
|
+
thisUrl.pathname = thisUrl.pathname.replace(/\/[^\/]*$/, '/edit.html');
|
163
|
+
thisUrl.searchParams.set('path', newUrl);
|
164
|
+
thisUrl.searchParams.set('from', window.location.href);
|
165
|
+
window.location.href = thisUrl.href;
|
166
|
+
return;
|
167
|
+
}
|
168
|
+
|
125
169
|
const file = uploadFileSelector.files[0];
|
126
170
|
let path = store.dirpath;
|
127
171
|
let fileName = uploadFileNameInput.value;
|
@@ -167,6 +211,9 @@ uploadFileNameInput.addEventListener('input', debounce(onFileNameInpuChange, 500
|
|
167
211
|
uploadFileSelector.files = e.dataTransfer.files;
|
168
212
|
uploadFileNameInput.value = e.dataTransfer.files[0].name;
|
169
213
|
uploadFileNameInput.focus();
|
214
|
+
// trigger change event
|
215
|
+
updateFileUploadButton();
|
216
|
+
onFileNameInpuChange();
|
170
217
|
return;
|
171
218
|
}
|
172
219
|
|
frontend/styles.css
CHANGED
@@ -1,41 +1,9 @@
|
|
1
|
+
@import "./base.css";
|
1
2
|
@import "./popup.css";
|
2
3
|
@import "./info.css";
|
3
4
|
@import "./thumb.css";
|
4
5
|
@import "./login.css";
|
5
6
|
|
6
|
-
html {
|
7
|
-
overflow: -moz-scrollbars-vertical;
|
8
|
-
overflow-y: scroll;
|
9
|
-
}
|
10
|
-
|
11
|
-
body{
|
12
|
-
font-family: Arial, sans-serif;
|
13
|
-
background-color: #f1f1f1;
|
14
|
-
display: flex;
|
15
|
-
justify-content: center;
|
16
|
-
align-items: center;
|
17
|
-
}
|
18
|
-
|
19
|
-
input[type=button], button{
|
20
|
-
background-color: #195f8b;
|
21
|
-
color: white;
|
22
|
-
padding: 0.8rem;
|
23
|
-
margin: 0;
|
24
|
-
border: none;
|
25
|
-
border-radius: 0.25rem;
|
26
|
-
cursor: pointer;
|
27
|
-
}
|
28
|
-
|
29
|
-
input[type=text], input[type=password]
|
30
|
-
{
|
31
|
-
width: 100%;
|
32
|
-
padding: 0.75rem;
|
33
|
-
border: 1px solid #ccc;
|
34
|
-
border-radius: 0.25rem;
|
35
|
-
height: 1rem;
|
36
|
-
}
|
37
|
-
|
38
|
-
|
39
7
|
div.container.header, div.container.footer{
|
40
8
|
position: fixed;
|
41
9
|
left: 0;
|
@@ -128,6 +96,11 @@ input#path{
|
|
128
96
|
border-radius: 0.2rem;
|
129
97
|
cursor: pointer;
|
130
98
|
}
|
99
|
+
#upload-btn.toedit{
|
100
|
+
background-color: white;
|
101
|
+
color: #195f8b;
|
102
|
+
box-shadow: 2px 2px 10px #195f8b33;
|
103
|
+
}
|
131
104
|
|
132
105
|
div#top-bar{
|
133
106
|
color: grey;
|
@@ -217,9 +190,12 @@ label#upload-file-prefix{
|
|
217
190
|
width: 800px;
|
218
191
|
text-align: right;
|
219
192
|
}
|
220
|
-
input#file-name.
|
193
|
+
input#file-name.red-bg{
|
221
194
|
background-color: #ff001511
|
222
195
|
}
|
196
|
+
input#file-name.duplicate{
|
197
|
+
background-color: #ffcc0011;
|
198
|
+
}
|
223
199
|
|
224
200
|
.action-container{
|
225
201
|
display: flex;
|