@saltcorn/filemanager 0.8.0-beta.1
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.
- package/README.md +69 -0
- package/package.json +25 -0
- package/public/build/bundle.css +1 -0
- package/public/build/bundle.js +6 -0
- package/public/build/bundle.js.map +1 -0
- package/rollup.config.js +76 -0
- package/scripts/setupTypeScript.js +121 -0
- package/src/App.svelte +387 -0
- package/src/main.js +10 -0
package/rollup.config.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import svelte from 'rollup-plugin-svelte';
|
|
2
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
3
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
4
|
+
import livereload from 'rollup-plugin-livereload';
|
|
5
|
+
import { terser } from 'rollup-plugin-terser';
|
|
6
|
+
import css from 'rollup-plugin-css-only';
|
|
7
|
+
|
|
8
|
+
const production = !process.env.ROLLUP_WATCH;
|
|
9
|
+
|
|
10
|
+
function serve() {
|
|
11
|
+
let server;
|
|
12
|
+
|
|
13
|
+
function toExit() {
|
|
14
|
+
if (server) server.kill(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
writeBundle() {
|
|
19
|
+
if (server) return;
|
|
20
|
+
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
|
|
21
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
22
|
+
shell: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.on('SIGTERM', toExit);
|
|
26
|
+
process.on('exit', toExit);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
input: 'src/main.js',
|
|
33
|
+
output: {
|
|
34
|
+
sourcemap: true,
|
|
35
|
+
format: 'iife',
|
|
36
|
+
name: 'app',
|
|
37
|
+
file: 'public/build/bundle.js'
|
|
38
|
+
},
|
|
39
|
+
plugins: [
|
|
40
|
+
svelte({
|
|
41
|
+
compilerOptions: {
|
|
42
|
+
// enable run-time checks when not in production
|
|
43
|
+
dev: !production
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
// we'll extract any component CSS out into
|
|
47
|
+
// a separate file - better for performance
|
|
48
|
+
css({ output: 'bundle.css' }),
|
|
49
|
+
|
|
50
|
+
// If you have external dependencies installed from
|
|
51
|
+
// npm, you'll most likely need these plugins. In
|
|
52
|
+
// some cases you'll need additional configuration -
|
|
53
|
+
// consult the documentation for details:
|
|
54
|
+
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
|
55
|
+
resolve({
|
|
56
|
+
browser: true,
|
|
57
|
+
dedupe: ['svelte']
|
|
58
|
+
}),
|
|
59
|
+
commonjs(),
|
|
60
|
+
|
|
61
|
+
// In dev mode, call `npm run start` once
|
|
62
|
+
// the bundle has been generated
|
|
63
|
+
!production && serve(),
|
|
64
|
+
|
|
65
|
+
// Watch the `public` directory and refresh the
|
|
66
|
+
// browser on changes when not in production
|
|
67
|
+
!production && livereload('public'),
|
|
68
|
+
|
|
69
|
+
// If we're building for production (npm run build
|
|
70
|
+
// instead of npm run dev), minify
|
|
71
|
+
production && terser()
|
|
72
|
+
],
|
|
73
|
+
watch: {
|
|
74
|
+
clearScreen: false
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** This script modifies the project to support TS code in .svelte files like:
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
export let name: string;
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
As well as validating the code for CI.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** To work on this script:
|
|
13
|
+
rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("fs")
|
|
17
|
+
const path = require("path")
|
|
18
|
+
const { argv } = require("process")
|
|
19
|
+
|
|
20
|
+
const projectRoot = argv[2] || path.join(__dirname, "..")
|
|
21
|
+
|
|
22
|
+
// Add deps to pkg.json
|
|
23
|
+
const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
|
|
24
|
+
packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
|
|
25
|
+
"svelte-check": "^2.0.0",
|
|
26
|
+
"svelte-preprocess": "^4.0.0",
|
|
27
|
+
"@rollup/plugin-typescript": "^8.0.0",
|
|
28
|
+
"typescript": "^4.0.0",
|
|
29
|
+
"tslib": "^2.0.0",
|
|
30
|
+
"@tsconfig/svelte": "^2.0.0"
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Add script for checking
|
|
34
|
+
packageJSON.scripts = Object.assign(packageJSON.scripts, {
|
|
35
|
+
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Write the package JSON
|
|
39
|
+
fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
|
|
40
|
+
|
|
41
|
+
// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
|
|
42
|
+
const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
|
|
43
|
+
const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
|
|
44
|
+
fs.renameSync(beforeMainJSPath, afterMainTSPath)
|
|
45
|
+
|
|
46
|
+
// Switch the app.svelte file to use TS
|
|
47
|
+
const appSveltePath = path.join(projectRoot, "src", "App.svelte")
|
|
48
|
+
let appFile = fs.readFileSync(appSveltePath, "utf8")
|
|
49
|
+
appFile = appFile.replace("<script>", '<script lang="ts">')
|
|
50
|
+
appFile = appFile.replace("export let name;", 'export let name: string;')
|
|
51
|
+
fs.writeFileSync(appSveltePath, appFile)
|
|
52
|
+
|
|
53
|
+
// Edit rollup config
|
|
54
|
+
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
|
|
55
|
+
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
|
|
56
|
+
|
|
57
|
+
// Edit imports
|
|
58
|
+
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
|
|
59
|
+
import sveltePreprocess from 'svelte-preprocess';
|
|
60
|
+
import typescript from '@rollup/plugin-typescript';`)
|
|
61
|
+
|
|
62
|
+
// Replace name of entry point
|
|
63
|
+
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
|
|
64
|
+
|
|
65
|
+
// Add preprocessor
|
|
66
|
+
rollupConfig = rollupConfig.replace(
|
|
67
|
+
'compilerOptions:',
|
|
68
|
+
'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Add TypeScript
|
|
72
|
+
rollupConfig = rollupConfig.replace(
|
|
73
|
+
'commonjs(),',
|
|
74
|
+
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
|
|
75
|
+
);
|
|
76
|
+
fs.writeFileSync(rollupConfigPath, rollupConfig)
|
|
77
|
+
|
|
78
|
+
// Add TSConfig
|
|
79
|
+
const tsconfig = `{
|
|
80
|
+
"extends": "@tsconfig/svelte/tsconfig.json",
|
|
81
|
+
|
|
82
|
+
"include": ["src/**/*"],
|
|
83
|
+
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
|
84
|
+
}`
|
|
85
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
|
|
86
|
+
fs.writeFileSync(tsconfigPath, tsconfig)
|
|
87
|
+
|
|
88
|
+
// Add global.d.ts
|
|
89
|
+
const dtsPath = path.join(projectRoot, "src", "global.d.ts")
|
|
90
|
+
fs.writeFileSync(dtsPath, `/// <reference types="svelte" />`)
|
|
91
|
+
|
|
92
|
+
// Delete this script, but not during testing
|
|
93
|
+
if (!argv[2]) {
|
|
94
|
+
// Remove the script
|
|
95
|
+
fs.unlinkSync(path.join(__filename))
|
|
96
|
+
|
|
97
|
+
// Check for Mac's DS_store file, and if it's the only one left remove it
|
|
98
|
+
const remainingFiles = fs.readdirSync(path.join(__dirname))
|
|
99
|
+
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
|
|
100
|
+
fs.unlinkSync(path.join(__dirname, '.DS_store'))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if the scripts folder is empty
|
|
104
|
+
if (fs.readdirSync(path.join(__dirname)).length === 0) {
|
|
105
|
+
// Remove the scripts folder
|
|
106
|
+
fs.rmdirSync(path.join(__dirname))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Adds the extension recommendation
|
|
111
|
+
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
|
|
112
|
+
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
|
|
113
|
+
"recommendations": ["svelte.svelte-vscode"]
|
|
114
|
+
}
|
|
115
|
+
`)
|
|
116
|
+
|
|
117
|
+
console.log("Converted to TypeScript.")
|
|
118
|
+
|
|
119
|
+
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
|
|
120
|
+
console.log("\nYou will need to re-run your dependency manager to get started.")
|
|
121
|
+
}
|
package/src/App.svelte
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import Fa from "svelte-fa";
|
|
4
|
+
import {
|
|
5
|
+
faTrashAlt,
|
|
6
|
+
faFileImage,
|
|
7
|
+
faFile,
|
|
8
|
+
faFolder,
|
|
9
|
+
faFileCsv,
|
|
10
|
+
faFileExcel,
|
|
11
|
+
faFileWord,
|
|
12
|
+
faFilePdf,
|
|
13
|
+
faFileAlt,
|
|
14
|
+
faFileAudio,
|
|
15
|
+
faFileVideo,
|
|
16
|
+
faFolderPlus,
|
|
17
|
+
faHome,
|
|
18
|
+
faCaretUp,
|
|
19
|
+
faCaretDown,
|
|
20
|
+
} from "@fortawesome/free-solid-svg-icons";
|
|
21
|
+
export let files = [];
|
|
22
|
+
export let directories = [];
|
|
23
|
+
export let roles = {};
|
|
24
|
+
export let currentFolder = "/";
|
|
25
|
+
let selectedList = [];
|
|
26
|
+
let selectedFiles = {};
|
|
27
|
+
let rolesList;
|
|
28
|
+
let lastSelected;
|
|
29
|
+
const fetchAndReset = async function (keepSelection) {
|
|
30
|
+
const response = await fetch(`/files?dir=${currentFolder}`, {
|
|
31
|
+
headers: { "X-Requested-With": "XMLHttpRequest" },
|
|
32
|
+
});
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
files = data.files;
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
file.mimetype =
|
|
37
|
+
file.mime_sub && file.mime_super
|
|
38
|
+
? `${file.mime_super}/${file.mime_sub}`
|
|
39
|
+
: "";
|
|
40
|
+
}
|
|
41
|
+
directories = data.directories;
|
|
42
|
+
rolesList = data.roles;
|
|
43
|
+
for (const role of data.roles) {
|
|
44
|
+
roles[role.id] = role.role;
|
|
45
|
+
}
|
|
46
|
+
if (!keepSelection) {
|
|
47
|
+
selectedList = [];
|
|
48
|
+
selectedFiles = {};
|
|
49
|
+
lastSelected = null;
|
|
50
|
+
} else if (lastSelected) {
|
|
51
|
+
lastSelected = files.find((f) => f.filename === lastSelected.filename);
|
|
52
|
+
}
|
|
53
|
+
clickHeader("filename");
|
|
54
|
+
};
|
|
55
|
+
onMount(fetchAndReset);
|
|
56
|
+
function rowClick(file, e) {
|
|
57
|
+
file.selected = true;
|
|
58
|
+
const prev = selectedFiles[file.filename];
|
|
59
|
+
if (!e.shiftKey) selectedFiles = {};
|
|
60
|
+
selectedFiles[file.filename] = !prev;
|
|
61
|
+
if (!prev) lastSelected = file;
|
|
62
|
+
else {
|
|
63
|
+
const firstSelected = Object.entries(selectedFiles).findLast(
|
|
64
|
+
([k, v]) => v
|
|
65
|
+
);
|
|
66
|
+
if (firstSelected)
|
|
67
|
+
lastSelected = files.find((f) => f.filename === firstSelected[0]);
|
|
68
|
+
else lastSelected = null;
|
|
69
|
+
}
|
|
70
|
+
document.getSelection().removeAllRanges();
|
|
71
|
+
console.log(lastSelected);
|
|
72
|
+
}
|
|
73
|
+
$: selectedList = Object.entries(selectedFiles)
|
|
74
|
+
.filter(([k, v]) => v)
|
|
75
|
+
.map(([k, v]) => k);
|
|
76
|
+
|
|
77
|
+
async function POST(url, body) {
|
|
78
|
+
return await fetch(url, {
|
|
79
|
+
headers: {
|
|
80
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
81
|
+
"CSRF-Token": window._sc_globalCsrf,
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: JSON.stringify(body || {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function goAction(e) {
|
|
90
|
+
const action = e?.target.value;
|
|
91
|
+
if (!action) return;
|
|
92
|
+
switch (action) {
|
|
93
|
+
case "Delete":
|
|
94
|
+
if (!confirm(`Delete files: ${selectedList.join()}`)) return;
|
|
95
|
+
for (const fileNm of selectedList) {
|
|
96
|
+
const file = files.find((f) => f.filename === fileNm);
|
|
97
|
+
await POST(`/files/delete/${file.location}`);
|
|
98
|
+
}
|
|
99
|
+
await fetchAndReset();
|
|
100
|
+
break;
|
|
101
|
+
case "Rename":
|
|
102
|
+
const newName = window.prompt(
|
|
103
|
+
`Rename ${lastSelected.filename} to:`,
|
|
104
|
+
lastSelected.filename
|
|
105
|
+
);
|
|
106
|
+
if (!newName) return;
|
|
107
|
+
await POST(`/files/setname/${lastSelected.location}`, {
|
|
108
|
+
value: newName,
|
|
109
|
+
});
|
|
110
|
+
await fetchAndReset();
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function changeAccessRole(e) {
|
|
115
|
+
const role = e.target.value;
|
|
116
|
+
for (const fileNm of selectedList) {
|
|
117
|
+
const file = files.find((f) => f.filename === fileNm);
|
|
118
|
+
await POST(`/files/setrole/${file.location}`, { role });
|
|
119
|
+
}
|
|
120
|
+
await fetchAndReset(true);
|
|
121
|
+
}
|
|
122
|
+
async function moveDirectory(e) {
|
|
123
|
+
for (const fileNm of selectedList) {
|
|
124
|
+
const new_path = e.target.value;
|
|
125
|
+
if (!new_path) return;
|
|
126
|
+
const file = files.find((f) => f.filename === fileNm);
|
|
127
|
+
await POST(`/files/move/${file.location}`, { new_path });
|
|
128
|
+
}
|
|
129
|
+
await fetchAndReset();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function gotoFolder(folder) {
|
|
133
|
+
currentFolder = folder;
|
|
134
|
+
fetchAndReset();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let pathSegments = [];
|
|
138
|
+
$: {
|
|
139
|
+
if (currentFolder === "/" || currentFolder === "")
|
|
140
|
+
pathSegments = [{ icon: faHome, location: "/" }];
|
|
141
|
+
else {
|
|
142
|
+
pathSegments = currentFolder.split("/").map((name, i) => ({
|
|
143
|
+
name,
|
|
144
|
+
location: currentFolder
|
|
145
|
+
.split("/")
|
|
146
|
+
.slice(0, i + 1)
|
|
147
|
+
.join("/"),
|
|
148
|
+
}));
|
|
149
|
+
pathSegments.unshift({ icon: faHome, location: "/" });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getIcon(file) {
|
|
154
|
+
if (file.mime_super === "image") return faFileImage;
|
|
155
|
+
if (file.mime_super === "audio") return faFileAudio;
|
|
156
|
+
if (file.mime_super === "video") return faFileVideo;
|
|
157
|
+
if (file.mime_sub === "pdf") return faFilePdf;
|
|
158
|
+
|
|
159
|
+
if (file.isDirectory) return faFolder;
|
|
160
|
+
const fname = file.filename.toLowerCase();
|
|
161
|
+
if (fname.endsWith(".csv")) return faFileCsv;
|
|
162
|
+
if (fname.endsWith(".xls")) return faFileExcel;
|
|
163
|
+
if (fname.endsWith(".xlsx")) return faFileExcel;
|
|
164
|
+
if (fname.endsWith(".doc")) return faFileWord;
|
|
165
|
+
if (fname.endsWith(".docx")) return faFileWord;
|
|
166
|
+
if (fname.endsWith(".txt")) return faFileAlt;
|
|
167
|
+
return faFile;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let sortBy;
|
|
171
|
+
let sortDesc = false;
|
|
172
|
+
function clickHeader(varNm) {
|
|
173
|
+
if (sortBy === varNm) sortDesc = !sortDesc;
|
|
174
|
+
else sortBy = varNm;
|
|
175
|
+
let getter = (x) => x[sortBy];
|
|
176
|
+
if (sortBy === "uploaded_at") getter = (x) => new Date(x[sortBy]);
|
|
177
|
+
if (sortBy === "filename") getter = (x) => (x[sortBy] || "").toLowerCase();
|
|
178
|
+
const cmp = (a, b) => {
|
|
179
|
+
if (getter(a) < getter(b)) return sortDesc ? 1 : -1;
|
|
180
|
+
if (getter(a) > getter(b)) return sortDesc ? -1 : 1;
|
|
181
|
+
return 0;
|
|
182
|
+
};
|
|
183
|
+
files = files.sort(cmp);
|
|
184
|
+
}
|
|
185
|
+
function getSorterIcon(varNm) {
|
|
186
|
+
console.log({ varNm, sortBy });
|
|
187
|
+
if (varNm !== sortBy) return null;
|
|
188
|
+
return sortDesc ? faCaretDown : faCaretUp;
|
|
189
|
+
}
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<main>
|
|
193
|
+
<div class="row">
|
|
194
|
+
<div class="col-8">
|
|
195
|
+
<div>
|
|
196
|
+
<nav aria-label="breadcrumb">
|
|
197
|
+
<ol class="breadcrumb">
|
|
198
|
+
{#each pathSegments as segment}
|
|
199
|
+
<li
|
|
200
|
+
class="breadcrumb-item"
|
|
201
|
+
on:click={gotoFolder(segment.location)}
|
|
202
|
+
>
|
|
203
|
+
{#if segment.icon}
|
|
204
|
+
<Fa icon={segment.icon} />
|
|
205
|
+
{:else}
|
|
206
|
+
{segment.name}
|
|
207
|
+
{/if}
|
|
208
|
+
</li>
|
|
209
|
+
{/each}
|
|
210
|
+
</ol>
|
|
211
|
+
</nav>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="filelist">
|
|
214
|
+
<table class="table table-sm">
|
|
215
|
+
<thead>
|
|
216
|
+
<tr>
|
|
217
|
+
<th />
|
|
218
|
+
<th on:click={() => clickHeader("filename")}>
|
|
219
|
+
Filename
|
|
220
|
+
<Fa icon={getSorterIcon("filename", sortBy, sortDesc)} />
|
|
221
|
+
</th>
|
|
222
|
+
<th on:click={() => clickHeader("mimetype")}>
|
|
223
|
+
Media type
|
|
224
|
+
<Fa icon={getSorterIcon("mimetype", sortBy, sortDesc)} />
|
|
225
|
+
</th>
|
|
226
|
+
<th
|
|
227
|
+
on:click={() => clickHeader("size_kb")}
|
|
228
|
+
style="text-align: right"
|
|
229
|
+
>
|
|
230
|
+
<Fa icon={getSorterIcon("size_kb", sortBy, sortDesc)} />
|
|
231
|
+
Size (KiB)
|
|
232
|
+
</th>
|
|
233
|
+
<th on:click={() => clickHeader("min_role_read")}>
|
|
234
|
+
Role to access
|
|
235
|
+
<Fa icon={getSorterIcon("min_role_read", sortBy, sortDesc)} />
|
|
236
|
+
</th>
|
|
237
|
+
<th on:click={() => clickHeader("uploaded_at")}>
|
|
238
|
+
Created
|
|
239
|
+
<Fa icon={getSorterIcon("uploaded_at", sortBy, sortDesc)} />
|
|
240
|
+
</th>
|
|
241
|
+
</tr>
|
|
242
|
+
</thead>
|
|
243
|
+
<tbody>
|
|
244
|
+
{#each files as file}
|
|
245
|
+
<tr
|
|
246
|
+
on:click={(e) => rowClick(file, e)}
|
|
247
|
+
on:dblclick={() => {
|
|
248
|
+
if (file.isDirectory) gotoFolder(file.location);
|
|
249
|
+
else window.open(`/files/serve/${file.location}`);
|
|
250
|
+
}}
|
|
251
|
+
class:selected={selectedFiles[file.filename]}
|
|
252
|
+
>
|
|
253
|
+
<td>
|
|
254
|
+
<Fa size="lg" icon={getIcon(file)} />
|
|
255
|
+
</td>
|
|
256
|
+
<td>
|
|
257
|
+
{#if file.isDirectory}
|
|
258
|
+
{file.filename}/
|
|
259
|
+
{:else}
|
|
260
|
+
{file.filename}
|
|
261
|
+
{/if}
|
|
262
|
+
</td>
|
|
263
|
+
<td>
|
|
264
|
+
{file.mimetype}
|
|
265
|
+
</td>
|
|
266
|
+
<td style="text-align: right">
|
|
267
|
+
{file.isDirectory ? "" : file.size_kb}
|
|
268
|
+
</td>
|
|
269
|
+
<td>
|
|
270
|
+
{roles[file.min_role_read]}
|
|
271
|
+
</td>
|
|
272
|
+
<td>
|
|
273
|
+
{new Date(file.uploaded_at).toLocaleString()}
|
|
274
|
+
</td>
|
|
275
|
+
</tr>
|
|
276
|
+
{/each}
|
|
277
|
+
<tr on:click={() => window.create_new_folder(currentFolder)}>
|
|
278
|
+
<td>
|
|
279
|
+
<Fa size="lg" icon={faFolderPlus} />
|
|
280
|
+
</td>
|
|
281
|
+
<td>Create new folder...</td>
|
|
282
|
+
<td />
|
|
283
|
+
<td />
|
|
284
|
+
<td />
|
|
285
|
+
</tr>
|
|
286
|
+
</tbody>
|
|
287
|
+
</table>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="col-4">
|
|
292
|
+
{#if selectedList.length > 0}
|
|
293
|
+
<h5>{lastSelected.filename}</h5>
|
|
294
|
+
|
|
295
|
+
{#if lastSelected.mime_super === "image"}
|
|
296
|
+
<img
|
|
297
|
+
class="file-preview my-2"
|
|
298
|
+
src={`/files/serve/${lastSelected.location}`}
|
|
299
|
+
alt={lastSelected.filename}
|
|
300
|
+
/>
|
|
301
|
+
{/if}
|
|
302
|
+
<table>
|
|
303
|
+
<tbody>
|
|
304
|
+
{#if !lastSelected.isDirectory}
|
|
305
|
+
<tr>
|
|
306
|
+
<th>Size</th>
|
|
307
|
+
<td>{lastSelected.size_kb} KB</td>
|
|
308
|
+
</tr>
|
|
309
|
+
{/if}
|
|
310
|
+
|
|
311
|
+
<tr>
|
|
312
|
+
<th>MIME type</th>
|
|
313
|
+
<td>
|
|
314
|
+
{#if lastSelected.isDirectory}
|
|
315
|
+
Directory
|
|
316
|
+
{:else}
|
|
317
|
+
{lastSelected.mime_super}/{lastSelected.mime_sub}
|
|
318
|
+
{/if}
|
|
319
|
+
</td>
|
|
320
|
+
</tr>
|
|
321
|
+
<tr>
|
|
322
|
+
<th>Created</th>
|
|
323
|
+
<td>{new Date(lastSelected.uploaded_at).toLocaleString()}</td>
|
|
324
|
+
</tr>
|
|
325
|
+
<tr>
|
|
326
|
+
<th class="pe-1">Role to access</th>
|
|
327
|
+
<td>{roles[lastSelected.min_role_read]}</td>
|
|
328
|
+
</tr>
|
|
329
|
+
</tbody>
|
|
330
|
+
</table>
|
|
331
|
+
<div>
|
|
332
|
+
<a href={`/files/serve/${lastSelected.location}`}>Link</a>
|
|
333
|
+
|
|
|
334
|
+
<a href={`/files/download/${lastSelected.location}`}>Download</a>
|
|
335
|
+
</div>
|
|
336
|
+
{#if selectedList.length > 1}
|
|
337
|
+
<strong
|
|
338
|
+
>and {selectedList.length - 1} other file{selectedList.length > 2
|
|
339
|
+
? "s"
|
|
340
|
+
: ""}:
|
|
341
|
+
</strong>
|
|
342
|
+
{/if}
|
|
343
|
+
<div class="file-actions d-flex">
|
|
344
|
+
<select class="form-select" on:change={changeAccessRole}>
|
|
345
|
+
<option value="" disabled selected>Set access</option>
|
|
346
|
+
{#each rolesList as role}
|
|
347
|
+
<option value={role.id}>{role.role}</option>
|
|
348
|
+
{/each}
|
|
349
|
+
</select>
|
|
350
|
+
|
|
351
|
+
<select class="form-select" on:change={moveDirectory}>
|
|
352
|
+
<option value="" disabled selected>Move to...</option>
|
|
353
|
+
{#each directories as dir}
|
|
354
|
+
<option>{dir.location || "/"}</option>
|
|
355
|
+
{/each}
|
|
356
|
+
</select>
|
|
357
|
+
<select class="form-select" on:change={goAction}>
|
|
358
|
+
<option value="" disabled selected>Action...</option>
|
|
359
|
+
<option>Delete</option>
|
|
360
|
+
{#if selectedList.length === 1}
|
|
361
|
+
<option>Rename</option>
|
|
362
|
+
{/if}
|
|
363
|
+
</select>
|
|
364
|
+
</div>
|
|
365
|
+
{/if}
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</main>
|
|
369
|
+
|
|
370
|
+
<style>
|
|
371
|
+
tr.selected {
|
|
372
|
+
background-color: rgb(213, 237, 255);
|
|
373
|
+
}
|
|
374
|
+
img.file-preview {
|
|
375
|
+
max-height: 200px;
|
|
376
|
+
max-width: 100%;
|
|
377
|
+
}
|
|
378
|
+
div.file-actions select {
|
|
379
|
+
width: unset;
|
|
380
|
+
display: inline;
|
|
381
|
+
max-width: 33%;
|
|
382
|
+
}
|
|
383
|
+
div.filelist {
|
|
384
|
+
max-height: 90vh;
|
|
385
|
+
overflow-y: scroll;
|
|
386
|
+
}
|
|
387
|
+
</style>
|