v0.3.4 initial release

This commit is contained in:
Graham Steffaniak 2024-12-26 11:31:04 -06:00
parent d2a3e50d37
commit 0270711a22
19 changed files with 399 additions and 352 deletions

View File

@ -7,22 +7,22 @@ on:
- "v[0-9]+.[0-9]+.[0-9]+"
jobs:
test_frontend:
name: Push release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.playwright
push: false
#test_frontend:
# name: Push release
# runs-on: ubuntu-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3.0.0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3.0.0
# - name: Build
# uses: docker/build-push-action@v6
# with:
# context: .
# file: ./Dockerfile.playwright
# push: false
push_pr_to_registry:
name: Push PR
runs-on: ubuntu-latest

View File

@ -61,7 +61,7 @@ jobs:
JSON="${{ steps.meta.outputs.tags }}"
# Use jq to remove 'v' from the version field
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
echo "CLEANED_TAG=$JSON" >> $GITHUB_OUTPUT
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
with:
@ -72,5 +72,5 @@ jobs:
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: ./Dockerfile
push: true
tags: ${{ steps.modify-json.outputs.CLEANED_TAG }}
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.3.4
**Bugfixes**:
- Safari right-click actions.
- Some small image viewer behavior
- Progressive webapp "install to homescreen" fix.
## v0.3.3
**New Features**

View File

