Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83492a4dfb | ||
|
|
bf3ba65782 | ||
|
|
f35b7c9d9d | ||
|
|
e9506c3eae | ||
|
|
bb4465548b | ||
|
|
cf8b5ca768 |
@@ -35,10 +35,5 @@ build/
|
||||
/frontend/dist/*
|
||||
!/frontend/dist/.gitkeep
|
||||
|
||||
# Playwright files
|
||||
/frontend/test-results/
|
||||
/frontend/playwright-report/
|
||||
/frontend/playwright/.cache/
|
||||
|
||||
default.nix
|
||||
Dockerfile.dev
|
||||
|
||||
@@ -5,14 +5,6 @@ All notable changes to this project will be documented in this file. See [commit
|
||||
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/shirou/gopsutil/v3 to v4 ([#5536](https://github.com/filebrowser/filebrowser/issues/5536)) ([fdff7a3](https://github.com/filebrowser/filebrowser/commit/fdff7a38f4711f2b58dfdd60bebbb057bd3a478d))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#5537](https://github.com/filebrowser/filebrowser/issues/5537)) ([f26a685](https://github.com/filebrowser/filebrowser/commit/f26a68587d8432b536453093f42dc255d19d10fa))
|
||||
|
||||
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/shirou/gopsutil/v3 to v4 ([#5536](https://github.com/filebrowser/filebrowser/issues/5536)) ([fdff7a3](https://github.com/filebrowser/filebrowser/commit/fdff7a38f4711f2b58dfdd60bebbb057bd3a478d))
|
||||
|
||||
+5
-1
@@ -12,7 +12,11 @@
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/img/icons/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/img/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
|
||||
@@ -11,18 +11,16 @@
|
||||
"dev": "vite dev",
|
||||
"build": "pnpm run typecheck && vite build",
|
||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.tsc.json --noEmit",
|
||||
"typecheck": "vue-tsc -p ./tsconfig.app.json --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint --fix src/",
|
||||
"format": "prettier --write .",
|
||||
"test": "playwright test"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-number-input": "^2.0.1",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"@vueuse/integrations": "^14.0.0",
|
||||
"ace-builds": "^1.43.2",
|
||||
"core-js": "^3.44.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.6",
|
||||
"epubjs": "^0.3.93",
|
||||
@@ -51,7 +49,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@tsconfig/node24": "^24.0.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^24.10.1",
|
||||
@@ -60,17 +57,16 @@
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "^9.2.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"jsdom": "^27.2.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-compression2": "^2.3.1",
|
||||
"vue-tsc": "^3.1.3"
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://127.0.0.1:5173",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
|
||||
/* Set default locale to English (US) */
|
||||
locale: "en-US",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "npm run dev",
|
||||
url: "http://127.0.0.1:5173",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
Generated
+144
-677
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,17 @@
|
||||
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="[{[ .StaticURL ]}]/img/icons/favicon.svg" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="[{[ .StaticURL ]}]/img/icons/favicon.svg"
|
||||
/>
|
||||
<link rel="shortcut icon" href="[{[ .StaticURL ]}]/img/icons/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="[{[ .StaticURL ]}]/img/icons/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="File Browser" />
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<!-- This component taken directly from vue-simple-progress
|
||||
since it didnt support Vue 3 but the component itself does
|
||||
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
@@ -44,182 +41,164 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// We're leaving this untouched as you can read in the beginning
|
||||
const isNumber = function (n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const isNumber = (n: number | string): boolean => {
|
||||
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "progress-bar",
|
||||
props: {
|
||||
val: {
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
default: 100,
|
||||
},
|
||||
size: {
|
||||
// either a number (pixel width/height) or 'tiny', 'small',
|
||||
// 'medium', 'large', 'huge', 'massive' for common sizes
|
||||
default: 3,
|
||||
},
|
||||
"bg-color": {
|
||||
type: String,
|
||||
default: "#eee",
|
||||
},
|
||||
"bar-color": {
|
||||
type: String,
|
||||
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
|
||||
},
|
||||
"bar-transition": {
|
||||
type: String,
|
||||
default: "all 0.5s ease",
|
||||
},
|
||||
"bar-border-radius": {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
spacing: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
"text-align": {
|
||||
type: String,
|
||||
default: "center", // 'left', 'right'
|
||||
},
|
||||
"text-position": {
|
||||
type: String,
|
||||
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
|
||||
},
|
||||
"font-size": {
|
||||
type: Number,
|
||||
default: 13,
|
||||
},
|
||||
"text-fg-color": {
|
||||
type: String,
|
||||
default: "#222",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pct() {
|
||||
let pct = (this.val / this.max) * 100;
|
||||
pct = pct.toFixed(2);
|
||||
return Math.min(pct, this.max);
|
||||
},
|
||||
size_px() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
return 2;
|
||||
case "small":
|
||||
return 4;
|
||||
case "medium":
|
||||
return 8;
|
||||
case "large":
|
||||
return 12;
|
||||
case "big":
|
||||
return 16;
|
||||
case "huge":
|
||||
return 32;
|
||||
case "massive":
|
||||
return 64;
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
val?: number;
|
||||
max?: number;
|
||||
size?: number | string;
|
||||
bgColor?: string;
|
||||
barColor?: string;
|
||||
barTransition?: string;
|
||||
barBorderRadius?: number;
|
||||
spacing?: number;
|
||||
text?: string;
|
||||
textAlign?: string;
|
||||
textPosition?: string;
|
||||
fontSize?: number;
|
||||
textFgColor?: string;
|
||||
}>(),
|
||||
{
|
||||
val: 0,
|
||||
max: 100,
|
||||
size: 3,
|
||||
bgColor: "#eee",
|
||||
barColor: "#2196f3",
|
||||
barTransition: "all 0.5s ease",
|
||||
barBorderRadius: 0,
|
||||
spacing: 4,
|
||||
text: "",
|
||||
textAlign: "center",
|
||||
textPosition: "bottom",
|
||||
fontSize: 13,
|
||||
textFgColor: "#222",
|
||||
}
|
||||
);
|
||||
|
||||
return isNumber(this.size) ? this.size : 32;
|
||||
},
|
||||
text_padding() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
case "small":
|
||||
case "medium":
|
||||
case "large":
|
||||
case "big":
|
||||
case "huge":
|
||||
case "massive":
|
||||
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
|
||||
}
|
||||
const pct = computed(() => {
|
||||
const pct = (props.val / props.max) * 100;
|
||||
const pctFixed = pct.toFixed(2);
|
||||
return Math.min(parseFloat(pctFixed), props.max);
|
||||
});
|
||||
|
||||
return isNumber(this.spacing) ? this.spacing : 4;
|
||||
},
|
||||
text_font_size() {
|
||||
switch (this.size) {
|
||||
case "tiny":
|
||||
case "small":
|
||||
case "medium":
|
||||
case "large":
|
||||
case "big":
|
||||
case "huge":
|
||||
case "massive":
|
||||
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
||||
}
|
||||
const size_px = computed(() => {
|
||||
switch (props.size) {
|
||||
case "tiny":
|
||||
return 2;
|
||||
case "small":
|
||||
return 4;
|
||||
case "medium":
|
||||
return 8;
|
||||
case "large":
|
||||
return 12;
|
||||
case "big":
|
||||
return 16;
|
||||
case "huge":
|
||||
return 32;
|
||||
case "massive":
|
||||
return 64;
|
||||
}
|
||||
|
||||
return isNumber(this.fontSize) ? this.fontSize : 13;
|
||||
},
|
||||
progress_style() {
|
||||
const style = {
|
||||
background: this.bgColor,
|
||||
};
|
||||
return isNumber(props.size) ? (props.size as number) : 32;
|
||||
});
|
||||
|
||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||
style["position"] = "relative";
|
||||
style["min-height"] = this.size_px + "px";
|
||||
style["z-index"] = "-2";
|
||||
}
|
||||
const text_padding = computed(() => {
|
||||
switch (props.size) {
|
||||
case "tiny":
|
||||
case "small":
|
||||
case "medium":
|
||||
case "large":
|
||||
case "big":
|
||||
case "huge":
|
||||
case "massive":
|
||||
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
|
||||
}
|
||||
|
||||
if (this.barBorderRadius > 0) {
|
||||
style["border-radius"] = this.barBorderRadius + "px";
|
||||
}
|
||||
return isNumber(props.spacing) ? props.spacing : 4;
|
||||
});
|
||||
|
||||
return style;
|
||||
},
|
||||
bar_style() {
|
||||
const style = {
|
||||
background: this.barColor,
|
||||
width: this.pct + "%",
|
||||
height: this.size_px + "px",
|
||||
transition: this.barTransition,
|
||||
};
|
||||
const text_font_size = computed(() => {
|
||||
switch (props.size) {
|
||||
case "tiny":
|
||||
case "small":
|
||||
case "medium":
|
||||
case "large":
|
||||
case "big":
|
||||
case "huge":
|
||||
case "massive":
|
||||
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
|
||||
}
|
||||
|
||||
if (this.barBorderRadius > 0) {
|
||||
style["border-radius"] = this.barBorderRadius + "px";
|
||||
}
|
||||
return isNumber(props.fontSize) ? props.fontSize : 13;
|
||||
});
|
||||
|
||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
||||
style["position"] = "absolute";
|
||||
style["top"] = "0";
|
||||
style["height"] = "100%";
|
||||
((style["min-height"] = this.size_px + "px"),
|
||||
(style["z-index"] = "-1"));
|
||||
}
|
||||
const progress_style = computed(() => {
|
||||
const style: Record<string, string> = {
|
||||
background: props.bgColor,
|
||||
};
|
||||
|
||||
return style;
|
||||
},
|
||||
text_style() {
|
||||
const style = {
|
||||
color: this.textFgColor,
|
||||
"font-size": this.text_font_size + "px",
|
||||
"text-align": this.textAlign,
|
||||
};
|
||||
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||
style["position"] = "relative";
|
||||
style["min-height"] = size_px.value + "px";
|
||||
style["z-index"] = "-2";
|
||||
}
|
||||
|
||||
if (
|
||||
this.textPosition == "top" ||
|
||||
this.textPosition == "middle" ||
|
||||
this.textPosition == "inside"
|
||||
)
|
||||
style["padding-bottom"] = this.text_padding + "px";
|
||||
if (
|
||||
this.textPosition == "bottom" ||
|
||||
this.textPosition == "middle" ||
|
||||
this.textPosition == "inside"
|
||||
)
|
||||
style["padding-top"] = this.text_padding + "px";
|
||||
if (props.barBorderRadius > 0) {
|
||||
style["border-radius"] = props.barBorderRadius + "px";
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
},
|
||||
};
|
||||
return style;
|
||||
});
|
||||
|
||||
const bar_style = computed(() => {
|
||||
const style: Record<string, string> = {
|
||||
background: props.barColor,
|
||||
width: pct.value + "%",
|
||||
height: size_px.value + "px",
|
||||
transition: props.barTransition,
|
||||
};
|
||||
|
||||
if (props.barBorderRadius > 0) {
|
||||
style["border-radius"] = props.barBorderRadius + "px";
|
||||
}
|
||||
|
||||
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||
style["position"] = "absolute";
|
||||
style["top"] = "0";
|
||||
style["height"] = "100%";
|
||||
style["min-height"] = size_px.value + "px";
|
||||
style["z-index"] = "-1";
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
|
||||
const text_style = computed(() => {
|
||||
const style: Record<string, string> = {
|
||||
color: props.textFgColor,
|
||||
"font-size": text_font_size.value + "px",
|
||||
"text-align": props.textAlign,
|
||||
};
|
||||
|
||||
if (
|
||||
props.textPosition == "top" ||
|
||||
props.textPosition == "middle" ||
|
||||
props.textPosition == "inside"
|
||||
)
|
||||
style["padding-bottom"] = text_padding.value + "px";
|
||||
if (
|
||||
props.textPosition == "bottom" ||
|
||||
props.textPosition == "middle" ||
|
||||
props.textPosition == "inside"
|
||||
)
|
||||
style["padding-top"] = text_padding.value + "px";
|
||||
|
||||
return style;
|
||||
});
|
||||
</script>
|
||||
|
||||
+160
-136
@@ -2,13 +2,13 @@
|
||||
<div
|
||||
class="shell"
|
||||
:class="{ ['shell--hidden']: !showShell }"
|
||||
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }"
|
||||
:style="{ height: `${shellHeight}em`, direction: 'ltr' }"
|
||||
>
|
||||
<div
|
||||
@pointerdown="startDrag()"
|
||||
@pointerup="stopDrag()"
|
||||
class="shell__divider"
|
||||
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''"
|
||||
:style="shellDrag ? { background: `${checkTheme()}` } : ''"
|
||||
></div>
|
||||
<div @click="focus" class="shell__content" ref="scrollable">
|
||||
<div v-for="(c, index) in content" :key="index" class="shell__result">
|
||||
@@ -39,13 +39,15 @@
|
||||
<div
|
||||
@pointerup="stopDrag()"
|
||||
class="shell__overlay"
|
||||
v-show="this.shellDrag"
|
||||
v-show="shellDrag"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
@@ -53,142 +55,164 @@ import { commands } from "@/api";
|
||||
import { throttle } from "lodash-es";
|
||||
import { theme } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "shell",
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["showShell"]),
|
||||
...mapState(useFileStore, ["isFiles"]),
|
||||
path: function () {
|
||||
if (this.isFiles) {
|
||||
return this.$route.path;
|
||||
}
|
||||
const route = useRoute();
|
||||
|
||||
return "";
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
content: [],
|
||||
history: [],
|
||||
historyPos: 0,
|
||||
canInput: true,
|
||||
shellDrag: false,
|
||||
shellHeight: 25,
|
||||
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
|
||||
}),
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.resize);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.resize);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["toggleShell"]),
|
||||
checkTheme() {
|
||||
if (theme == "dark") {
|
||||
return "rgba(255, 255, 255, 0.4)";
|
||||
}
|
||||
return "rgba(127, 127, 127, 0.4)";
|
||||
},
|
||||
startDrag() {
|
||||
document.addEventListener("pointermove", this.handleDrag);
|
||||
this.shellDrag = true;
|
||||
},
|
||||
stopDrag() {
|
||||
document.removeEventListener("pointermove", this.handleDrag);
|
||||
this.shellDrag = false;
|
||||
},
|
||||
handleDrag: throttle(function (event) {
|
||||
const top = window.innerHeight / this.fontsize - 4;
|
||||
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
|
||||
const bottom =
|
||||
2.25 +
|
||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
if (userPos <= top && userPos >= bottom) {
|
||||
this.shellHeight = userPos.toFixed(2);
|
||||
}
|
||||
}, 32),
|
||||
resize: throttle(function () {
|
||||
const top = window.innerHeight / this.fontsize - 4;
|
||||
const bottom =
|
||||
2.25 +
|
||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
||||
const { showShell } = storeToRefs(layoutStore);
|
||||
const { isFiles } = storeToRefs(fileStore);
|
||||
const { toggleShell } = layoutStore;
|
||||
|
||||
if (this.shellHeight > top) {
|
||||
this.shellHeight = top;
|
||||
} else if (this.shellHeight < bottom) {
|
||||
this.shellHeight = bottom;
|
||||
}
|
||||
}, 32),
|
||||
scroll: function () {
|
||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
||||
},
|
||||
focus: function () {
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
historyUp() {
|
||||
if (this.historyPos > 0) {
|
||||
this.$refs.input.innerText = this.history[--this.historyPos];
|
||||
this.focus();
|
||||
}
|
||||
},
|
||||
historyDown() {
|
||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
||||
this.$refs.input.innerText = this.history[++this.historyPos];
|
||||
this.focus();
|
||||
} else {
|
||||
this.historyPos = this.history.length;
|
||||
this.$refs.input.innerText = "";
|
||||
}
|
||||
},
|
||||
submit: function (event) {
|
||||
const cmd = event.target.innerText.trim();
|
||||
const scrollable = ref<HTMLElement | null>(null);
|
||||
const input = ref<HTMLElement | null>(null);
|
||||
|
||||
if (cmd === "") {
|
||||
return;
|
||||
}
|
||||
const content = ref<Array<{ text: string }>>([]);
|
||||
const history = ref<string[]>([]);
|
||||
const historyPos = ref(0);
|
||||
const canInput = ref(true);
|
||||
const shellDrag = ref(false);
|
||||
const shellHeight = ref(25);
|
||||
const fontsize = ref(
|
||||
parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
);
|
||||
|
||||
if (cmd === "clear") {
|
||||
this.content = [];
|
||||
event.target.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
const path = computed(() => {
|
||||
if (isFiles.value) {
|
||||
return route.path;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
if (cmd === "exit") {
|
||||
event.target.innerHTML = "";
|
||||
this.toggleShell();
|
||||
return;
|
||||
}
|
||||
|
||||
this.canInput = false;
|
||||
event.target.innerHTML = "";
|
||||
|
||||
const results = {
|
||||
text: `${cmd}\n\n`,
|
||||
};
|
||||
|
||||
this.history.push(cmd);
|
||||
this.historyPos = this.history.length;
|
||||
this.content.push(results);
|
||||
|
||||
commands(
|
||||
this.path,
|
||||
cmd,
|
||||
(event) => {
|
||||
results.text += `${event.data}\n`;
|
||||
this.scroll();
|
||||
},
|
||||
() => {
|
||||
results.text = results.text
|
||||
|
||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||
.trimEnd();
|
||||
this.canInput = true;
|
||||
this.$refs.input.focus();
|
||||
this.scroll();
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
const checkTheme = () => {
|
||||
if (theme == "dark") {
|
||||
return "rgba(255, 255, 255, 0.4)";
|
||||
}
|
||||
return "rgba(127, 127, 127, 0.4)";
|
||||
};
|
||||
|
||||
const scroll = () => {
|
||||
if (scrollable.value) {
|
||||
scrollable.value.scrollTop = scrollable.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const focus = () => {
|
||||
input.value?.focus();
|
||||
};
|
||||
|
||||
const handleDrag = throttle((event: PointerEvent) => {
|
||||
const top = window.innerHeight / fontsize.value - 4;
|
||||
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
|
||||
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
|
||||
|
||||
if (userPos <= top && userPos >= bottom) {
|
||||
shellHeight.value = parseFloat(userPos.toFixed(2));
|
||||
}
|
||||
}, 32);
|
||||
|
||||
const resize = throttle(() => {
|
||||
const top = window.innerHeight / fontsize.value - 4;
|
||||
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
|
||||
|
||||
if (shellHeight.value > top) {
|
||||
shellHeight.value = top;
|
||||
} else if (shellHeight.value < bottom) {
|
||||
shellHeight.value = bottom;
|
||||
}
|
||||
}, 32);
|
||||
|
||||
const startDrag = () => {
|
||||
document.addEventListener("pointermove", handleDrag as any);
|
||||
shellDrag.value = true;
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
document.removeEventListener("pointermove", handleDrag as any);
|
||||
shellDrag.value = false;
|
||||
};
|
||||
|
||||
const historyUp = () => {
|
||||
if (historyPos.value > 0 && input.value) {
|
||||
historyPos.value--;
|
||||
input.value.innerText = history.value[historyPos.value];
|
||||
focus();
|
||||
}
|
||||
};
|
||||
|
||||
const historyDown = () => {
|
||||
if (
|
||||
historyPos.value >= 0 &&
|
||||
historyPos.value < history.value.length - 1 &&
|
||||
input.value
|
||||
) {
|
||||
historyPos.value++;
|
||||
input.value.innerText = history.value[historyPos.value];
|
||||
focus();
|
||||
} else {
|
||||
historyPos.value = history.value.length;
|
||||
if (input.value) {
|
||||
input.value.innerText = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submit = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const cmd = target.innerText.trim();
|
||||
|
||||
if (cmd === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "clear") {
|
||||
content.value = [];
|
||||
target.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "exit") {
|
||||
target.innerHTML = "";
|
||||
toggleShell();
|
||||
return;
|
||||
}
|
||||
|
||||
canInput.value = false;
|
||||
target.innerHTML = "";
|
||||
|
||||
const results = {
|
||||
text: `${cmd}\n\n`,
|
||||
};
|
||||
|
||||
history.value.push(cmd);
|
||||
historyPos.value = history.value.length;
|
||||
content.value.push(results);
|
||||
|
||||
commands(
|
||||
path.value,
|
||||
cmd,
|
||||
(event: MessageEvent) => {
|
||||
results.text += `${event.data}\n`;
|
||||
scroll();
|
||||
},
|
||||
() => {
|
||||
results.text = results.text
|
||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||
.trimEnd();
|
||||
canInput.value = true;
|
||||
input.value?.focus();
|
||||
scroll();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", resize as any);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resize as any);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<template v-if="isLoggedIn">
|
||||
<button @click="toAccountSettings" class="action">
|
||||
<i class="material-icons">person</i>
|
||||
<span>{{ user.username }}</span>
|
||||
<span>{{ user?.username }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action"
|
||||
@@ -16,7 +16,7 @@
|
||||
<span>{{ $t("sidebar.myFiles") }}</span>
|
||||
</button>
|
||||
|
||||
<div v-if="user.perm.create">
|
||||
<div v-if="user?.perm.create">
|
||||
<button
|
||||
@click="showHover('newDir')"
|
||||
class="action"
|
||||
@@ -38,7 +38,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="user.perm.admin">
|
||||
<div v-if="user?.perm.admin">
|
||||
<button
|
||||
class="action"
|
||||
@click="toGlobalSettings"
|
||||
@@ -113,9 +113,10 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, watch, onUnmounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
@@ -135,84 +136,85 @@ import prettyBytes from "pretty-bytes";
|
||||
|
||||
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
|
||||
|
||||
export default {
|
||||
name: "sidebar",
|
||||
setup() {
|
||||
const usage = reactive(USAGE_DEFAULT);
|
||||
return { usage, usageAbortController: new AbortController() };
|
||||
},
|
||||
components: {
|
||||
ProgressBar,
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
||||
active() {
|
||||
return this.currentPromptName === "sidebar";
|
||||
},
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
disableExternal: () => disableExternal,
|
||||
disableUsedPercentage: () => disableUsedPercentage,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
|
||||
abortOngoingFetchUsage() {
|
||||
this.usageAbortController.abort();
|
||||
},
|
||||
async fetchUsage() {
|
||||
const path = this.$route.path.endsWith("/")
|
||||
? this.$route.path
|
||||
: this.$route.path + "/";
|
||||
let usageStats = USAGE_DEFAULT;
|
||||
if (this.disableUsedPercentage) {
|
||||
return Object.assign(this.usage, usageStats);
|
||||
}
|
||||
try {
|
||||
this.abortOngoingFetchUsage();
|
||||
this.usageAbortController = new AbortController();
|
||||
const usage = await api.usage(path, this.usageAbortController.signal);
|
||||
usageStats = {
|
||||
used: prettyBytes(usage.used, { binary: true }),
|
||||
total: prettyBytes(usage.total, { binary: true }),
|
||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
||||
};
|
||||
} finally {
|
||||
return Object.assign(this.usage, usageStats);
|
||||
}
|
||||
},
|
||||
toRoot() {
|
||||
this.$router.push({ path: "/files" });
|
||||
this.closeHovers();
|
||||
},
|
||||
toAccountSettings() {
|
||||
this.$router.push({ path: "/settings/profile" });
|
||||
this.closeHovers();
|
||||
},
|
||||
toGlobalSettings() {
|
||||
this.$router.push({ path: "/settings/global" });
|
||||
this.closeHovers();
|
||||
},
|
||||
help() {
|
||||
this.showHover("help");
|
||||
},
|
||||
logout: auth.logout,
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler(to) {
|
||||
if (to.path.includes("/files")) {
|
||||
this.fetchUsage();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
this.abortOngoingFetchUsage();
|
||||
},
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const { user, isLoggedIn } = storeToRefs(authStore);
|
||||
const { isFiles } = storeToRefs(fileStore);
|
||||
const { currentPromptName } = storeToRefs(layoutStore);
|
||||
const { closeHovers, showHover } = layoutStore;
|
||||
|
||||
const usage = reactive(USAGE_DEFAULT);
|
||||
const usageAbortController = ref(new AbortController());
|
||||
|
||||
const active = computed(() => {
|
||||
return currentPromptName.value === "sidebar";
|
||||
});
|
||||
|
||||
const canLogout = !noAuth && loginPage;
|
||||
|
||||
const abortOngoingFetchUsage = () => {
|
||||
usageAbortController.value.abort();
|
||||
};
|
||||
|
||||
const fetchUsage = async () => {
|
||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
let usageStats = USAGE_DEFAULT;
|
||||
if (disableUsedPercentage) {
|
||||
return Object.assign(usage, usageStats);
|
||||
}
|
||||
try {
|
||||
abortOngoingFetchUsage();
|
||||
usageAbortController.value = new AbortController();
|
||||
const usageData = await api.usage(path, usageAbortController.value.signal);
|
||||
usageStats = {
|
||||
used: prettyBytes(usageData.used, { binary: true }),
|
||||
total: prettyBytes(usageData.total, { binary: true }),
|
||||
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
|
||||
};
|
||||
} finally {
|
||||
return Object.assign(usage, usageStats);
|
||||
}
|
||||
};
|
||||
|
||||
const toRoot = () => {
|
||||
router.push({ path: "/files" });
|
||||
closeHovers();
|
||||
};
|
||||
|
||||
const toAccountSettings = () => {
|
||||
router.push({ path: "/settings/profile" });
|
||||
closeHovers();
|
||||
};
|
||||
|
||||
const toGlobalSettings = () => {
|
||||
router.push({ path: "/settings/global" });
|
||||
closeHovers();
|
||||
};
|
||||
|
||||
const help = () => {
|
||||
showHover("help");
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
auth.logout();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
if (newPath.includes("/files")) {
|
||||
fetchUsage();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
abortOngoingFetchUsage();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p>{{ $t("prompts.copyMessage") }}</p>
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
@update:selected="(val: string) => (dest = val)"
|
||||
tabindex="1"
|
||||
/>
|
||||
</div>
|
||||
@@ -17,10 +17,10 @@
|
||||
class="card-action"
|
||||
style="display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<template v-if="user.perm.create">
|
||||
<template v-if="user?.perm.create">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="$refs.fileList.createDir()"
|
||||
@click="fileList?.createDir()"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
style="justify-self: left"
|
||||
@@ -53,8 +53,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { ref, inject } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
@@ -64,90 +66,84 @@ import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { removePrefix } from "@/api/utils";
|
||||
|
||||
export default {
|
||||
name: "copy",
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
copy: async function (event) {
|
||||
event.preventDefault();
|
||||
const items = [];
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (const item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||
name: this.req.items[item].name,
|
||||
});
|
||||
}
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const action = async (overwrite, rename) => {
|
||||
buttons.loading("copy");
|
||||
const { req, selected } = storeToRefs(fileStore);
|
||||
const { user } = storeToRefs(authStore);
|
||||
const { showHover, closeHovers } = layoutStore;
|
||||
|
||||
await api
|
||||
.copy(items, overwrite, rename)
|
||||
.then(() => {
|
||||
buttons.success("copy");
|
||||
this.preselect = removePrefix(items[0].to);
|
||||
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||
const dest = ref<string | null>(null);
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.reload = true;
|
||||
const copy = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||
|
||||
return;
|
||||
}
|
||||
// Create a new promise for each file.
|
||||
for (const item of selected.value) {
|
||||
items.push({
|
||||
from: req.value!.items[item].url,
|
||||
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||
name: req.value!.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
this.$router.push({ path: this.dest });
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("copy");
|
||||
this.$showError(e);
|
||||
});
|
||||
};
|
||||
const action = async (overwrite: boolean, rename: boolean) => {
|
||||
buttons.loading("copy");
|
||||
|
||||
if (this.$route.path === this.dest) {
|
||||
this.closeHovers();
|
||||
action(false, true);
|
||||
await api
|
||||
.copy(items, overwrite, rename)
|
||||
.then(() => {
|
||||
buttons.success("copy");
|
||||
fileStore.preselect = removePrefix(items[0].to);
|
||||
|
||||
return;
|
||||
}
|
||||
if (route.path === dest.value) {
|
||||
fileStore.reload = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const dstItems = (await api.fetch(this.dest)).items;
|
||||
const conflict = upload.checkConflict(items, dstItems);
|
||||
router.push({ path: dest.value! });
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("copy");
|
||||
$showError?.(e);
|
||||
});
|
||||
};
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
if (route.path === dest.value) {
|
||||
closeHovers();
|
||||
action(false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (conflict) {
|
||||
this.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
const dstItems = (await api.fetch(dest.value!)).items;
|
||||
const conflict = upload.checkConflict(items as any, dstItems);
|
||||
|
||||
event.preventDefault();
|
||||
this.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
return;
|
||||
}
|
||||
if (conflict) {
|
||||
showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event: Event, option: string) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
event.preventDefault();
|
||||
closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="card floating">
|
||||
<div class="card-content">
|
||||
<p v-if="!this.isListing || selectedCount === 1">
|
||||
<p v-if="!isListing || selectedCount === 1">
|
||||
{{ $t("prompts.deleteMessageSingle") }}
|
||||
</p>
|
||||
<p v-else>
|
||||
@@ -32,67 +32,62 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute } from "vue-router";
|
||||
import { files as api } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "delete",
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, [
|
||||
"isListing",
|
||||
"selectedCount",
|
||||
"req",
|
||||
"selected",
|
||||
]),
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
submit: async function () {
|
||||
buttons.loading("delete");
|
||||
const route = useRoute();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
|
||||
try {
|
||||
if (!this.isListing) {
|
||||
await api.remove(this.$route.path);
|
||||
buttons.success("delete");
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
this.currentPrompt?.confirm();
|
||||
this.closeHovers();
|
||||
return;
|
||||
}
|
||||
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
|
||||
const { currentPrompt } = storeToRefs(layoutStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
|
||||
this.closeHovers();
|
||||
const submit = async () => {
|
||||
buttons.loading("delete");
|
||||
|
||||
if (this.selectedCount === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!isListing.value) {
|
||||
await api.remove(route.path);
|
||||
buttons.success("delete");
|
||||
|
||||
const promises = [];
|
||||
for (const index of this.selected) {
|
||||
promises.push(api.remove(this.req.items[index].url));
|
||||
}
|
||||
currentPrompt.value?.confirm();
|
||||
closeHovers();
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
closeHovers();
|
||||
|
||||
const nearbyItem =
|
||||
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
|
||||
if (selectedCount.value === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preselect = nearbyItem?.path;
|
||||
const promises = [];
|
||||
for (const index of selected.value) {
|
||||
promises.push(api.remove(req.value!.items[index].url));
|
||||
}
|
||||
|
||||
this.reload = true;
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
this.$showError(e);
|
||||
if (this.isListing) this.reload = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
await Promise.all(promises);
|
||||
buttons.success("delete");
|
||||
|
||||
const nearbyItem =
|
||||
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
|
||||
|
||||
fileStore.preselect = nearbyItem?.path;
|
||||
|
||||
fileStore.reload = true;
|
||||
} catch (e) {
|
||||
buttons.done("delete");
|
||||
$showError?.(e);
|
||||
if (isListing.value) fileStore.reload = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -33,8 +33,4 @@ import { useI18n } from "vue-i18n";
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// const emit = defineEmits<{
|
||||
// (e: "confirm"): void;
|
||||
// }>();
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="currentPrompt.saveAction"
|
||||
@click="currentPrompt?.saveAction"
|
||||
:aria-label="$t('buttons.saveChanges')"
|
||||
:title="$t('buttons.saveChanges')"
|
||||
tabindex="1"
|
||||
@@ -26,7 +26,7 @@
|
||||
</button>
|
||||
<button
|
||||
id="focus-prompt"
|
||||
@click="currentPrompt.confirm"
|
||||
@click="currentPrompt?.confirm"
|
||||
class="button button--flat button--red"
|
||||
:aria-label="$t('buttons.discardChanges')"
|
||||
:title="$t('buttons.discardChanges')"
|
||||
@@ -38,17 +38,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
|
||||
export default {
|
||||
name: "discardEditorChanges",
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
const layoutStore = useLayoutStore();
|
||||
const { currentPrompt } = storeToRefs(layoutStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
</script>
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, onMounted, onUnmounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
@@ -34,147 +36,162 @@ import url from "@/utils/url";
|
||||
import { files } from "@/api";
|
||||
import { StatusError } from "@/api/utils.js";
|
||||
|
||||
export default {
|
||||
name: "file-list",
|
||||
props: {
|
||||
exclude: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
items: [],
|
||||
touches: {
|
||||
id: "",
|
||||
count: 0,
|
||||
},
|
||||
selected: null,
|
||||
current: window.location.pathname,
|
||||
nextAbortController: new AbortController(),
|
||||
};
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapState(useFileStore, ["req"]),
|
||||
nav() {
|
||||
return decodeURIComponent(this.current);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fillOptions(this.req);
|
||||
},
|
||||
unmounted() {
|
||||
this.abortOngoingNext();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover"]),
|
||||
abortOngoingNext() {
|
||||
this.nextAbortController.abort();
|
||||
},
|
||||
fillOptions(req) {
|
||||
// Sets the current path and resets
|
||||
// the current items.
|
||||
this.current = req.url;
|
||||
this.items = [];
|
||||
const props = defineProps<{
|
||||
exclude?: string[];
|
||||
}>();
|
||||
|
||||
this.$emit("update:selected", this.current);
|
||||
const emit = defineEmits<{
|
||||
"update:selected": [value: string];
|
||||
}>();
|
||||
|
||||
// If the path isn't the root path,
|
||||
// show a button to navigate to the previous
|
||||
// directory.
|
||||
if (req.url !== "/files/") {
|
||||
this.items.push({
|
||||
name: "..",
|
||||
url: url.removeLastDir(req.url) + "/",
|
||||
});
|
||||
}
|
||||
const route = useRoute();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
|
||||
// If this folder is empty, finish here.
|
||||
if (req.items === null) return;
|
||||
const authStore = useAuthStore();
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
// Otherwise we add every directory to the
|
||||
// move options.
|
||||
for (const item of req.items) {
|
||||
if (!item.isDir) continue;
|
||||
if (this.exclude?.includes(item.url)) continue;
|
||||
const { user } = storeToRefs(authStore);
|
||||
const { req } = storeToRefs(fileStore);
|
||||
const { showHover } = layoutStore;
|
||||
|
||||
this.items.push({
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
});
|
||||
}
|
||||
},
|
||||
next: function (event) {
|
||||
// Retrieves the URL of the directory the user
|
||||
// just clicked in and fill the options with its
|
||||
// content.
|
||||
const uri = event.currentTarget.dataset.url;
|
||||
this.abortOngoingNext();
|
||||
this.nextAbortController = new AbortController();
|
||||
files
|
||||
.fetch(uri, this.nextAbortController.signal)
|
||||
.then(this.fillOptions)
|
||||
.catch((e) => {
|
||||
if (e instanceof StatusError && e.is_canceled) {
|
||||
return;
|
||||
}
|
||||
this.$showError(e);
|
||||
});
|
||||
},
|
||||
touchstart(event) {
|
||||
const url = event.currentTarget.dataset.url;
|
||||
const items = ref<Array<{ name: string; url: string }>>([]);
|
||||
const touches = ref({
|
||||
id: "",
|
||||
count: 0,
|
||||
});
|
||||
const selected = ref<string | null>(null);
|
||||
const current = ref(window.location.pathname);
|
||||
const nextAbortController = ref(new AbortController());
|
||||
|
||||
// In 300 milliseconds, we shall reset the count.
|
||||
setTimeout(() => {
|
||||
this.touches.count = 0;
|
||||
}, 300);
|
||||
const nav = computed(() => {
|
||||
return decodeURIComponent(current.value);
|
||||
});
|
||||
|
||||
// If the element the user is touching
|
||||
// is different from the last one he touched,
|
||||
// reset the count.
|
||||
if (this.touches.id !== url) {
|
||||
this.touches.id = url;
|
||||
this.touches.count = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.touches.count++;
|
||||
|
||||
// If there is more than one touch already,
|
||||
// open the next screen.
|
||||
if (this.touches.count > 1) {
|
||||
this.next(event);
|
||||
}
|
||||
},
|
||||
itemClick: function (event) {
|
||||
if (this.user.singleClick) this.next(event);
|
||||
else this.select(event);
|
||||
},
|
||||
select: function (event) {
|
||||
// If the element is already selected, unselect it.
|
||||
if (this.selected === event.currentTarget.dataset.url) {
|
||||
this.selected = null;
|
||||
this.$emit("update:selected", this.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise select the element.
|
||||
this.selected = event.currentTarget.dataset.url;
|
||||
this.$emit("update:selected", this.selected);
|
||||
},
|
||||
createDir: async function () {
|
||||
this.showHover({
|
||||
prompt: "newDir",
|
||||
action: null,
|
||||
confirm: null,
|
||||
props: {
|
||||
redirect: false,
|
||||
base: this.current === this.$route.path ? null : this.current,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
const abortOngoingNext = () => {
|
||||
nextAbortController.value.abort();
|
||||
};
|
||||
|
||||
const fillOptions = (reqData: any) => {
|
||||
// Sets the current path and resets
|
||||
// the current items.
|
||||
current.value = reqData.url;
|
||||
items.value = [];
|
||||
|
||||
emit("update:selected", current.value);
|
||||
|
||||
// If the path isn't the root path,
|
||||
// show a button to navigate to the previous
|
||||
// directory.
|
||||
if (reqData.url !== "/files/") {
|
||||
items.value.push({
|
||||
name: "..",
|
||||
url: url.removeLastDir(reqData.url) + "/",
|
||||
});
|
||||
}
|
||||
|
||||
// If this folder is empty, finish here.
|
||||
if (reqData.items === null) return;
|
||||
|
||||
// Otherwise we add every directory to the
|
||||
// move options.
|
||||
for (const item of reqData.items) {
|
||||
if (!item.isDir) continue;
|
||||
if (props.exclude?.includes(item.url)) continue;
|
||||
|
||||
items.value.push({
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const next = (event: Event) => {
|
||||
// Retrieves the URL of the directory the user
|
||||
// just clicked in and fill the options with its
|
||||
// content.
|
||||
const uri = (event.currentTarget as HTMLElement).dataset.url!;
|
||||
abortOngoingNext();
|
||||
nextAbortController.value = new AbortController();
|
||||
files
|
||||
.fetch(uri, nextAbortController.value.signal)
|
||||
.then(fillOptions)
|
||||
.catch((e) => {
|
||||
if (e instanceof StatusError && e.is_canceled) {
|
||||
return;
|
||||
}
|
||||
$showError?.(e);
|
||||
});
|
||||
};
|
||||
|
||||
const touchstart = (event: Event) => {
|
||||
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||
|
||||
// In 300 milliseconds, we shall reset the count.
|
||||
setTimeout(() => {
|
||||
touches.value.count = 0;
|
||||
}, 300);
|
||||
|
||||
// If the element the user is touching
|
||||
// is different from the last one he touched,
|
||||
// reset the count.
|
||||
if (touches.value.id !== urlValue) {
|
||||
touches.value.id = urlValue;
|
||||
touches.value.count = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
touches.value.count++;
|
||||
|
||||
// If there is more than one touch already,
|
||||
// open the next screen.
|
||||
if (touches.value.count > 1) {
|
||||
next(event);
|
||||
}
|
||||
};
|
||||
|
||||
const itemClick = (event: Event) => {
|
||||
if (user.value?.singleClick) next(event);
|
||||
else select(event);
|
||||
};
|
||||
|
||||
const select = (event: Event) => {
|
||||
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||
// If the element is already selected, unselect it.
|
||||
if (selected.value === urlValue) {
|
||||
selected.value = null;
|
||||
emit("update:selected", current.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise select the element.
|
||||
selected.value = urlValue;
|
||||
emit("update:selected", selected.value);
|
||||
};
|
||||
|
||||
const createDir = async () => {
|
||||
showHover({
|
||||
prompt: "newDir",
|
||||
action: undefined,
|
||||
confirm: undefined,
|
||||
props: {
|
||||
redirect: false,
|
||||
base: current.value === route.path ? null : current.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (req.value) {
|
||||
fillOptions(req.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
abortOngoingNext();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
createDir,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -34,14 +34,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "help",
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
const { closeHovers } = useLayoutStore();
|
||||
</script>
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
|
||||
<template v-if="dir && selected.length === 0">
|
||||
<p>
|
||||
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }}
|
||||
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }}
|
||||
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -99,98 +99,100 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { filesize } from "@/utils";
|
||||
import dayjs from "dayjs";
|
||||
import { files as api } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "info",
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
humanSize: function () {
|
||||
if (this.selectedCount === 0 || !this.isListing) {
|
||||
return filesize(this.req.size);
|
||||
}
|
||||
const route = useRoute();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
|
||||
let sum = 0;
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
for (const selected of this.selected) {
|
||||
sum += this.req.items[selected].size;
|
||||
}
|
||||
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
|
||||
return filesize(sum);
|
||||
},
|
||||
humanTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return dayjs(this.req.modified).fromNow();
|
||||
}
|
||||
const humanSize = computed(() => {
|
||||
if (selectedCount.value === 0 || !isListing.value) {
|
||||
return filesize(req.value?.size ?? 0);
|
||||
}
|
||||
|
||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
||||
},
|
||||
modTime: function () {
|
||||
if (this.selectedCount === 0) {
|
||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
||||
}
|
||||
let sum = 0;
|
||||
|
||||
return new Date(
|
||||
Date.parse(this.req.items[this.selected[0]].modified)
|
||||
).toLocaleString();
|
||||
},
|
||||
name: function () {
|
||||
return this.selectedCount === 0
|
||||
? this.req.name
|
||||
: this.req.items[this.selected[0]].name;
|
||||
},
|
||||
dir: function () {
|
||||
return (
|
||||
this.selectedCount > 1 ||
|
||||
(this.selectedCount === 0
|
||||
? this.req.isDir
|
||||
: this.req.items[this.selected[0]].isDir)
|
||||
);
|
||||
},
|
||||
resolution: function () {
|
||||
if (this.selectedCount === 1) {
|
||||
const selectedItem = this.req.items[this.selected[0]];
|
||||
if (selectedItem && selectedItem.type === "image") {
|
||||
return selectedItem.resolution;
|
||||
}
|
||||
} else if (this.req && this.req.type === "image") {
|
||||
return this.req.resolution;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
checksum: async function (event, algo) {
|
||||
event.preventDefault();
|
||||
for (const selectedIdx of selected.value) {
|
||||
sum += req.value?.items[selectedIdx]?.size ?? 0;
|
||||
}
|
||||
|
||||
let link;
|
||||
return filesize(sum);
|
||||
});
|
||||
|
||||
if (this.selectedCount) {
|
||||
link = this.req.items[this.selected[0]].url;
|
||||
} else {
|
||||
link = this.$route.path;
|
||||
}
|
||||
const humanTime = computed(() => {
|
||||
if (selectedCount.value === 0) {
|
||||
return dayjs(req.value?.modified).fromNow();
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo);
|
||||
event.target.textContent = hash;
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
|
||||
});
|
||||
|
||||
const modTime = computed(() => {
|
||||
if (selectedCount.value === 0) {
|
||||
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
|
||||
}
|
||||
|
||||
return new Date(
|
||||
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
|
||||
).toLocaleString();
|
||||
});
|
||||
|
||||
const name = computed(() => {
|
||||
return selectedCount.value === 0
|
||||
? (req.value?.name ?? "")
|
||||
: (req.value?.items[selected.value[0]]?.name ?? "");
|
||||
});
|
||||
|
||||
const dir = computed(() => {
|
||||
return (
|
||||
selectedCount.value > 1 ||
|
||||
(selectedCount.value === 0
|
||||
? (req.value?.isDir ?? false)
|
||||
: (req.value?.items[selected.value[0]]?.isDir ?? false))
|
||||
);
|
||||
});
|
||||
|
||||
const resolution = computed(() => {
|
||||
if (selectedCount.value === 1) {
|
||||
const selectedItem = req.value?.items[selected.value[0]];
|
||||
if (selectedItem && selectedItem.type === "image") {
|
||||
return (selectedItem as any).resolution;
|
||||
}
|
||||
} else if (req.value && req.value.type === "image") {
|
||||
return (req.value as any).resolution;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const checksum = async (event: Event, algo: string) => {
|
||||
event.preventDefault();
|
||||
|
||||
let link;
|
||||
|
||||
if (selectedCount.value) {
|
||||
link = req.value?.items[selected.value[0]]?.url ?? "";
|
||||
} else {
|
||||
link = route.path;
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = await api.checksum(link, algo as any);
|
||||
(event.target as HTMLElement).textContent = hash;
|
||||
} catch (e) {
|
||||
$showError?.(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="card-content">
|
||||
<file-list
|
||||
ref="fileList"
|
||||
@update:selected="(val) => (dest = val)"
|
||||
@update:selected="(val: string) => (dest = val)"
|
||||
:exclude="excludedFolders"
|
||||
tabindex="1"
|
||||
/>
|
||||
@@ -17,10 +17,10 @@
|
||||
class="card-action"
|
||||
style="display: flex; align-items: center; justify-content: space-between"
|
||||
>
|
||||
<template v-if="user.perm.create">
|
||||
<template v-if="user?.perm.create">
|
||||
<button
|
||||
class="button button--flat"
|
||||
@click="$refs.fileList.createDir()"
|
||||
@click="fileList?.createDir()"
|
||||
:aria-label="$t('sidebar.newFolder')"
|
||||
:title="$t('sidebar.newFolder')"
|
||||
style="justify-self: left"
|
||||
@@ -54,8 +54,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
@@ -65,80 +67,76 @@ import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { removePrefix } from "@/api/utils";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
components: { FileList },
|
||||
data: function () {
|
||||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
};
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, ["req", "selected"]),
|
||||
...mapState(useAuthStore, ["user"]),
|
||||
...mapWritableState(useFileStore, ["preselect"]),
|
||||
excludedFolders() {
|
||||
return this.selected
|
||||
.filter((idx) => this.req.items[idx].isDir)
|
||||
.map((idx) => this.req.items[idx].url);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
||||
move: async function (event) {
|
||||
event.preventDefault();
|
||||
const items = [];
|
||||
const router = useRouter();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
|
||||
for (const item of this.selected) {
|
||||
items.push({
|
||||
from: this.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
||||
name: this.req.items[item].name,
|
||||
});
|
||||
}
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const action = async (overwrite, rename) => {
|
||||
buttons.loading("move");
|
||||
const { req, selected } = storeToRefs(fileStore);
|
||||
const { user } = storeToRefs(authStore);
|
||||
const { showHover, closeHovers } = layoutStore;
|
||||
|
||||
await api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
buttons.success("move");
|
||||
this.preselect = removePrefix(items[0].to);
|
||||
this.$router.push({ path: this.dest });
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("move");
|
||||
this.$showError(e);
|
||||
});
|
||||
};
|
||||
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||
const dest = ref<string | null>(null);
|
||||
|
||||
const dstItems = (await api.fetch(this.dest)).items;
|
||||
const conflict = upload.checkConflict(items, dstItems);
|
||||
const excludedFolders = computed(() => {
|
||||
return selected.value
|
||||
.filter((idx) => req.value!.items[idx].isDir)
|
||||
.map((idx) => req.value!.items[idx].url);
|
||||
});
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
const move = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||
|
||||
if (conflict) {
|
||||
this.showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
for (const item of selected.value) {
|
||||
items.push({
|
||||
from: req.value!.items[item].url,
|
||||
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||
name: req.value!.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
const action = async (overwrite: boolean, rename: boolean) => {
|
||||
buttons.loading("move");
|
||||
|
||||
return;
|
||||
}
|
||||
await api
|
||||
.move(items, overwrite, rename)
|
||||
.then(() => {
|
||||
buttons.success("move");
|
||||
fileStore.preselect = removePrefix(items[0].to);
|
||||
router.push({ path: dest.value! });
|
||||
})
|
||||
.catch((e) => {
|
||||
buttons.done("move");
|
||||
$showError?.(e);
|
||||
});
|
||||
};
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
const dstItems = (await api.fetch(dest.value!)).items;
|
||||
const conflict = upload.checkConflict(items as any, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
showHover({
|
||||
prompt: "replace-rename",
|
||||
confirm: (event: Event, option: string) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -40,80 +40,74 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, inject } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import url from "@/utils/url";
|
||||
import { files as api } from "@/api";
|
||||
import { removePrefix } from "@/api/utils";
|
||||
|
||||
export default {
|
||||
name: "rename",
|
||||
data: function () {
|
||||
return {
|
||||
name: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.name = this.oldName();
|
||||
},
|
||||
inject: ["$showError"],
|
||||
computed: {
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
cancel: function () {
|
||||
this.closeHovers();
|
||||
},
|
||||
oldName: function () {
|
||||
if (!this.isListing) {
|
||||
return this.req.name;
|
||||
}
|
||||
const router = useRouter();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return;
|
||||
}
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
return this.req.items[this.selected[0]].name;
|
||||
},
|
||||
submit: async function () {
|
||||
let oldLink = "";
|
||||
let newLink = "";
|
||||
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
|
||||
if (!this.isListing) {
|
||||
oldLink = this.req.url;
|
||||
} else {
|
||||
oldLink = this.req.items[this.selected[0]].url;
|
||||
}
|
||||
const name = ref("");
|
||||
|
||||
newLink =
|
||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
||||
const oldName = (): string => {
|
||||
if (!isListing.value) {
|
||||
return req.value?.name ?? "";
|
||||
}
|
||||
|
||||
try {
|
||||
await api.move([{ from: oldLink, to: newLink }]);
|
||||
if (!this.isListing) {
|
||||
this.$router.push({ path: newLink });
|
||||
return;
|
||||
}
|
||||
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||
// This shouldn't happen.
|
||||
return "";
|
||||
}
|
||||
|
||||
this.preselect = removePrefix(newLink);
|
||||
return req.value?.items[selected.value[0]].name ?? "";
|
||||
};
|
||||
|
||||
this.reload = true;
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
onMounted(() => {
|
||||
name.value = oldName();
|
||||
});
|
||||
|
||||
this.closeHovers();
|
||||
},
|
||||
},
|
||||
const submit = async () => {
|
||||
let oldLink = "";
|
||||
let newLink = "";
|
||||
|
||||
if (!req.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isListing.value) {
|
||||
oldLink = req.value.url;
|
||||
} else {
|
||||
oldLink = req.value.items[selected.value[0]].url;
|
||||
}
|
||||
|
||||
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
|
||||
|
||||
try {
|
||||
await api.move([{ from: oldLink, to: newLink }]);
|
||||
if (!isListing.value) {
|
||||
router.push({ path: newLink });
|
||||
return;
|
||||
}
|
||||
|
||||
fileStore.preselect = removePrefix(newLink);
|
||||
|
||||
fileStore.reload = true;
|
||||
} catch (e) {
|
||||
$showError?.(e);
|
||||
}
|
||||
|
||||
closeHovers();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="currentPrompt.action"
|
||||
@click="currentPrompt?.action"
|
||||
:aria-label="$t('buttons.continue')"
|
||||
:title="$t('buttons.continue')"
|
||||
tabindex="2"
|
||||
@@ -30,7 +30,7 @@
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--red"
|
||||
@click="currentPrompt.confirm"
|
||||
@click="currentPrompt?.confirm"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')"
|
||||
tabindex="1"
|
||||
@@ -41,17 +41,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "replace",
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
const layoutStore = useLayoutStore();
|
||||
const { currentPrompt } = storeToRefs(layoutStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="button button--flat button--blue"
|
||||
@click="(event) => currentPrompt.confirm(event, 'rename')"
|
||||
@click="(event) => currentPrompt?.confirm(event, 'rename')"
|
||||
:aria-label="$t('buttons.rename')"
|
||||
:title="$t('buttons.rename')"
|
||||
tabindex="2"
|
||||
@@ -30,7 +30,7 @@
|
||||
<button
|
||||
id="focus-prompt"
|
||||
class="button button--flat button--red"
|
||||
@click="(event) => currentPrompt.confirm(event, 'overwrite')"
|
||||
@click="(event) => currentPrompt?.confirm(event, 'overwrite')"
|
||||
:aria-label="$t('buttons.replace')"
|
||||
:title="$t('buttons.replace')"
|
||||
tabindex="1"
|
||||
@@ -41,17 +41,11 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "replace-rename",
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
},
|
||||
};
|
||||
const layoutStore = useLayoutStore();
|
||||
const { currentPrompt } = storeToRefs(layoutStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
</script>
|
||||
|
||||
@@ -129,138 +129,146 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, onBeforeMount } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useFileStore } from "@/stores/file";
|
||||
import { share as api } from "@/api";
|
||||
import dayjs from "dayjs";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
import { copy } from "@/utils/clipboard";
|
||||
|
||||
export default {
|
||||
name: "share",
|
||||
data: function () {
|
||||
return {
|
||||
time: 0,
|
||||
unit: "hours",
|
||||
links: [],
|
||||
clip: null,
|
||||
password: "",
|
||||
listing: true,
|
||||
};
|
||||
},
|
||||
inject: ["$showError", "$showSuccess"],
|
||||
computed: {
|
||||
...mapState(useFileStore, [
|
||||
"req",
|
||||
"selected",
|
||||
"selectedCount",
|
||||
"isListing",
|
||||
]),
|
||||
url() {
|
||||
if (!this.isListing) {
|
||||
return this.$route.path;
|
||||
}
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const $showError = inject<(error: unknown) => void>("$showError");
|
||||
const $showSuccess = inject<(message: string) => void>("$showSuccess");
|
||||
|
||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
||||
// This shouldn't happen.
|
||||
return;
|
||||
}
|
||||
const fileStore = useFileStore();
|
||||
const layoutStore = useLayoutStore();
|
||||
|
||||
return this.req.items[this.selected[0]].url;
|
||||
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
|
||||
const time = ref(0);
|
||||
const unit = ref("hours");
|
||||
const links = ref<any[]>([]);
|
||||
const password = ref("");
|
||||
const listing = ref(true);
|
||||
|
||||
const url = computed(() => {
|
||||
if (!isListing.value) {
|
||||
return route.path;
|
||||
}
|
||||
|
||||
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||
// This shouldn't happen.
|
||||
return "";
|
||||
}
|
||||
|
||||
return req.value?.items[selected.value[0]].url ?? "";
|
||||
});
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
copy({ text }).then(
|
||||
() => {
|
||||
// clipboard successfully set
|
||||
$showSuccess?.(t("success.linkCopied"));
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
try {
|
||||
const links = await api.get(this.url);
|
||||
this.links = links;
|
||||
this.sort();
|
||||
|
||||
if (this.links.length == 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
copyToClipboard: function (text) {
|
||||
copy({ text }).then(
|
||||
() => {
|
||||
// clipboard write failed
|
||||
copy({ text }, { permission: true }).then(
|
||||
() => {
|
||||
// clipboard successfully set
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
$showSuccess?.(t("success.linkCopied"));
|
||||
},
|
||||
() => {
|
||||
(e) => {
|
||||
// clipboard write failed
|
||||
copy({ text }, { permission: true }).then(
|
||||
() => {
|
||||
// clipboard successfully set
|
||||
this.$showSuccess(this.$t("success.linkCopied"));
|
||||
},
|
||||
(e) => {
|
||||
// clipboard write failed
|
||||
this.$showError(e);
|
||||
}
|
||||
);
|
||||
$showError?.(e);
|
||||
}
|
||||
);
|
||||
},
|
||||
submit: async function () {
|
||||
try {
|
||||
let res = null;
|
||||
|
||||
if (!this.time) {
|
||||
res = await api.create(this.url, this.password);
|
||||
} else {
|
||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
||||
}
|
||||
|
||||
this.links.push(res);
|
||||
this.sort();
|
||||
|
||||
this.time = 0;
|
||||
this.unit = "hours";
|
||||
this.password = "";
|
||||
|
||||
this.listing = true;
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
deleteLink: async function (event, link) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api.remove(link.hash);
|
||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
||||
|
||||
if (this.links.length == 0) {
|
||||
this.listing = false;
|
||||
}
|
||||
} catch (e) {
|
||||
this.$showError(e);
|
||||
}
|
||||
},
|
||||
humanTime(time) {
|
||||
return dayjs(time * 1000).fromNow();
|
||||
},
|
||||
buildLink(share) {
|
||||
return api.getShareURL(share);
|
||||
},
|
||||
sort() {
|
||||
this.links = this.links.sort((a, b) => {
|
||||
if (a.expire === 0) return -1;
|
||||
if (b.expire === 0) return 1;
|
||||
return new Date(a.expire) - new Date(b.expire);
|
||||
});
|
||||
},
|
||||
switchListing() {
|
||||
if (this.links.length == 0 && !this.listing) {
|
||||
this.closeHovers();
|
||||
}
|
||||
|
||||
this.listing = !this.listing;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
let res = null;
|
||||
|
||||
if (!time.value) {
|
||||
res = await api.create(url.value, password.value);
|
||||
} else {
|
||||
res = await api.create(
|
||||
url.value,
|
||||
password.value,
|
||||
String(time.value),
|
||||
unit.value
|
||||
);
|
||||
}
|
||||
|
||||
links.value.push(res);
|
||||
sort();
|
||||
|
||||
time.value = 0;
|
||||
unit.value = "hours";
|
||||
password.value = "";
|
||||
|
||||
listing.value = true;
|
||||
} catch (e) {
|
||||
$showError?.(e);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteLink = async (event: Event, link: any) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
await api.remove(link.hash);
|
||||
links.value = links.value.filter((item) => item.hash !== link.hash);
|
||||
|
||||
if (links.value.length == 0) {
|
||||
listing.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
$showError?.(e);
|
||||
}
|
||||
};
|
||||
|
||||
const humanTime = (time: number) => {
|
||||
return dayjs(time * 1000).fromNow();
|
||||
};
|
||||
|
||||
const buildLink = (share: any) => {
|
||||
return api.getShareURL(share);
|
||||
};
|
||||
|
||||
const sort = () => {
|
||||
links.value = links.value.sort((a, b) => {
|
||||
if (a.expire === 0) return -1;
|
||||
if (b.expire === 0) return 1;
|
||||
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
|
||||
});
|
||||
};
|
||||
|
||||
const switchListing = () => {
|
||||
if (links.value.length == 0 && !listing.value) {
|
||||
closeHovers();
|
||||
}
|
||||
|
||||
listing.value = !listing.value;
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
try {
|
||||
const fetchedLinks = await api.get(url.value);
|
||||
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
|
||||
sort();
|
||||
|
||||
if (links.value.length == 0) {
|
||||
listing.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
$showError?.(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -27,20 +27,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from "pinia";
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useLayoutStore } from "@/stores/layout";
|
||||
|
||||
export default {
|
||||
name: "share-delete",
|
||||
computed: {
|
||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
||||
submit: function () {
|
||||
this.currentPrompt?.confirm();
|
||||
},
|
||||
},
|
||||
const layoutStore = useLayoutStore();
|
||||
const { currentPrompt } = storeToRefs(layoutStore);
|
||||
const { closeHovers } = layoutStore;
|
||||
|
||||
const submit = () => {
|
||||
currentPrompt.value?.confirm();
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,23 +8,27 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "permissions",
|
||||
props: ["commands"],
|
||||
computed: {
|
||||
raw: {
|
||||
get() {
|
||||
return this.commands.join(" ");
|
||||
},
|
||||
set(value) {
|
||||
if (value !== "") {
|
||||
this.$emit("update:commands", value.split(" "));
|
||||
} else {
|
||||
this.$emit("update:commands", []);
|
||||
}
|
||||
},
|
||||
},
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
commands: string[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:commands": [commands: string[]];
|
||||
}>();
|
||||
|
||||
const raw = computed({
|
||||
get() {
|
||||
return props.commands.join(" ");
|
||||
},
|
||||
};
|
||||
set(value: string) {
|
||||
if (value !== "") {
|
||||
emit("update:commands", value.split(" "));
|
||||
} else {
|
||||
emit("update:commands", []);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,61 +6,50 @@
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { markRaw } from "vue";
|
||||
|
||||
export default {
|
||||
name: "languages",
|
||||
props: ["locale"],
|
||||
data() {
|
||||
const dataObj = {};
|
||||
const locales = {
|
||||
he: "עברית",
|
||||
hr: "Hrvatski",
|
||||
hu: "Magyar",
|
||||
ar: "العربية",
|
||||
ca: "Català",
|
||||
cs: "Čeština",
|
||||
de: "Deutsch",
|
||||
el: "Ελληνικά",
|
||||
en: "English",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
is: "Icelandic",
|
||||
it: "Italiano",
|
||||
ja: "日本語",
|
||||
ko: "한국어",
|
||||
"nl-be": "Dutch (Belgium)",
|
||||
no: "Norsk",
|
||||
pl: "Polski",
|
||||
"pt-br": "Português",
|
||||
pt: "Português (Brasil)",
|
||||
ro: "Romanian",
|
||||
ru: "Русский",
|
||||
sk: "Slovenčina",
|
||||
"sv-se": "Swedish (Sweden)",
|
||||
tr: "Türkçe",
|
||||
uk: "Українська",
|
||||
vi: "Tiếng Việt",
|
||||
"zh-cn": "中文 (简体)",
|
||||
"zh-tw": "中文 (繁體)",
|
||||
};
|
||||
defineProps<{
|
||||
locale: string;
|
||||
}>();
|
||||
|
||||
// Vue3 reactivity breaks with this configuration
|
||||
// so we need to use markRaw as a workaround
|
||||
// https://github.com/vuejs/core/issues/3024
|
||||
Object.defineProperty(dataObj, "locales", {
|
||||
value: markRaw(locales),
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
"update:locale": [locale: string];
|
||||
}>();
|
||||
|
||||
return dataObj;
|
||||
},
|
||||
methods: {
|
||||
change(event) {
|
||||
this.$emit("update:locale", event.target.value);
|
||||
},
|
||||
},
|
||||
const locales = markRaw({
|
||||
he: "עברית",
|
||||
hr: "Hrvatski",
|
||||
hu: "Magyar",
|
||||
ar: "العربية",
|
||||
ca: "Català",
|
||||
cs: "Čeština",
|
||||
de: "Deutsch",
|
||||
el: "Ελληνικά",
|
||||
en: "English",
|
||||
es: "Español",
|
||||
fr: "Français",
|
||||
is: "Icelandic",
|
||||
it: "Italiano",
|
||||
ja: "日本語",
|
||||
ko: "한국어",
|
||||
"nl-be": "Dutch (Belgium)",
|
||||
no: "Norsk",
|
||||
pl: "Polski",
|
||||
"pt-br": "Português",
|
||||
pt: "Português (Brasil)",
|
||||
ro: "Romanian",
|
||||
ru: "Русский",
|
||||
sk: "Slovenčina",
|
||||
"sv-se": "Swedish (Sweden)",
|
||||
tr: "Türkçe",
|
||||
uk: "Українська",
|
||||
vi: "Tiếng Việt",
|
||||
"zh-cn": "中文 (简体)",
|
||||
"zh-tw": "中文 (繁體)",
|
||||
});
|
||||
|
||||
const change = (event: Event) => {
|
||||
emit("update:locale", (event.target as HTMLSelectElement).value);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -39,27 +39,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { enableExec } from "@/utils/constants";
|
||||
export default {
|
||||
name: "permissions",
|
||||
props: ["perm"],
|
||||
computed: {
|
||||
admin: {
|
||||
get() {
|
||||
return this.perm.admin;
|
||||
},
|
||||
set(value) {
|
||||
if (value) {
|
||||
for (const key in this.perm) {
|
||||
this.perm[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.perm.admin = value;
|
||||
},
|
||||
},
|
||||
isExecEnabled: () => enableExec,
|
||||
const props = defineProps<{
|
||||
perm: UserPermissions;
|
||||
}>();
|
||||
|
||||
const admin = computed({
|
||||
get() {
|
||||
return props.perm.admin;
|
||||
},
|
||||
};
|
||||
set(value: boolean) {
|
||||
if (value) {
|
||||
for (const key in props.perm) {
|
||||
props.perm[key as keyof UserPermissions] = true;
|
||||
}
|
||||
}
|
||||
|
||||
props.perm.admin = value;
|
||||
},
|
||||
});
|
||||
|
||||
const isExecEnabled = enableExec;
|
||||
</script>
|
||||
|
||||
@@ -32,32 +32,44 @@
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "rules-textarea",
|
||||
props: ["rules"],
|
||||
methods: {
|
||||
remove(event, index) {
|
||||
event.preventDefault();
|
||||
const rules = [...this.rules];
|
||||
rules.splice(index, 1);
|
||||
this.$emit("update:rules", [...rules]);
|
||||
},
|
||||
create(event) {
|
||||
event.preventDefault();
|
||||
<script setup lang="ts">
|
||||
interface Rule {
|
||||
allow: boolean;
|
||||
path: string;
|
||||
regex: boolean;
|
||||
regexp: {
|
||||
raw: string;
|
||||
};
|
||||
}
|
||||
|
||||
this.$emit("update:rules", [
|
||||
...this.rules,
|
||||
{
|
||||
allow: true,
|
||||
path: "",
|
||||
regex: false,
|
||||
regexp: {
|
||||
raw: "",
|
||||
},
|
||||
},
|
||||
]);
|
||||
const props = defineProps<{
|
||||
rules: Rule[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:rules": [rules: Rule[]];
|
||||
}>();
|
||||
|
||||
const remove = (event: Event, index: number) => {
|
||||
event.preventDefault();
|
||||
const rules = [...props.rules];
|
||||
rules.splice(index, 1);
|
||||
emit("update:rules", [...rules]);
|
||||
};
|
||||
|
||||
const create = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
emit("update:rules", [
|
||||
...props.rules,
|
||||
{
|
||||
allow: true,
|
||||
path: "",
|
||||
regex: false,
|
||||
regexp: {
|
||||
raw: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -80,12 +80,20 @@ const { t } = useI18n();
|
||||
const createUserDirData = ref<boolean | null>(null);
|
||||
const originalUserScope = ref<string | null>(null);
|
||||
|
||||
const props = defineProps<{
|
||||
user: IUserForm;
|
||||
isNew: boolean;
|
||||
isDefault: boolean;
|
||||
createUserDir?: boolean;
|
||||
}>();
|
||||
const props = defineProps<
|
||||
| {
|
||||
user: IUserForm;
|
||||
isNew: boolean;
|
||||
isDefault: false;
|
||||
createUserDir?: boolean;
|
||||
}
|
||||
| {
|
||||
user: SettingsDefaults;
|
||||
isNew: boolean;
|
||||
isDefault: true;
|
||||
createUserDir?: boolean;
|
||||
}
|
||||
>();
|
||||
|
||||
onMounted(() => {
|
||||
if (props.user.scope) {
|
||||
@@ -108,6 +116,7 @@ watch(
|
||||
() => props.user,
|
||||
() => {
|
||||
if (!props.user?.perm?.admin) return;
|
||||
if (props.isDefault) return;
|
||||
props.user.lockPassword = false;
|
||||
}
|
||||
);
|
||||
|
||||
+15
-12
@@ -63,8 +63,8 @@
|
||||
local("Roboto"),
|
||||
local("Roboto-Regular"),
|
||||
url(../assets/fonts/roboto/normal-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
unicode-range:
|
||||
U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -75,8 +75,9 @@
|
||||
local("Roboto"),
|
||||
local("Roboto-Regular"),
|
||||
url(../assets/fonts/roboto/normal-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -142,8 +143,8 @@
|
||||
local("Roboto Medium"),
|
||||
local("Roboto-Medium"),
|
||||
url(../assets/fonts/roboto/medium-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
unicode-range:
|
||||
U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -154,8 +155,9 @@
|
||||
local("Roboto Medium"),
|
||||
local("Roboto-Medium"),
|
||||
url(../assets/fonts/roboto/medium-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -221,8 +223,8 @@
|
||||
local("Roboto Bold"),
|
||||
local("Roboto-Bold"),
|
||||
url(../assets/fonts/roboto/bold-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
unicode-range:
|
||||
U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -233,8 +235,9 @@
|
||||
local("Roboto Bold"),
|
||||
local("Roboto-Bold"),
|
||||
url(../assets/fonts/roboto/bold-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC,
|
||||
U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||
U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
|
||||
Vendored
+1
-1
@@ -17,7 +17,7 @@ interface SettingsDefaults {
|
||||
viewMode: ViewModeType;
|
||||
singleClick: boolean;
|
||||
sorting: Sorting;
|
||||
perm: Permissions;
|
||||
perm: UserPermissions;
|
||||
commands: any[];
|
||||
hideDotfiles: boolean;
|
||||
dateFormat: boolean;
|
||||
|
||||
Vendored
+13
-13
@@ -4,7 +4,7 @@ interface IUser {
|
||||
password: string;
|
||||
scope: string;
|
||||
locale: string;
|
||||
perm: Permissions;
|
||||
perm: UserPermissions;
|
||||
commands: string[];
|
||||
rules: IRule[];
|
||||
lockPassword: boolean;
|
||||
@@ -20,20 +20,20 @@ type ViewModeType = "list" | "mosaic" | "mosaic gallery";
|
||||
|
||||
interface IUserForm {
|
||||
id?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
scope?: string;
|
||||
locale?: string;
|
||||
perm?: Permissions;
|
||||
commands?: string[];
|
||||
rules?: IRule[];
|
||||
lockPassword?: boolean;
|
||||
hideDotfiles?: boolean;
|
||||
singleClick?: boolean;
|
||||
dateFormat?: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
scope: string;
|
||||
locale: string;
|
||||
perm: UserPermissions;
|
||||
commands: string[];
|
||||
rules: IRule[];
|
||||
lockPassword: boolean;
|
||||
hideDotfiles: boolean;
|
||||
singleClick: boolean;
|
||||
dateFormat: boolean;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
interface UserPermissions {
|
||||
admin: boolean;
|
||||
copy: boolean;
|
||||
create: boolean;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -1,489 +0,0 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("https://demo.playwright.dev/todomvc");
|
||||
});
|
||||
|
||||
const TODO_ITEMS = [
|
||||
"buy some cheese",
|
||||
"feed the cat",
|
||||
"book a doctors appointment",
|
||||
];
|
||||
|
||||
test.describe("New Todo", () => {
|
||||
test("should allow me to add todo items", async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
// Create 1st todo.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press("Enter");
|
||||
|
||||
// Make sure the list only has one todo item.
|
||||
await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]);
|
||||
|
||||
// Create 2nd todo.
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press("Enter");
|
||||
|
||||
// Make sure the list now has two todo items.
|
||||
await expect(page.getByTestId("todo-title")).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
TODO_ITEMS[1],
|
||||
]);
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
|
||||
test("should clear text input field when an item is added", async ({
|
||||
page,
|
||||
}) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
// Create one todo item.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press("Enter");
|
||||
|
||||
// Check that input is empty.
|
||||
await expect(newTodo).toBeEmpty();
|
||||
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||
});
|
||||
|
||||
test("should append new items to the bottom of the list", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create 3 items.
|
||||
await createDefaultTodos(page);
|
||||
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId("todo-count");
|
||||
|
||||
// Check test using different methods.
|
||||
await expect(page.getByText("3 items left")).toBeVisible();
|
||||
await expect(todoCount).toHaveText("3 items left");
|
||||
await expect(todoCount).toContainText("3");
|
||||
await expect(todoCount).toHaveText(/3/);
|
||||
|
||||
// Check all items in one call.
|
||||
await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Mark all as completed", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test("should allow me to mark all items as completed", async ({ page }) => {
|
||||
// Complete all todos.
|
||||
await page.getByLabel("Mark all as complete").check();
|
||||
|
||||
// Ensure all todos have 'completed' class.
|
||||
await expect(page.getByTestId("todo-item")).toHaveClass([
|
||||
"completed",
|
||||
"completed",
|
||||
"completed",
|
||||
]);
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test("should allow me to clear the complete state of all items", async ({
|
||||
page,
|
||||
}) => {
|
||||
const toggleAll = page.getByLabel("Mark all as complete");
|
||||
// Check and then immediately uncheck.
|
||||
await toggleAll.check();
|
||||
await toggleAll.uncheck();
|
||||
|
||||
// Should be no completed classes.
|
||||
await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]);
|
||||
});
|
||||
|
||||
test("complete all checkbox should update state when items are completed / cleared", async ({
|
||||
page,
|
||||
}) => {
|
||||
const toggleAll = page.getByLabel("Mark all as complete");
|
||||
await toggleAll.check();
|
||||
await expect(toggleAll).toBeChecked();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// Uncheck first todo.
|
||||
const firstTodo = page.getByTestId("todo-item").nth(0);
|
||||
await firstTodo.getByRole("checkbox").uncheck();
|
||||
|
||||
// Reuse toggleAll locator and make sure its not checked.
|
||||
await expect(toggleAll).not.toBeChecked();
|
||||
|
||||
await firstTodo.getByRole("checkbox").check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// Assert the toggle all is checked again.
|
||||
await expect(toggleAll).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Item", () => {
|
||||
test("should allow me to mark items as complete", async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press("Enter");
|
||||
}
|
||||
|
||||
// Check first item.
|
||||
const firstTodo = page.getByTestId("todo-item").nth(0);
|
||||
await firstTodo.getByRole("checkbox").check();
|
||||
await expect(firstTodo).toHaveClass("completed");
|
||||
|
||||
// Check second item.
|
||||
const secondTodo = page.getByTestId("todo-item").nth(1);
|
||||
await expect(secondTodo).not.toHaveClass("completed");
|
||||
await secondTodo.getByRole("checkbox").check();
|
||||
|
||||
// Assert completed class.
|
||||
await expect(firstTodo).toHaveClass("completed");
|
||||
await expect(secondTodo).toHaveClass("completed");
|
||||
});
|
||||
|
||||
test("should allow me to un-mark items as complete", async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press("Enter");
|
||||
}
|
||||
|
||||
const firstTodo = page.getByTestId("todo-item").nth(0);
|
||||
const secondTodo = page.getByTestId("todo-item").nth(1);
|
||||
const firstTodoCheckbox = firstTodo.getByRole("checkbox");
|
||||
|
||||
await firstTodoCheckbox.check();
|
||||
await expect(firstTodo).toHaveClass("completed");
|
||||
await expect(secondTodo).not.toHaveClass("completed");
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
await firstTodoCheckbox.uncheck();
|
||||
await expect(firstTodo).not.toHaveClass("completed");
|
||||
await expect(secondTodo).not.toHaveClass("completed");
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||
});
|
||||
|
||||
test("should allow me to edit an item", async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
const secondTodo = todoItems.nth(1);
|
||||
await secondTodo.dblclick();
|
||||
await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(
|
||||
TODO_ITEMS[1]
|
||||
);
|
||||
await secondTodo
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.fill("buy some sausages");
|
||||
await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter");
|
||||
|
||||
// Explicitly assert the new text value.
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
"buy some sausages",
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, "buy some sausages");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Editing", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test("should hide other controls when editing", async ({ page }) => {
|
||||
const todoItem = page.getByTestId("todo-item").nth(1);
|
||||
await todoItem.dblclick();
|
||||
await expect(todoItem.getByRole("checkbox")).not.toBeVisible();
|
||||
await expect(
|
||||
todoItem.locator("label", {
|
||||
hasText: TODO_ITEMS[1],
|
||||
})
|
||||
).not.toBeVisible();
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test("should save edits on blur", async ({ page }) => {
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.fill("buy some sausages");
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.dispatchEvent("blur");
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
"buy some sausages",
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, "buy some sausages");
|
||||
});
|
||||
|
||||
test("should trim entered text", async ({ page }) => {
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.fill(" buy some sausages ");
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.press("Enter");
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
"buy some sausages",
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, "buy some sausages");
|
||||
});
|
||||
|
||||
test("should remove the item if an empty text string was entered", async ({
|
||||
page,
|
||||
}) => {
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("");
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.press("Enter");
|
||||
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test("should cancel edits on escape", async ({ page }) => {
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.fill("buy some sausages");
|
||||
await todoItems
|
||||
.nth(1)
|
||||
.getByRole("textbox", { name: "Edit" })
|
||||
.press("Escape");
|
||||
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Counter", () => {
|
||||
test("should display the current number of todo items", async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId("todo-count");
|
||||
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press("Enter");
|
||||
|
||||
await expect(todoCount).toContainText("1");
|
||||
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press("Enter");
|
||||
await expect(todoCount).toContainText("2");
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Clear completed button", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
});
|
||||
|
||||
test("should display the correct text", async ({ page }) => {
|
||||
await page.locator(".todo-list li .toggle").first().check();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Clear completed" })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should remove completed items when clicked", async ({ page }) => {
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
await todoItems.nth(1).getByRole("checkbox").check();
|
||||
await page.getByRole("button", { name: "Clear completed" }).click();
|
||||
await expect(todoItems).toHaveCount(2);
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test("should be hidden when there are no items that are completed", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.locator(".todo-list li .toggle").first().check();
|
||||
await page.getByRole("button", { name: "Clear completed" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Clear completed" })
|
||||
).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Persistence", () => {
|
||||
test("should persist its data", async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press("Enter");
|
||||
}
|
||||
|
||||
const todoItems = page.getByTestId("todo-item");
|
||||
const firstTodoCheck = todoItems.nth(0).getByRole("checkbox");
|
||||
await firstTodoCheck.check();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(["completed", ""]);
|
||||
|
||||
// Ensure there is 1 completed item.
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
// Now reload.
|
||||
await page.reload();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(["completed", ""]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Routing", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
// make sure the app had a chance to save updated todos in storage
|
||||
// before navigating to a new view, otherwise the items can get lost :(
|
||||
// in some frameworks like Durandal
|
||||
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||
});
|
||||
|
||||
test("should allow me to display active items", async ({ page }) => {
|
||||
const todoItem = page.getByTestId("todo-item");
|
||||
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
|
||||
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole("link", { name: "Active" }).click();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test("should respect the back button", async ({ page }) => {
|
||||
const todoItem = page.getByTestId("todo-item");
|
||||
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
|
||||
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
await test.step("Showing all items", async () => {
|
||||
await page.getByRole("link", { name: "All" }).click();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
|
||||
await test.step("Showing active items", async () => {
|
||||
await page.getByRole("link", { name: "Active" }).click();
|
||||
});
|
||||
|
||||
await test.step("Showing completed items", async () => {
|
||||
await page.getByRole("link", { name: "Completed" }).click();
|
||||
});
|
||||
|
||||
await expect(todoItem).toHaveCount(1);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("should allow me to display completed items", async ({ page }) => {
|
||||
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole("link", { name: "Completed" }).click();
|
||||
await expect(page.getByTestId("todo-item")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("should allow me to display all items", async ({ page }) => {
|
||||
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole("link", { name: "Active" }).click();
|
||||
await page.getByRole("link", { name: "Completed" }).click();
|
||||
await page.getByRole("link", { name: "All" }).click();
|
||||
await expect(page.getByTestId("todo-item")).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("should highlight the currently applied filter", async ({ page }) => {
|
||||
await expect(page.getByRole("link", { name: "All" })).toHaveClass(
|
||||
"selected"
|
||||
);
|
||||
|
||||
//create locators for active and completed links
|
||||
const activeLink = page.getByRole("link", { name: "Active" });
|
||||
const completedLink = page.getByRole("link", { name: "Completed" });
|
||||
await activeLink.click();
|
||||
|
||||
// Page change - active items.
|
||||
await expect(activeLink).toHaveClass("selected");
|
||||
await completedLink.click();
|
||||
|
||||
// Page change - completed items.
|
||||
await expect(completedLink).toHaveClass("selected");
|
||||
});
|
||||
});
|
||||
|
||||
async function createDefaultTodos(page: Page) {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder("What needs to be done?");
|
||||
|
||||
for (const item of TODO_ITEMS) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press("Enter");
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
|
||||
return await page.waitForFunction((e) => {
|
||||
return JSON.parse(localStorage["react-todos"]).length === e;
|
||||
}, expected);
|
||||
}
|
||||
|
||||
async function checkNumberOfCompletedTodosInLocalStorage(
|
||||
page: Page,
|
||||
expected: number
|
||||
) {
|
||||
return await page.waitForFunction((e) => {
|
||||
return (
|
||||
JSON.parse(localStorage["react-todos"]).filter(
|
||||
(todo: any) => todo.completed
|
||||
).length === e
|
||||
);
|
||||
}, expected);
|
||||
}
|
||||
|
||||
async function checkTodosInLocalStorage(page: Page, title: string) {
|
||||
return await page.waitForFunction((t) => {
|
||||
return JSON.parse(localStorage["react-todos"])
|
||||
.map((todo: any) => todo.title)
|
||||
.includes(t);
|
||||
}, title);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { test, expect } from "./fixtures/auth";
|
||||
|
||||
test("redirect to login", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveURL(/\/login\?redirect=\/files\//);
|
||||
});
|
||||
|
||||
test("login and logout", async ({ authPage, page, context }) => {
|
||||
await authPage.goto();
|
||||
await expect(page).toHaveTitle(/Login - File Browser$/);
|
||||
|
||||
await authPage.loginAs("fake", "fake");
|
||||
await expect(authPage.wrongCredentials).toBeVisible();
|
||||
|
||||
await authPage.loginAs();
|
||||
await expect(authPage.wrongCredentials).toBeHidden();
|
||||
// await page.waitForURL("**/files/", { timeout: 5000 });
|
||||
await expect(page).toHaveTitle(/.*Files - File Browser$/);
|
||||
|
||||
let cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
|
||||
|
||||
await authPage.logout();
|
||||
// await page.waitForURL("**/login", { timeout: 5000 });
|
||||
await expect(page).toHaveTitle(/Login - File Browser$/);
|
||||
|
||||
cookies = await context.cookies();
|
||||
expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
|
||||
});
|
||||
Vendored
-40
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
type Page,
|
||||
type Locator,
|
||||
test as base,
|
||||
expect,
|
||||
} from "@playwright/test";
|
||||
|
||||
export class AuthPage {
|
||||
public readonly wrongCredentials: Locator;
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.wrongCredentials = this.page.locator("div.wrong");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/login");
|
||||
}
|
||||
|
||||
async loginAs(username = "admin", password = "admin") {
|
||||
await this.page.getByPlaceholder("Username").fill(username);
|
||||
await this.page.getByPlaceholder("Password").fill(password);
|
||||
await this.page.getByRole("button", { name: "Login" }).click();
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.page.getByRole("button", { name: "Logout" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
const test = base.extend<{ authPage: AuthPage }>({
|
||||
authPage: async ({ page }, use) => {
|
||||
const authPage = new AuthPage(page);
|
||||
await authPage.goto();
|
||||
await authPage.loginAs();
|
||||
await use(authPage);
|
||||
// await authPage.logout();
|
||||
},
|
||||
});
|
||||
|
||||
export { test, expect };
|
||||
Vendored
-61
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
type Locator,
|
||||
type Page,
|
||||
test as base,
|
||||
expect,
|
||||
} from "@playwright/test";
|
||||
import { AuthPage } from "./auth";
|
||||
|
||||
type SettingsType = "profile" | "shares" | "global" | "users";
|
||||
|
||||
export class SettingsPage {
|
||||
public readonly hideDotfiles: Locator; // checkbox
|
||||
public readonly singleClick: Locator; // checkbox
|
||||
public readonly dateFormat: Locator; // checkbox
|
||||
private readonly languages: Locator; // selection
|
||||
private readonly submitProfile: Locator; // submit
|
||||
private readonly submitPassword: Locator; // submit
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.hideDotfiles = this.page.locator('input[name="hideDotfiles"]');
|
||||
this.singleClick = this.page.locator('input[name="singleClick"]');
|
||||
this.dateFormat = this.page.locator('input[name="dateFormat"]');
|
||||
this.languages = this.page.locator('select[name="selectLanguage"]');
|
||||
this.submitProfile = this.page.locator('input[name="submitProfile"]');
|
||||
this.submitPassword = this.page.locator('input[name="submitPassword"]');
|
||||
}
|
||||
|
||||
async goto(type: SettingsType = "profile") {
|
||||
await this.page.goto(`/settings/${type}`);
|
||||
}
|
||||
|
||||
async setLanguage(locale: string = "en") {
|
||||
await this.languages.selectOption(locale);
|
||||
}
|
||||
|
||||
async saveProfile() {
|
||||
await this.submitProfile.click();
|
||||
}
|
||||
|
||||
async savePassword() {
|
||||
await this.submitPassword.click();
|
||||
}
|
||||
}
|
||||
|
||||
const test = base.extend<{ settingsPage: SettingsPage }>({
|
||||
page: async ({ page }, use) => {
|
||||
// Sign in with our account.
|
||||
const authPage = new AuthPage(page);
|
||||
await authPage.goto();
|
||||
await authPage.loginAs();
|
||||
await expect(page).toHaveTitle(/.*Files - File Browser$/);
|
||||
// Use signed-in page in the test.
|
||||
await use(page);
|
||||
},
|
||||
settingsPage: async ({ page }, use) => {
|
||||
const settingsPage = new SettingsPage(page);
|
||||
await use(settingsPage);
|
||||
},
|
||||
});
|
||||
|
||||
export { test, expect };
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
//classes: Vue-Toastification__toast Vue-Toastification__toast--success bottom-center
|
||||
import { type Page, type Locator, expect } from "@playwright/test";
|
||||
|
||||
export class Toast {
|
||||
private readonly success: Locator;
|
||||
private readonly error: Locator;
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.success = this.page.locator("div.Vue-Toastification__toast--success");
|
||||
this.error = this.page.locator("div.Vue-Toastification__toast--error");
|
||||
}
|
||||
|
||||
async isSuccess() {
|
||||
await expect(this.success).toBeVisible();
|
||||
}
|
||||
|
||||
async isError() {
|
||||
await expect(this.error).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { test, expect } from "./fixtures/settings";
|
||||
import { Toast } from "./fixtures/toast";
|
||||
|
||||
// test.describe("profile settings", () => {
|
||||
test("settings button", async ({ page }) => {
|
||||
const button = page.getByLabel("Settings", { exact: true });
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
await expect(page).toHaveTitle(/^Profile Settings/);
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Profile Settings" })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("set locale", async ({ settingsPage, page }) => {
|
||||
const toast = new Toast(page);
|
||||
|
||||
await settingsPage.goto("profile");
|
||||
await expect(page).toHaveTitle(/^Profile Settings/);
|
||||
// await settingsPage.saveProfile();
|
||||
// await toast.isSuccess();
|
||||
// await expect(
|
||||
// page.getByText("Settings updated!", { exact: true })
|
||||
// ).toBeVisible();
|
||||
|
||||
await settingsPage.setLanguage("hu");
|
||||
await settingsPage.saveProfile();
|
||||
await toast.isSuccess();
|
||||
await expect(
|
||||
page.getByText("Beállítások frissítve!", { exact: true })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Profilbeállítások" })
|
||||
).toBeVisible();
|
||||
|
||||
await settingsPage.setLanguage("en");
|
||||
await settingsPage.saveProfile();
|
||||
await toast.isSuccess();
|
||||
await expect(
|
||||
page.getByText("Settings updated!", { exact: true })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Profile Settings" })
|
||||
).toBeVisible();
|
||||
});
|
||||
// });
|
||||
@@ -1,13 +1,29 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"exclude": [
|
||||
"src/**/__tests__/*",
|
||||
// Excluding non-TS Vue files which use the old Options API.
|
||||
// This can be removed once those files are properly migrated to
|
||||
// the new Composition API with TS support.
|
||||
"src/components/Shell.vue",
|
||||
"src/components/prompts/Copy.vue",
|
||||
"src/components/prompts/Move.vue",
|
||||
"src/components/prompts/Delete.vue",
|
||||
"src/components/prompts/FileList.vue",
|
||||
"src/components/prompts/Rename.vue",
|
||||
"src/components/prompts/Share.vue"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client", "@intlify/unplugin-vue-i18n/messages"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
// Version 0.8.0 of @vue/tsconfig enabled this automatically.
|
||||
// Disabling for now since it's causing quite a lot of errors.
|
||||
// Should be revisited.
|
||||
"noUncheckedIndexedAccess": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
"nightwatch.conf.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
@@ -13,8 +12,6 @@
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
// vue-tsc wont shut up about error TS9005
|
||||
// in non-TS vue files so exclude them
|
||||
"exclude": [
|
||||
"src/components/Shell.vue",
|
||||
"src/components/prompts/Copy.vue",
|
||||
"src/components/prompts/Move.vue",
|
||||
"src/components/prompts/Delete.vue",
|
||||
"src/components/prompts/FileList.vue",
|
||||
"src/components/prompts/Rename.vue",
|
||||
"src/components/prompts/Share.vue"
|
||||
]
|
||||
}
|
||||
+2
-1
@@ -4,7 +4,8 @@
|
||||
"config:recommended",
|
||||
"group:allNonMajor",
|
||||
"group:allDigest",
|
||||
":disableDependencyDashboard"
|
||||
":disableDependencyDashboard",
|
||||
":semanticCommitTypeAll(chore)"
|
||||
],
|
||||
"postUpdateOptions": [
|
||||
"gomodUpdateImportPaths",
|
||||
|
||||
Reference in New Issue
Block a user