lfss 0.7.14__py3-none-any.whl → 0.8.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.
frontend/scripts.js CHANGED
@@ -1,18 +1,18 @@
1
- import Connector from './api.js';
2
- import { permMap } from './api.js';
1
+ import { permMap, listPath } from './api.js';
3
2
  import { showFloatingWindowLineInput, showPopup } from './popup.js';
4
3
  import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI, asHtmlText } from './utils.js';
5
4
  import { showInfoPanel, showDirInfoPanel } from './info.js';
6
5
  import { makeThumbHtml } from './thumb.js';
6
+ import { store } from './state.js';
7
+ import { maybeShowLoginPanel } from './login.js';
7
8
 
8
- const conn = new Connector();
9
+ /** @type {import('./api.js').UserRecord}*/
9
10
  let userRecord = null;
11
+
10
12
  const ensureSlashEnd = (path) => {
11
13
  return path.endsWith('/') ? path : path + '/';
12
14
  }
13
15
 
14
- const endpointInput = document.querySelector('input#endpoint');
15
- const tokenInput = document.querySelector('input#token');
16
16
  const pathInput = document.querySelector('input#path');
17
17
  const pathBackButton = document.querySelector('span#back-btn');
18
18
  const pathHintDiv = document.querySelector('#position-hint');
@@ -23,53 +23,49 @@ const uploadFileSelector = document.querySelector('#file-selector');
23
23
  const uploadFileNameInput = document.querySelector('#file-name');
24
24
  const uploadButton = document.querySelector('#upload-btn');
25
25
  const randomizeFnameButton = document.querySelector('#randomize-fname-btn');
26
+ const pageLimitSelect = document.querySelector('#page-limit-sel');
27
+ const pageNumInput = document.querySelector('#page-num-input');
28
+ const pageCountLabel = document.querySelector('#page-count-lbl');
26
29
  const sortBySelect = document.querySelector('#sort-by-sel');
27
30
  const sortOrderSelect = document.querySelector('#sort-order-sel');
28
31
 
29
- conn.config.endpoint = endpointInput.value;
30
- conn.config.token = tokenInput.value;
32
+ const conn = store.conn;
31
33
 
32
34
  {
33
- const endpoint = window.localStorage.getItem('endpoint');
34
- if (endpoint){
35
- endpointInput.value = endpoint;
36
- conn.config.endpoint = endpoint;
37
- }
38
- const token = window.localStorage.getItem('token');
39
- if (token){
40
- tokenInput.value = token;
41
- conn.config.token = token;
42
- }
43
- const path = window.localStorage.getItem('path');
44
- if (path){
45
- pathInput.value = path;
46
- }
35
+ // initialization
36
+ store.init();
37
+ pathInput.value = store.dirpath;
47
38
  uploadFilePrefixLabel.textContent = pathInput.value;
48
- maybeRefreshUserRecord().then(
49
- () => maybeRefreshFileList()
50
- );
39
+ sortBySelect.value = store.orderby;
40
+ sortOrderSelect.value = store.sortorder;
41
+ pageLimitSelect.value = store.pagelim;
42
+ pageNumInput.value = store.pagenum;
43
+
44
+ maybeShowLoginPanel(store,).then(
45
+ (user) => {
46
+ console.log("User record", user);
47
+ userRecord = user;
48
+ maybeRefreshFileList();
49
+ }
50
+ )
51
51
  }
52
52
 
53
+ pathHintDiv.addEventListener('click', () => {
54
+ maybeShowLoginPanel(store, true).then(
55
+ (user) => {
56
+ console.log("User record", user);
57
+ userRecord = user;
58
+ maybeRefreshFileList();
59
+ }
60
+ );
61
+ });
62
+
53
63
  function onPathChange(){
54
64
  uploadFilePrefixLabel.textContent = pathInput.value;
55
- window.localStorage.setItem('path', pathInput.value);
65
+ store.dirpath = pathInput.value;
56
66
  maybeRefreshFileList();
57
67
  }