@ -10,16 +10,15 @@
</p>
> [!Note]
> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/<database_file>`. Please read the usage below to properly update your config to point the new config location.
> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/<database_file>`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed)
> [!WARNING]
> - There is no stable version yet. Always check release notes for bug fixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon.
> - If on windows, please use docker. The windows binary is unstable and may not work.
FileBrowser Quantum is a fork of the file browser opensource project with the following changes:
1. [x] Indexes files efficiently. See [indexing readme](./docs/indexing.md)
- Real-time search results as you type
1. [x] Indexes files efficiently. (See [indexing readme](./docs/indexing.md) for more info.)
- Real-time search results as you type!
- Search supports file/folder sizes and many file type filters.
- Enhanced interactive results that show file/folder sizes.
2. [x] Revamped and simplified GUI navbar and sidebar menu.
@ -27,21 +26,21 @@ FileBrowser Quantum is a fork of the file browser opensource project with the fo
styles.
- Many graphical and user experience improvements.
- right-click context menu
3. [x] Revamped and simplified configuration via `filebrowser.yml` config file.
3. [x] Revamped and simplified configuration via `config.yaml` config file.
4. [x] Better listing browsing
- Switching view modes is instant
- Folder sizes are shown as well
- Changing Sort order is instant
- The entire directory is loaded in 1/3 the time
5. [x] Developer API support
- Can create long-live API Tokens.
- Ability to create long-live API Tokens.
- Helpful Swagger page available at `/swagger` endpoint.
Notable features that this fork *does not* have (removed):
- jobs/runners are not supported yet (planned).
- shell commands are completely removed and will not be returned.
- themes and branding are not fully supported yet (planned).
- Themes and branding are not fully supported yet (planned).
- see feature matrix below for more.
- pagination for directory items, so large directories with more than 100,000 items may be slow to load or not load at all.
@ -67,10 +66,11 @@ focus of this fork is on a few key principles:
- Minimize external dependencies and standard library usage.
- Of course -- adding much-needed features.
For more questions, see the [Q&A Readme](./docs/questions.md)
## Look
One way you can observe the improved user experience is how I changed
the UI. The Navbar is simplified to a three-component system :
The UI has a simple three-component navigation system :
1. (Left) The slide-out action panel button
2. (Middle) The powerful search bar.
@ -162,7 +162,7 @@ There are very few commands available. There are 3 actions done via the command
## API Usage
FileBrowser Quantum allows for the creation of API tokens which can create users, access file information, and update user settings just like what can be done from the UI. You can create API tokens from the settings page via "API Management" section. This section will only show up if the user has "API" permissions, which can be granted by editing the user in user management.
API tokens can be created to perform actions, access file information, and update user settings just like what can be done from the UI. You can create API tokens from the settings page via "API Management" section. This section will only show up if the user has "API" permissions, which can be granted by editing the user in user management.
Regardless of whether a user has API permissions, anyone can visit the swagger page which is found at `/swagger`. This swagger page uses a short-live token (2-hour exp) that the UI uses, but allows for quick access to all the API's and their described usage and requirements:
@ -191,7 +191,6 @@ configuration options and other help.
## Migration from the original filebrowser
If you currently use the original filebrowser but want to try using this.
I would recommend that you start fresh without reusing the database. However,
If you want to migrate your existing database to FileBrowser Quantum,
visit the [migration

View File

@ -24,15 +24,15 @@ builds:
- upx {{ .Path }} # Compress the binary with UPX
# Build configuration for windows without arm
- id: windows
ldflags: *ldflags
main: main.go
binary: filebrowser
goos:
- windows
goarch:
- amd64
- arm64
# - id: windows
# ldflags: *ldflags
# main: main.go
# binary: filebrowser
# goos:
# - windows
# goarch:
# - amd64
# - arm64
archives:
- name_template: "{{.Os}}-{{.Arch}}{{if .Arm}}v{{.Arm}}{{end}}-{{ .ProjectName }}"

View File

@ -7,7 +7,6 @@ import (
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"text/template"
@ -52,7 +51,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
"BaseURL": config.Server.BaseURL,
"Version": version.Version,
"CommitSHA": version.CommitSHA,
"StaticURL": path.Join(config.Server.BaseURL, "static"),
"StaticURL": config.Server.BaseURL + "static",
"Signup": settings.Config.Auth.Signup,
"NoAuth": config.Auth.Method == "noauth",
"AuthMethod": config.Auth.Method,
@ -62,6 +61,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
"EnableThumbs": config.Server.EnableThumbnails,
"ResizePreview": config.Server.ResizePreview,
"EnableExec": config.Server.EnableExec,
"ReCaptchaHost": config.Auth.Recaptcha.Host,
}
if config.Frontend.Files != "" {

50
docs/questions.md Normal file
View File

@ -0,0 +1,50 @@
# Q&A topics
## When will there be a stable release?
Please see [roadmap](roadmap.md) for future plans -- yes a stable release will eventually come.
## Can you add FileBrowser Quantum features to the original file browser?
This "Quantum" version fork was created because I wanted certain features that are a dramatic and opinionated departure from the OG filebrowser. If you look at the original filebrowser repo pull requests, you will find there are many basic features that remain open for many months or years with very little attention.
If the original filebrowser maintainers were more active and if I didn't have to worry about spending months or years playing politics about the concequences of these drastic changes, I would contribute to the original repository.
However, **I will not make an modifications to the original filebrowser**, for these reasons:
1. My changes are opinionated and I want full control over the experience (and consequences) of the changes. For example I removed the terminal, runners, command line flags, and more. These are changes that would probably be highly contested. I think the experience is better without them, or with the changes -- and I hope you agree.
2. Contributing to that open source project takes a long time, I may never see my changes actually make it in and don't want to waste my time trying for years, I only have so much time.
3. This project was originally a fork, but that quickly changed. There are hundereds of thousands of changes and complete departures from the original codebase. I can't simply "port" the features I write on this repo over to the OG file browser.
Both of these repos being open source means YOU can migrate these features if you want! Feel free to spend the effort and do so for the community. I am not the only one capable of doing it and I encourage this if you have the time, energy, and knowhow to do it.
## I notice a lot of things that don't work like the original file browser repo, how can I get this fixed?
Please open issues and/or pull requests from a forked repo if you notice issues that should be fixed. Some changes are intentional and I may have left things broken. For example, I am not fully confident user rules work in 100% of places. When you notice things, please let me know and I will check if its an intentional change or a bug.
## Is there a way to donate or support this project?
Nope... not yet! It's still "unfinished" in my opinion, so I don't want to ask for any money from it. But if you have a strong desire to donate, email info@quantumx-apps.com to get in touch.
## Is there an email or phone I can contact?
Yes - Please contact info@quantumx-apps.com for any off-github topics. If its related to a specific application or repo such as this filebrowser, please open an issue or pull request instead. Email is only for corrospondance unrelated to technical changes or issues.
## Can I fork this repo and use it?
This repo has the same license as the original filebrowser, apache-2.0. Feel free to use in any way that follows the license. I have no issues with anything personally -- its open source please do as you like. However, since this is a fork of the OG repo, I am not sure what the consequences are for a fork of this repo.
## Who are the maintainers for FileBrowser Quantum?
Right now, just me as a personal hobby and some small contributions from the community. Once I can release a confident stable version, I plan to publicize this application more on social media. Hopefully, in the future I could pick up some extra contributors.
I'm not looking for contributors at the moment, but if you want to me a contributor feel free to email me at info@quantumx-apps.com to see about getting contributor access.
## Are there plans to charge for this product?
No, this repo and project will always be free to use.
## Is there a discord for this fork?
Not yet, generally most interactions should happen on github for now.

View File

@ -1,22 +1,46 @@
# Planned Roadmap
upcoming 0.3.x releases, ordered by priority:
- more indexing flexability
- option not to index hidden files/folders
- options folders to include/exclude from indexing
- implement more indexing runners for more efficienct filesystem watching
- more filetype previews: eg. raw img, office, photoshop, vector, 3d files.
- introduce jobs as replacement to runners.
Upcoming 0.3.x releases, ordered by priority:
- Bring Themes and Branding back.
- openoffice support https://github.com/filebrowser/filebrowser/pull/2954
- More filetype previews: eg. raw img, office, photoshop, vector, 3d files.
- Introduce jobs as replacement to runners.
- Add Job status to the sidebar
- index status.
- Job status from users
- upload status
- opentelemetry metrics
- Opentelemetry metrics
- user access,
- file access
- download count
- last login
- more sign in support
- LDAP
- 2FA
- SSO
Upcoming 0.4.x release:
- Support for multiple filesystem sources https://github.com/filebrowser/filebrowser/issues/2514
- Onboarding process to add sources and configure them on first run.
- More indexing flexability
- option not to index hidden files/folders
- options folders to include/exclude from indexing
- implement more indexing runners for more efficienct filesystem watching
- tags support
Stable release (v1.0.0) - Planned 2025:
- Once under the hood changes for things like multiple sources, jobs support, etc
- More robust backend and frontend testing
- Currently a stable release does not exist primarily because things are still changing, configuration changes are happening frequently and will for the next
- Rebrand to QuantumX App suite umbrella branding and github repo change.
Unplanned Future releases:
- multiple sources https://github.com/filebrowser/filebrowser/issues/2514
- Add tools to sidebar
- duplicate file detector.
- bulk rename https://github.com/filebrowser/filebrowser/issues/2473
- metrics tracker - user access, file access, download count, last login, etc
- support minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544
- support more source types such as minio, s3, and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544
- Activity Log
- Comments support
- Trash Support
- starred/pinned files
- event based notifications

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
{{ if .ReCaptcha }}
<script src="{{ .ReCaptchaHost }}/recaptcha/api.js?render=explicit" data-vite-ignore></script>
<script src="{{ .ReCaptchaHost }}/recaptcha/api.js?render=explicit" data-vite-ignore></script>
{{ end }}
<title>{{ if .Name }}{{ .Name }}{{ else }}FileBrowser Quantum{{ end }}</title>
@ -19,7 +20,7 @@
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="assets">
<link rel="apple-touch-icon" href="{{ .StaticURL }}/img/icons/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="180x180" href="{{ .StaticURL }}/img/icons/android-chrome-256x256.png">
<!-- Add to home screen for Windows -->
<meta name="msapplication-TileImage" content="{{ .StaticURL }}/img/icons/mstile-144x144.png">
@ -27,94 +28,111 @@
<!-- Inject Some Variables and generate the manifest json -->
<script>
window.FileBrowser = JSON.parse('{{ .globalVars }}');
var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
window.FileBrowser = JSON.parse('{{ .globalVars }}');
var dynamicManifest = {
"name": window.FileBrowser.Name || 'FileBrowser Quantum',
"short_name": window.FileBrowser.Name || 'FileBrowser',
"icons": [
{
"src": fullStaticURL + "/img/icons/android-chrome-256x256.png",
"src": window.location.origin + "{{ .StaticURL }}/img/icons/android-chrome-256x256.png",
"sizes": "512x512",
"type": "image/png"
}
},
{
"src": window.location.origin + "{{ .StaticURL }}/img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
],
"start_url": fullStaticURL,
"start_url": window.location.origin + "{{ .BaseURL }}",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": window.FileBrowser.Color || "#455a64"
}
const stringManifest = JSON.stringify(dynamicManifest);
const blob = new Blob([stringManifest], {type: 'application/json'});
const blob = new Blob([stringManifest], { type: 'application/json' });
const manifestURL = URL.createObjectURL(blob);
document.querySelector('#manifestPlaceholder').setAttribute('href', manifestURL);
</script>
<style>
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: .1s ease opacity;
-webkit-transition: .1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner > div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
-webkit-transform: scale(0);
transform: scale(0);
} 40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
z-index: 9999;
transition: .1s ease opacity;
-webkit-transition: .1s ease opacity;
}
#loading.done {
opacity: 0;
}
#loading .spinner {
width: 70px;
text-align: center;
position: fixed;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
#loading .spinner>div {
width: 18px;
height: 18px;
background-color: #333;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
#loading .spinner .bounce1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loading .spinner .bounce2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0)
}
40% {
-webkit-transform: scale(1.0)
}
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
}
}
}
</style>
</head>
<body>
<div id="app"></div>
@ -138,7 +156,8 @@
<script type="module" src="/src/main.ts"></script>
{{ if .CSS }}
<link rel="stylesheet" href="{{ .StaticURL }}/custom.css" >
<link rel="stylesheet" href="{{ .StaticURL }}/custom.css">
{{ end }}
</body>
</html>
</html>

