v0.2.7 update

This commit is contained in:
Graham Steffaniak 2024-08-03 10:34:12 -05:00 committed by GitHub
parent d8085c1f1b
commit fa3ed6b948
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 921 additions and 662 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@ rice-box.go
/frontend/package-lock.json
/backend/vendor
/backend/*.cov
/backend/test_config.yaml
.DS_Store
node_modules

View File

@ -2,6 +2,14 @@
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.2.7
- **Change**: New sidebar style and behavior
- **Change**: make search view and button behavior more consistent.
- **Fix**: [upload file bug](https://github.com/gtsteffaniak/filebrowser/issues/153)
- **Fix**: user lock out bug introduced in 0.2.6
- **Fix**: many minor state related issues.
## v0.2.6
This change focuses on minimizing and simplifying build process.

View File

@ -2,7 +2,7 @@
<a href="https://opensource.org/license/apache-2-0/"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="License: Apache-2.0"></a>
</p>
<p align="center">
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
<img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
</p>
<h3 align="center">Filebrowser - A modern web-based file manager</h3>
<p align="center">
@ -10,10 +10,13 @@
</p>
> [!WARNING]
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file.
> Starting with v0.2.4 *ALL* share links need to be re-created (due to security fix).
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml`
> configuration file.
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
> security fix).
This fork makes the following significant changes to filebrowser for origin:
This fork makes the following significant changes to filebrowser for
origin:
1. [x] Better search
- Lightning fast
@ -21,7 +24,8 @@ This fork makes the following significant changes to filebrowser for origin:
- Works with more type filters
- interactive results page.
2. [x] Revamped and simplified GUI navbar and sidebar menu.
- Additional compact view mode as well as refreshed view mode styles.
- Additional compact view mode as well as refreshed view mode
styles.
3. [x] Revamped configuration via `filebrowser.yml` config file.
- More configurations possible at a per-user level
- <img width="450" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/625bd7c4-5ee9-4011-aaae-2a388ab0813b">
@ -49,23 +53,24 @@ focus of this fork is on a few key principles:
## 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 :
One way you can observe the improved user experience is how I changed
the UI. The Navbar is simplified to a three component system :
1. (Left) The slide-out action panel button
2. (Middle) The powerful search bar.
3. (Right) The view change toggle.
All other functions are moved either into the action menu or popup menus.
If the action is does not depend on context, it will exist in the slide-out action panel.
If the action is available based on context, it will showup as a popup menu.
If the action is does not depend on context, it will exist in the slide-out
action panel. If the action is available based on context, it will showup as
a popup menu.
<p align="center">
<img width="500" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/35cdeb3b-ab79-4b04-8001-8f51f6ea06bb" title="Dark mode">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/55fa4f5c-440e-4a97-b711-96139208a163">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/c76f4100-949b-4e17-a3e6-e410fb8ec08f">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/0bde26f3-fa90-411e-bd0b-abaa47506d62">
<img width="560" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/71d8f2b8-6fe6-4fdc-8aac-503d08c28d86">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/55fa4f5c-440e-4a97-b711-96139208a163">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/c76f4100-949b-4e17-a3e6-e410fb8ec08f">
<img width="500" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/0bde26f3-fa90-411e-bd0b-abaa47506d62">
<img width="560" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/71d8f2b8-6fe6-4fdc-8aac-503d08c28d86">
</p>
## Install
@ -119,43 +124,67 @@ volumes:
```
Not using docker (not recommended) (Must donwload asset with frontend directory next to filebrowser binary)
Not using docker (not recommended)
Note: Must download asset with frontend directory next to filebrowser binary
```
./filebrowser -f <filebrowser.yml or other /path/to/config.yaml>
./filebrowser -c <filebrowser.yml or other /path/to/config.yaml>
```
## Configuration
All configuration is now done via a single configuration file: `filebrowser.yaml`, here is an example minimal [configuration file](./backend/filebrowser.yaml).
All configuration is now done via a single configuration file:
`filebrowser.yaml`, here is an example minimal [configuration
file](./backend/filebrowser.yaml).
View the [Configuration Help Page](./configuration.md) for available configuration options and other help.
View the [Configuration Help Page](./configuration.md) for available
configuration options and other help.
## Migration from filebrowser/filebrowser
If you are currently using filebrowser from the filebrowser/filebrowser repo, but want to try using this. I recommend you start fresh without reusing the database, but there are a few things you'll need to do if you must migrate:
If you are currently using filebrowser from the filebrowser/filebrowser
repo, but want to try using this. I recommend you start fresh without
reusing the database, but there are a few things you'll need to do if you
must migrate:
1. Create a configuration file as mentioned above.
2. Copy your database file from the original filebrowser to the path of the new one.
3. Update the configuration file to use the database (under server in filebrowser.yml)
4. If you are using docker, update the docker-compose file or docker run command to use the config file as described in the install section above.
5. If you are not using docker, just make sure you run filebrowser -f filebrowser.yml and have valid filebrowser config.
2. Copy your database file from the original filebrowser to the path of
the new one.
3. Update the configuration file to use the database (under server in
filebrowser.yml)
4. If you are using docker, update the docker-compose file or docker run
command to use the config file as described in the install section
above.
5. If you are not using docker, just make sure you run filebrowser -c
filebrowser.yml and have valid filebrowser config.
The filebrowser application should run with the same user and rules that you have from the original. But keep in mind the differences that are mentioned at the top of this readme.
The filebrowser application should run with the same user and rules that
you have from the original. But keep in mind the differences that are
mentioned at the top of this readme.
### background & help
The original project filebrowser/filebrowser used multiple different ways to configure the server.
This was confusing and difficult to work with from a user and from a developer's perspective.
So I completely redesigned the program to use one single human-readable config file.
The original project filebrowser/filebrowser used multiple different ways
to configure the server. This was confusing and difficult to work with
from a user and from a developer's perspective. So I completely redesigned
the program to use one single human-readable config file.
I understand many coming from the original fork may notice differences which make using this improved version more difficult. If you notice issues that you believe should be fixed, please open an issue here and it will very likely be addressed with a PR within a few weeks.
I understand many coming from the original fork may notice differences
which make using this improved version more difficult. If you notice
issues that you believe should be fixed, please open an issue here and it
will very likely be addressed with a PR within a few weeks.
This version of filebrowser is going through a configuration overhaul as mentioned above. Certain features related to rules and commands may not work as they do on the original filebrowser. The purpose of this is to create a more consistent experience where configuration is done via files rather than running commands, so that it's very clear what the current state of the configuration is. When running commands its not clear what the configuration is.
This version of filebrowser is going through a configuration overhaul as
mentioned above. Certain features related to rules and commands may not
work as they do on the original filebrowser. The purpose of this is to
create a more consistent experience where configuration is done via files
rather than running commands, so that it's very clear what the current
state of the configuration is. When running commands its not clear what
the configuration is.
## Roadmap
see [Roadmap Page](./roadmap.md)

View File

@ -46,7 +46,7 @@ var withHashFile = func(fn handleFunc) handleFunc {
Fs: fsPath,
Path: lastComponent,
Modify: d.user.Perm.Modify,
Expand: false,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
Token: link.Token,

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
"reflect"
"sort"
"strconv"
@ -155,30 +156,24 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
return http.StatusBadRequest, nil
}
if len(req.Which) == 0 || (len(req.Which) == 1 && req.Which[0] == "all") {
if !d.user.Perm.Admin {
return http.StatusForbidden, nil
}
if req.Data.Password != "" {
req.Data.Password, err = users.HashPwd(req.Data.Password)
} else {
var suser *users.User
suser, err = d.store.Users.Get(d.server.Root, d.raw.(uint))
req.Data.Password = suser.Password
}
if err != nil {
return http.StatusInternalServerError, err
}
if len(req.Which) == 0 || req.Which[0] == "all" {
req.Which = []string{}
v := reflect.ValueOf(req.Data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Name != "Password" && field.Name != "Fs" {
req.Which = append(req.Which, field.Name)
}
}
}
for k, v := range req.Which {
v = cases.Title(language.English, cases.NoLower).String(v)
req.Which[k] = v
if v == "Password" {
if !d.user.Perm.Admin && d.user.LockPassword {
return http.StatusForbidden, nil
@ -195,7 +190,6 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
}
}
}
err = d.store.Users.Update(req.Data, req.Which...)
if err != nil {
return http.StatusInternalServerError, err

View File

@ -71,6 +71,7 @@ func setDefaults() Settings {
},
},
UserDefaults: UserDefaults{
StickySidebar: true,
Scope: ".",
LockPassword: false,
HideDotfiles: true,
@ -92,6 +93,7 @@ func setDefaults() Settings {
// Apply applies the default options to a user.
func (d *UserDefaults) Apply(u *users.User) {
u.StickySidebar = d.StickySidebar
u.DisableSettings = d.DisableSettings
u.DarkMode = d.DarkMode
u.Scope = d.Scope

View File

@ -68,6 +68,7 @@ type Frontend struct {
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
LockPassword bool `json:"lockPassword"`
DisableSettings bool `json:"disableSettings,omitempty"`

View File

@ -55,7 +55,6 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
if len(fields) == 0 {
return st.Save(user)
}
for _, field := range fields {
userField := reflect.ValueOf(user).Elem().FieldByName(field)
if !userField.IsValid() {
@ -63,10 +62,9 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
}
val := userField.Interface()
if err := st.db.UpdateField(user, field, val); err != nil {
return err
return fmt.Errorf("Error updating user field: %s, error: %v", field, err.Error())
}
}
return nil
}

View File

@ -28,6 +28,7 @@ type Sorting struct {
// User describes a user.
type User struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"`

View File

@ -24,6 +24,10 @@ export async function create(user) {
}
export async function update(user, which = ["all"]) {
if (which[0] != "password") {
user.password = "";
}
console.log("updating user",user,which)
await fetchURL(`/api/users/${user.id}`, {
method: "PUT",
body: JSON.stringify({

View File

@ -1,14 +1,18 @@
<template>
<div class="button-group">
<button v-if="isDisabled" disabled>
No options for folders
</button>
<template v-else>
<button
v-for="(btn, index) in buttons"
:key="index"
:disabled="isDisabled"
:class="{ active: activeButton === index && !isDisabled }"
:class="{ active: activeButton === index }"
@click="setActiveButton(index, btn.label)"
>
{{ btn.label }}
</button>
</template>
</div>
</template>
@ -103,6 +107,10 @@ button:hover {
background: #e0e0e0;
}
button:disabled {
cursor: not-allowed !important;
}
button.active {
background-color: var(--blue) !important;
color: #ffffff;

View File

@ -0,0 +1,168 @@
<template>
<div v-if="selectedCount > 0" id="file-selection" :class="{ 'dark-mode': isDarkMode }">
<span>{{ selectedCount }} selected</span>
<div>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="headerButtons.select"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</div>
</template>
<script>
import { state, getters, mutations } from "@/store"; // Import your custom store
import { files as api } from "@/api";
import Action from "@/components/header/Action.vue";
export default {
name: "fileSelection",
components: {
Action,
},
computed: {
isDarkMode() {
return getters.isDarkMode();
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return getters.selectedCount();
},
},
methods: {
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
download() {
if (getters.isSingleFileSelected()) {
api.download(null, getters.selectedDownloadUrl());
return;
}
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
let files = [];
if (state.selected.length > 0) {
for (let i of state.selected) {
files.push(state.req.items[i].url);
}
} else {
files.push(state.route.path);
}
try {
api.download(format, ...files);
showSuccess("download started");
} catch (e) {
showError("error downloading", e);
}
},
});
},
},
};
</script>
<style>
@media (min-width: 800px) {
#file-selection {
bottom: 4em;
}
}
#file-selection .action {
border-radius: 50%;
width: auto;
}
#file-selection > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#file-selection .action span {
display: none;
}
/* File Selection */
#file-selection {
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
position: fixed;
bottom: 4em;
left: 50%;
transform: translateX(-50%);
align-items: center;
background: #fff;
max-width: 30em;
z-index: 3;
border-radius: 1em;
display: flex;
width: 90%;
}
/* File selection */
#file-selection.dark-mode {
background: var(--surfaceSecondary) !important;
}
#file-selection.dark-mode span {
color: var(--textPrimary) !important;
}
</style>