58
68
 
59
- endpointInput.addEventListener('blur', () => {
60
- conn.config.endpoint = endpointInput.value;
61
- window.localStorage.setItem('endpoint', endpointInput.value);
62
- maybeRefreshUserRecord().then(
63
- () => maybeRefreshFileList()
64
- );
65
- });
66
- tokenInput.addEventListener('blur', () => {
67
- conn.config.token = tokenInput.value;
68
- window.localStorage.setItem('token', tokenInput.value);
69
- maybeRefreshUserRecord().then(
70
- () => maybeRefreshFileList()
71
- );
72
- });
73
69
  pathInput.addEventListener('input', () => {
74
70
  onPathChange();
75
71
  });
@@ -94,7 +90,7 @@ function onFileNameInpuChange(){
94
90
  uploadFileNameInput.classList.remove('duplicate');
95
91
  }
96
92
  else {
97
- const p = ensurePathURI(pathInput.value + fileName);
93
+ const p = ensurePathURI(store.dirpath + fileName);
98
94
  conn.getMetadata(p).then(
99
95
  (data) => {
100
96
  console.log("Got file meta", data);
@@ -126,7 +122,7 @@ uploadFileSelector.addEventListener('change', () => {
126
122
  });
127
123
  uploadButton.addEventListener('click', () => {
128
124
  const file = uploadFileSelector.files[0];
129
- let path = pathInput.value;
125
+ let path = store.dirpath;
130
126
  let fileName = uploadFileNameInput.value;
131
127
  if (fileName.length === 0){
132
128
  throw new Error('File name cannot be empty');
@@ -172,7 +168,7 @@ uploadFileNameInput.addEventListener('input', debounce(onFileNameInpuChange, 500
172
168
  uploadFileNameInput.focus();
173
169
  }
174
170
  else if (files.length > 1){
175
- let dstPath = pathInput.value + uploadFileNameInput.value;
171
+ let dstPath = store.dirpath + uploadFileNameInput.value;
176
172
  if (!dstPath.endsWith('/')){ dstPath += '/'; }
177
173
  if (!confirm(`
178
174
  You are trying to upload multiple files at once.
@@ -216,62 +212,69 @@ Are you sure you want to proceed?
216
212
 
217
213
  function maybeRefreshFileList(){
218
214
  if (
219
- pathInput.value && pathInput.value.length > 0 && pathInput.value.endsWith('/')
215
+ store.dirpath && store.dirpath.length > 0 && store.dirpath.endsWith('/')
220
216
  ){
221
217
  refreshFileList();
222
218
  }
223
219
  }
224
220
 
225
- let sortBy = sortBySelect.value;
226
- let sortOrder = sortOrderSelect.value;
227
- /** @param {import('./api.js').DirectoryRecord} dirs */
228
- function sortDirList(dirs){
229
- if (sortBy === 'name'){
230
- dirs.sort((a, b) => { return a.url.localeCompare(b.url); });
231
- }
232
- if (sortOrder === 'desc'){ dirs.reverse(); }
233
- }
234
- /** @param {import('./api.js').FileRecord} files */
235
- function sortFileList(files){
236
- function timestr2num(timestr){
237
- return new Date(timestr).getTime();
238
- }
239
- if (sortBy === 'name'){
240
- files.sort((a, b) => { return a.url.localeCompare(b.url); });
241
- }
242
- if (sortBy === 'size'){
243
- files.sort((a, b) => { return a.file_size - b.file_size; });
244
- }
245
- if (sortBy === 'access'){
246
- files.sort((a, b) => { return timestr2num(a.access_time) - timestr2num(b.access_time); });
221
+ sortBySelect.addEventListener('change', (elem) => {store.orderby = elem.target.value; refreshFileList();});
222
+ sortOrderSelect.addEventListener('change', (elem) => {store.sortorder = elem.target.value; refreshFileList();});
223
+ pageLimitSelect.addEventListener('change', (elem) => {store.pagelim = elem.target.value; refreshFileList();});
224
+ pageNumInput.addEventListener('change', (elem) => {store.pagenum = elem.target.value; refreshFileList();});
225
+
226
+ window.addEventListener('keydown', (e) => {
227
+ if (document.activeElement !== document.body){
228
+ return;
247
229
  }
248
- if (sortBy === 'create'){
249
- files.sort((a, b) => { return timestr2num(a.create_time) - timestr2num(b.create_time); });
230
+ if (e.key === 'ArrowLeft'){
231
+ const num = Math.max(store.pagenum - 1, 1);
232
+ pageNumInput.value = num;
233
+ store.pagenum = num;
234
+ refreshFileList();
250
235
  }
251
- if (sortBy === 'mime'){
252
- files.sort((a, b) => { return a.mime_type.localeCompare(b.mime_type); });
236
+ else if (e.key === 'ArrowRight'){
237
+ const num = Math.min(Math.max(store.pagenum + 1, 1), parseInt(pageCountLabel.textContent));
238
+ pageNumInput.value = num;
239
+ store.pagenum = num;
240
+ refreshFileList();
253
241
  }
254
- if (sortOrder === 'desc'){ files.reverse(); }
255
- }
256
- sortBySelect.addEventListener('change', (elem) => {sortBy = elem.target.value; refreshFileList();});
257
- sortOrderSelect.addEventListener('change', (elem) => {sortOrder = elem.target.value; refreshFileList();});
242
+ })
258
243
 
259
- function refreshFileList(){
260
- conn.listPath(pathInput.value)
261
- .then(data => {
244
+ async function refreshFileList(){
245
+
246
+ listPath(conn, store.dirpath, {
247
+ offset: (store.pagenum - 1) * store.pagelim,
248
+ limit: store.pagelim,
249
+ orderBy: store.orderby,
250
+ orderDesc: store.sortorder === 'desc'
251
+ })
252
+ .then(async (res) => {
262
253
  pathHintDiv.classList.remove('disconnected');
263
254
  pathHintDiv.classList.add('connected');
264
- pathHintLabel.textContent = pathInput.value;
255
+ pathHintLabel.textContent = `[${userRecord.username}] ${store.endpoint}/${store.dirpath.startsWith('/') ? store.dirpath.slice(1) : store.dirpath}`;
265
256
  tbody.innerHTML = '';
257
+ console.log("Got data", res);
258
+
259
+ const [data, count] = res;
266
260
 
267
- console.log("Got data", data);
261
+ {
262
+ const total = count.dirs + count.files;
263
+ const pageCount = Math.max(Math.ceil(total / store.pagelim), 1);
264
+ pageCountLabel.textContent = pageCount;
265
+ if (store.pagenum > pageCount){
266
+ store.pagenum = pageCount;
267
+ pageNumInput.value = pageCount;
268
268
 
269
+ await refreshFileList();
270
+ return;
271
+ }
272
+ }
273
+
274
+ // maybe undefined
269
275
  if (!data.dirs){ data.dirs = []; }
270
276
  if (!data.files){ data.files = []; }
271
277
 
272
- sortDirList(data.dirs);
273
- sortFileList(data.files);
274
-
275
278
  data.dirs.forEach(dir => {
276
279
  const tr = document.createElement('tr');
277
280
  const sizeTd = document.createElement('td');
@@ -460,12 +463,6 @@ function refreshFileList(){
460
463
  });
461
464
  actContainer.appendChild(infoButton);
462
465
 
463
- const viewButton = document.createElement('a');
464
- viewButton.textContent = 'View';
465
- viewButton.href = conn.config.endpoint + '/' + file.url + '?token=' + conn.config.token;
466
- viewButton.target = '_blank';
467
- actContainer.appendChild(viewButton);
468
-
469
466
  const moveButton = document.createElement('a');
470
467
  moveButton.textContent = 'Move';
471
468
  moveButton.style.cursor = 'pointer';
@@ -520,7 +517,7 @@ function refreshFileList(){
520
517
  (err) => {
521
518
  pathHintDiv.classList.remove('connected');
522
519
  pathHintDiv.classList.add('disconnected');
523
- pathHintLabel.textContent = pathInput.value;
520
+ pathHintLabel.textContent = store.dirpath;
524
521
  tbody.innerHTML = '';
525
522
  console.log("Error");
526
523
  console.error(err);
@@ -528,27 +525,4 @@ function refreshFileList(){
528
525
  );
529
526
  }
530
527
 
531
-
532
- async function maybeRefreshUserRecord(){
533
- if (endpointInput.value && tokenInput.value){
534
- await refreshUserRecord();
535
- }
536
- }
537
-
538
- async function refreshUserRecord(){
539
- try{
540
- userRecord = await conn.whoami();
541
- console.log("User record: ", userRecord);
542
- }
543
- catch (err){
544
- userRecord = null;
545
- console.error("Failed to get user record");
546
- return false;
547
- }
548
-
549
- // UI updates.
550
-
551
- return true;
552
- }
553
-
554
528
  console.log("Hello World");
frontend/state.js ADDED
@@ -0,0 +1,72 @@
1
+
2
+ import Connector from './api.js';
3
+
4
+ function loadPersistedState(key, defaultValue) {
5
+ const persistedValue = window.localStorage.getItem(key);
6
+ return persistedValue ? persistedValue : defaultValue;
7
+ }
8
+
9
+ function setPersistedState(key, value) {
10
+ window.localStorage.setItem(key, value);
11
+ }
12
+
13
+ export const store = {
14
+ conn: new Connector(),
15
+
16
+ init() {
17
+ this.conn.config.token = this.token;
18
+ this.conn.config.endpoint = this.endpoint;
19
+ },
20
+
21
+ get token() {
22
+ return loadPersistedState('token', '');
23
+ },
24
+ set token(t) {
25
+ setPersistedState('token', t);
26
+ this.conn.config.token = t;
27
+ },
28
+
29
+ get endpoint() {
30
+ return loadPersistedState('endpoint', 'http://localhost:8000');
31
+ },
32
+ set endpoint(url) {
33
+ setPersistedState('endpoint', url);
34
+ this.conn.config.endpoint = url;
35
+ },
36
+
37
+ get dirpath() {
38
+ return loadPersistedState('dirpath', '/');
39
+ },
40
+ set dirpath(pth) {
41
+ setPersistedState('dirpath', pth);
42
+ },
43
+
44
+ get orderby () {
45
+ return loadPersistedState('orderby', 'none');
46
+ },
47
+ set orderby (sb) {
48
+ setPersistedState('orderby', sb);
49
+ },
50
+
51
+ get sortorder () {
52
+ return loadPersistedState('sortorder', 'asc');
53
+ },
54
+ set sortorder (so) {
55
+ setPersistedState('sortorder', so);
56
+ },
57
+
58
+ get pagenum () {
59
+ return parseInt(loadPersistedState('pagenum', '1'));
60
+ },
61
+ set pagenum (pn) {
62
+ setPersistedState('pagenum', pn.toString());
63
+ },
64
+
65
+ get pagelim () {
66
+ return parseInt(loadPersistedState('pagelim', '100'));
67
+ },
68
+ set pagelim (ps) {
69
+ setPersistedState('pagelim', ps.toString());
70
+ },
71
+
72
+ };
frontend/styles.css CHANGED
@@ -1,6 +1,12 @@
1
1
  @import "./popup.css";
2
2
  @import "./info.css";
3
3
  @import "./thumb.css";
4
+ @import "./login.css";
5
+
6
+ html {
7
+ overflow: -moz-scrollbars-vertical;
8
+ overflow-y: scroll;
9
+ }
4
10
 
5
11
  body{
6
12
  font-family: Arial, sans-serif;
@@ -44,7 +50,9 @@ div.container.header, div.container.footer{
44
50
 
45
51
  div.container.header{
46
52
  top: 0;
47
- height: 10rem;
53
+ height: 6.5rem;
54
+ justify-content: flex-end;
55
+ gap: 0.2rem;
48
56
  }
49
57
  div.container.footer{
50
58
  bottom: 0;
@@ -54,7 +62,7 @@ div.container.footer{
54
62
  div.container.content{
55
63
  width: 100%;
56
64
  padding-inline: 0.5rem;
57
- margin-top: calc(10rem + 1rem);
65
+ margin-top: calc(6.5rem + 1rem);
58
66
  margin-bottom: calc(4rem + 0.5rem);
59
67
  }
60
68
 
@@ -122,13 +130,22 @@ input#path{
122
130
  }
123
131
 
124
132
  div#top-bar{
133
+ color: grey;
134
+ display: flex;
135
+ flex-direction: row;
136
+ width: calc(100% - 2rem);
137
+ }
138
+ div#settings-bar{
139
+ width: calc(100% - 2rem);
125
140
  display: flex;
126
141
  flex-direction: row;
127
142
  justify-content: space-between;
128
143
  align-items: center;
129
144
  gap: 1rem;
145
+ padding-bottom: 0.2rem;
130
146
  }
131
147
  div#position-hint{
148
+ cursor: pointer;
132
149
  color: rgb(138, 138, 138);
133
150
  border-radius: 0.5rem;
134
151
  display: flex;
@@ -137,8 +154,15 @@ div#position-hint{
137
154
  gap: 0.25rem;
138
155
  height: 1rem;
139
156
  margin-bottom: 0.25rem;
157
+ font-size: x-small;
140
158
 
141
159
  padding: 0.25rem;
160
+ transition: all 0.2s;
161
+ }
162
+ div#position-hint:hover{
163
+ background-color: rgb(240, 244, 246);
164
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
165
+ color:rgb(91, 107, 116)
142
166
  }
143
167
  div#position-hint span{
144
168
  width: 0.75rem;
@@ -152,12 +176,6 @@ div#position-hint.connected span{
152
176
  background-color: rgb(0, 166, 0);
153
177
  }
154
178
 
155
- div#settings {
156
- display: flex;
157
- flex-direction: column;
158
- gap: 10px;
159
- }
160
-
161
179
  label#bucket-label{
162
180
  color: #195f8b;
163
181
  }
frontend/thumb.css CHANGED
@@ -7,4 +7,10 @@ img.thumb{
7
7
  width: 32px;
8
8
  object-fit: contain;
9
9
  border-radius: 0.1rem;
10
+ transition: all 0.2s;
11
+ }
12
+ img.thumb:hover{
13
+ cursor: pointer;
14
+ height: 48px; /* smaller than backend */
15
+ width: 48px;
10
16
  }
frontend/thumb.js CHANGED
@@ -24,25 +24,26 @@ function getIconSVGFromMimeType(mimeType){
24
24
  if (mimeType.startsWith('image/')){
25
25
  return ICON_IMAGE;
26
26
  }
27
- switch (mimeType){
28
- case
29
- 'application/pdf' || 'application/x-pdf':
30
- return ICON_PDF;
31
- case
32
- 'application/x-msdownload' || 'application/x-msdos-program' || 'application/x-msi' || 'application/x-ms-wim' || 'application/octet-stream' || 'application/x-apple-diskimage':
33
- return ICON_EXE;
34
- case
35
- 'application/zip' || 'application/x-zip-compressed' || 'application/x-7z-compressed' || 'application/x-rar-compressed' || 'application/x-tar' || 'application/x-gzip':
36
- return ICON_ZIP;
37
- case
38
- "text/html" || "application/xhtml+xml" || "application/xml" || "text/css" || "application/javascript" || "text/javascript" || "application/json" || "text/x-python" || "text/x-java-source" ||
39
- "application/x-httpd-php" || "text/x-ruby" || "text/x-perl" || "application/x-sh" || "application/sql" || "text/x-csrc" || "text/x-c++src" || "text/x-csharp" || "text/x-go" || "text/x-haskell" ||
40
- "text/x-lua" || "text/x-markdown" || "application/wasm" || "application/x-tcl" || "text/x-yaml" || "application/x-latex" || "application/x-tex" || "text/x-scss" || "application/x-lisp" ||
41
- "application/x-rustsrc" || "application/x-ruby" || "text/x-asm":
42
- return ICON_CODE;
43
- default:
44
- return ICON_FILE;
27
+
28
+ if (['application/pdf', 'application/x-pdf'].includes(mimeType)){
29
+ return ICON_PDF;
30
+ }
31
+ if (['application/x-msdownload', 'application/x-msdos-program', 'application/x-msi', 'application/x-ms-wim', 'application/octet-stream', 'application/x-apple-diskimage'].includes(mimeType)){
32
+ return ICON_EXE;
33
+ }
34
+ if (['application/zip', 'application/x-zip-compressed', 'application/x-7z-compressed', 'application/x-rar-compressed', 'application/x-tar', 'application/x-gzip'].includes(mimeType)){
35
+ return ICON_ZIP;
36
+ }
37
+ if ([
38
+ "text/html", "application/xhtml+xml", "application/xml", "text/css", "application/javascript", "text/javascript", "application/json", "text/x-python", "text/x-java-source",
39
+ "application/x-httpd-php", "text/x-ruby", "text/x-perl", "application/x-sh", "application/sql", "text/x-c", "text/x-c++", "text/x-csharp", "text/x-go", "text/x-haskell",
40
+ "text/x-lua", "text/x-markdown", "application/wasm", "application/x-tcl", "text/x-yaml", "application/x-latex", "application/x-tex", "text/x-scss", "application/x-lisp",
41
+ "application/x-rust", "application/x-ruby", "text/x-asm"
42
+ ].includes(mimeType)){
43
+ return ICON_CODE;
45
44
  }
45
+
46
+ return ICON_FILE;
46
47
  }
47
48
 
48
49
  function getSafeIconUrl(icon_str){
@@ -66,10 +67,24 @@ export function makeThumbHtml(c, r){
66
67
  const mtype = r.mime_type? r.mime_type : 'directory';
67
68
  const thumb_id = `thumb-${thumb_counter++}`;
68
69
  const url = mtype == 'directory'? ensureSlashEnd(r.url) : r.url;
69
- return `
70
- <div class="thumb" id="${thumb_id}"> \
71
- <img src="${c.config.endpoint}/${url}?token=${token}&thumb=true" alt="${r.url}" class="thumb" \
72
- onerror="this.src='${getSafeIconUrl(getIconSVGFromMimeType(mtype))}';this.classList.add('thumb-svg');" \
73
- </div>
74
- `;
70
+
71
+ if (mtype.startsWith('image/')){
72
+ return `
73
+ <div class="thumb" id="${thumb_id}"> \
74
+ <img src="${c.config.endpoint}/${url}?token=${token}&thumb=true" alt="${r.url}" class="thumb" \
75
+ onerror="this.src='${getSafeIconUrl(getIconSVGFromMimeType(mtype))}';this.classList.add('thumb-svg');" \
76
+ onclick="window.open('${c.config.endpoint}/${url}?token=${token}', '_blank');" \
77
+ /> \
78
+ </div>
79
+ `;
80
+ }
81
+ else{
82
+ return `
83
+ <div class="thumb" id="${thumb_id}"> \
84
+ <img src="${getSafeIconUrl(getIconSVGFromMimeType(mtype))}" alt="${r.url}" class="thumb thumb-svg" \
85
+ onclick="window.open('${c.config.endpoint}/${url}?token=${token}', '_blank');" \
86
+ / > \
87
+ </div>
88
+ `;
89
+ }
75
90
  }