Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c829330b53 | ||
|
|
c14cf86f83 | ||
|
|
6d620c00a1 | ||
|
|
06e8713fa5 | ||
|
|
af9b42549f | ||
|
|
75baf7ce33 | ||
|
|
4ff6347155 | ||
|
|
14ee054359 | ||
|
|
7f559ffd07 | ||
|
|
619f6837b0 | ||
|
|
d778c192ae | ||
|
|
a290c6d7db | ||
|
|
c1b0207800 | ||
|
|
c7a5c7efee | ||
|
|
cbeec6d225 | ||
|
|
25e47c3ce8 | ||
|
|
5eb3bf4058 | ||
|
|
07dfdce8e4 |
6
.github/workflows/main.yaml
vendored
6
.github/workflows/main.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.24'
|
||||||
- run: make lint-backend
|
- run: make lint-backend
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.24'
|
||||||
- run: make test-backend
|
- run: make test-backend
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: '1.23'
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
package_json_file: "frontend/package.json"
|
package_json_file: "frontend/package.json"
|
||||||
|
|||||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -2,6 +2,57 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [2.42.2](https://github.com/filebrowser/filebrowser/compare/v2.42.1...v2.42.2) (2025-08-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* show file upload errors ([06e8713](https://github.com/filebrowser/filebrowser/commit/06e8713fa55065d38f02499d3e8d39fc86926cab))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* upload progress calculation ([#5350](https://github.com/filebrowser/filebrowser/issues/5350)) ([c14cf86](https://github.com/filebrowser/filebrowser/commit/c14cf86f8304e01d804e01a7eef5ea093627ef37))
|
||||||
|
|
||||||
|
### [2.42.1](https://github.com/filebrowser/filebrowser/compare/v2.42.0...v2.42.1) (2025-07-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Translate frontend/src/i18n/en.json in sk ([14ee054](https://github.com/filebrowser/filebrowser/commit/14ee0543599f2ec73b7f5d2dbd8415f47fe592aa))
|
||||||
|
* Translate frontend/src/i18n/en.json in vi ([75baf7c](https://github.com/filebrowser/filebrowser/commit/75baf7ce337671a1045f897ba4a19967a31b1aec))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* directory mode on config init ([4ff6347](https://github.com/filebrowser/filebrowser/commit/4ff634715543b65878943273dff70f340167900b))
|
||||||
|
|
||||||
|
## [2.42.0](https://github.com/filebrowser/filebrowser/compare/v2.41.0...v2.42.0) (2025-07-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add Norwegian support ([#5332](https://github.com/filebrowser/filebrowser/issues/5332)) ([25e47c3](https://github.com/filebrowser/filebrowser/commit/25e47c3ce8b35b820b5370a4b8bfdf682bd5ae0b))
|
||||||
|
* select item on file list after navigating back ([#5329](https://github.com/filebrowser/filebrowser/issues/5329)) ([cbeec6d](https://github.com/filebrowser/filebrowser/commit/cbeec6d225691723c4750d7f84122ebb14d662bf))
|
||||||
|
* Translate frontend/src/i18n/en.json in no ([5eb3bf4](https://github.com/filebrowser/filebrowser/commit/5eb3bf40586c2ffc32f4834b5dd59f0eb719c1f7))
|
||||||
|
* Translate frontend/src/i18n/en.json in sk ([07dfdce](https://github.com/filebrowser/filebrowser/commit/07dfdce8e4c371f4ca7480f3cef0bd66ff5c9abb))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* norsk loading ([619f683](https://github.com/filebrowser/filebrowser/commit/619f6837b0d1ec6c654d30f4ecedd6696874721f))
|
||||||
|
|
||||||
|
|
||||||
|
### Reverts
|
||||||
|
|
||||||
|
* Revert "chore(release): 2.42.0" ([d778c19](https://github.com/filebrowser/filebrowser/commit/d778c192ae02c5e73781f7632e3b7276c5811e17))
|
||||||
|
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* bump go version to 1.23.11 ([c7a5c7e](https://github.com/filebrowser/filebrowser/commit/c7a5c7efee2b2bede89ec90bafd1af61c39519ff))
|
||||||
|
* bump to go 1.24 ([c1b0207](https://github.com/filebrowser/filebrowser/commit/c1b0207800b4bb52c8dd459c1d69ce0f785473b6))
|
||||||
|
|
||||||
## [2.41.0](https://github.com/filebrowser/filebrowser/compare/v2.40.2...v2.41.0) (2025-07-22)
|
## [2.41.0](https://github.com/filebrowser/filebrowser/compare/v2.40.2...v2.41.0) (2025-07-22)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ override the options.`,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.DirMode, err = getMode(flags, "file-mode")
|
s.DirMode, err = getMode(flags, "dir-mode")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default async function search(base: string, query: string) {
|
|||||||
|
|
||||||
let data = await res.json();
|
let data = await res.json();
|
||||||
|
|
||||||
data = data.map((item: UploadItem) => {
|
data = data.map((item: ResourceItem & { dir: boolean }) => {
|
||||||
item.url = `/files${base}` + url.encodePath(item.path);
|
item.url = `/files${base}` + url.encodePath(item.path);
|
||||||
|
|
||||||
if (item.dir) {
|
if (item.dir) {
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
import { baseURL, tusEndpoint, tusSettings, origin } from "@/utils/constants";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { removePrefix } from "@/api/utils";
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000;
|
const RETRY_BASE_DELAY = 1000;
|
||||||
const RETRY_MAX_DELAY = 20000;
|
const RETRY_MAX_DELAY = 20000;
|
||||||
const SPEED_UPDATE_INTERVAL = 1000;
|
const CURRENT_UPLOAD_LIST: { [key: string]: tus.Upload } = {};
|
||||||
const ALPHA = 0.2;
|
|
||||||
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
|
||||||
const RECENT_SPEEDS_LIMIT = 5;
|
|
||||||
const MB_DIVISOR = 1024 * 1024;
|
|
||||||
const CURRENT_UPLOAD_LIST: CurrentUploadList = {};
|
|
||||||
|
|
||||||
export async function upload(
|
export async function upload(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
@@ -55,48 +49,35 @@ export async function upload(
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onError: function (error) {
|
onError: function (error: Error | tus.DetailedError) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
reject(new Error(`Upload failed: ${error.message}`));
|
|
||||||
|
if (error.message === "Upload aborted") {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const message =
|
||||||
|
error instanceof tus.DetailedError
|
||||||
|
? error.originalResponse === null
|
||||||
|
? "000 No connection"
|
||||||
|
: error.originalResponse.getBody()
|
||||||
|
: "Upload failed";
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
reject(new Error(message));
|
||||||
},
|
},
|
||||||
onProgress: function (bytesUploaded) {
|
onProgress: function (bytesUploaded) {
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
fileData.currentBytesUploaded = bytesUploaded;
|
|
||||||
|
|
||||||
if (!fileData.hasStarted) {
|
|
||||||
fileData.hasStarted = true;
|
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
|
||||||
|
|
||||||
fileData.interval = window.setInterval(() => {
|
|
||||||
calcProgress(filePath);
|
|
||||||
}, SPEED_UPDATE_INTERVAL);
|
|
||||||
}
|
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
onupload({ loaded: bytesUploaded });
|
onupload({ loaded: bytesUploaded });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: function () {
|
onSuccess: function () {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
|
||||||
}
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
CURRENT_UPLOAD_LIST[filePath] = {
|
CURRENT_UPLOAD_LIST[filePath] = upload;
|
||||||
upload: upload,
|
|
||||||
recentSpeeds: [],
|
|
||||||
initialBytesUploaded: 0,
|
|
||||||
currentBytesUploaded: 0,
|
|
||||||
currentAverageSpeed: 0,
|
|
||||||
lastProgressTimestamp: null,
|
|
||||||
sumOfRecentSpeeds: 0,
|
|
||||||
hasStarted: false,
|
|
||||||
interval: undefined,
|
|
||||||
};
|
|
||||||
upload.start();
|
upload.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,76 +109,11 @@ function isTusSupported() {
|
|||||||
return tus.isSupported === true;
|
return tus.isSupported === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeETA(speed?: number) {
|
|
||||||
const state = useUploadStore();
|
|
||||||
if (state.speedMbyte === 0) {
|
|
||||||
return Infinity;
|
|
||||||
}
|
|
||||||
const totalSize = state.sizes.reduce(
|
|
||||||
(acc: number, size: number) => acc + size,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const uploadedSize = state.progress.reduce((a, b) => a + b, 0);
|
|
||||||
const remainingSize = totalSize - uploadedSize;
|
|
||||||
const speedBytesPerSecond = (speed ?? state.speedMbyte) * 1024 * 1024;
|
|
||||||
return remainingSize / speedBytesPerSecond;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeGlobalSpeedAndETA() {
|
|
||||||
let totalSpeed = 0;
|
|
||||||
let totalCount = 0;
|
|
||||||
|
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
|
||||||
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
|
||||||
totalCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
|
||||||
|
|
||||||
const averageSpeed = totalSpeed / totalCount;
|
|
||||||
const averageETA = computeETA(averageSpeed);
|
|
||||||
|
|
||||||
return { speed: averageSpeed, eta: averageETA };
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcProgress(filePath: string) {
|
|
||||||
const uploadStore = useUploadStore();
|
|
||||||
const fileData = CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
|
|
||||||
const elapsedTime =
|
|
||||||
(Date.now() - (fileData.lastProgressTimestamp ?? 0)) / 1000;
|
|
||||||
const bytesSinceLastUpdate =
|
|
||||||
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
|
||||||
const currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
|
||||||
|
|
||||||
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
|
||||||
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift() ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileData.recentSpeeds.push(currentSpeed);
|
|
||||||
fileData.sumOfRecentSpeeds += currentSpeed;
|
|
||||||
|
|
||||||
const avgRecentSpeed =
|
|
||||||
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
|
||||||
fileData.currentAverageSpeed =
|
|
||||||
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
|
||||||
|
|
||||||
const { speed, eta } = computeGlobalSpeedAndETA();
|
|
||||||
uploadStore.setUploadSpeed(speed);
|
|
||||||
uploadStore.setETA(eta);
|
|
||||||
|
|
||||||
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
|
||||||
fileData.lastProgressTimestamp = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function abortAllUploads() {
|
export function abortAllUploads() {
|
||||||
for (const filePath in CURRENT_UPLOAD_LIST) {
|
for (const filePath in CURRENT_UPLOAD_LIST) {
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
if (CURRENT_UPLOAD_LIST[filePath]) {
|
||||||
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
CURRENT_UPLOAD_LIST[filePath].abort(true);
|
||||||
}
|
CURRENT_UPLOAD_LIST[filePath].options!.onError!(
|
||||||
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
|
||||||
CURRENT_UPLOAD_LIST[filePath].upload.options!.onError!(
|
|
||||||
new Error("Upload aborted")
|
new Error("Upload aborted")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ import {
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import ProgressBar from "@/components/ProgressBar.vue";
|
import ProgressBar from "@/components/ProgressBar.vue";
|
||||||
import prettyBytes from "pretty-bytes";
|
import prettyBytes from "pretty-bytes";
|
||||||
import { StatusError } from "@/api/utils.js";
|
|
||||||
|
|
||||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||||
|
|
||||||
@@ -181,13 +180,9 @@ export default {
|
|||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usage.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} finally {
|
||||||
if (error instanceof StatusError && error.is_canceled) {
|
return Object.assign(this.usage, usageStats);
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$showError(error);
|
|
||||||
}
|
}
|
||||||
return Object.assign(this.usage, usageStats);
|
|
||||||
},
|
},
|
||||||
toRoot() {
|
toRoot() {
|
||||||
this.$router.push({ path: "/files" });
|
this.$router.push({ path: "/files" });
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import FileList from "./FileList.vue";
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "copy",
|
name: "copy",
|
||||||
@@ -76,7 +77,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
...mapState(useFileStore, ["req", "selected"]),
|
||||||
...mapState(useAuthStore, ["user"]),
|
...mapState(useAuthStore, ["user"]),
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||||
@@ -100,6 +101,7 @@ export default {
|
|||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("copy");
|
buttons.success("copy");
|
||||||
|
this.preselect = removePrefix(items[0].to);
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (this.$route.path === this.dest) {
|
||||||
this.reload = true;
|
this.reload = true;
|
||||||
|
|||||||
@@ -48,16 +48,15 @@ export default {
|
|||||||
"selectedCount",
|
"selectedCount",
|
||||||
"req",
|
"req",
|
||||||
"selected",
|
"selected",
|
||||||
"currentPrompt",
|
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||||
|
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
submit: async function () {
|
submit: async function () {
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
window.sessionStorage.setItem("modified", "true");
|
|
||||||
try {
|
try {
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
await api.remove(this.$route.path);
|
await api.remove(this.$route.path);
|
||||||
@@ -81,6 +80,12 @@ export default {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
|
||||||
|
const nearbyItem =
|
||||||
|
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
|
||||||
|
|
||||||
|
this.preselect = nearbyItem?.path;
|
||||||
|
|
||||||
this.reload = true;
|
this.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="focus-prompt"
|
id="focus-prompt"
|
||||||
@click="submit"
|
@click="currentPrompt.confirm"
|
||||||
class="button button--flat button--red"
|
class="button button--flat button--red"
|
||||||
:aria-label="$t('buttons.discardChanges')"
|
:aria-label="$t('buttons.discardChanges')"
|
||||||
:title="$t('buttons.discardChanges')"
|
:title="$t('buttons.discardChanges')"
|
||||||
@@ -30,22 +30,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from "pinia";
|
import { mapState, mapActions } from "pinia";
|
||||||
import url from "@/utils/url";
|
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useFileStore } from "@/stores/file";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "discardEditorChanges",
|
name: "discardEditorChanges",
|
||||||
|
computed: {
|
||||||
|
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
...mapActions(useFileStore, ["updateRequest"]),
|
|
||||||
submit: async function () {
|
|
||||||
this.updateRequest(null);
|
|
||||||
|
|
||||||
const uri = url.removeLastDir(this.$route.path) + "/";
|
|
||||||
this.$router.push({ path: uri });
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
@@ -63,6 +63,7 @@ import FileList from "./FileList.vue";
|
|||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
import * as upload from "@/utils/upload";
|
import * as upload from "@/utils/upload";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "move",
|
name: "move",
|
||||||
@@ -77,6 +78,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
...mapState(useFileStore, ["req", "selected"]),
|
||||||
...mapState(useAuthStore, ["user"]),
|
...mapState(useAuthStore, ["user"]),
|
||||||
|
...mapWritableState(useFileStore, ["preselect"]),
|
||||||
excludedFolders() {
|
excludedFolders() {
|
||||||
return this.selected
|
return this.selected
|
||||||
.filter((idx) => this.req.items[idx].isDir)
|
.filter((idx) => this.req.items[idx].isDir)
|
||||||
@@ -104,6 +106,7 @@ export default {
|
|||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("move");
|
buttons.success("move");
|
||||||
|
this.preselect = removePrefix(items[0].to);
|
||||||
this.$router.push({ path: this.dest });
|
this.$router.push({ path: this.dest });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { useFileStore } from "@/stores/file";
|
|||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import url from "@/utils/url";
|
import url from "@/utils/url";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "rename",
|
name: "rename",
|
||||||
@@ -65,7 +66,7 @@ export default {
|
|||||||
"selectedCount",
|
"selectedCount",
|
||||||
"isListing",
|
"isListing",
|
||||||
]),
|
]),
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||||
@@ -97,7 +98,6 @@ export default {
|
|||||||
newLink =
|
newLink =
|
||||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
||||||
|
|
||||||
window.sessionStorage.setItem("modified", "true");
|
|
||||||
try {
|
try {
|
||||||
await api.move([{ from: oldLink, to: newLink }]);
|
await api.move([{ from: oldLink, to: newLink }]);
|
||||||
if (!this.isListing) {
|
if (!this.isListing) {
|
||||||
@@ -105,6 +105,8 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.preselect = removePrefix(newLink);
|
||||||
|
|
||||||
this.reload = true;
|
this.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
this.$showError(e);
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="filesInUploadCount > 0"
|
v-if="uploadStore.activeUploads.size > 0"
|
||||||
class="upload-files"
|
class="upload-files"
|
||||||
v-bind:class="{ closed: !open }"
|
v-bind:class="{ closed: !open }"
|
||||||
>
|
>
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
<h2>
|
||||||
|
{{
|
||||||
|
$t("prompts.uploadFiles", {
|
||||||
|
files: uploadStore.pendingUploadCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
<div class="upload-info">
|
<div class="upload-info">
|
||||||
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
|
<div class="upload-speed">{{ speedMbytes }}/s</div>
|
||||||
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
||||||
<div class="upload-percentage">
|
<div class="upload-percentage">{{ sentPercent }}% Completed</div>
|
||||||
{{ getProgressDecimal }}% Completed
|
|
||||||
</div>
|
|
||||||
<div class="upload-fraction">
|
<div class="upload-fraction">
|
||||||
{{ getTotalProgressBytes }} / {{ getTotalSize }}
|
{{ sentMbytes }} /
|
||||||
|
{{ totalMbytes }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -40,17 +45,21 @@
|
|||||||
<div class="card-content file-icons">
|
<div class="card-content file-icons">
|
||||||
<div
|
<div
|
||||||
class="file"
|
class="file"
|
||||||
v-for="file in filesInUpload"
|
v-for="upload in uploadStore.activeUploads"
|
||||||
:key="file.id"
|
:key="upload.path"
|
||||||
:data-dir="file.isDir"
|
:data-dir="upload.type === 'dir'"
|
||||||
:data-type="file.type"
|
:data-type="upload.type"
|
||||||
:aria-label="file.name"
|
:aria-label="upload.name"
|
||||||
>
|
>
|
||||||
<div class="file-name">
|
<div class="file-name">
|
||||||
<i class="material-icons"></i> {{ file.name }}
|
<i class="material-icons"></i> {{ upload.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="file-progress">
|
<div class="file-progress">
|
||||||
<div v-bind:style="{ width: file.progress + '%' }"></div>
|
<div
|
||||||
|
v-bind:style="{
|
||||||
|
width: (upload.sentBytes / upload.totalBytes) * 100 + '%',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,63 +67,126 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapState, mapWritableState, mapActions } from "pinia";
|
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { abortAllUploads } from "@/api/tus";
|
import { useUploadStore } from "@/stores/upload";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { partial } from "filesize";
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n({});
|
||||||
name: "uploadFiles",
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(useUploadStore, [
|
|
||||||
"filesInUpload",
|
|
||||||
"filesInUploadCount",
|
|
||||||
"uploadSpeed",
|
|
||||||
"getETA",
|
|
||||||
"getProgress",
|
|
||||||
"getProgressDecimal",
|
|
||||||
"getTotalProgressBytes",
|
|
||||||
"getTotalSize",
|
|
||||||
]),
|
|
||||||
...mapWritableState(useFileStore, ["reload"]),
|
|
||||||
formattedETA() {
|
|
||||||
if (!this.getETA || this.getETA === Infinity) {
|
|
||||||
return "--:--:--";
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalSeconds = this.getETA;
|
const open = ref<boolean>(false);
|
||||||
const hours = Math.floor(totalSeconds / 3600);
|
const speed = ref<number>(0);
|
||||||
totalSeconds %= 3600;
|
const eta = ref<number>(Infinity);
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
|
||||||
const seconds = Math.round(totalSeconds % 60);
|
|
||||||
|
|
||||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
const fileStore = useFileStore();
|
||||||
.toString()
|
const uploadStore = useUploadStore();
|
||||||
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
|
||||||
},
|
const { sentBytes, totalBytes } = storeToRefs(uploadStore);
|
||||||
},
|
|
||||||
methods: {
|
const byteToMbyte = partial({ exponent: 2 });
|
||||||
...mapActions(useUploadStore, ["reset"]), // Mapping reset action from upload store
|
|
||||||
toggle: function () {
|
const sentPercent = computed(() =>
|
||||||
this.open = !this.open;
|
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
|
||||||
},
|
);
|
||||||
abortAll() {
|
|
||||||
if (confirm(this.$t("upload.abortUpload"))) {
|
const sentMbytes = computed(() => byteToMbyte(uploadStore.sentBytes));
|
||||||
abortAllUploads();
|
const totalMbytes = computed(() => byteToMbyte(uploadStore.totalBytes));
|
||||||
buttons.done("upload");
|
const speedMbytes = computed(() => byteToMbyte(speed.value));
|
||||||
this.open = false;
|
|
||||||
this.reset(); // Resetting the upload store state
|
let lastSpeedUpdate: number = 0;
|
||||||
this.reload = true; // Trigger reload in the file store
|
let recentSpeeds: number[] = [];
|
||||||
}
|
|
||||||
},
|
const calculateSpeed = (sentBytes: number, oldSentBytes: number) => {
|
||||||
},
|
// Reset the state when the uploads batch is complete
|
||||||
|
if (sentBytes === 0) {
|
||||||
|
lastSpeedUpdate = 0;
|
||||||
|
recentSpeeds = [];
|
||||||
|
|
||||||
|
eta.value = Infinity;
|
||||||
|
speed.value = 0;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedTime = (Date.now() - (lastSpeedUpdate ?? 0)) / 1000;
|
||||||
|
const bytesSinceLastUpdate = sentBytes - oldSentBytes;
|
||||||
|
const currentSpeed = bytesSinceLastUpdate / elapsedTime;
|
||||||
|
|
||||||
|
recentSpeeds.push(currentSpeed);
|
||||||
|
if (recentSpeeds.length > 5) {
|
||||||
|
recentSpeeds.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentSpeedsAverage =
|
||||||
|
recentSpeeds.reduce((acc, curr) => acc + curr) / recentSpeeds.length;
|
||||||
|
|
||||||
|
// Use the current speed for the first update to avoid smoothing lag
|
||||||
|
if (recentSpeeds.length === 1) {
|
||||||
|
speed.value = currentSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
speed.value = recentSpeedsAverage * 0.2 + speed.value * 0.8;
|
||||||
|
|
||||||
|
lastSpeedUpdate = Date.now();
|
||||||
|
|
||||||
|
calculateEta();
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateEta = () => {
|
||||||
|
if (speed.value === 0) {
|
||||||
|
eta.value = Infinity;
|
||||||
|
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingSize = uploadStore.totalBytes - uploadStore.sentBytes;
|
||||||
|
const speedBytesPerSecond = speed.value;
|
||||||
|
|
||||||
|
eta.value = remainingSize / speedBytesPerSecond;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(sentBytes, calculateSpeed);
|
||||||
|
|
||||||
|
watch(totalBytes, (totalBytes, oldTotalBytes) => {
|
||||||
|
if (oldTotalBytes !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the start time of a new upload batch
|
||||||
|
lastSpeedUpdate = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedETA = computed(() => {
|
||||||
|
if (!eta.value || eta.value === Infinity) {
|
||||||
|
return "--:--:--";
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSeconds = eta.value;
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
totalSeconds %= 3600;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = Math.round(totalSeconds % 60);
|
||||||
|
|
||||||
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
open.value = !open.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abortAll = () => {
|
||||||
|
if (confirm(t("upload.abortUpload"))) {
|
||||||
|
buttons.done("upload");
|
||||||
|
open.value = false;
|
||||||
|
uploadStore.abort();
|
||||||
|
fileStore.reload = true; // Trigger reload in the file store
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default {
|
|||||||
ja: "日本語",
|
ja: "日本語",
|
||||||
ko: "한국어",
|
ko: "한국어",
|
||||||
"nl-be": "Dutch (Belgium)",
|
"nl-be": "Dutch (Belgium)",
|
||||||
|
no: "Norsk",
|
||||||
pl: "Polski",
|
pl: "Polski",
|
||||||
"pt-br": "Português",
|
"pt-br": "Português",
|
||||||
pt: "Português (Brasil)",
|
pt: "Português (Brasil)",
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ main {
|
|||||||
height: 3em;
|
height: 3em;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
border-bottom: 1px solid var(--divider);
|
border-bottom: 1px solid var(--divider);
|
||||||
|
position: sticky;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs span,
|
.breadcrumbs span,
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ main .spinner .bounce2 {
|
|||||||
#editor-container {
|
#editor-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
padding-top: 4em;
|
padding-top: 4em;
|
||||||
@@ -351,6 +352,8 @@ main .spinner .bounce2 {
|
|||||||
#editor-container .breadcrumbs {
|
#editor-container .breadcrumbs {
|
||||||
height: 2.3em;
|
height: 2.3em;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** RTL - flip and position arrow of path ***/
|
/*** RTL - flip and position arrow of path ***/
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import("dayjs/locale/vi");
|
|||||||
import("dayjs/locale/zh-cn");
|
import("dayjs/locale/zh-cn");
|
||||||
import("dayjs/locale/zh-tw");
|
import("dayjs/locale/zh-tw");
|
||||||
import("dayjs/locale/cs");
|
import("dayjs/locale/cs");
|
||||||
|
import("dayjs/locale/nb");
|
||||||
|
|
||||||
// All i18n resources specified in the plugin `include` option can be loaded
|
// All i18n resources specified in the plugin `include` option can be loaded
|
||||||
// at once using the import syntax
|
// at once using the import syntax
|
||||||
@@ -101,7 +102,6 @@ export function detectLocale() {
|
|||||||
case /^tr\b/.test(locale):
|
case /^tr\b/.test(locale):
|
||||||
locale = "tr";
|
locale = "tr";
|
||||||
break;
|
break;
|
||||||
// ua wasnt a valid locale for ukraine
|
|
||||||
case /^uk\b/.test(locale):
|
case /^uk\b/.test(locale):
|
||||||
locale = "uk";
|
locale = "uk";
|
||||||
break;
|
break;
|
||||||
@@ -115,6 +115,10 @@ export function detectLocale() {
|
|||||||
case /^nl-be\b/.test(locale):
|
case /^nl-be\b/.test(locale):
|
||||||
locale = "nl-be";
|
locale = "nl-be";
|
||||||
break;
|
break;
|
||||||
|
case /^nb\b/.test(locale):
|
||||||
|
case /^no\b/.test(locale):
|
||||||
|
locale = "no";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
locale = "en";
|
locale = "en";
|
||||||
}
|
}
|
||||||
|
|||||||
266
frontend/src/i18n/no.json
Normal file
266
frontend/src/i18n/no.json
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
{
|
||||||
|
"buttons": {
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"clear": "Fjern",
|
||||||
|
"close": "Lukk",
|
||||||
|
"continue": "Fortsett",
|
||||||
|
"copy": "Kopier",
|
||||||
|
"copyFile": "Fortsett",
|
||||||
|
"copyToClipboard": "Kopier til utklippstavlen",
|
||||||
|
"copyDownloadLinkToClipboard": "Kopier nedlastingslenken til utklippstavlen",
|
||||||
|
"create": "Opprett",
|
||||||
|
"delete": "Slett",
|
||||||
|
"download": "Nedlast",
|
||||||
|
"file": "Fil",
|
||||||
|
"folder": "Mappe",
|
||||||
|
"fullScreen": "Skru på fullskjerm",
|
||||||
|
"hideDotfiles": "Skjul punktfiler",
|
||||||
|
"info": "Info",
|
||||||
|
"more": "Meir",
|
||||||
|
"move": "Flytt",
|
||||||
|
"moveFile": "Flytt Fil",
|
||||||
|
"new": "Ny",
|
||||||
|
"next": "Neste",
|
||||||
|
"ok": "Ok",
|
||||||
|
"permalink": "Få permanent link",
|
||||||
|
"previous": "Tidligere",
|
||||||
|
"preview": "Forhåndsvisning",
|
||||||
|
"publish": "Publiser",
|
||||||
|
"rename": "Gi nytt navn",
|
||||||
|
"replace": "Bytt ut\n ",
|
||||||
|
"reportIssue": "Rapporter problem",
|
||||||
|
"save": "Lagre",
|
||||||
|
"schedule": "Planlegg ",
|
||||||
|
"search": "Søk",
|
||||||
|
"select": "Velg",
|
||||||
|
"selectMultiple": "Velg Fleire",
|
||||||
|
"share": "Del",
|
||||||
|
"shell": "Skru på shell",
|
||||||
|
"submit": "Send",
|
||||||
|
"switchView": "Skift visning",
|
||||||
|
"toggleSidebar": "Skru på sidebar",
|
||||||
|
"update": "Opptater",
|
||||||
|
"upload": "Last opp",
|
||||||
|
"openFile": "Open file",
|
||||||
|
"discardChanges": "Slett"
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"downloadFile": "Nedlast filen",
|
||||||
|
"downloadFolder": "Nedlast mappen",
|
||||||
|
"downloadSelected": "Nedlast merket"
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"abortUpload": "Er du sikker på at du ønsker å avbryte?"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"forbidden": "Du har ikkje tilgang til denne filen.",
|
||||||
|
"internal": "Noko gikk virkelig galt.",
|
||||||
|
"notFound": "Denne lokasjonen kan ikkje bli nådd.",
|
||||||
|
"connection": "Denne serveren kan ikkje nås."
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"body": "Kropp",
|
||||||
|
"closePreview": "Lukk forhandsvisning",
|
||||||
|
"files": "Filer",
|
||||||
|
"folders": "Mappe",
|
||||||
|
"home": "Hjem",
|
||||||
|
"lastModified": "Sist endret",
|
||||||
|
"loading": "Laster....",
|
||||||
|
"lonely": "Det føltes ensomt her...",
|
||||||
|
"metadata": "Metadata",
|
||||||
|
"multipleSelectionEnabled": "Fleire seksjoner på",
|
||||||
|
"name": "Navn",
|
||||||
|
"size": "Størrelse",
|
||||||
|
"sortByLastModified": "Sorter etter sist endret",
|
||||||
|
"sortByName": "Sorter etter navn",
|
||||||
|
"sortBySize": "Sorter etter størrelse",
|
||||||
|
"noPreview": "Forhåndsvisning er ikkje tilgjengeleg for denne filen."
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"click": "velg fil eller katalog",
|
||||||
|
"ctrl": {
|
||||||
|
"click": "velg flere filer eller mapper",
|
||||||
|
"f": "opner søk",
|
||||||
|
"s": "lagr en fil eller last ned direktoratet der du er"
|
||||||
|
},
|
||||||
|
"del": "slett markert filer",
|
||||||
|
"doubleClick": "open en fil eller direktorat",
|
||||||
|
"esc": "visk av seleksjon og/eller lukk dette varselet",
|
||||||
|
"f1": "denne informasjonen",
|
||||||
|
"f2": "gi nytt navn til denne filen",
|
||||||
|
"help": "Hjelp"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"createAnAccount": "Opprett ein konto",
|
||||||
|
"loginInstead": "Du har allerede ein konto",
|
||||||
|
"password": "Passord",
|
||||||
|
"passwordConfirm": "Passordbekreftelse",
|
||||||
|
"passwordsDontMatch": "Passordene samsvarer ikkje",
|
||||||
|
"signup": "Registrer deg",
|
||||||
|
"submit": "Logg inn",
|
||||||
|
"username": "Brukernavn",
|
||||||
|
"usernameTaken": "Brukernavn er allerede i bruk",
|
||||||
|
"wrongCredentials": "Feil legitimasjon"
|
||||||
|
},
|
||||||
|
"permanent": "Permanent",
|
||||||
|
"prompts": {
|
||||||
|
"copy": "Kopiere",
|
||||||
|
"copyMessage": "Velg hvor du vil kopiere filene dine:",
|
||||||
|
"currentlyNavigating": "Navigerer nå på:",
|
||||||
|
"deleteMessageMultiple": "Er du sikker på at du vil slette {count} fil(er)?",
|
||||||
|
"deleteMessageSingle": "Er du sikker på at du vil slette denne filen/mappen?",
|
||||||
|
"deleteMessageShare": "Er du sikker på at du vil slette denne delingen ({path})?",
|
||||||
|
"deleteUser": "Er du sikker at du vil slette denne brukeren?",
|
||||||
|
"deleteTitle": "Slett filer",
|
||||||
|
"displayName": "Vis Navn:",
|
||||||
|
"download": "Last ned filer",
|
||||||
|
"downloadMessage": "Velg kva format du ønsker å laste ned.",
|
||||||
|
"error": "Noko gikk galt.",
|
||||||
|
"fileInfo": "Fil informasjon",
|
||||||
|
"filesSelected": "{count} filer valgt.",
|
||||||
|
"lastModified": "Sist endret",
|
||||||
|
"move": "Flytt",
|
||||||
|
"moveMessage": "Velg nytt hjem for filen(e)/mappen(e)din:",
|
||||||
|
"newArchetype": "Opprett et nytt innlegg basert på en arketype. Filen din opprettes i innholdsmappen.",
|
||||||
|
"newDir": "Nytt Direktorat",
|
||||||
|
"newDirMessage": "Navn gi ditt nye direktorat",
|
||||||
|
"newFile": "Ny fil",
|
||||||
|
"newFileMessage": "Navn gi ditt nye fil",
|
||||||
|
"numberDirs": "Nummer av direktorat",
|
||||||
|
"numberFiles": "Nummer av filer",
|
||||||
|
"rename": "Gi nytt navn",
|
||||||
|
"renameMessage": "Sett inn nytt navn for",
|
||||||
|
"replace": "Bytt ut",
|
||||||
|
"replaceMessage": "En av filene du prøver å laste opp har et motstridende navn. Vil du hoppe over denne filen og fortsette opplastingen eller erstatte den eksisterende?\n",
|
||||||
|
"schedule": "Planlegg",
|
||||||
|
"scheduleMessage": "Velg en dato og et klokkeslett for å planlegge publiseringen av dette innlegget.",
|
||||||
|
"show": "Vis",
|
||||||
|
"size": "Størrelse",
|
||||||
|
"upload": "Last opp",
|
||||||
|
"uploadFiles": "Laster opp {filer} filer...",
|
||||||
|
"uploadMessage": "Velg et alternativ for opplasting.",
|
||||||
|
"optionalPassword": "Valgfritt passord",
|
||||||
|
"resolution": "Oppløysning",
|
||||||
|
"discardEditorChanges": "Er du sikker på at du vil forkaste endringene du har gjort?"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"images": "Bilde",
|
||||||
|
"music": "Musikk",
|
||||||
|
"pdf": "PDF",
|
||||||
|
"pressToSearch": "Trykk enter for å søke...",
|
||||||
|
"search": "Søk...",
|
||||||
|
"typeToSearch": "Trykk for å søke...",
|
||||||
|
"types": "Typer",
|
||||||
|
"video": "Video"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"admin": "Admin",
|
||||||
|
"administrator": "Administrator",
|
||||||
|
"allowCommands": "Utfør kommandoer",
|
||||||
|
"allowEdit": "Rediger, gi nytt navn til og slett filer eller mapper",
|
||||||
|
"allowNew": "Opprett nye filer og direktorater",
|
||||||
|
"allowPublish": "Publiser nye innlegg og sider",
|
||||||
|
"allowSignup": "Tilat brukere å registrere seg",
|
||||||
|
"avoidChanges": "(la stå tomt for å unngå endringer)",
|
||||||
|
"branding": "Merkevarebygging",
|
||||||
|
"brandingDirectoryPath": "Bane for merkevarekatalog",
|
||||||
|
"brandingHelp": "Du kan tilpasse hvordan Filleser-instansen din ser ut og føles ved å endre navnet, erstatte logoen, legge til egendefinerte stiler og til og med deaktivere eksterne lenker til GitHub.\n\nFor mer informasjon om tilpasset merkevarebygging, se {0}.",
|
||||||
|
"changePassword": "Skift Passord",
|
||||||
|
"commandRunner": "Kommandoløper",
|
||||||
|
"commandRunnerHelp": "Her kan du angi kommandoer som skal utføres i de navngitte hendelsene. Du må skrive én per linje. Miljøvariablene {0} og {1} vil være tilgjengelige, siden de er {0} relative til {1}. For mer informasjon om denne funksjonen og de tilgjengelige miljøvariablene, vennligst les {2}.",
|
||||||
|
"commandsUpdated": "Komando opptatert!",
|
||||||
|
"createUserDir": "Opprett brukerens hjemmappe automatisk når du legger til en ny bruker",
|
||||||
|
"minimumPasswordLength": "Minimum passord lengde",
|
||||||
|
"tusUploads": "Klumpede opplastinger",
|
||||||
|
"tusUploadsHelp": "Filleseren støtter opplasting av delte filer, noe som gjør det mulig å lage effektive, pålitelige, gjenopptakbare og delte filer, selv på upålitelige nettverk.",
|
||||||
|
"tusUploadsChunkSize": "Angir maksimal størrelse på en forespørsel (direkte opplastinger vil bli brukt for mindre opplastinger). Du kan legge inn et heltall som angir bytestørrelsen, eller en streng som 10 MB, 1 GB osv.",
|
||||||
|
"tusUploadsRetryCount": "Antall nye forsøk som skal utføres hvis en del ikke lastes opp.",
|
||||||
|
"userHomeBasePath": "Basissti for brukerens hjemmekataloger",
|
||||||
|
"userScopeGenerationPlaceholder": "Omfanget vil bli generert automatisk",
|
||||||
|
"createUserHomeDirectory": "Opprett bruker hjemme direktorat",
|
||||||
|
"customStylesheet": "Egendefinert stilark",
|
||||||
|
"defaultUserDescription": "Dette er standardinnstillingene for nye brukere.",
|
||||||
|
"disableExternalLinks": "Deaktiver eksterne lenker (unntatt dokumentasjon)",
|
||||||
|
"disableUsedDiskPercentage": "Deaktiver grafen for prosentandelen brukt disk",
|
||||||
|
"documentation": "dokumentasjon",
|
||||||
|
"examples": "Eksempel",
|
||||||
|
"executeOnShell": "Kjør på skall",
|
||||||
|
"executeOnShellDescription": "Som standard kjører Filleseren kommandoene ved å kalle binærfilene direkte. Hvis du heller ønsker å kjøre dem på et skall (som Bash eller PowerShell), kan du definere det her med de nødvendige argumentene og flaggene. Hvis dette er angitt, vil kommandoen du kjører bli lagt til som et argument. Dette gjelder både brukerkommandoer og hendelseshooker.",
|
||||||
|
"globalRules": "Dette er et globalt sett med regler for tillatelse og forbud. De gjelder for alle brukere. Du kan definere spesifikke regler for hver brukers innstillinger for å overstyre disse.",
|
||||||
|
"globalSettings": "Globale Innstillinger",
|
||||||
|
"hideDotfiles": "Skjul punktfiler",
|
||||||
|
"insertPath": "Sett inn banen",
|
||||||
|
"insertRegex": "sett inn regex-uttrykk",
|
||||||
|
"instanceName": "Forekomstnavn",
|
||||||
|
"language": "Språk",
|
||||||
|
"lockPassword": "Hindre brukeren i å endre passordet",
|
||||||
|
"newPassword": "Sett ditt nye passord",
|
||||||
|
"newPasswordConfirm": "Bekreft ditt nye passord",
|
||||||
|
"newUser": "Ny bruker",
|
||||||
|
"password": "Passord",
|
||||||
|
"passwordUpdated": "Passord opptatert!",
|
||||||
|
"path": "Veg",
|
||||||
|
"perm": {
|
||||||
|
"create": "Opprett filer og direktorater",
|
||||||
|
"delete": "Slett filer og direktorater",
|
||||||
|
"download": "Nedlast",
|
||||||
|
"execute": "Utfør kommandoer",
|
||||||
|
"modify": "Endre filer",
|
||||||
|
"rename": "Gi nytt navn eller flytt filer og direktorater",
|
||||||
|
"share": "Del filer"
|
||||||
|
},
|
||||||
|
"permissions": "Tilaterser",
|
||||||
|
"permissionsHelp": "Du kan angi brukeren som administrator eller velge tillatelsene individuelt. Hvis du velger «Administrator», vil alle de andre alternativene bli automatisk avkrysset. Administrasjon av brukere er fortsatt et privilegium for en administrator.\n",
|
||||||
|
"profileSettings": "Profil Innstilinger",
|
||||||
|
"ruleExample1": "forhindrer tilgang til noen dotfiler (som .git, .gitignore) i alle mapper.\n",
|
||||||
|
"ruleExample2": "blokkerer tilgangen til filen med navnet Caddyfile på roten av omfanget.",
|
||||||
|
"rules": "Regler",
|
||||||
|
"rulesHelp": "Her kan du definere et sett med tillatelses- og forbudsregler for denne spesifikke brukeren. De blokkerte filene vil ikke vises i listene, og de vil ikke være tilgjengelige for brukeren. Vi støtter regex og stier i forhold til brukerens omfang.",
|
||||||
|
"scope": "Omfang",
|
||||||
|
"setDateFormat": "Sett eksakt dato format",
|
||||||
|
"settingsUpdated": "Innstilinger opptatert!",
|
||||||
|
"shareDuration": "Del tidsbruk",
|
||||||
|
"shareManagement": "Del Ledelse",
|
||||||
|
"shareDeleted": "Delte ting slettet!",
|
||||||
|
"singleClick": "Bruk enkeltklikk for å åpne filer og mapper",
|
||||||
|
"themes": {
|
||||||
|
"default": "Systemstandard",
|
||||||
|
"dark": "Mørk",
|
||||||
|
"light": "Lyst",
|
||||||
|
"title": "Tema"
|
||||||
|
},
|
||||||
|
"user": "Bruker",
|
||||||
|
"userCommands": "Kommando",
|
||||||
|
"userCommandsHelp": "En mellomromsseparert liste med tilgjengelige kommandoer for denne brukeren. Eksempel:\n",
|
||||||
|
"userCreated": "Bruker opprettet!",
|
||||||
|
"userDefaults": "Bruker systemstandard instillinger",
|
||||||
|
"userDeleted": "Bruker slettet!",
|
||||||
|
"userManagement": "Brukeradministrasjon",
|
||||||
|
"userUpdated": "Bruker opprettet!",
|
||||||
|
"username": "Brukernavn",
|
||||||
|
"users": "Bruker"
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"help": "Hjelp",
|
||||||
|
"hugoNew": "Hugo Ny",
|
||||||
|
"login": "Logg inn",
|
||||||
|
"logout": "Logg Ut",
|
||||||
|
"myFiles": "Mine filer",
|
||||||
|
"newFile": "Ny fil",
|
||||||
|
"newFolder": "Ny mappe",
|
||||||
|
"preview": "Forhåndsvis",
|
||||||
|
"settings": "Innstillinger",
|
||||||
|
"signup": "Registrer deg",
|
||||||
|
"siteSettings": "Side innstillinger"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"linkCopied": "Link koppiert!"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"days": "Dager",
|
||||||
|
"hours": "Timer",
|
||||||
|
"minutes": "Minutt",
|
||||||
|
"seconds": "Sekunder",
|
||||||
|
"unit": "Time format"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,17 @@
|
|||||||
"cancel": "Zrušiť",
|
"cancel": "Zrušiť",
|
||||||
"clear": "Zrušiť výber",
|
"clear": "Zrušiť výber",
|
||||||
"close": "Zavrieť",
|
"close": "Zavrieť",
|
||||||
"continue": "Continue",
|
"continue": "Pokračovať",
|
||||||
"copy": "Kopírovať",
|
"copy": "Kopírovať",
|
||||||
"copyFile": "Kopírovať súbor",
|
"copyFile": "Kopírovať súbor",
|
||||||
"copyToClipboard": "Kopírovať do schránky",
|
"copyToClipboard": "Kopírovať do schránky",
|
||||||
"copyDownloadLinkToClipboard": "Copy download link to clipboard",
|
"copyDownloadLinkToClipboard": "Kopírovať odkaz na stiahnutie do schránky",
|
||||||
"create": "Vytvoriť",
|
"create": "Vytvoriť",
|
||||||
"delete": "Odstrániť",
|
"delete": "Odstrániť",
|
||||||
"download": "Stiahnuť",
|
"download": "Stiahnuť",
|
||||||
"file": "Súbor",
|
"file": "Súbor",
|
||||||
"folder": "Priečinok",
|
"folder": "Priečinok",
|
||||||
"fullScreen": "Toggle full screen",
|
"fullScreen": "Prepnúť na celú obrazovku",
|
||||||
"hideDotfiles": "Skryť súbory začínajúce bodkou",
|
"hideDotfiles": "Skryť súbory začínajúce bodkou",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"more": "Viac",
|
"more": "Viac",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"permalink": "Získať trvalý odkaz",
|
"permalink": "Získať trvalý odkaz",
|
||||||
"previous": "Predošlé",
|
"previous": "Predošlé",
|
||||||
"preview": "Preview",
|
"preview": "Náhľad",
|
||||||
"publish": "Zverejniť",
|
"publish": "Zverejniť",
|
||||||
"rename": "Premenovať",
|
"rename": "Premenovať",
|
||||||
"replace": "Nahradiť",
|
"replace": "Nahradiť",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"update": "Aktualizovať",
|
"update": "Aktualizovať",
|
||||||
"upload": "Nahrať",
|
"upload": "Nahrať",
|
||||||
"openFile": "Otvoriť súbor",
|
"openFile": "Otvoriť súbor",
|
||||||
"discardChanges": "Discard"
|
"discardChanges": "Zahodiť"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Stiahnuť súbor",
|
"downloadFile": "Stiahnuť súbor",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"downloadSelected": "Stiahnuť vybraté"
|
"downloadSelected": "Stiahnuť vybraté"
|
||||||
},
|
},
|
||||||
"upload": {
|
"upload": {
|
||||||
"abortUpload": "Are you sure you wish to abort?"
|
"abortUpload": "Naozaj chcete prerušiť?"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"forbidden": "You don't have permissions to access this.",
|
"forbidden": "You don't have permissions to access this.",
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
"deleteMessageMultiple": "Naozaj chcete odstrániť {count} súbor(ov)?",
|
"deleteMessageMultiple": "Naozaj chcete odstrániť {count} súbor(ov)?",
|
||||||
"deleteMessageSingle": "Naozaj chcete odstrániť tento súbor/priečinok?",
|
"deleteMessageSingle": "Naozaj chcete odstrániť tento súbor/priečinok?",
|
||||||
"deleteMessageShare": "Naozaj chcete odstrániť toto zdieľanie({path})?",
|
"deleteMessageShare": "Naozaj chcete odstrániť toto zdieľanie({path})?",
|
||||||
"deleteUser": "Are you sure you want to delete this user?",
|
"deleteUser": "Naozaj chcete odstrániť tohto používateľa?",
|
||||||
"deleteTitle": "Odstránenie súborov",
|
"deleteTitle": "Odstránenie súborov",
|
||||||
"displayName": "Zobrazený názov:",
|
"displayName": "Zobrazený názov:",
|
||||||
"download": "Stiahnuť súbory",
|
"download": "Stiahnuť súbory",
|
||||||
@@ -137,11 +137,11 @@
|
|||||||
"show": "Zobraziť",
|
"show": "Zobraziť",
|
||||||
"size": "Veľkosť",
|
"size": "Veľkosť",
|
||||||
"upload": "Nahrať",
|
"upload": "Nahrať",
|
||||||
"uploadFiles": "Uploading {files} files...",
|
"uploadFiles": "Nahráva sa {files} súborov...",
|
||||||
"uploadMessage": "Zvoľte možnosť nahrávania.",
|
"uploadMessage": "Zvoľte možnosť nahrávania.",
|
||||||
"optionalPassword": "Voliteľné heslo",
|
"optionalPassword": "Voliteľné heslo",
|
||||||
"resolution": "Resolution",
|
"resolution": "Rozlíšenie",
|
||||||
"discardEditorChanges": "Are you sure you wish to discard the changes you've made?"
|
"discardEditorChanges": "Naozaj chcete zahodiť vykonané zmeny?"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"images": "Obrázky",
|
"images": "Obrázky",
|
||||||
@@ -170,14 +170,14 @@
|
|||||||
"commandRunnerHelp": "Sem môžete nastaviť príkazy, ktoré sa vykonajú pri určitých udalostiach. Musíte písať jeden na riadok. Premenné prostredia {0} a {1} sú k dispozícii, s tým že {0} relatívne k {1}. Viac informácií o tejto funkcionalite a dostupných premenných prostredia nájdete na {2}.",
|
"commandRunnerHelp": "Sem môžete nastaviť príkazy, ktoré sa vykonajú pri určitých udalostiach. Musíte písať jeden na riadok. Premenné prostredia {0} a {1} sú k dispozícii, s tým že {0} relatívne k {1}. Viac informácií o tejto funkcionalite a dostupných premenných prostredia nájdete na {2}.",
|
||||||
"commandsUpdated": "Príkazy upravené!",
|
"commandsUpdated": "Príkazy upravené!",
|
||||||
"createUserDir": "Automaticky vytvoriť domovský priečinok pri pridaní používateľa",
|
"createUserDir": "Automaticky vytvoriť domovský priečinok pri pridaní používateľa",
|
||||||
"minimumPasswordLength": "Minimum password length",
|
"minimumPasswordLength": "Minimálna dĺžka hesla",
|
||||||
"tusUploads": "Chunked Uploads",
|
"tusUploads": "Nahrávanie po častiach",
|
||||||
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
|
"tusUploadsHelp": "Prehliadač súborov podporuje nahrávanie súborov po častiach, čo umožňuje vytváranie efektívnych, spoľahlivých, obnoviteľných a po častiach nahrávaných súborov aj v prípade nespoľahlivých sietí.",
|
||||||
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting byte size input or a string like 10MB, 1GB etc.",
|
"tusUploadsChunkSize": "Označuje maximálnu veľkosť požiadavky (pre menšie nahratia sa použijú priame nahratia). Môžete zadať celé číslo označujúce veľkosť v bajtoch alebo reťazec ako 10 MB, 1 GB atď.",
|
||||||
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
|
"tusUploadsRetryCount": "Počet opakovaných pokusov, ktoré sa majú vykonať, ak sa nepodarí nahrať časť súboru.",
|
||||||
"userHomeBasePath": "Base path for user home directories",
|
"userHomeBasePath": "Východisková cesta pre domáce adresáre používateľov",
|
||||||
"userScopeGenerationPlaceholder": "The scope will be auto generated",
|
"userScopeGenerationPlaceholder": "Rozsah bude automaticky generovaný",
|
||||||
"createUserHomeDirectory": "Create user home directory",
|
"createUserHomeDirectory": "Vytvoriť domovský adresár používateľa",
|
||||||
"customStylesheet": "Vlastný Stylesheet",
|
"customStylesheet": "Vlastný Stylesheet",
|
||||||
"defaultUserDescription": "Toto sú predvolané nastavenia nového používateľa.",
|
"defaultUserDescription": "Toto sú predvolané nastavenia nového používateľa.",
|
||||||
"disableExternalLinks": "Vypnúť externé odkazy (okrem dokumentácie)",
|
"disableExternalLinks": "Vypnúť externé odkazy (okrem dokumentácie)",
|
||||||
@@ -217,14 +217,14 @@
|
|||||||
"rules": "Pravidlá",
|
"rules": "Pravidlá",
|
||||||
"rulesHelp": "Tu môžete definovať pravidlá pre konkrétneho používateľa. Blokované súbory používateľ nebude vidieť a ani nebude k nim mať prístup. Podporujeme regex a cesty relatívne k používateľovi.\n",
|
"rulesHelp": "Tu môžete definovať pravidlá pre konkrétneho používateľa. Blokované súbory používateľ nebude vidieť a ani nebude k nim mať prístup. Podporujeme regex a cesty relatívne k používateľovi.\n",
|
||||||
"scope": "Scope",
|
"scope": "Scope",
|
||||||
"setDateFormat": "Set exact date format",
|
"setDateFormat": "Nastaviť presný formát dátumu",
|
||||||
"settingsUpdated": "Nastavenia upravené!",
|
"settingsUpdated": "Nastavenia upravené!",
|
||||||
"shareDuration": "Trvanie zdieľania",
|
"shareDuration": "Trvanie zdieľania",
|
||||||
"shareManagement": "Správa zdieľania",
|
"shareManagement": "Správa zdieľania",
|
||||||
"shareDeleted": "Zdieľanie odstránené!",
|
"shareDeleted": "Zdieľanie odstránené!",
|
||||||
"singleClick": "Používať jeden klik na otváranie súborov a priečinkov",
|
"singleClick": "Používať jeden klik na otváranie súborov a priečinkov",
|
||||||
"themes": {
|
"themes": {
|
||||||
"default": "System default",
|
"default": "Predvolené nastavenie systému",
|
||||||
"dark": "Tmavá",
|
"dark": "Tmavá",
|
||||||
"light": "Svetlá",
|
"light": "Svetlá",
|
||||||
"title": "Téma"
|
"title": "Téma"
|
||||||
|
|||||||
@@ -170,7 +170,7 @@
|
|||||||
"commandRunnerHelp": "Tại đây, bạn có thể thiết lập các lệnh được thực thi trong các sự kiện đã định. Bạn phải viết một lệnh trên mỗi dòng. Các biến môi trường {0} và {1} sẽ có sẵn, trong đó {0} tương đối với {1}. Để biết thêm thông tin về tính năng này và các biến môi trường có sẵn, vui lòng đọc {2}.",
|
"commandRunnerHelp": "Tại đây, bạn có thể thiết lập các lệnh được thực thi trong các sự kiện đã định. Bạn phải viết một lệnh trên mỗi dòng. Các biến môi trường {0} và {1} sẽ có sẵn, trong đó {0} tương đối với {1}. Để biết thêm thông tin về tính năng này và các biến môi trường có sẵn, vui lòng đọc {2}.",
|
||||||
"commandsUpdated": "Lệnh đã được cập nhật!",
|
"commandsUpdated": "Lệnh đã được cập nhật!",
|
||||||
"createUserDir": "Tự động tạo thư mục chính của người dùng khi thêm người dùng mới",
|
"createUserDir": "Tự động tạo thư mục chính của người dùng khi thêm người dùng mới",
|
||||||
"minimumPasswordLength": "Minimum password length",
|
"minimumPasswordLength": "Độ dài mật khẩu tối thiểu",
|
||||||
"tusUploads": "Tải lên theo phân đoạn",
|
"tusUploads": "Tải lên theo phân đoạn",
|
||||||
"tusUploadsHelp": "File Browser hỗ trợ tải lên tệp theo phân đoạn, giúp việc tải lên trở nên hiệu quả, đáng tin cậy, có thể tiếp tục và phù hợp với mạng không ổn định.",
|
"tusUploadsHelp": "File Browser hỗ trợ tải lên tệp theo phân đoạn, giúp việc tải lên trở nên hiệu quả, đáng tin cậy, có thể tiếp tục và phù hợp với mạng không ổn định.",
|
||||||
"tusUploadsChunkSize": "Kích thước tối đa của một yêu cầu (tải lên trực tiếp sẽ được sử dụng cho các tệp nhỏ hơn). Bạn có thể nhập một số nguyên biểu thị kích thước theo byte hoặc một chuỗi như 10MB, 1GB, v.v.",
|
"tusUploadsChunkSize": "Kích thước tối đa của một yêu cầu (tải lên trực tiếp sẽ được sử dụng cho các tệp nhỏ hơn). Bạn có thể nhập một số nguyên biểu thị kích thước theo byte hoặc một chuỗi như 10MB, 1GB, v.v.",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const useFileStore = defineStore("file", {
|
|||||||
selected: number[];
|
selected: number[];
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
isFiles: boolean;
|
isFiles: boolean;
|
||||||
|
preselect: string | null;
|
||||||
} => ({
|
} => ({
|
||||||
req: null,
|
req: null,
|
||||||
oldReq: null,
|
oldReq: null,
|
||||||
@@ -16,6 +17,7 @@ export const useFileStore = defineStore("file", {
|
|||||||
selected: [],
|
selected: [],
|
||||||
multiple: false,
|
multiple: false,
|
||||||
isFiles: false,
|
isFiles: false,
|
||||||
|
preselect: null,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
selectedCount: (state) => state.selected.length,
|
selectedCount: (state) => state.selected.length,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useFileStore } from "./file";
|
import { useFileStore } from "./file";
|
||||||
import { files as api } from "@/api";
|
import { files as api } from "@/api";
|
||||||
import { throttle } from "lodash-es";
|
|
||||||
import buttons from "@/utils/buttons";
|
import buttons from "@/utils/buttons";
|
||||||
|
import { computed, inject, markRaw, ref } from "vue";
|
||||||
|
import * as tus from "@/api/tus";
|
||||||
|
|
||||||
// TODO: make this into a user setting
|
// TODO: make this into a user setting
|
||||||
const UPLOADS_LIMIT = 5;
|
const UPLOADS_LIMIT = 5;
|
||||||
@@ -13,208 +14,167 @@ const beforeUnload = (event: Event) => {
|
|||||||
// event.returnValue = "";
|
// event.returnValue = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility function to format bytes into a readable string
|
export const useUploadStore = defineStore("upload", () => {
|
||||||
function formatSize(bytes: number): string {
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
if (bytes === 0) return "0.00 Bytes";
|
|
||||||
|
|
||||||
const k = 1024;
|
let progressInterval: number | null = null;
|
||||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
// Return the rounded size with two decimal places
|
//
|
||||||
return (bytes / k ** i).toFixed(2) + " " + sizes[i];
|
// STATE
|
||||||
}
|
//
|
||||||
|
|
||||||
export const useUploadStore = defineStore("upload", {
|
const allUploads = ref<Upload[]>([]);
|
||||||
// convert to a function
|
const activeUploads = ref<Set<Upload>>(new Set());
|
||||||
state: (): {
|
const lastUpload = ref<number>(-1);
|
||||||
id: number;
|
const totalBytes = ref<number>(0);
|
||||||
sizes: number[];
|
const sentBytes = ref<number>(0);
|
||||||
progress: number[];
|
|
||||||
queue: UploadItem[];
|
//
|
||||||
uploads: Uploads;
|
// ACTIONS
|
||||||
speedMbyte: number;
|
//
|
||||||
eta: number;
|
|
||||||
error: Error | null;
|
const upload = (
|
||||||
} => ({
|
path: string,
|
||||||
id: 0,
|
name: string,
|
||||||
sizes: [],
|
file: File | null,
|
||||||
progress: [],
|
overwrite: boolean,
|
||||||
queue: [],
|
type: ResourceType
|
||||||
uploads: {},
|
) => {
|
||||||
speedMbyte: 0,
|
if (!hasActiveUploads() && !hasPendingUploads()) {
|
||||||
eta: 0,
|
window.addEventListener("beforeunload", beforeUnload);
|
||||||
error: null,
|
buttons.loading("upload");
|
||||||
}),
|
}
|
||||||
getters: {
|
|
||||||
// user and jwt getter removed, no longer needed
|
const upload: Upload = {
|
||||||
getProgress: (state) => {
|
path,
|
||||||
if (state.progress.length === 0) {
|
name,
|
||||||
return 0;
|
file,
|
||||||
|
overwrite,
|
||||||
|
type,
|
||||||
|
totalBytes: file?.size || 1,
|
||||||
|
sentBytes: 0,
|
||||||
|
// Stores rapidly changing sent bytes value without causing component re-renders
|
||||||
|
rawProgress: markRaw({
|
||||||
|
sentBytes: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
totalBytes.value += upload.totalBytes;
|
||||||
|
allUploads.value.push(upload);
|
||||||
|
|
||||||
|
processUploads();
|
||||||
|
};
|
||||||
|
|
||||||
|
const abort = () => {
|
||||||
|
// Resets the state by preventing the processing of the remaning uploads
|
||||||
|
lastUpload.value = Infinity;
|
||||||
|
tus.abortAllUploads();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// GETTERS
|
||||||
|
//
|
||||||
|
|
||||||
|
const pendingUploadCount = computed(
|
||||||
|
() =>
|
||||||
|
allUploads.value.length -
|
||||||
|
(lastUpload.value + 1) +
|
||||||
|
activeUploads.value.size
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// PRIVATE FUNCTIONS
|
||||||
|
//
|
||||||
|
|
||||||
|
const hasActiveUploads = () => activeUploads.value.size > 0;
|
||||||
|
|
||||||
|
const hasPendingUploads = () =>
|
||||||
|
allUploads.value.length > lastUpload.value + 1;
|
||||||
|
|
||||||
|
const isActiveUploadsOnLimit = () => activeUploads.value.size < UPLOADS_LIMIT;
|
||||||
|
|
||||||
|
const processUploads = async () => {
|
||||||
|
if (!hasActiveUploads() && !hasPendingUploads()) {
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
window.removeEventListener("beforeunload", beforeUnload);
|
||||||
|
buttons.success("upload");
|
||||||
|
reset();
|
||||||
|
fileStore.reload = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActiveUploadsOnLimit() && hasPendingUploads()) {
|
||||||
|
if (!hasActiveUploads()) {
|
||||||
|
// Update the state in a fixed time interval
|
||||||
|
progressInterval = window.setInterval(syncState, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
const upload = nextUpload();
|
||||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
|
||||||
return Math.ceil((sum / totalSize) * 100);
|
if (upload.type === "dir") {
|
||||||
},
|
await api.post(upload.path).catch($showError);
|
||||||
getProgressDecimal: (state) => {
|
} else {
|
||||||
if (state.progress.length === 0) {
|
const onUpload = (event: ProgressEvent) => {
|
||||||
return 0;
|
upload.rawProgress.sentBytes = event.loaded;
|
||||||
|
};
|
||||||
|
|
||||||
|
await api
|
||||||
|
.post(upload.path, upload.file!, upload.overwrite, onUpload)
|
||||||
|
.catch((err) => err.message !== "Upload aborted" && $showError(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
finishUpload(upload);
|
||||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
}
|
||||||
return ((sum / totalSize) * 100).toFixed(2);
|
};
|
||||||
},
|
|
||||||
getTotalProgressBytes: (state) => {
|
|
||||||
if (state.progress.length === 0 || state.sizes.length === 0) {
|
|
||||||
return "0 Bytes";
|
|
||||||
}
|
|
||||||
const sum = state.progress.reduce((a, b) => a + b, 0);
|
|
||||||
return formatSize(sum);
|
|
||||||
},
|
|
||||||
getTotalSize: (state) => {
|
|
||||||
if (state.sizes.length === 0) {
|
|
||||||
return "0 Bytes";
|
|
||||||
}
|
|
||||||
const totalSize = state.sizes.reduce((a, b) => a + b, 0);
|
|
||||||
return formatSize(totalSize);
|
|
||||||
},
|
|
||||||
filesInUploadCount: (state) => {
|
|
||||||
return Object.keys(state.uploads).length + state.queue.length;
|
|
||||||
},
|
|
||||||
filesInUpload: (state) => {
|
|
||||||
const files = [];
|
|
||||||
|
|
||||||
for (const index in state.uploads) {
|
const nextUpload = (): Upload => {
|
||||||
const upload = state.uploads[index];
|
lastUpload.value++;
|
||||||
const id = upload.id;
|
|
||||||
const type = upload.type;
|
|
||||||
const name = upload.file.name;
|
|
||||||
const size = state.sizes[id];
|
|
||||||
const isDir = upload.file.isDir;
|
|
||||||
const progress = isDir
|
|
||||||
? 100
|
|
||||||
: Math.ceil((state.progress[id] / size) * 100);
|
|
||||||
|
|
||||||
files.push({
|
const upload = allUploads.value[lastUpload.value];
|
||||||
id,
|
activeUploads.value.add(upload);
|
||||||
name,
|
|
||||||
progress,
|
|
||||||
type,
|
|
||||||
isDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return files.sort((a, b) => a.progress - b.progress);
|
return upload;
|
||||||
},
|
};
|
||||||
uploadSpeed: (state) => {
|
|
||||||
return state.speedMbyte;
|
|
||||||
},
|
|
||||||
getETA: (state) => state.eta,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
// no context as first argument, use `this` instead
|
|
||||||
setProgress({ id, loaded }: { id: number; loaded: number }) {
|
|
||||||
this.progress[id] = loaded;
|
|
||||||
},
|
|
||||||
setError(error: Error) {
|
|
||||||
this.error = error;
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this.id = 0;
|
|
||||||
this.sizes = [];
|
|
||||||
this.progress = [];
|
|
||||||
this.queue = [];
|
|
||||||
this.uploads = {};
|
|
||||||
this.speedMbyte = 0;
|
|
||||||
this.eta = 0;
|
|
||||||
this.error = null;
|
|
||||||
},
|
|
||||||
addJob(item: UploadItem) {
|
|
||||||
this.queue.push(item);
|
|
||||||
this.sizes[this.id] = item.file.size;
|
|
||||||
this.id++;
|
|
||||||
},
|
|
||||||
moveJob() {
|
|
||||||
const item = this.queue[0];
|
|
||||||
this.queue.shift();
|
|
||||||
this.uploads[item.id] = item;
|
|
||||||
},
|
|
||||||
removeJob(id: number) {
|
|
||||||
delete this.uploads[id];
|
|
||||||
},
|
|
||||||
upload(item: UploadItem) {
|
|
||||||
const uploadsCount = Object.keys(this.uploads).length;
|
|
||||||
|
|
||||||
const isQueueEmpty = this.queue.length == 0;
|
const finishUpload = (upload: Upload) => {
|
||||||
const isUploadsEmpty = uploadsCount == 0;
|
sentBytes.value += upload.totalBytes - upload.sentBytes;
|
||||||
|
upload.sentBytes = upload.totalBytes;
|
||||||
|
upload.file = null;
|
||||||
|
|
||||||
if (isQueueEmpty && isUploadsEmpty) {
|
activeUploads.value.delete(upload);
|
||||||
window.addEventListener("beforeunload", beforeUnload);
|
processUploads();
|
||||||
buttons.loading("upload");
|
};
|
||||||
}
|
|
||||||
|
|
||||||
this.addJob(item);
|
const syncState = () => {
|
||||||
this.processUploads();
|
for (const upload of activeUploads.value) {
|
||||||
},
|
sentBytes.value += upload.rawProgress.sentBytes - upload.sentBytes;
|
||||||
finishUpload(item: UploadItem) {
|
upload.sentBytes = upload.rawProgress.sentBytes;
|
||||||
this.setProgress({ id: item.id, loaded: item.file.size });
|
}
|
||||||
this.removeJob(item.id);
|
};
|
||||||
this.processUploads();
|
|
||||||
},
|
|
||||||
async processUploads() {
|
|
||||||
const uploadsCount = Object.keys(this.uploads).length;
|
|
||||||
|
|
||||||
const isBelowLimit = uploadsCount < UPLOADS_LIMIT;
|
const reset = () => {
|
||||||
const isQueueEmpty = this.queue.length == 0;
|
if (progressInterval !== null) {
|
||||||
const isUploadsEmpty = uploadsCount == 0;
|
clearInterval(progressInterval);
|
||||||
|
progressInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
const isFinished = isQueueEmpty && isUploadsEmpty;
|
allUploads.value = [];
|
||||||
const canProcess = isBelowLimit && !isQueueEmpty;
|
activeUploads.value = new Set();
|
||||||
|
lastUpload.value = -1;
|
||||||
|
totalBytes.value = 0;
|
||||||
|
sentBytes.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
if (isFinished) {
|
return {
|
||||||
const fileStore = useFileStore();
|
// STATE
|
||||||
window.removeEventListener("beforeunload", beforeUnload);
|
activeUploads,
|
||||||
buttons.success("upload");
|
totalBytes,
|
||||||
this.reset();
|
sentBytes,
|
||||||
fileStore.reload = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canProcess) {
|
// ACTIONS
|
||||||
const item = this.queue[0];
|
upload,
|
||||||
this.moveJob();
|
abort,
|
||||||
|
|
||||||
if (item.file.isDir) {
|
// GETTERS
|
||||||
await api.post(item.path).catch(this.setError);
|
pendingUploadCount,
|
||||||
} else {
|
};
|
||||||
const onUpload = throttle(
|
|
||||||
(event: ProgressEvent) =>
|
|
||||||
this.setProgress({
|
|
||||||
id: item.id,
|
|
||||||
loaded: event.loaded,
|
|
||||||
}),
|
|
||||||
100,
|
|
||||||
{ leading: true, trailing: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
await api
|
|
||||||
.post(item.path, item.file.file as File, item.overwrite, onUpload)
|
|
||||||
.catch(this.setError);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.finishUpload(item);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setUploadSpeed(value: number) {
|
|
||||||
this.speedMbyte = value;
|
|
||||||
},
|
|
||||||
setETA(value: number) {
|
|
||||||
this.eta = value;
|
|
||||||
},
|
|
||||||
// easily reset state using `$reset`
|
|
||||||
clearUpload() {
|
|
||||||
this.$reset();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
1
frontend/src/types/file.d.ts
vendored
1
frontend/src/types/file.d.ts
vendored
@@ -29,6 +29,7 @@ interface ResourceItem extends ResourceBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResourceType =
|
type ResourceType =
|
||||||
|
| "dir"
|
||||||
| "video"
|
| "video"
|
||||||
| "audio"
|
| "audio"
|
||||||
| "image"
|
| "image"
|
||||||
|
|||||||
43
frontend/src/types/upload.d.ts
vendored
43
frontend/src/types/upload.d.ts
vendored
@@ -1,22 +1,15 @@
|
|||||||
interface Uploads {
|
type Upload = {
|
||||||
[key: number]: Upload;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Upload {
|
|
||||||
id: number;
|
|
||||||
file: UploadEntry;
|
|
||||||
type?: ResourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadItem {
|
|
||||||
id: number;
|
|
||||||
url?: string;
|
|
||||||
path: string;
|
path: string;
|
||||||
file: UploadEntry;
|
name: string;
|
||||||
dir?: boolean;
|
file: File | null;
|
||||||
overwrite?: boolean;
|
type: ResourceType;
|
||||||
type?: ResourceType;
|
overwrite: boolean;
|
||||||
}
|
totalBytes: number;
|
||||||
|
sentBytes: number;
|
||||||
|
rawProgress: {
|
||||||
|
sentBytes: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
interface UploadEntry {
|
interface UploadEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,17 +20,3 @@ interface UploadEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UploadList = UploadEntry[];
|
type UploadList = UploadEntry[];
|
||||||
|
|
||||||
type CurrentUploadList = {
|
|
||||||
[key: string]: {
|
|
||||||
upload: import("tus-js-client").Upload;
|
|
||||||
recentSpeeds: number[];
|
|
||||||
initialBytesUploaded: number;
|
|
||||||
currentBytesUploaded: number;
|
|
||||||
currentAverageSpeed: number;
|
|
||||||
lastProgressTimestamp: number | null;
|
|
||||||
sumOfRecentSpeeds: number;
|
|
||||||
hasStarted: boolean;
|
|
||||||
interval: number | undefined;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ export function handleFiles(
|
|||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const id = uploadStore.id;
|
|
||||||
let path = base;
|
let path = base;
|
||||||
|
|
||||||
if (file.fullPath !== undefined) {
|
if (file.fullPath !== undefined) {
|
||||||
@@ -145,14 +144,8 @@ export function handleFiles(
|
|||||||
path += "/";
|
path += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const item: UploadItem = {
|
const type = file.isDir ? "dir" : detectType((file.file as File).type);
|
||||||
id,
|
|
||||||
path,
|
|
||||||
file,
|
|
||||||
overwrite,
|
|
||||||
...(!file.isDir && { type: detectType((file.file as File).type) }),
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadStore.upload(item);
|
uploadStore.upload(path, file.name, file.file ?? null, overwrite, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<header-bar
|
<header-bar
|
||||||
v-if="error || fileStore.req?.type === null"
|
v-if="error || fileStore.req?.type === undefined"
|
||||||
showMenu
|
showMenu
|
||||||
showLogo
|
showLogo
|
||||||
/>
|
/>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<breadcrumbs base="/files" />
|
<breadcrumbs base="/files" />
|
||||||
<errors v-if="error" :errorCode="error.status" />
|
<errors v-if="error" :errorCode="error.status" />
|
||||||
<component v-else-if="currentView" :is="currentView"></component>
|
<component v-else-if="currentView" :is="currentView"></component>
|
||||||
<div v-else-if="currentView !== null">
|
<div v-else>
|
||||||
<h2 class="message delayed">
|
<h2 class="message delayed">
|
||||||
<div class="spinner">
|
<div class="spinner">
|
||||||
<div class="bounce1"></div>
|
<div class="bounce1"></div>
|
||||||
@@ -36,7 +36,6 @@ import { files as api } from "@/api";
|
|||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { useUploadStore } from "@/stores/upload";
|
|
||||||
|
|
||||||
import HeaderBar from "@/components/header/HeaderBar.vue";
|
import HeaderBar from "@/components/header/HeaderBar.vue";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||||
@@ -52,10 +51,8 @@ const Preview = defineAsyncComponent(() => import("@/views/files/Preview.vue"));
|
|||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
const uploadStore = useUploadStore();
|
|
||||||
|
|
||||||
const { reload } = storeToRefs(fileStore);
|
const { reload } = storeToRefs(fileStore);
|
||||||
const { error: uploadError } = storeToRefs(uploadStore);
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@@ -102,26 +99,41 @@ onUnmounted(() => {
|
|||||||
fetchDataController.abort();
|
fetchDataController.abort();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(route, (to, from) => {
|
watch(route, () => {
|
||||||
if (from.path.endsWith("/")) {
|
|
||||||
window.sessionStorage.setItem(
|
|
||||||
"listFrozen",
|
|
||||||
(!to.path.endsWith("/")).toString()
|
|
||||||
);
|
|
||||||
} else if (to.path.endsWith("/")) {
|
|
||||||
fileStore.updateRequest(null);
|
|
||||||
}
|
|
||||||
fetchData();
|
fetchData();
|
||||||
});
|
});
|
||||||
watch(reload, (newValue) => {
|
watch(reload, (newValue) => {
|
||||||
newValue && fetchData();
|
newValue && fetchData();
|
||||||
});
|
});
|
||||||
watch(uploadError, (newValue) => {
|
|
||||||
newValue && layoutStore.showError();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define functions
|
// Define functions
|
||||||
|
|
||||||
|
const applyPreSelection = () => {
|
||||||
|
const preselect = fileStore.preselect;
|
||||||
|
fileStore.preselect = null;
|
||||||
|
|
||||||
|
if (!fileStore.req?.isDir || fileStore.oldReq === null) return;
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
if (preselect) {
|
||||||
|
// Find item with the specified path
|
||||||
|
index = fileStore.req.items.findIndex((item) => item.path === preselect);
|
||||||
|
} else if (fileStore.oldReq.path.startsWith(fileStore.req.path)) {
|
||||||
|
// Get immediate child folder of the previous path
|
||||||
|
const name = fileStore.oldReq.path
|
||||||
|
.substring(fileStore.req.path.length)
|
||||||
|
.split("/")
|
||||||
|
.shift();
|
||||||
|
|
||||||
|
index = fileStore.req.items.findIndex(
|
||||||
|
(val) => val.path == fileStore.req!.path + name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === -1) return;
|
||||||
|
fileStore.selected.push(index);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
// Reset view information.
|
// Reset view information.
|
||||||
fileStore.reload = false;
|
fileStore.reload = false;
|
||||||
@@ -130,12 +142,7 @@ const fetchData = async () => {
|
|||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
|
|
||||||
// Set loading to true and reset the error.
|
// Set loading to true and reset the error.
|
||||||
if (
|
layoutStore.loading = true;
|
||||||
window.sessionStorage.getItem("listFrozen") !== "true" &&
|
|
||||||
window.sessionStorage.getItem("modified") !== "true"
|
|
||||||
) {
|
|
||||||
layoutStore.loading = true;
|
|
||||||
}
|
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
let url = route.path;
|
let url = route.path;
|
||||||
@@ -149,6 +156,9 @@ const fetchData = async () => {
|
|||||||
fileStore.updateRequest(res);
|
fileStore.updateRequest(res);
|
||||||
document.title = `${res.name || t("sidebar.myFiles")} - ${t("files.files")} - ${name}`;
|
document.title = `${res.name || t("sidebar.myFiles")} - ${t("files.files")} - ${name}`;
|
||||||
layoutStore.loading = false;
|
layoutStore.loading = false;
|
||||||
|
|
||||||
|
// Selects the post-reload target item or the previously visited child folder
|
||||||
|
applyPreSelection();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && err.is_canceled) {
|
if (err instanceof StatusError && err.is_canceled) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="uploadStore.getProgress" class="progress">
|
<div v-if="uploadStore.totalBytes" class="progress">
|
||||||
<div v-bind:style="{ width: uploadStore.getProgress + '%' }"></div>
|
<div
|
||||||
|
v-bind:style="{
|
||||||
|
width: sentPercent + '%',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<sidebar></sidebar>
|
<sidebar></sidebar>
|
||||||
<main>
|
<main>
|
||||||
@@ -27,7 +31,7 @@ import Prompts from "@/components/prompts/Prompts.vue";
|
|||||||
import Shell from "@/components/Shell.vue";
|
import Shell from "@/components/Shell.vue";
|
||||||
import UploadFiles from "@/components/prompts/UploadFiles.vue";
|
import UploadFiles from "@/components/prompts/UploadFiles.vue";
|
||||||
import { enableExec } from "@/utils/constants";
|
import { enableExec } from "@/utils/constants";
|
||||||
import { watch } from "vue";
|
import { computed, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
@@ -36,6 +40,10 @@ const fileStore = useFileStore();
|
|||||||
const uploadStore = useUploadStore();
|
const uploadStore = useUploadStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const sentPercent = computed(() =>
|
||||||
|
((uploadStore.sentBytes / uploadStore.totalBytes) * 100).toFixed(2)
|
||||||
|
);
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
fileStore.selected = [];
|
fileStore.selected = [];
|
||||||
fileStore.multiple = false;
|
fileStore.multiple = false;
|
||||||
|
|||||||
@@ -32,17 +32,25 @@
|
|||||||
/>
|
/>
|
||||||
</header-bar>
|
</header-bar>
|
||||||
|
|
||||||
<Breadcrumbs base="/files" noLink />
|
|
||||||
|
|
||||||
<!-- preview container -->
|
<!-- preview container -->
|
||||||
<div
|
<div class="loading delayed" v-if="layoutStore.loading">
|
||||||
v-show="isPreview && isMarkdownFile"
|
<div class="spinner">
|
||||||
id="preview-container"
|
<div class="bounce1"></div>
|
||||||
class="md_preview"
|
<div class="bounce2"></div>
|
||||||
v-html="previewContent"
|
<div class="bounce3"></div>
|
||||||
></div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<Breadcrumbs base="/files" noLink />
|
||||||
|
|
||||||
<form v-show="!isPreview || !isMarkdownFile" id="editor"></form>
|
<div
|
||||||
|
v-show="isPreview && isMarkdownFile"
|
||||||
|
id="preview-container"
|
||||||
|
class="md_preview"
|
||||||
|
v-html="previewContent"
|
||||||
|
></div>
|
||||||
|
<form v-show="!isPreview || !isMarkdownFile" id="editor"></form>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -146,12 +154,19 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteUpdate((to, from, next) => {
|
onBeforeRouteUpdate((to, from, next) => {
|
||||||
if (!editor.value?.session.getUndoManager().isClean()) {
|
if (editor.value?.session.getUndoManager().isClean()) {
|
||||||
layoutStore.showHover("discardEditorChanges");
|
|
||||||
next(false);
|
|
||||||
} else {
|
|
||||||
next();
|
next();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layoutStore.showHover({
|
||||||
|
prompt: "discardEditorChanges",
|
||||||
|
confirm: (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyEvent = (event: KeyboardEvent) => {
|
const keyEvent = (event: KeyboardEvent) => {
|
||||||
@@ -216,13 +231,6 @@ const decreaseFontSize = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (!editor.value?.session.getUndoManager().isClean()) {
|
|
||||||
layoutStore.showHover("discardEditorChanges");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStore.updateRequest(null);
|
|
||||||
|
|
||||||
const uri = url.removeLastDir(route.path) + "/";
|
const uri = url.removeLastDir(route.path) + "/";
|
||||||
router.push({ path: uri });
|
router.push({ path: uri });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ import {
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
|
import { removePrefix } from "@/api/utils";
|
||||||
|
|
||||||
const showLimit = ref<number>(50);
|
const showLimit = ref<number>(50);
|
||||||
const columnWidth = ref<number>(280);
|
const columnWidth = ref<number>(280);
|
||||||
@@ -420,25 +421,19 @@ const isMobile = computed(() => {
|
|||||||
|
|
||||||
watch(req, () => {
|
watch(req, () => {
|
||||||
// Reset the show value
|
// Reset the show value
|
||||||
if (
|
showLimit.value = 50;
|
||||||
window.sessionStorage.getItem("listFrozen") !== "true" &&
|
|
||||||
window.sessionStorage.getItem("modified") !== "true"
|
|
||||||
) {
|
|
||||||
showLimit.value = 50;
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// Ensures that the listing is displayed
|
// Ensures that the listing is displayed
|
||||||
// How much every listing item affects the window height
|
// How much every listing item affects the window height
|
||||||
setItemWeight();
|
setItemWeight();
|
||||||
|
|
||||||
|
// Scroll to the item opened previously
|
||||||
|
if (!revealPreviousItem()) {
|
||||||
// Fill and fit the window with listing items
|
// Fill and fit the window with listing items
|
||||||
fillWindow(true);
|
fillWindow(true);
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
if (req.value?.isDir) {
|
|
||||||
window.sessionStorage.setItem("listFrozen", "false");
|
|
||||||
window.sessionStorage.setItem("modified", "false");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -448,8 +443,11 @@ onMounted(() => {
|
|||||||
// How much every listing item affects the window height
|
// How much every listing item affects the window height
|
||||||
setItemWeight();
|
setItemWeight();
|
||||||
|
|
||||||
// Fill and fit the window with listing items
|
// Scroll to the item opened previously
|
||||||
fillWindow(true);
|
if (!revealPreviousItem()) {
|
||||||
|
// Fill and fit the window with listing items
|
||||||
|
fillWindow(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the needed event listeners to the window and document.
|
// Add the needed event listeners to the window and document.
|
||||||
window.addEventListener("keydown", keyEvent);
|
window.addEventListener("keydown", keyEvent);
|
||||||
@@ -589,10 +587,13 @@ const paste = (event: Event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preselect = removePrefix(route.path) + items[0].name;
|
||||||
|
|
||||||
let action = (overwrite: boolean, rename: boolean) => {
|
let action = (overwrite: boolean, rename: boolean) => {
|
||||||
api
|
api
|
||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
fileStore.preselect = preselect;
|
||||||
fileStore.reload = true;
|
fileStore.reload = true;
|
||||||
})
|
})
|
||||||
.catch($showError);
|
.catch($showError);
|
||||||
@@ -604,6 +605,7 @@ const paste = (event: Event) => {
|
|||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
clipboardStore.resetClipboard();
|
clipboardStore.resetClipboard();
|
||||||
|
fileStore.preselect = preselect;
|
||||||
fileStore.reload = true;
|
fileStore.reload = true;
|
||||||
})
|
})
|
||||||
.catch($showError);
|
.catch($showError);
|
||||||
@@ -731,6 +733,8 @@ const drop = async (event: DragEvent) => {
|
|||||||
|
|
||||||
const conflict = upload.checkConflict(files, items);
|
const conflict = upload.checkConflict(files, items);
|
||||||
|
|
||||||
|
const preselect = removePrefix(path) + (files[0].fullPath || files[0].name);
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
prompt: "replace",
|
prompt: "replace",
|
||||||
@@ -738,11 +742,13 @@ const drop = async (event: DragEvent) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
upload.handleFiles(files, path, false);
|
upload.handleFiles(files, path, false);
|
||||||
|
fileStore.preselect = preselect;
|
||||||
},
|
},
|
||||||
confirm: (event: Event) => {
|
confirm: (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
layoutStore.closeHovers();
|
layoutStore.closeHovers();
|
||||||
upload.handleFiles(files, path, true);
|
upload.handleFiles(files, path, true);
|
||||||
|
fileStore.preselect = preselect;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -750,6 +756,7 @@ const drop = async (event: DragEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upload.handleFiles(files, path);
|
upload.handleFiles(files, path);
|
||||||
|
fileStore.preselect = preselect;
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadInput = (event: Event) => {
|
const uploadInput = (event: Event) => {
|
||||||
@@ -953,4 +960,21 @@ const fillWindow = (fit = false) => {
|
|||||||
// Set the number of displayed items
|
// Set the number of displayed items
|
||||||
showLimit.value = showQuantity > totalItems ? totalItems : showQuantity;
|
showLimit.value = showQuantity > totalItems ? totalItems : showQuantity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const revealPreviousItem = () => {
|
||||||
|
if (!fileStore.req || !fileStore.oldReq) return;
|
||||||
|
|
||||||
|
const index = fileStore.selected[0];
|
||||||
|
if (index === undefined) return;
|
||||||
|
|
||||||
|
showLimit.value =
|
||||||
|
index + Math.ceil((window.innerHeight * 2) / itemWeight.value);
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const items = document.querySelectorAll("#listing .item");
|
||||||
|
items[index].scrollIntoView({ block: "center" });
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -301,10 +301,8 @@ watch(route, () => {
|
|||||||
// Specify hooks
|
// Specify hooks
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener("keydown", key);
|
window.addEventListener("keydown", key);
|
||||||
if (fileStore.oldReq) {
|
listing.value = fileStore.oldReq?.items ?? null;
|
||||||
listing.value = fileStore.oldReq.items;
|
updatePreview();
|
||||||
updatePreview();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => window.removeEventListener("keydown", key));
|
onBeforeUnmount(() => window.removeEventListener("keydown", key));
|
||||||
@@ -317,11 +315,16 @@ const deleteFile = () => {
|
|||||||
if (listing.value === null) {
|
if (listing.value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
listing.value = listing.value.filter((item) => item.name !== name.value);
|
|
||||||
|
const index = listing.value.findIndex((item) => item.name == name.value);
|
||||||
|
listing.value.splice(index, 1);
|
||||||
|
|
||||||
if (hasNext.value) {
|
if (hasNext.value) {
|
||||||
next();
|
next();
|
||||||
} else if (!hasPrevious.value && !hasNext.value) {
|
} else if (!hasPrevious.value && !hasNext.value) {
|
||||||
|
const nearbyItem = listing.value[Math.max(0, index - 1)];
|
||||||
|
fileStore.preselect = nearbyItem?.path;
|
||||||
|
|
||||||
close();
|
close();
|
||||||
} else {
|
} else {
|
||||||
prev();
|
prev();
|
||||||
@@ -427,8 +430,6 @@ const toggleNavigation = throttle(function () {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
fileStore.updateRequest(null);
|
|
||||||
|
|
||||||
const uri = url.removeLastDir(route.path) + "/";
|
const uri = url.removeLastDir(route.path) + "/";
|
||||||
router.push({ path: uri });
|
router.push({ path: uri });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"exclude": [
|
"exclude": [
|
||||||
"src/components/Shell.vue",
|
"src/components/Shell.vue",
|
||||||
"src/components/prompts/Copy.vue",
|
"src/components/prompts/Copy.vue",
|
||||||
|
"src/components/prompts/Move.vue",
|
||||||
"src/components/prompts/Delete.vue",
|
"src/components/prompts/Delete.vue",
|
||||||
"src/components/prompts/FileList.vue",
|
"src/components/prompts/FileList.vue",
|
||||||
"src/components/prompts/Rename.vue",
|
"src/components/prompts/Rename.vue",
|
||||||
"src/components/prompts/Share.vue",
|
"src/components/prompts/Share.vue"
|
||||||
"src/components/prompts/UploadFiles.vue"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/filebrowser/filebrowser/v2
|
module github.com/filebrowser/filebrowser/v2
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asdine/storm/v3 v3.2.1
|
github.com/asdine/storm/v3 v3.2.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/filebrowser/filebrowser/v2/tools
|
module github.com/filebrowser/filebrowser/v2/tools
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golangci/golangci-lint/v2 v2.1.6
|
github.com/golangci/golangci-lint/v2 v2.1.6
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Most of the configuration can be understood through our Command Line Interface documentation. Although there are some specific topics that we want to cover on this section.
|
Most of the configuration can be understood through the command line interface documentation. To access it, you need to install File Browser and run `filebrowser --help`. In this page, we cover some specific, more complex, topics.
|
||||||
|
|
||||||
## Custom Branding
|
## Custom Branding
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user