View File

@ -1,20 +0,0 @@
{
"name": "FileBrowser",
"short_name": "FileBrowser",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./static/img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#455a64"
}

View File

@ -201,6 +201,7 @@ export default {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
}
#context-menu.centered {

View File

@ -1,24 +1,9 @@
<template>
<div
class="image-ex-container"
ref="container"
@touchstart="touchStart"
@touchmove="touchMove"
@dblclick="zoomAuto"
@mousedown="mousedownStart"
@mousemove="mouseMove"
@mouseup="mouseUp"
@wheel="wheelMove"
>
<div class="image-ex-container" ref="container" @touchstart="touchStart" @touchmove="touchMove" @dblclick="zoomAuto"
@mousedown="mousedownStart" @mousemove="mouseMove" @mouseup="mouseUp" @wheel="wheelMove">
<div v-if="!isLoaded">Loading image...</div>
<img
v-if="!isTiff && isLoaded"
:src="src"
class="image-ex-img"
ref="imgex"
@load="onLoad"
/>
<img v-if="!isTiff && isLoaded" :src="src" class="image-ex-img" ref="imgex" @load="onLoad" />
<canvas v-else-if="isLoaded" ref="imgex" class="image-ex-img"></canvas>
</div>
</template>
@ -92,7 +77,7 @@ export default {
},
watch: {
src: function () {
if (this.src == undefined || this.$refs.imgex == undefined) {
if (!this.src || !this.$refs.imgex) {
mutations.setLoading("preview-img", false);
return;
}
@ -102,15 +87,18 @@ export default {
} else {
this.$refs.imgex.src = this.src;
}
this.scale = 1;
this.setZoom();
this.setCenter();
mutations.setLoading("preview-img", false);
this.showSpinner = false;
this.scale = 1; // Reset zoom level
this.position.relative = { x: 0, y: 0 }; // Reset position
this.showSpinner = true; // Show spinner while loading
},
},
methods: {
onLoad() {
this.imageLoaded = true;
this.setCenter(); // Center the image after loading
this.showSpinner = false;
mutations.setLoading("preview-img", false);
},
checkIfTiff(src) {
const sufs = ["tif", "tiff", "dng", "cr2", "nef"];
const suff = src.split(".").pop().toLowerCase();
@ -144,16 +132,18 @@ export default {
}
}, 100),
setCenter() {
let container = this.$refs.container;
let img = this.$refs.imgex;
const container = this.$refs.container;
const img = this.$refs.imgex;
if (!container || !img || !img.clientWidth || !img.clientHeight) {
return; // Exit if dimensions are unavailable
}
this.position.center.x = Math.floor((container.clientWidth - img.clientWidth) / 2);
this.position.center.y = Math.floor(
(container.clientHeight - img.clientHeight) / 2
);
this.position.center.y = Math.floor((container.clientHeight - img.clientHeight) / 2);
img.style.left = this.position.center.x + "px";
img.style.top = this.position.center.y + "px";
img.style.left = `${this.position.center.x}px`;
img.style.top = `${this.position.center.y}px`;
},
mousedownStart(event) {
this.lastX = null;
@ -247,11 +237,13 @@ export default {
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
},
wheelMove(event) {
event.preventDefault()
this.scale += -Math.sign(event.deltaY) * this.zoomStep;
this.setZoom();
},
setZoom() {
this.scale = Math.max(this.minScale, Math.min(this.maxScale, this.scale));
// Update the transform with both translate and scale values
this.$refs.imgex.style.transform = `translate(${this.position.relative.x}px, ${this.position.relative.y}px) scale(${this.scale})`;
},
@ -264,17 +256,23 @@ export default {
<style>
.image-ex-container {
max-width: 100%; /* Image container max width */
max-height: 100%; /* Image container max height */
overflow: hidden; /* Hide overflow if image exceeds container */
position: relative; /* Required for absolute positioning of child */
max-width: 100%;
/* Image container max width */
max-height: 100%;
/* Image container max height */
overflow: hidden;
/* Hide overflow if image exceeds container */
position: relative;
/* Required for absolute positioning of child */
display: flex;
justify-content: center;
}
.image-ex-img {
max-width: 100%; /* Image max width */
max-height: 100%; /* Image max height */
max-width: 100%;
/* Image max width */
max-height: 100%;
/* Image max height */
position: absolute;
}
</style>

View File

@ -16,8 +16,12 @@
:data-type="type"
:aria-label="name"
:aria-selected="isSelected"
@contextmenu="onRightClick"
@contextmenu="onRightClick($event)"
@click="click($event)"
@touchstart="addSelected($event)"
@touchmove="handleTouchMove($event)"
@touchend="cancelContext($event)"
@mouseup="cancelContext($event)"
>
<div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
<img
@ -91,6 +95,10 @@ export default {
isThumbnailInView: false,
isMaximized: false,
touches: 0,
touchStartX: 0,
touchStartY: 0,
isLongPress: false,
isSwipe: false,
};
},
props: [
@ -169,6 +177,30 @@ export default {
}
},
methods: {
handleTouchMove(event) {
if (!state.isSafari) return
const touch = event.touches[0];
const deltaX = Math.abs(touch.clientX - this.touchStartX);
const deltaY = Math.abs(touch.clientY - this.touchStartY);
// Set a threshold for movement to detect a swipe
const movementThreshold = 10; // Adjust as needed
if (deltaX > movementThreshold || deltaY > movementThreshold) {
this.isSwipe = true;
this.cancelContext(); // Cancel long press if swipe is detected
}
},
handleTouchEnd() {
if (!state.isSafari) return
this.cancelContext(); // Clear timeout
this.isSwipe = false; // Reset swipe state
},
cancelContext() {
if (this.contextTimeout) {
clearTimeout(this.contextTimeout);
this.contextTimeout = null;
}
this.isLongPress = false;
},
updateHashAndNavigate(path) {
// Update hash in the browser without full page reload
window.location.hash = path;
@ -181,7 +213,6 @@ export default {
},
onRightClick(event) {
event.preventDefault(); // Prevent default context menu
// If no items are selected, select the right-clicked item
if (!state.multiple) {
mutations.resetSelected();
@ -296,6 +327,22 @@ export default {
action(overwrite, rename);
},
addSelected(event) {
if (!state.isSafari) return
const touch = event.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.isLongPress = false; // Reset state
this.isSwipe = false; // Reset swipe detection
if (!state.multiple) {
this.contextTimeout = setTimeout(() => {
if (!this.isSwipe) {
mutations.resetSelected();
mutations.addSelected(this.index);
}
}, 500);
}
},
click(event) {
if (event.button === 0) {
// Left-click
@ -305,13 +352,12 @@ export default {
}
}
if (!this.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
event.preventDefault();
}
setTimeout(() => {
this.touches = 0;
}, 500);
this.touches++;
if (this.touches > 1) {
this.open();
@ -342,7 +388,7 @@ export default {
return;
}
if (!this.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
mutations.resetSelected();
}
mutations.addSelected(this.index);
@ -355,3 +401,10 @@ export default {
},
};
</script>
<style>
.item {
-webkit-touch-callout: none; /* Disable the default long press preview */
user-select: none; /* Optional: Disable text selection for better UX */
}
</style>

View File

@ -112,7 +112,7 @@ export default {
}
action(overwrite, rename);
} catch (e) {
notify.error(e);
notify.showError(e);
}
return;
},

View File

@ -2,6 +2,7 @@
--item-selected: white;
transition: all;
animation-duration: 0.25s;
user-select: none;
}
body.rtl #listingView {

View File

@ -2,6 +2,7 @@ import { reactive } from 'vue';
import { detectLocale } from "@/i18n";
export const state = reactive({
isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent),
activeSettingsView: "",
isMobile: window.innerWidth <= 800,
showSidebar: false,
@ -13,6 +14,7 @@ export const state = reactive({
editor: null,
user: {
gallarySize: 0,
singleClick: false,
stickySidebar: stickyStartup(),
locale: detectLocale(), // Default to the locale from moment
viewMode: 'normal', // Default to mosaic view

View File

@ -16,64 +16,26 @@
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t("files.lonely") }}</span>
</h2>
<input
style="display: none"
type="file"
id="upload-input"
@change="uploadInput($event)"
multiple
/>
<input
style="display: none"
type="file"
id="upload-folder-input"
@change="uploadInput($event)"
webkitdirectory
multiple
/>
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
multiple />
</div>
<div
v-else
id="listingView"
ref="listingView"
:class="listingViewMode + ' file-icons'"
>
<div v-else id="listingView" ref="listingView" :class="listingViewMode + ' file-icons'">
<div>
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
<p
:class="{ active: nameSorted }"
class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')"
>
<p :class="{ active: nameSorted }" class="name" role="button" tabindex="0" @click="sort('name')"
:title="$t('files.sortByName')" :aria-label="$t('files.sortByName')">
<span>{{ $t("files.name") }}</span>
<i class="material-icons">{{ nameIcon }}</i>
</p>
<p
:class="{ active: sizeSorted }"
class="size"
role="button"
tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')"
>
<p :class="{ active: sizeSorted }" class="size" role="button" tabindex="0" @click="sort('size')"
:title="$t('files.sortBySize')" :aria-label="$t('files.sortBySize')">
<span>{{ $t("files.size") }}</span>
<i class="material-icons">{{ sizeIcon }}</i>
</p>
<p
:class="{ active: modifiedSorted }"
class="modified"
role="button"
tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')"
>
<p :class="{ active: modifiedSorted }" class="modified" role="button" tabindex="0" @click="sort('modified')"
:title="$t('files.sortByLastModified')" :aria-label="$t('files.sortByLastModified')">
<span>{{ $t("files.lastModified") }}</span>
<i class="material-icons">{{ modifiedIcon }}</i>
</p>
@ -84,23 +46,10 @@
<h2>{{ $t("files.folders") }}</h2>
</div>
</div>
<div
v-if="numDirs > 0"
class="folder-items"
:class="{ lastGroup: numFiles === 0 }"
>
<item
v-for="item in dirs"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
v-bind:path="item.path"
/>
<div v-if="numDirs > 0" class="folder-items" :class="{ lastGroup: numFiles === 0 }">
<item v-for="item in dirs" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified"
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" />
</div>
<div v-if="numFiles > 0">
<div class="header-items">
@ -108,35 +57,14 @@
</div>
</div>
<div v-if="numFiles > 0" class="file-items" :class="{ lastGroup: numFiles > 0 }">
<item
v-for="item in files"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
v-bind:path="item.path"
/>
<item v-for="item in files" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified"
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" />
</div>
<input
style="display: none"
type="file"
id="upload-input"
@change="uploadInput($event)"
multiple
/>
<input
style="display: none"
type="file"
id="upload-folder-input"
@change="uploadInput($event)"
webkitdirectory
multiple
/>
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
multiple />
</div>
</div>
</div>
@ -290,14 +218,12 @@ export default {
window.addEventListener("resize", this.windowsResize);
this.$el.addEventListener("click", this.clickClear);
// Detect Safari
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
// Adjust contextmenu listener based on browser
if (isSafari) {
if (state.isSafari) {
// For Safari, add touchstart or mousedown to open the context menu
this.$el.addEventListener("touchstart", this.openContextForSafari);
this.$el.addEventListener("mousedown", this.openContextForSafari);
this.$el.addEventListener("touchmove", this.handleTouchMove);
// Also clear the timeout if the user clicks or taps quickly
this.$el.addEventListener("touchend", this.cancelContext);
@ -319,29 +245,57 @@ export default {
window.removeEventListener("scroll", this.scrollEvent);
window.removeEventListener("resize", this.windowsResize);
// If Safari, remove touchstart listener
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari) {
if (state.isSafari) {
this.$el.removeEventListener("touchstart", this.openContextForSafari);
this.$el.removeEventListener("mousedown", this.openContextForSafari);
this.$el.removeEventListener("touchend", this.cancelContext);
this.$el.removeEventListener("mouseup", this.cancelContext);
this.$el.removeEventListener("touchmove", this.handleTouchMove);
} else {
window.removeEventListener("contextmenu", this.openContext);
}
},
methods: {
cancelContext(event) {
cancelContext() {
if (this.contextTimeout) {
clearTimeout(this.contextTimeout);
this.contextTimeout = null;
}
this.isLongPress = false;
},
openContextForSafari(event) {
event.preventDefault();
event.stopPropagation();
// Set a timeout that triggers after 500ms of hold
this.cancelContext(); // Clear any previous timeouts
this.isLongPress = false; // Reset state
this.isSwipe = false; // Reset swipe detection
const touch = event.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
// Start the long press detection
this.contextTimeout = setTimeout(() => {
this.openContext(event);
}, 500); // You can adjust the delay (500ms) to mimic "click and hold"
if (!this.isSwipe) {
this.isLongPress = true;
event.preventDefault(); // Suppress Safari's callout menu
this.openContext(event); // Open the custom context menu
}
}, 500); // Long press delay (adjust as needed)
},
handleTouchMove(event) {
const touch = event.touches[0];
const deltaX = Math.abs(touch.clientX - this.touchStartX);
const deltaY = Math.abs(touch.clientY - this.touchStartY);
// Set a threshold for movement to detect a swipe
const movementThreshold = 10; // Adjust as needed
if (deltaX > movementThreshold || deltaY > movementThreshold) {
this.isSwipe = true;
this.cancelContext(); // Cancel long press if swipe is detected
}
},
handleTouchEnd() {
this.cancelContext(); // Clear timeout
this.isSwipe = false; // Reset swipe state
},
base64(name) {
return url.base64Encode(name);
@ -354,7 +308,6 @@ export default {
mutations.addSelected(allItems[0].index);
}
},
// Helper method to select an item by index
selectItem(index) {
mutations.resetSelected();
@ -877,6 +830,7 @@ export default {
.dark-mode-item-header {
border-color: var(--divider) !important;
background: var(--surfacePrimary) !important;
user-select: none;
}
.header-items {

View File

@ -3,39 +3,17 @@
<div class="preview">
<ExtendedImage v-if="getSimpleType(currentItem.type) == 'image'" :src="raw">
</ExtendedImage>
<audio
v-else-if="getSimpleType(currentItem.type) == 'audio'"
ref="player"
:src="raw"
controls
:autoplay="autoPlay"
@play="autoPlay = true"
></audio>
<video
v-else-if="getSimpleType(currentItem.type) == 'video'"
ref="player"
:src="raw"
controls
:autoplay="autoPlay"
@play="autoPlay = true"
>
<track
kind="captions"
v-for="(sub, index) in subtitles"
:key="index"
:src="sub"
:label="'Subtitle ' + index"
:default="index === 0"
/>
<audio v-else-if="getSimpleType(currentItem.type) == 'audio'" ref="player" :src="raw" controls
:autoplay="autoPlay" @play="autoPlay = true"></audio>
<video v-else-if="getSimpleType(currentItem.type) == 'video'" ref="player" :src="raw" controls
:autoplay="autoPlay" @play="autoPlay = true">
<track kind="captions" v-for="(sub, index) in subtitles" :key="index" :src="sub" :label="'Subtitle ' + index"
:default="index === 0" />
Sorry, your browser doesn't support embedded videos, but don't worry, you can
<a :href="downloadUrl">download it</a>
and watch it with your favorite video player!
</video>
<object
v-else-if="getSimpleType(currentItem.type) == 'pdf'"
class="pdf"
:data="raw"
></object>
<object v-else-if="getSimpleType(currentItem.type) == 'pdf'" class="pdf" :data="raw"></object>
<div v-else class="info">
<div class="title">
<i class="material-icons">feedback</i>
@ -47,12 +25,7 @@
<i class="material-icons">file_download</i>{{ $t("buttons.download") }}
</div>
</a>
<a
target="_blank"
:href="raw"
class="button button--flat"
v-if="currentItem.type != 'directory'"
>
<a target="_blank" :href="raw" class="button button--flat" v-if="currentItem.type != 'directory'">
<div>
<i class="material-icons">open_in_new</i>{{ $t("buttons.openFile") }}
</div>
@ -61,24 +34,13 @@
</div>
</div>
<button
@click="prev"
@mouseover="hoverNav = true"
@mouseleave="hoverNav = false"
:class="{ hidden: !hasPrevious || !showNav }"
:aria-label="$t('buttons.previous')"
:title="$t('buttons.previous')"
>
<button @click="prev" @mouseover="hoverNav = true" @mouseleave="hoverNav = false"
:class="{ hidden: !hasPrevious || !showNav }" :aria-label="$t('buttons.previous')"
:title="$t('buttons.previous')">
<i class="material-icons">chevron_left</i>
</button>
<button
@click="next"
@mouseover="hoverNav = true"
@mouseleave="hoverNav = false"
:class="{ hidden: !hasNext || !showNav }"
:aria-label="$t('buttons.next')"
:title="$t('buttons.next')"
>
<button @click="next" @mouseover="hoverNav = true" @mouseleave="hoverNav = false"
:class="{ hidden: !hasNext || !showNav }" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
<i class="material-icons">chevron_right</i>
</button>
<link rel="prefetch" :href="previousRaw" />
@ -145,10 +107,10 @@ export default {
const previewUrl = this.fullSize
? filesApi.getDownloadURL(this.currentItem.url, "large")
: filesApi.getPreviewURL(
this.currentItem.url,
"small",
this.currentItem.modified
);
this.currentItem.url,
"small",
this.currentItem.modified
);
return previewUrl;
},
showMore() {

View File

@ -23,8 +23,6 @@ const resolve = {
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
// command === 'build'
return {
plugins,
resolve,
@ -56,11 +54,10 @@ export default defineConfig(({ command }) => {
},
test: {
globals: true,
include: ["src/**/*.test.js"], // Explicitly include test files only
exclude: ["src/**/*.vue"], // Exclude Vue files unless tested directly
environment: "jsdom", // jsdom environment
setupFiles: "tests/mocks/setup.js", // Setup file for tests
include: ["src/**/*.test.js"],
exclude: ["src/**/*.vue"],
environment: "jsdom",
setupFiles: "tests/mocks/setup.js",
},
};
});