View File

@ -101,6 +101,47 @@
<div class="searchContext">Search Context: {{ getContext }}</div>
<div id="result-list">
<div>
<div>
<!-- Button groups for filtering search results -->
<ButtonGroup
:buttons="folderSelect"
@button-clicked="addToTypes"
@remove-button-clicked="removeFromTypes"
@disableAll="folderSelectClicked()"
@enableAll="resetButtonGroups()"
/>
<ButtonGroup
:buttons="typeSelect"
@button-clicked="addToTypes"
@remove-button-clicked="removeFromTypes"
:isDisabled="isTypeSelectDisabled"
/>
<!-- Inputs for filtering by file size -->
<div v-if="!foldersOnly" class="sizeConstraints">
<div class="sizeInputWrapper">
<p>Smaller Than:</p>
<input
class="sizeInput"
v-model="smallerThan"
type="number"
min="0"
placeholder="number"
/>
<p>MB</p>
</div>
<div class="sizeInputWrapper">
<p>Larger Than:</p>
<input
class="sizeInput"
v-model="largerThan"
type="number"
placeholder="number"
/>
<p>MB</p>
</div>
</div>
</div>
</div>
<!-- Loading icon when search is ongoing -->
<p v-show="isEmpty && isRunning" id="renew">
<i class="material-icons spin">autorenew</i>
@ -130,51 +171,10 @@
for example <code>"test|not"</code> searches for both terms independently.
</p>
<p>
<b>File size:</b> Searching files by size may have significantly longer
search times.
<b>File size:</b> Searching files by size may have significantly longer search
times.
</p>
</div>
<div>
<!-- Button groups for filtering search results -->
<ButtonGroup
:buttons="folderSelect"
@button-clicked="addToTypes"
@remove-button-clicked="removeFromTypes"
@disableAll="folderSelectClicked()"
@enableAll="resetButtonGroups()"
/>
<ButtonGroup
:buttons="typeSelect"
@button-clicked="addToTypes"
@remove-button-clicked="removeFromTypes"
:isDisabled="isTypeSelectDisabled"
/>
<!-- Inputs for filtering by file size -->
<div class="sizeConstraints">
<div class="sizeInputWrapper">
<p>Smaller Than:</p>
<input
class="sizeInput"
v-model="smallerThan"
type="number"
min="0"
placeholder="number"
/>
<p>MB</p>
</div>
<div class="sizeInputWrapper">
<p>Larger Than:</p>
<input
class="sizeInput"
v-model="largerThan"
type="number"
placeholder="number"
/>
<p>MB</p>
</div>
</div>
</div>
</div>
<!-- List of search results -->
<ul v-show="results.length > 0">
<li
@ -249,6 +249,15 @@ export default {
};
},
watch: {
largerThan() {
this.submit();
},
smallerThan() {
this.submit();
},
searchTypes() {
this.submit();
},
active(active) {
const resultList = document.getElementById("result-list");
if (!active) {
@ -286,6 +295,9 @@ export default {
},
},
computed: {
foldersOnly() {
return this.isTypeSelectDisabled;
},
active() {
return getters.currentPromptName() === "search";
},
@ -314,7 +326,7 @@ export default {
: this.$t("search.pressToSearch");
},
isMobile() {
return this.width <= 800;
return getters.isMobile();
},
isRunning() {
return this.ongoing;
@ -363,6 +375,7 @@ export default {
mutations.showHover("search");
},
close(event) {
this.value = "";
event.stopPropagation();
mutations.closeHovers();
},
@ -402,7 +415,9 @@ export default {
},
async submit(event) {
this.showHelp = false;
if (event != undefined) {
event.preventDefault();
}
if (this.value === "" || this.value.length < 3) {
this.ongoing = false;
this.results = [];
@ -519,7 +534,7 @@ export default {
/* Search */
#search {
background-color: unset !important;
z-index: 3;
z-index: 5;
position: fixed;
top: 0.5em;
min-width: 35em;
@ -639,6 +654,10 @@ body.rtl #search #result ul > * {
border-bottom-left-radius: 0;
}
input.sizeInput:disabled {
cursor: not-allowed;
}
/* Search Input Placeholder */
#search::-webkit-input-placeholder {
color: rgba(255, 255, 255, 0.5);

View File

@ -1,18 +1,48 @@
<template>
<nav :class="{ active, 'dark-mode': isDarkMode }">
<!-- Section for logged-in users -->
<template v-if="isLoggedIn">
<!-- My Files button -->
<button
class="action"
@click="toRoot"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
<nav
id="sidebar"
:class="{ active: active, 'dark-mode': isDarkMode, sticky: user?.stickySidebar }"
>
<i class="material-icons">folder</i>
<span>{{ $t("sidebar.myFiles") }}</span>
<div class="card">
<button
v-if="user.username"
@click="navigateTo('/settings/profile')"
class="action"
>
<i class="material-icons">person</i>
<span>{{ user.username }}</span>
</button>
</div>
<div class="card card-wrapper" @mouseleave="resetHoverTextToDefault">
<span>{{ hoverText }}</span>
<div class="quick-toggles">
<div
:class="{ active: user?.singleClick }"
@click="toggleClick"
@mouseover="updateHoverText('Toggle single click')"
>
<i class="material-icons">ads_click</i>
</div>
<div
:class="{ active: user?.darkMode }"
@click="toggleDarkMode"
@mouseover="updateHoverText('Toggle dark mode')"
>
<i class="material-icons">dark_mode</i>
</div>
<div
:class="{ active: user?.stickySidebar }"
@click="toggleSticky"
@mouseover="updateHoverText('Toggle sticky sidebar')"
>
<i class="material-icons">push_pin</i>
</div>
</div>
</div>
<!-- Section for logged-in users -->
<div v-if="isLoggedIn">
<!-- Buttons visible if user has create permission -->
<div v-if="user.perm?.create">
<!-- New Folder button -->
@ -36,7 +66,7 @@
<span>{{ $t("sidebar.newFile") }}</span>
</button>
<!-- Upload button -->
<button id="upload-button" @click="upload($event)" class="action">
<button id="upload-button" @click="uploadFunc" class="action">
<i class="material-icons">file_upload</i>
<span>Upload file</span>
</button>
@ -47,7 +77,7 @@
<!-- Settings button -->
<button
class="action"
@click="toSettings"
@click="navigateTo('/settings/global')"
:aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')"
>
@ -67,10 +97,30 @@
<span>{{ $t("sidebar.logout") }}</span>
</button>
</div>
</template>
<div class="sources card card-wrapper">
<span>Sources</span>
<div class="inner-card">
<!-- My Files button -->
<button
class="action"
@click="navigateTo('/files/')"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
>
<i class="material-icons">folder</i>
<span>{{ $t("sidebar.myFiles") }}</span>
<div class="usage-info">
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
<span style="text-align: center">{{ usage.usedPercentage }}%</span>
<span>{{ usage.used }} of {{ usage.total }} used</span>
</div>
</button>
</div>
</div>
</div>
<!-- Section for non-logged-in users -->
<template v-else>
<div v-else>
<!-- Login button -->
<router-link
class="action"
@ -92,14 +142,11 @@
<i class="material-icons">person_add</i>
<span>{{ $t("sidebar.signup") }}</span>
</router-link>
</template>
</div>
<div class="buffer"></div>
<!-- Credits and usage information section -->
<div class="credits" v-if="isFiles && !disableUsedPercentage && usage">
<progress-bar :val="usage.usedPercentage" size="medium"></progress-bar>
<span style="text-align: center">{{ usage.usedPercentage }}%</span>
<span>{{ usage.used }} of {{ usage.total }} used</span>
<br />
<span v-if="disableExternal">File Browser</span>
<span v-else>
<a
@ -119,7 +166,6 @@
</template>
<script>
import * as upload from "@/utils/upload";
import * as auth from "@/utils/auth";
import {
version,
@ -129,7 +175,7 @@ import {
noAuth,
loginPage,
} from "@/utils/constants";
import { files as api } from "@/api";
import { files, users } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { state, getters, mutations } from "@/store"; // Import your custom store
@ -140,6 +186,11 @@ export default {
components: {
ProgressBar,
},
data() {
return {
hoverText: "Quick Toggles", // Initially empty
};
},
mounted() {
this.updateUsage();
},
@ -148,6 +199,9 @@ export default {
return getters.isFiles();
},
user() {
if (!getters.isLoggedIn()) {
return {};
}
return state.user;
},
isDarkMode() {
@ -160,7 +214,7 @@ export default {
return getters.currentPrompt();
},
active() {
return getters.currentPromptName() === "sidebar";
return getters.isSidebarVisible() && getters.currentPromptName() == null;
},
signup: () => signup,
version: () => version,
@ -168,85 +222,221 @@ export default {
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage,
usage: () => state.usage,
route: () => state.route,
},
watch: {
route() {
if (!getters.isLoggedIn()) {
return;
}
if (!state.user.stickySidebar) {
mutations.closeSidebar();
}
},
},
methods: {
updateHoverText(text) {
this.hoverText = text;
},
resetHoverTextToDefault() {
this.hoverText = "Quick Toggles"; // Reset to default hover text
},
toggleClick() {
mutations.updateUser({ singleClick: !state.user.singleClick });
},
toggleDarkMode() {
mutations.toggleDarkMode();
},
toggleSticky() {
let newSettings = state.user;
newSettings.stickySidebar = !state.user.stickySidebar;
users.update(newSettings, ["stickySidebar"]);
},
async updateUsage() {
console.log("updating usage");
if (!getters.isLoggedIn()) {
return;
}
let path = getters.getRoutePath();
let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 };
if (this.disableUsedPercentage) {
return usageStats;
}
try {
let usage = await api.usage(path);
let usage = await files.usage(path);
usageStats = {
used: getHumanReadableFilesize(usage.used / 1024),
total: getHumanReadableFilesize(usage.total / 1024),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} catch (error) {
showError("Error fetching usage:", error);
showError("Error fetching usage", error);
}
console.log(usageStats);
mutations.setUsage(usageStats);
},
showHover(value) {
return mutations.showHover(value);
},
// Navigate to the root files directory
toRoot() {
this.$router.push({ path: "/files/" }, () => {});
mutations.closeHovers();
},
// Navigate to the settings page
toSettings() {
this.$router.push({ path: "/settings" }, () => {});
navigateTo(path) {
this.$router.push({ path: path }, () => {});
mutations.closeHovers();
},
// Show the help overlay
help() {
mutations.showHover("help");
},
// Handle file upload
upload(event) {
return this.$upload(event);
},
// Handle files selected for upload
uploadInput(event) {
mutations.closeHovers();
let files = event.currentTarget.files;
let folder_upload =
files[0].webkitRelativePath !== undefined && files[0].webkitRelativePath !== "";
if (folder_upload) {
for (let i = 0; i < files.length; i++) {
let file = files[i];
files[i].fullPath = file.webkitRelativePath;
}
}
let path = getters.getRoutePath();
let conflict = upload.checkConflict(files, state.req.items);
if (conflict) {
mutations.showHover({
name: "replace",
confirm: (event) => {
event.preventDefault();
mutations.closeHovers();
upload.handleFiles(files, path, true);
},
});
return;
}
upload.handleFiles(files, path);
uploadFunc() {
mutations.showHover("upload");
},
// Logout the user
logout: auth.logout,
},
};
</script>
<style>
#sidebar {
top: 0;
display: flex;
flex-direction: column;
padding: 1em;
padding-top: 5em;
width: 20em;
position: fixed;
z-index: 4;
left: -20em;
height: 100%;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: 0.5s ease;
background-color: #ededed;
}
#sidebar.sticky {
z-index: 3;
}
@supports (backdrop-filter: none) {
nav {
background-color: transparent;
backdrop-filter: blur(16px) invert(0.1);
}
}
.usage-info {
padding: 0.5em;
}
body.rtl nav {
left: unset;
right: -17em;
}
#sidebar.active {
left: 0;
}
#sidebar.rtl nav.active {
left: unset;
right: 0;
}
#sidebar > div {
border-top: 1px solid rgba(0, 0, 0, 0.05);
margin-bottom: 0.5em;
}
#sidebar .action {
width: 100%;
display: block;
padding: 0.5em;
white-space: nowrap;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
body.rtl .action {
direction: rtl;
text-align: right;
}
#sidebar .action > * {
vertical-align: middle;
}
/* * * * * * * * * * * * * * * *
* FOOTER *
* * * * * * * * * * * * * * * */
.credits {
font-size: 1em;
color: var(--textSecondary);
padding: 1em;
}
.credits > span {
display: block;
margin-top: 0.5em;
margin-left: 0;
}
.credits a,
.credits a:hover {
color: inherit;
cursor: pointer;
}
.buffer {
flex-grow: 1;
}
.quick-toggles {
display: flex;
justify-content: space-evenly;
width: 100%;
padding-bottom: 1em !important;
}
.quick-toggles button {
border-radius: 10em;
cursor: pointer;
flex: none;
}
.card-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0.5em;
}
.sources {
padding: 0.5em;
margin-top: 0.5em;
}
.inner-card {
border-radius: 0.5em;
padding: 0px !important;
}
.quick-toggles div {
border-radius: 10em;
background-color: var(--surfaceSecondary);
}
.quick-toggles div i {
font-size: 2em;
padding: 0.25em;
border-radius: 10em;
cursor: pointer;
}
button.action {
border-radius: 0.5em;
}
.quick-toggles .active {
background-color: var(--blue) !important;
border-radius: 10em;
}
</style>

View File

@ -23,4 +23,6 @@ export default {
};
</script>
<style></style>
<style>
</style>

View File

@ -68,7 +68,7 @@ export default {
return state.user;
},
closeHovers() {
return mutations.closeHovers()
return mutations.closeHovers();
},
},
methods: {
@ -91,7 +91,7 @@ export default {
.then(() => {
buttons.success("move");
this.$router.push({ path: this.dest });
mutations.setReload(true)
mutations.setReload(true);
})
.catch((e) => {
buttons.done("move");
@ -115,7 +115,7 @@ export default {
event.preventDefault();
mutations.closeHovers();
action(overwrite, rename);
mutations.setReload(true)
mutations.setReload(true);
},
});

View File

@ -9,30 +9,134 @@
</div>
<div class="card-action full">
<div @click="uploadFile" class="action">
<div
@click="uploadFile"
@keypress.enter="uploadFile"
class="action"
id="focus-prompt"
tabindex="1"
>
<i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div>
</div>
<div @click="uploadFolder" class="action">
<div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div>
</div>
<input ref="fileInput" @change="onFilePicked" type="file" style="display: none" />
<input
ref="folderInput"
@change="onFolderPicked"
type="file"
webkitdirectory
directory
style="display: none"
/>
</div>
</div>
</template>
<script>
import { ref } from "vue";
import * as upload from "@/utils/upload";
import { mutations, state, getters } from "@/store";
export default {
name: "upload",
methods: {
uploadFile: function () {
document.getElementById("upload-input").value = "";
document.getElementById("upload-input").click();
name: "UploadFiles",
setup() {
const fileInput = ref(null);
const folderInput = ref(null);
const triggerFilePicker = () => {
fileInput.value.click();
};
const triggerFolderPicker = () => {
folderInput.value.click();
};
const onFilePicked = (event) => {
handleFiles(event);
};
const onFolderPicked = (event) => {
handleFiles(event);
};
const handleFiles = (event) => {
mutations.closeHovers();
const files = event.target.files;
if (!files) return;
const folderUpload = !!files[0].webkitRelativePath;
const uploadFiles = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fullPath = folderUpload ? file.webkitRelativePath : undefined;
uploadFiles.push({
file, // File object directly
name: file.name,
size: file.size,
isDir: false,
fullPath,
});
}
const path = getters.getRoutePath();
const conflict = upload.checkConflict(uploadFiles, state.req.items);
if (conflict) {
mutations.showHover({
name: "replace",
action: (event) => {
event.preventDefault();
mutations.closeHovers();
upload.handleFiles(uploadFiles, path, false);
},
uploadFolder: function () {
document.getElementById("upload-folder-input").value = "";
document.getElementById("upload-folder-input").click();
confirm: (event) => {
event.preventDefault();
mutations.closeHovers();
upload.handleFiles(uploadFiles, path, true);
},
});
return;
}
upload.handleFiles(uploadFiles, path, true);
mutations.setReload(true);
};
const openUpload = (isFolder) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.webkitdirectory = isFolder;
input.addEventListener("change", handleFiles);
input.click();
};
const uploadFile = () => {
openUpload(false);
};
const uploadFolder = () => {
openUpload(true);
};
return {
triggerFilePicker,
triggerFolderPicker,
uploadFile,
uploadFolder,
onFilePicked,
onFolderPicked,
};
},
};
</script>

View File

@ -1,65 +0,0 @@
<template>
<div
v-if="filesInUploadCount > 0"
class="upload-files"
v-bind:class="{ closed: !open }"
>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
<button
class="action"
@click="toggle"
aria-label="Toggle file upload list"
title="Toggle file upload list"
>
<i class="material-icons">{{
open ? "keyboard_arrow_down" : "keyboard_arrow_up"
}}</i>
</button>
</div>
<div class="card-content file-icons">
<div
class="file"
v-for="file in filesInUpload"
:key="file.id"
:data-dir="file.isDir"
:data-type="file.type"
:aria-label="file.name"
>
<div class="file-name"><i class="material-icons"></i> {{ file.name }}</div>
<div class="file-progress">
<div v-bind:style="{ width: file.progress + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getters } from "@/store"; // Import your custom store
export default {
name: "uploadFiles",
data() {
return {
open: false,
};
},
computed: {
filesInUpload() {
return getters.filesInUpload(); // Access the getter directly from the store
},
filesInUploadCount() {
return getters.filesInUploadCount(); // Access the getter directly from the store
},
},
methods: {
toggle() {
this.open = !this.open;
},
},
};
</script>

View File

@ -1,12 +1,12 @@
<template>
<select v-on:change="change" :value="locale">
<select @change="change" :value="locale">
<option v-for="(value, label) in locales" :key="label" :value="label">
{{ $t("languages." + label) }}
</option>
</select>
</template>
<script lang="ts">
<script>
import { defineComponent } from "vue";
export default defineComponent({
@ -47,8 +47,8 @@ export default defineComponent({
};
},
methods: {
change(event: Event) {
const target = event.target as HTMLSelectElement;
change(event) {
const target = event.target;
this.$emit("update:locale", target.value);
},
},

View File

@ -1,4 +1,12 @@
/* Basic Styles */
:root {
--background: white;
--surfacePrimary: gray;
--surfaceSecondary: lightgray;
--textPrimary: white;
--textSecondary: gray;
}
body {
font-family: "Roboto", sans-serif;
padding-top: 4em;
@ -66,57 +74,6 @@ over
padding-bottom: 4em;
}
/* Navigation Styles */
nav {
width: 18em;
position: fixed;
top: 0;
padding-top: 4em;
left: -19em;
z-index: 4;
background: #fff;
height: 100%;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
transition: .1s ease left;
}
body.rtl nav {
left: unset;
right: -17em;
}
nav.active {
left: 0;
}
body.rtl nav.active {
left: unset;
right: 0;
}
nav > div {
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
nav .action {
width: 100%;
display: block;
border-radius: 0;
font-size: 1.1em;
padding: 0.5em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body.rtl .action {
direction: rtl;
text-align: right;
}
nav .action > * {
vertical-align: middle;
}
/* Main Content */
main {
@ -188,22 +145,6 @@ body.rtl .breadcrumbs a {
/* File Selection */
#file-selection {
box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
position: fixed;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
align-items: center;
background: #fff;
max-width: 30em;
z-index: 1;
border-radius: 1em;
display: flex;
width: 90%;
}
button {
flex: 1;
height: 3em;
@ -211,31 +152,13 @@ button {
border: none;
background-color: #f5f5f5;
transition: background-color 0.3s;
/* Add borders */
border-right: 1px solid #ccc;
}
@media (min-width: 800px) {
#file-selection {
bottom: 4em;
}
}
#file-selection .action {
border-radius: 50%;
width: auto;
}
#file-selection > span {
display: inline-block;
margin-left: 1em;
color: #6f6f6f;
margin-right: auto;
}
#file-selection .action span {
display: none;
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#popup-notification {

View File

@ -226,15 +226,6 @@
background: var(--surfaceSecondary) !important;
}
/* File selection */
.dark-mode #file-selection {
background: var(--surfaceSecondary) !important;
}
.dark-mode #file-selection span {
color: var(--textPrimary) !important;
}
/* Dropdown */
.dark-mode #dropdown {
background: var(--surfaceSecondary) !important;

View File

@ -300,11 +300,9 @@ body.rtl .card .card-title>*:first-child {
.overlay {
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 4em;
left: 0;
height: 100%;
width: 100%;
z-index: 3;
z-index: 4;
animation: .3s show ease-in;
}

View File

@ -237,7 +237,6 @@ body.rtl #listingView {
#listingView.compact .header,
#listingView.list .header {
border: 1px solid rgba(0, 0, 0, .1);
border-color: var(--divider);
}
#listingView.compact .header>div:first-child {
@ -315,8 +314,9 @@ body.rtl #listingView {
#listingView .header {
display: none;
background: var(--surfacePrimary);
background: white;
border-radius: 1em;
border: 1px solid rgba(0, 0, 0, .1);
}
#listingView.list .header i {
@ -334,9 +334,7 @@ body.rtl #listingView {
padding: .85em;
width: 100%;
}
#listingView.compact .header {
}
#listingView.list .item:first-child {
margin-top: .5em;
border-top-left-radius: 1em;

View File

@ -8,7 +8,6 @@
@import "./header.css";
@import "./listing.css";
@import "./listing-icons.css";
@import "./upload-files.css";
@import "./dashboard.css";
@import "./login.css";
@import './mobile.css';
@ -90,7 +89,7 @@ main .spinner .bounce2 {
border-radius: 50%;
}
.action:hover {
.action:not(:disabled):hover {
background-color: rgba(0, 0, 0, .1);
}
@ -305,44 +304,6 @@ body.rtl .breadcrumbs .chevron {
font-size: 1rem;
}
/* * * * * * * * * * * * * * * *
* PROMPT *
* * * * * * * * * * * * * * * */
.noty_buttons {
text-align: right;
padding: 0 10px 10px !important;
}
.noty_buttons button {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 0 0 0;
font-size: 1rem;
}
/* * * * * * * * * * * * * * * *
* FOOTER *
* * * * * * * * * * * * * * * */
.credits {
font-size: 1em;
color: #a5a5a5;
padding: 1em;
}
.credits > span {
display: block;
margin-top: .5em;
margin-left: 0;
}
.credits a,
.credits a:hover {
color: inherit;
cursor: pointer;
}
/* * * * * * * * * * * * * * * *
* ANIMATIONS *

View File

@ -1,61 +0,0 @@
.upload-files .card.floating {
left: auto;
top: auto;
margin: 0;
right: 0;
bottom: 0;
transform: none;
}
.upload-files .file {
margin-bottom: 8px;
}
.upload-files .file .file-name {
font-size: 1.1em;
display: flex;
align-items: center;
}
.upload-files .file .file-name i {
margin-right: 5px;
}
.upload-files .file .file-progress {
margin-top: 2px;
width: 100%;
height: 5px;
}
.upload-files .file .file-progress div {
height: 100%;
background-color: #40c4ff;
width: 0;
transition: 0.2s ease width;
border-radius: 10px;
}
.upload-files.closed .card-content {
display: none;
padding: 0em 1em 1em 1em;
}
.upload-files .card .card-title {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8em;
padding: 1em 1em 0em;
}
.upload-files.closed .card-title {
font-size: 0.7em;
padding: 0.5em 1em;
}
@media (max-width: 450px) {
.upload-files .card.floating {
max-width: 100%;
width: 100%;
}
}

View File

@ -1,13 +1,16 @@
import { state } from "./state.js";
export const getters = {
isMobile: () => window.innerWidth <= 800,
isDarkMode: () => {
if (state.user == null) {
return true;
}
return state.user.darkMode === true;
},
isLoggedIn: () => state.user !== null,
isLoggedIn: () => {
return state.user !== null && state.user?.username != undefined && state.user?.username != "publicUser";
},
isAdmin: () => state.user.perm?.admin == true,
isFiles: () => state.route.name === "Files",
isListing: () => getters.isFiles() && state.req.isDir,
@ -17,6 +20,20 @@ export const getters = {
let selectedItem = state.selected[0]
return state.req.items[selectedItem].url;
},
isSidebarVisible: () => {
if (!getters.isLoggedIn()) {
return false
}
return state.showSidebar || state.user.stickySidebar
},
showOverlay: () => {
if (!getters.isLoggedIn()) {
return false
}
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
const sidebarHover = (!state.user.stickySidebar && state.showSidebar) || getters.isMobile();
return hasPrompt || sidebarHover;
},
getRoutePath: () => {
return state.route.path.endsWith("/")
? state.route.path
@ -57,10 +74,8 @@ export const getters = {
},
filesInUploadCount: () => {
// Ensure state.upload.uploads is an object and state.upload.queue is an array
const uploadsCount = typeof state.upload.uploads === 'object' ? Object.keys(state.upload.uploads).length : 0;
const queueCount = Array.isArray(state.upload.queue) ? state.upload.queue.length : 0;
const uploadsCount = state.upload.length
const queueCount = state.queue.length
return uploadsCount + queueCount;
},

View File

@ -1,8 +1,32 @@
import * as i18n from "@/i18n";
import { state } from "./state.js";
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
import { users } from "@/api";
export const mutations = {
toggleDarkMode() {
mutations.updateUser({ "darkMode": !state.user.darkMode });
emitStateChanged();
},
toggleSidebar() {
if (!state.showSidebar && state.user.stickySidebar) {
state.user.stickySidebar = false;
mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed
return
}
state.showSidebar = !state.showSidebar;
emitStateChanged();
},
closeSidebar() {
if (state.showSidebar) {
state.showSidebar = false;
emitStateChanged();
}
},
setUpload(value) {
state.upload = value;
emitStateChanged();
},
setUsage: (value) => {
state.usage = value;
emitStateChanged();
@ -54,11 +78,13 @@ export const mutations = {
let locale = value.locale;
if (locale === "") {
locale = i18n.detectLocale();
value.locale = i18n.detectLocale();
}
i18n.setLocale(locale);
i18n.default.locale = locale;
let previousUser = state.user
state.user = value;
if (state.user != previousUser && state.user.username != "publicUser") {
users.update(state.user);
}
emitStateChanged();
},
setJWT: (value) => {
@ -93,11 +119,15 @@ export const mutations = {
if (state.user === null) {
state.user = {};
}
for (let field in value) {
if (field === "locale") {
i18n.setLocale(value[field]);
let previousUser = state.user;
state.user = { ...state.user, ...value };
if (state.user.locale !== previousUser.locale) {
state.user.locale = i18n.detectLocale();
i18n.setLocale(state.user.locale);
i18n.default.locale = state.user.locale;
}
state.user[field] = value[field];
if (state.user != previousUser) {
users.update(state.user);
}
emitStateChanged();
},
@ -106,7 +136,6 @@ export const mutations = {
state.oldReq = state.req;
state.req = value;
state.selected = [];
if (!state.req?.items) return;
state.selected = state.req.items
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))

View File

@ -2,6 +2,7 @@ import { reactive } from 'vue';
import { detectLocale } from "@/i18n";
export const state = reactive({
showSidebar: false,
usage: {
used: "0 B",
total: "0 B",
@ -9,6 +10,7 @@ export const state = reactive({
},
editor: null,
user: {
stickySidebar: false,
locale: detectLocale(), // Default to the locale from moment
viewMode: 'mosaic', // Default to mosaic view
hideDotfiles: false, // Default to false, assuming this is a boolean
@ -37,18 +39,18 @@ export const state = reactive({
items: [],
},
jwt: "",
progress: 0,
loading: false,
reload: false,
selected: [],
multiple: false,
upload: {
progress: [], // Array of progress values
sizes: [], // Array of sizes
uploads: {},
queue: [],
progress: [],
sizes: [],
},
prompts: [],
show: null,
showShell: false,
showConfirm: null,
route: {},
});

View File

@ -1,5 +1,6 @@
import { state } from "@/store";
import url from "@/utils/url";
import { files as api } from "@/api";
export function checkConflict(files, items) {
if (typeof items === "undefined" || items === null) {
@ -101,10 +102,9 @@ export function scanFiles(dt) {
}
export function handleFiles(files, base, overwrite = false) {
for (let i = 0; i < files.length; i++) {
let id = state.upload.id;
for (const file of files) {
const id = state.upload.id;
let path = base;
let file = files[i];
if (file.fullPath !== undefined) {
path += url.encodePath(file.fullPath);
@ -119,11 +119,19 @@ export function handleFiles(files, base, overwrite = false) {
const item = {
id,
path,
file,
file: file.file, // Ensure `file.file` is the Blob or File
overwrite,
...(!file.isDir && { type: file.type }),
};
state.dispatch("upload/upload", item);
// Upload the file using your API
api.post(item.path, item.file, item.overwrite, (event) => {
console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`);
})
.then(response => {
console.log("Upload successful:", response);
})
.catch(error => {
console.error("Upload error:", error);
});
}
}

View File

@ -15,6 +15,7 @@
</div>
</div>
</template>
<script>
import { files as api } from "@/api";
@ -49,7 +50,7 @@ export default {
return getters.currentView() !== null;
},
reload() {
return state.reload; // Access reload from state
return state.reload;
},
},
created() {
@ -58,8 +59,8 @@ export default {
watch: {
$route: "fetchData",
reload(value) {
if (value === true) {
console.log("reloading")
if (value) {
console.log("reloading");
this.fetchData();
}
},
@ -78,16 +79,16 @@ export default {
},
methods: {
async fetchData() {
// Set loading to true and reset the error.
mutations.setLoading(true);
this.error = null;
// Reset view information using mutations
mutations.setReload(false);
mutations.resetSelected();
mutations.setMultiple(false);
mutations.closeHovers();
// Set loading to true and reset the error.
mutations.setLoading(true);
this.error = null;
let url = state.route.path;
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
@ -106,9 +107,10 @@ export default {
}
} catch (e) {
this.error = e;
}
} finally {
mutations.setLoading(false);
mutations.replaceRequest(data);
}
},
keyEvent(event) {
// F1!

View File

@ -1,4 +1,4 @@
<template>
<template v-if="isLoggedIn">
<div>
<div v-show="showOverlay" @click="resetPrompts" class="overlay"></div>
<div v-if="progress" class="progress">
@ -14,16 +14,17 @@
></editorBar>
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar>
<sidebar></sidebar>
<main :class="{ 'dark-mode': isDarkMode }">
<search v-if="currentView == 'listingView'"></search>
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }">
<router-view></router-view>
</main>
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
<upload-files></upload-files>
</div>
<div class="card" id="popup-notification">
<i v-on:click="closePopUp" class="material-icons">close</i>
<div id="popup-notification-content">no info</div>
</div>
<fileSelection> </fileSelection>
</template>
<script>
import editorBar from "./bars/EditorBar.vue";
@ -31,7 +32,9 @@ import defaultBar from "./bars/Default.vue";
import listingBar from "./bars/ListingBar.vue";
import Prompts from "@/components/prompts/Prompts.vue";
import Sidebar from "@/components/Sidebar.vue";
import UploadFiles from "../components/prompts/UploadFiles.vue";
import Search from "@/components/Search.vue";
import fileSelection from "@/components/FileSelection.vue";
import { closePopUp } from "@/notify";
import { enableExec } from "@/utils/constants";
import { state, getters, mutations } from "@/store";
@ -39,12 +42,13 @@ import { state, getters, mutations } from "@/store";
export default {
name: "layout",
components: {
fileSelection,
Search,
defaultBar,
editorBar,
listingBar,
Sidebar,
Prompts,
UploadFiles,
},
data() {
return {
@ -55,6 +59,14 @@ export default {
};
},
computed: {
isLoggedIn() {
return getters.isLoggedIn();
},
moveWithSidebar() {
return (
getters.isSidebarVisible() && !getters.isMobile() && state.user?.stickySidebar
);
},
closePopUp() {
return closePopUp;
},
@ -77,7 +89,7 @@ export default {
return state.user; // Access state directly from the store
},
showOverlay() {
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
return getters.showOverlay();
},
isDarkMode() {
return getters.isDarkMode();
@ -91,6 +103,9 @@ export default {
},
watch: {
$route() {
if (!getters.isLoggedIn()) {
return;
}
mutations.resetSelected();
mutations.setMultiple(false);
if (getters.currentPromptName() !== "success") {
@ -100,6 +115,7 @@ export default {
},
methods: {
resetPrompts() {
mutations.closeSidebar();
mutations.closeHovers();
},
getTitle() {
@ -117,7 +133,13 @@ export default {
main {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
transition: 0.5s ease;
}
main.moveWithSidebar {
padding-left: 21em;
}
main::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}

View File

@ -259,13 +259,10 @@ export default {
// Set loading to true and reset the error.
mutations.setLoading(true);
this.error = null;
// Reset view information.
if (state.user == undefined) {
if (!getters.isLoggedIn()) {
let userData = await api.getPublicUser();
let req = state.req;
req.user = userData;
mutations.replaceRequest(req);
mutations.setUser(userData);
}
mutations.setReload(false);
mutations.resetSelected();
@ -276,17 +273,12 @@ export default {
if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url;
try {
let file = await api.fetchPub(url, this.password);
file.hash = this.hash;
this.token = file.token || "";
mutations.updateRequest(file);
document.title = `${file.name} - ${document.title}`;
} catch (e) {
this.error = e;
} finally {
mutations.setLoading(false);
}
},
keyEvent(event) {
// Esc!

View File

@ -221,13 +221,6 @@ export default {
let uri = url.removeLastDir(state.route.path) + "/";
this.$router.push({ path: uri });
},
toggleSidebar() {
if (state.show == "sidebar") {
mutations.closeHovers();
} else {
mutations.showHover("sidebar");
}
},
base64(name) {
return window.btoa(unescape(encodeURIComponent(name)));
},

View File

@ -5,13 +5,14 @@
icon="menu"
:label="$t('buttons.toggleSidebar')"
@action="toggleSidebar()"
:disabled="showOverlay"
/>
<search />
<action
class="menu-button"
icon="grid_view"
:label="$t('buttons.switchView')"
@action="switchView"
:disabled="showOverlay"
/>
</header>
</template>
@ -25,16 +26,13 @@
</style>
<script>
import { state, mutations, getters } from "@/store";
import { users, files as api } from "@/api";
import Action from "@/components/header/Action.vue";
import Search from "@/components/Search.vue";
import { showError } from "@/notify";
export default {
name: "listingView",
components: {
Action,
Search,
},
data: function () {
return {
@ -43,6 +41,9 @@ export default {
};
},
computed: {
showOverlay() {
return getters.currentPrompt() !== null && getters.currentPromptName() !== "more";
},
viewIcon() {
const icons = {
list: "view_module",
@ -71,21 +72,15 @@ export default {
}
},
toggleSidebar() {
if (getters.currentPromptName() === "sidebar") {
mutations.closeHovers();
} else {
mutations.showHover("sidebar");
}
mutations.toggleSidebar();
},
async switchView() {
mutations.closeHovers();
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
const nextIndex = (currentIndex + 1) % this.viewModes.length;
let data = state.user;
data.viewMode = this.viewModes[nextIndex];
const newView = this.viewModes[nextIndex];
try {
users.update(data, ["viewMode"]);
mutations.setUser(data);
mutations.updateUser({ viewMode: newView });
} catch (e) {
showError(e);
}

View File

@ -1,60 +1,5 @@
<template>
<div style="padding-bottom: 5em">
<div v-if="selectedCount > 0" id="file-selection">
<span>{{ selectedCount }} selected</span>
<div>
<action
v-if="headerButtons.select"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="headerButtons.select"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="headerButtons.download"
icon="file_download"
:label="$t('buttons.download')"
@action="download"
:counter="selectedCount"
/>
<action
v-if="headerButtons.share"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="headerButtons.rename"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="headerButtons.copy"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="headerButtons.move"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="headerButtons.delete"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
</div>
<div v-if="loading">
<h2 class="message delayed">
<div class="spinner">
@ -94,7 +39,7 @@
:class="listingViewMode + ' file-icons'"
>
<div>
<div class="header">
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }" >
<p
:class="{ active: nameSorted }"
class="name"
@ -209,13 +154,7 @@
</div>
</template>
<style>
.header-items {
width: 100% !important;
max-width: 100% !important;
justify-content: center;
}
</style>
<script>
import { files as api } from "@/api";
import * as upload from "@/utils/upload";
@ -224,12 +163,10 @@ import throttle from "@/utils/throttle";
import { state, mutations, getters } from "@/store";
import { showError } from "@/notify";
import Action from "@/components/header/Action.vue";
import Item from "@/components/files/ListingItem.vue";
export default {
name: "listingView",
components: {
Action,
Item,
},
data() {
@ -241,6 +178,9 @@ export default {
};
},
computed: {
isDarkMode() {
return state.user?.darkMode;
},
getMultiple() {
return state.multiple;
},
@ -321,18 +261,7 @@ export default {
listingViewMode() {
return state.user?.viewMode;
},
headerButtons() {
return {
select: state.selected.length > 0,
upload: state.user.perm?.create && state.selected.length > 0,
download: state.user.perm.download && state.selected.length > 0,
delete: state.selected.length > 0 && state.user.perm.delete,
rename: state.selected.length === 1 && state.user.perm.rename,
share: state.selected.length === 1 && state.user.perm.share,
move: state.selected.length > 0 && state.user.perm.rename,
copy: state.selected.length > 0 && state.user.perm?.create,
};
},
selectedCount() {
return state.selected.length;
},
@ -637,38 +566,12 @@ export default {
openSearch() {
this.currentPrompt = "search";
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
windowsResize: throttle(function () {
this.colunmsResize();
this.width = window.innerWidth;
// Listing element is not displayed
if (this.$refs.listingView == null) return;
}, 100),
download() {
if (getters.isSingleFileSelected()) {
api.download(null, getters.selectedDownloadUrl());
return;
}
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
let files = [];
if (state.selected.length > 0) {
for (let i of state.selected) {
files.push(state.req.items[i].url);
}
} else {
files.push(state.route.path);
}
api.download(format, ...files);
},
});
},
upload() {
if (
typeof window.DataTransferItem !== "undefined" &&
@ -682,3 +585,15 @@ export default {
},
};
</script>
<style>
.dark-mode-item-header {
border-color: var(--divider) !important;
background: var(--surfacePrimary) !important;
}
.header-items {
width: 100% !important;
max-width: 100% !important;
justify-content: center;
}
</style>

View File

@ -155,6 +155,9 @@ export default {
},
watch: {
$route() {
if (!getters.isLoggedIn()) {
return;
}
this.updatePreview();
this.toggleNavigation();
},

View File

@ -85,7 +85,7 @@
<script>
import { showSuccess, showError } from "@/notify";
import { state, mutations } from "@/store";
import { users as api } from "@/api";
import { users } from "@/api";
import Languages from "@/components/settings/Languages.vue";
import ViewMode from "@/components/settings/ViewMode.vue";
import i18n, { rtlLanguages } from "@/i18n";
@ -135,6 +135,16 @@ export default {
this.singleClick = state.user.singleClick;
this.dateFormat = state.user.dateFormat;
},
watch: {
user() {
this.darkMode = state.user.darkMode;
this.locale = state.user.locale;
this.viewMode = state.user.viewMode;
this.hideDotfiles = state.user.hideDotfiles;
this.singleClick = state.user.singleClick;
this.dateFormat = state.user.dateFormat;
},
},
methods: {
async updatePassword(event) {
event.preventDefault();
@ -147,8 +157,7 @@ export default {
let newUserSettings = state.user;
newUserSettings.id = state.user.id;
newUserSettings.password = this.password;
await api.update(newUserSettings, ["password"]);
muations.updateUser(newUserSettings);
await users.update(newUserSettings, ["password"]);
showSuccess(this.$t("settings.passwordUpdated"));
} catch (e) {
showError(e);
@ -168,7 +177,7 @@ export default {
};
const shouldReload =
rtlLanguages.includes(data.locale) !== rtlLanguages.includes(i18n.locale);
await api.update(data, [
await users.update(data, [
"locale",
"darkMode",
"viewMode",

View File

@ -1,5 +1,8 @@
setup:
cd frontend && npm i
if [ ! -f backend/test__config.yaml ]; then \
cp backend/filebrowser.yaml backend/test_config.yaml; \
fi
build:
docker build -t gtstef/filebrwoser .
@ -8,7 +11,7 @@ dev:
# Kill processes matching exe/filebrowser, ignore errors if process does not exist
-pkill -f "exe/filebrowser" || true
# Start backend and frontend concurrently
cd backend && go run . & BACKEND_PID=$$!; \
cd backend && go run . -c test_config.yaml & BACKEND_PID=$$!; \
cd frontend && npm run watch & FRONTEND_PID=$$!; \
wait $$BACKEND_PID $$FRONTEND_PID

View File

@ -1,17 +1,14 @@
# Planned Roadmap
Next version (v0.2.7) :
Next version :
- Replace http routes for gorilla/mux
- Replace vue-router with simple vanilla js
- Theme configuration from settings
- Replace afero requests with std library
- Add Job status to the sidebar
- index status.
- new jobs as they come
- new jobs as they come via pocketbase
Future releases:
- Replace http routes for gorilla/mux with pocketbase
- Allow multiple volumes to show up in the same filebrowser container. https://github.com/filebrowser/filebrowser/issues/2514
- enable/disable indexing for certain mounts
- Add tools to sidebar