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 /frontend/package-lock.json
/backend/vendor /backend/vendor
/backend/*.cov /backend/*.cov
/backend/test_config.yaml
.DS_Store .DS_Store
node_modules 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). 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 ## v0.2.6
This change focuses on minimizing and simplifying build process. 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> <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>
<p align="center"> <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> </p>
<h3 align="center">Filebrowser - A modern web-based file manager</h3> <h3 align="center">Filebrowser - A modern web-based file manager</h3>
<p align="center"> <p align="center">
@ -10,10 +10,13 @@
</p> </p>
> [!WARNING] > [!WARNING]
> Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml` configuration file. > Starting with v0.2.0, *ALL* configuration is done via `filebrowser.yaml`
> Starting with v0.2.4 *ALL* share links need to be re-created (due to security fix). > 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 1. [x] Better search
- Lightning fast - Lightning fast
@ -21,7 +24,8 @@ This fork makes the following significant changes to filebrowser for origin:
- Works with more type filters - Works with more type filters
- interactive results page. - interactive results page.
2. [x] Revamped and simplified GUI navbar and sidebar menu. 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. 3. [x] Revamped configuration via `filebrowser.yml` config file.
- More configurations possible at a per-user level - 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"> - <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 ## Look
One way you can observe the improved user experience is how I changed the UI. One way you can observe the improved user experience is how I changed
The Navbar is simplified to a three component system : the UI. The Navbar is simplified to a three component system :
1. (Left) The slide-out action panel button 1. (Left) The slide-out action panel button
2. (Middle) The powerful search bar. 2. (Middle) The powerful search bar.
3. (Right) The view change toggle. 3. (Right) The view change toggle.
All other functions are moved either into the action menu or popup menus. 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 does not depend on context, it will exist in the slide-out
If the action is available based on context, it will showup as a popup menu. action panel. If the action is available based on context, it will showup as
a popup menu.
<p align="center"> <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" 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/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/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="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="560" alt="image" src="https://github.com/gtsteffaniak/filebrowser/assets/42989099/71d8f2b8-6fe6-4fdc-8aac-503d08c28d86">
</p> </p>
## Install ## 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 ## 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 ## 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. 1. Create a configuration file as mentioned above.
2. Copy your database file from the original filebrowser to the path of the new one. 2. Copy your database file from the original filebrowser to the path of
3. Update the configuration file to use the database (under server in filebrowser.yml) the new one.
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. 3. Update the configuration file to use the database (under server in
5. If you are not using docker, just make sure you run filebrowser -f filebrowser.yml and have valid filebrowser config. 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 ### background & help
The original project filebrowser/filebrowser used multiple different ways to configure the server. The original project filebrowser/filebrowser used multiple different ways
This was confusing and difficult to work with from a user and from a developer's perspective. to configure the server. This was confusing and difficult to work with
So I completely redesigned the program to use one single human-readable config file. 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 ## Roadmap
see [Roadmap Page](./roadmap.md) see [Roadmap Page](./roadmap.md)

View File

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

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"reflect"
"sort" "sort"
"strconv" "strconv"
@ -155,30 +156,24 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
return http.StatusBadRequest, nil return http.StatusBadRequest, nil
} }
if len(req.Which) == 0 || (len(req.Which) == 1 && req.Which[0] == "all") { if len(req.Which) == 0 || 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
}
req.Which = []string{} 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 { for k, v := range req.Which {
v = cases.Title(language.English, cases.NoLower).String(v) v = cases.Title(language.English, cases.NoLower).String(v)
req.Which[k] = v req.Which[k] = v
if v == "Password" { if v == "Password" {
if !d.user.Perm.Admin && d.user.LockPassword { if !d.user.Perm.Admin && d.user.LockPassword {
return http.StatusForbidden, nil 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...) err = d.store.Users.Update(req.Data, req.Which...)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err

View File

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

View File

@ -68,6 +68,7 @@ type Frontend struct {
// UserDefaults is a type that holds the default values // UserDefaults is a type that holds the default values
// for some fields on User. // for some fields on User.
type UserDefaults struct { type UserDefaults struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"` DarkMode bool `json:"darkMode"`
LockPassword bool `json:"lockPassword"` LockPassword bool `json:"lockPassword"`
DisableSettings bool `json:"disableSettings,omitempty"` 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 { if len(fields) == 0 {
return st.Save(user) return st.Save(user)
} }
for _, field := range fields { for _, field := range fields {
userField := reflect.ValueOf(user).Elem().FieldByName(field) userField := reflect.ValueOf(user).Elem().FieldByName(field)
if !userField.IsValid() { if !userField.IsValid() {
@ -63,10 +62,9 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
} }
val := userField.Interface() val := userField.Interface()
if err := st.db.UpdateField(user, field, val); err != nil { 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 return nil
} }

View File

@ -28,6 +28,7 @@ type Sorting struct {
// User describes a user. // User describes a user.
type User struct { type User struct {
StickySidebar bool `json:"stickySidebar"`
DarkMode bool `json:"darkMode"` DarkMode bool `json:"darkMode"`
DisableSettings bool `json:"disableSettings"` DisableSettings bool `json:"disableSettings"`
ID uint `storm:"id,increment" json:"id"` 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"]) { 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}`, { await fetchURL(`/api/users/${user.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({

View File

@ -1,14 +1,18 @@
<template> <template>
<div class="button-group"> <div class="button-group">
<button <button v-if="isDisabled" disabled>
v-for="(btn, index) in buttons" No options for folders
:key="index"
:disabled="isDisabled"
:class="{ active: activeButton === index && !isDisabled }"
@click="setActiveButton(index, btn.label)"
>
{{ btn.label }}
</button> </button>
<template v-else>
<button
v-for="(btn, index) in buttons"
:key="index"
:class="{ active: activeButton === index }"
@click="setActiveButton(index, btn.label)"
>
{{ btn.label }}
</button>
</template>
</div> </div>
</template> </template>
@ -103,6 +107,10 @@ button:hover {
background: #e0e0e0; background: #e0e0e0;
} }
button:disabled {
cursor: not-allowed !important;
}
button.active { button.active {
background-color: var(--blue) !important; background-color: var(--blue) !important;
color: #ffffff; 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,39 +101,6 @@
<div class="searchContext">Search Context: {{ getContext }}</div> <div class="searchContext">Search Context: {{ getContext }}</div>
<div id="result-list"> <div id="result-list">
<div> <div>
<!-- Loading icon when search is ongoing -->
<p v-show="isEmpty && isRunning" id="renew">
<i class="material-icons spin">autorenew</i>
</p>
<!-- Message when no results are found -->
<div class="searchPrompt" v-show="isEmpty && !isRunning">
<p>{{ noneMessage }}</p>
<div class="helpButton" @click="toggleHelp()">Help</div>
</div>
<!-- Help text section -->
<div class="helpText" v-if="showHelp">
<p>
Search occurs on each character you type (3 character minimum for search
terms).
</p>
<p>
<b>The index:</b> Search utilizes the index which automatically gets updated
on the configured interval (default: 5 minutes). Searching when the program
has just started may result in incomplete results.
</p>
<p>
<b>Filter by type:</b> You can have multiple type filters by adding
<code>type:condition</code> followed by search terms.
</p>
<p>
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
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.
</p>
</div>
<div> <div>
<!-- Button groups for filtering search results --> <!-- Button groups for filtering search results -->
<ButtonGroup <ButtonGroup
@ -150,7 +117,7 @@
:isDisabled="isTypeSelectDisabled" :isDisabled="isTypeSelectDisabled"
/> />
<!-- Inputs for filtering by file size --> <!-- Inputs for filtering by file size -->
<div class="sizeConstraints"> <div v-if="!foldersOnly" class="sizeConstraints">
<div class="sizeInputWrapper"> <div class="sizeInputWrapper">
<p>Smaller Than:</p> <p>Smaller Than:</p>
<input <input
@ -175,6 +142,39 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Loading icon when search is ongoing -->
<p v-show="isEmpty && isRunning" id="renew">
<i class="material-icons spin">autorenew</i>
</p>
<!-- Message when no results are found -->
<div class="searchPrompt" v-show="isEmpty && !isRunning">
<p>{{ noneMessage }}</p>
<div class="helpButton" @click="toggleHelp()">Help</div>
</div>
<!-- Help text section -->
<div class="helpText" v-if="showHelp">
<p>
Search occurs on each character you type (3 character minimum for search
terms).
</p>
<p>
<b>The index:</b> Search utilizes the index which automatically gets updated
on the configured interval (default: 5 minutes). Searching when the program
has just started may result in incomplete results.
</p>
<p>
<b>Filter by type:</b> You can have multiple type filters by adding
<code>type:condition</code> followed by search terms.
</p>
<p>
<b>Multiple Search terms:</b> Additional terms separated by <code>|</code>,
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.
</p>
</div>
<!-- List of search results --> <!-- List of search results -->
<ul v-show="results.length > 0"> <ul v-show="results.length > 0">
<li <li
@ -249,6 +249,15 @@ export default {
}; };
}, },
watch: { watch: {
largerThan() {
this.submit();
},
smallerThan() {
this.submit();
},
searchTypes() {
this.submit();
},
active(active) { active(active) {
const resultList = document.getElementById("result-list"); const resultList = document.getElementById("result-list");
if (!active) { if (!active) {
@ -286,6 +295,9 @@ export default {
}, },
}, },
computed: { computed: {
foldersOnly() {
return this.isTypeSelectDisabled;
},
active() { active() {
return getters.currentPromptName() === "search"; return getters.currentPromptName() === "search";
}, },
@ -314,7 +326,7 @@ export default {
: this.$t("search.pressToSearch"); : this.$t("search.pressToSearch");
}, },
isMobile() { isMobile() {
return this.width <= 800; return getters.isMobile();
}, },
isRunning() { isRunning() {
return this.ongoing; return this.ongoing;
@ -363,6 +375,7 @@ export default {
mutations.showHover("search"); mutations.showHover("search");
}, },
close(event) { close(event) {
this.value = "";
event.stopPropagation(); event.stopPropagation();
mutations.closeHovers(); mutations.closeHovers();
}, },
@ -402,7 +415,9 @@ export default {
}, },
async submit(event) { async submit(event) {
this.showHelp = false; this.showHelp = false;
event.preventDefault(); if (event != undefined) {
event.preventDefault();
}
if (this.value === "" || this.value.length < 3) { if (this.value === "" || this.value.length < 3) {
this.ongoing = false; this.ongoing = false;
this.results = []; this.results = [];
@ -519,7 +534,7 @@ export default {
/* Search */ /* Search */
#search { #search {
background-color: unset !important; background-color: unset !important;
z-index: 3; z-index: 5;
position: fixed; position: fixed;
top: 0.5em; top: 0.5em;
min-width: 35em; min-width: 35em;
@ -639,6 +654,10 @@ body.rtl #search #result ul > * {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
input.sizeInput:disabled {
cursor: not-allowed;
}
/* Search Input Placeholder */ /* Search Input Placeholder */
#search::-webkit-input-placeholder { #search::-webkit-input-placeholder {
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);

View File

@ -1,18 +1,48 @@
<template> <template>
<nav :class="{ active, 'dark-mode': isDarkMode }"> <nav
<!-- Section for logged-in users --> id="sidebar"
<template v-if="isLoggedIn"> :class="{ active: active, 'dark-mode': isDarkMode, sticky: user?.stickySidebar }"
<!-- My Files button --> >
<div class="card">
<button <button
v-if="user.username"
@click="navigateTo('/settings/profile')"
class="action" class="action"
@click="toRoot"
:aria-label="$t('sidebar.myFiles')"
:title="$t('sidebar.myFiles')"
> >
<i class="material-icons">folder</i> <i class="material-icons">person</i>
<span>{{ $t("sidebar.myFiles") }}</span> <span>{{ user.username }}</span>
</button> </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 --> <!-- Buttons visible if user has create permission -->
<div v-if="user.perm?.create"> <div v-if="user.perm?.create">
<!-- New Folder button --> <!-- New Folder button -->
@ -36,7 +66,7 @@
<span>{{ $t("sidebar.newFile") }}</span> <span>{{ $t("sidebar.newFile") }}</span>
</button> </button>
<!-- Upload 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> <i class="material-icons">file_upload</i>
<span>Upload file</span> <span>Upload file</span>
</button> </button>
@ -47,7 +77,7 @@
<!-- Settings button --> <!-- Settings button -->
<button <button
class="action" class="action"
@click="toSettings" @click="navigateTo('/settings/global')"
:aria-label="$t('sidebar.settings')" :aria-label="$t('sidebar.settings')"
:title="$t('sidebar.settings')" :title="$t('sidebar.settings')"
> >
@ -67,10 +97,30 @@
<span>{{ $t("sidebar.logout") }}</span> <span>{{ $t("sidebar.logout") }}</span>
</button> </button>
</div> </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 --> <!-- Section for non-logged-in users -->
<template v-else> <div v-else>
<!-- Login button --> <!-- Login button -->
<router-link <router-link
class="action" class="action"
@ -92,14 +142,11 @@
<i class="material-icons">person_add</i> <i class="material-icons">person_add</i>
<span>{{ $t("sidebar.signup") }}</span> <span>{{ $t("sidebar.signup") }}</span>
</router-link> </router-link>
</template> </div>
<div class="buffer"></div>
<!-- Credits and usage information section --> <!-- Credits and usage information section -->
<div class="credits" v-if="isFiles && !disableUsedPercentage && usage"> <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-if="disableExternal">File Browser</span>
<span v-else> <span v-else>
<a <a
@ -119,7 +166,6 @@
</template> </template>
<script> <script>
import * as upload from "@/utils/upload";
import * as auth from "@/utils/auth"; import * as auth from "@/utils/auth";
import { import {
version, version,
@ -129,7 +175,7 @@ import {
noAuth, noAuth,
loginPage, loginPage,
} from "@/utils/constants"; } from "@/utils/constants";
import { files as api } from "@/api"; import { files, users } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue"; import ProgressBar from "@/components/ProgressBar.vue";
import { getHumanReadableFilesize } from "@/utils/filesizes"; import { getHumanReadableFilesize } from "@/utils/filesizes";
import { state, getters, mutations } from "@/store"; // Import your custom store import { state, getters, mutations } from "@/store"; // Import your custom store
@ -140,6 +186,11 @@ export default {
components: { components: {
ProgressBar, ProgressBar,
}, },
data() {
return {
hoverText: "Quick Toggles", // Initially empty
};
},
mounted() { mounted() {
this.updateUsage(); this.updateUsage();
}, },
@ -148,6 +199,9 @@ export default {
return getters.isFiles(); return getters.isFiles();
}, },
user() { user() {
if (!getters.isLoggedIn()) {
return {};
}
return state.user; return state.user;
}, },
isDarkMode() { isDarkMode() {
@ -160,7 +214,7 @@ export default {
return getters.currentPrompt(); return getters.currentPrompt();
}, },
active() { active() {
return getters.currentPromptName() === "sidebar"; return getters.isSidebarVisible() && getters.currentPromptName() == null;
}, },
signup: () => signup, signup: () => signup,
version: () => version, version: () => version,
@ -168,85 +222,221 @@ export default {
disableUsedPercentage: () => disableUsedPercentage, disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage, canLogout: () => !noAuth && loginPage,
usage: () => state.usage, usage: () => state.usage,
route: () => state.route,
},
watch: {
route() {
if (!getters.isLoggedIn()) {
return;
}
if (!state.user.stickySidebar) {
mutations.closeSidebar();
}
},
}, },
methods: { 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() { async updateUsage() {
console.log("updating usage"); if (!getters.isLoggedIn()) {
return;
}
let path = getters.getRoutePath(); let path = getters.getRoutePath();
let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 }; let usageStats = { used: "0 B", total: "0 B", usedPercentage: 0 };
if (this.disableUsedPercentage) { if (this.disableUsedPercentage) {
return usageStats; return usageStats;
} }
try { try {
let usage = await api.usage(path); let usage = await files.usage(path);
usageStats = { usageStats = {
used: getHumanReadableFilesize(usage.used / 1024), used: getHumanReadableFilesize(usage.used / 1024),
total: getHumanReadableFilesize(usage.total / 1024), total: getHumanReadableFilesize(usage.total / 1024),
usedPercentage: Math.round((usage.used / usage.total) * 100), usedPercentage: Math.round((usage.used / usage.total) * 100),
}; };
} catch (error) { } catch (error) {
showError("Error fetching usage:", error); showError("Error fetching usage", error);
} }
console.log(usageStats);
mutations.setUsage(usageStats); mutations.setUsage(usageStats);
}, },
showHover(value) { showHover(value) {
return mutations.showHover(value); return mutations.showHover(value);
}, },
// Navigate to the root files directory navigateTo(path) {
toRoot() { this.$router.push({ path: path }, () => {});
this.$router.push({ path: "/files/" }, () => {});
mutations.closeHovers();
},
// Navigate to the settings page
toSettings() {
this.$router.push({ path: "/settings" }, () => {});
mutations.closeHovers(); mutations.closeHovers();
}, },
// Show the help overlay // Show the help overlay
help() { help() {
mutations.showHover("help"); mutations.showHover("help");
}, },
// Handle file upload uploadFunc() {
upload(event) { mutations.showHover("upload");
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);
}, },
// Logout the user // Logout the user
logout: auth.logout, logout: auth.logout,
}, },
}; };
</script> </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> </script>
<style></style> <style>
</style>

View File

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

View File

@ -9,30 +9,134 @@
</div> </div>
<div class="card-action full"> <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> <i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div> <div class="title">{{ $t("buttons.file") }}</div>
</div> </div>
<div @click="uploadFolder" class="action"> <div
@click="uploadFolder"
@keypress.enter="uploadFolder"
class="action"
tabindex="2"
>
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div> <div class="title">{{ $t("buttons.folder") }}</div>
</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>
</div> </div>
</template> </template>
<script> <script>
import { ref } from "vue";
import * as upload from "@/utils/upload";
import { mutations, state, getters } from "@/store";
export default { export default {
name: "upload", name: "UploadFiles",
methods: { setup() {
uploadFile: function () { const fileInput = ref(null);
document.getElementById("upload-input").value = ""; const folderInput = ref(null);
document.getElementById("upload-input").click();
}, const triggerFilePicker = () => {
uploadFolder: function () { fileInput.value.click();
document.getElementById("upload-folder-input").value = ""; };
document.getElementById("upload-folder-input").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);
},
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> </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> <template>
<select v-on:change="change" :value="locale"> <select @change="change" :value="locale">
<option v-for="(value, label) in locales" :key="label" :value="label"> <option v-for="(value, label) in locales" :key="label" :value="label">
{{ $t("languages." + label) }} {{ $t("languages." + label) }}
</option> </option>
</select> </select>
</template> </template>
<script lang="ts"> <script>
import { defineComponent } from "vue"; import { defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
@ -47,8 +47,8 @@ export default defineComponent({
}; };
}, },
methods: { methods: {
change(event: Event) { change(event) {
const target = event.target as HTMLSelectElement; const target = event.target;
this.$emit("update:locale", target.value); this.$emit("update:locale", target.value);
}, },
}, },

View File

@ -1,4 +1,12 @@
/* Basic Styles */ /* Basic Styles */
:root {
--background: white;
--surfacePrimary: gray;
--surfaceSecondary: lightgray;
--textPrimary: white;
--textSecondary: gray;
}
body { body {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
padding-top: 4em; padding-top: 4em;
@ -66,57 +74,6 @@ over
padding-bottom: 4em; 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 Content */
main { 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 { button {
flex: 1; flex: 1;
height: 3em; height: 3em;
@ -211,31 +152,13 @@ button {
border: none; border: none;
background-color: #f5f5f5; background-color: #f5f5f5;
transition: background-color 0.3s; transition: background-color 0.3s;
/* Add borders */ /* Add borders */
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
} }
@media (min-width: 800px) { button:disabled {
#file-selection { opacity: 0.5;
bottom: 4em; cursor: not-allowed;
}
}
#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;
} }
#popup-notification { #popup-notification {

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@
@import "./header.css"; @import "./header.css";
@import "./listing.css"; @import "./listing.css";
@import "./listing-icons.css"; @import "./listing-icons.css";
@import "./upload-files.css";
@import "./dashboard.css"; @import "./dashboard.css";
@import "./login.css"; @import "./login.css";
@import './mobile.css'; @import './mobile.css';
@ -90,7 +89,7 @@ main .spinner .bounce2 {
border-radius: 50%; border-radius: 50%;
} }
.action:hover { .action:not(:disabled):hover {
background-color: rgba(0, 0, 0, .1); background-color: rgba(0, 0, 0, .1);
} }
@ -305,44 +304,6 @@ body.rtl .breadcrumbs .chevron {
font-size: 1rem; 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 * * 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"; import { state } from "./state.js";
export const getters = { export const getters = {
isMobile: () => window.innerWidth <= 800,
isDarkMode: () => { isDarkMode: () => {
if (state.user == null) { if (state.user == null) {
return true; return true;
} }
return state.user.darkMode === 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, isAdmin: () => state.user.perm?.admin == true,
isFiles: () => state.route.name === "Files", isFiles: () => state.route.name === "Files",
isListing: () => getters.isFiles() && state.req.isDir, isListing: () => getters.isFiles() && state.req.isDir,
@ -17,6 +20,20 @@ export const getters = {
let selectedItem = state.selected[0] let selectedItem = state.selected[0]
return state.req.items[selectedItem].url; 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: () => { getRoutePath: () => {
return state.route.path.endsWith("/") return state.route.path.endsWith("/")
? state.route.path ? state.route.path
@ -57,10 +74,8 @@ export const getters = {
}, },
filesInUploadCount: () => { filesInUploadCount: () => {
// Ensure state.upload.uploads is an object and state.upload.queue is an array const uploadsCount = state.upload.length
const uploadsCount = typeof state.upload.uploads === 'object' ? Object.keys(state.upload.uploads).length : 0; const queueCount = state.queue.length
const queueCount = Array.isArray(state.upload.queue) ? state.upload.queue.length : 0;
return uploadsCount + queueCount; return uploadsCount + queueCount;
}, },

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { state } from "@/store"; import { state } from "@/store";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api";
export function checkConflict(files, items) { export function checkConflict(files, items) {
if (typeof items === "undefined" || items === null) { if (typeof items === "undefined" || items === null) {
@ -101,10 +102,9 @@ export function scanFiles(dt) {
} }
export function handleFiles(files, base, overwrite = false) { export function handleFiles(files, base, overwrite = false) {
for (let i = 0; i < files.length; i++) { for (const file of files) {
let id = state.upload.id; const id = state.upload.id;
let path = base; let path = base;
let file = files[i];
if (file.fullPath !== undefined) { if (file.fullPath !== undefined) {
path += url.encodePath(file.fullPath); path += url.encodePath(file.fullPath);
@ -119,11 +119,19 @@ export function handleFiles(files, base, overwrite = false) {
const item = { const item = {
id, id,
path, path,
file, file: file.file, // Ensure `file.file` is the Blob or File
overwrite, 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>
</div> </div>
</template> </template>
<script> <script>
import { files as api } from "@/api"; import { files as api } from "@/api";
@ -49,7 +50,7 @@ export default {
return getters.currentView() !== null; return getters.currentView() !== null;
}, },
reload() { reload() {
return state.reload; // Access reload from state return state.reload;
}, },
}, },
created() { created() {
@ -58,8 +59,8 @@ export default {
watch: { watch: {
$route: "fetchData", $route: "fetchData",
reload(value) { reload(value) {
if (value === true) { if (value) {
console.log("reloading") console.log("reloading");
this.fetchData(); this.fetchData();
} }
}, },
@ -78,16 +79,16 @@ export default {
}, },
methods: { methods: {
async fetchData() { async fetchData() {
// Set loading to true and reset the error.
mutations.setLoading(true);
this.error = null;
// Reset view information using mutations // Reset view information using mutations
mutations.setReload(false); mutations.setReload(false);
mutations.resetSelected(); mutations.resetSelected();
mutations.setMultiple(false); mutations.setMultiple(false);
mutations.closeHovers(); mutations.closeHovers();
// Set loading to true and reset the error.
mutations.setLoading(true);
this.error = null;
let url = state.route.path; let url = state.route.path;
if (url === "") url = "/"; if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url; if (url[0] !== "/") url = "/" + url;
@ -106,9 +107,10 @@ export default {
} }
} catch (e) { } catch (e) {
this.error = e; this.error = e;
} finally {
mutations.setLoading(false);
mutations.replaceRequest(data);
} }
mutations.setLoading(false);
mutations.replaceRequest(data);
}, },
keyEvent(event) { keyEvent(event) {
// F1! // F1!

View File

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

View File

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

View File

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

View File

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

View File

@ -1,60 +1,5 @@
<template> <template>
<div style="padding-bottom: 5em"> <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"> <div v-if="loading">
<h2 class="message delayed"> <h2 class="message delayed">
<div class="spinner"> <div class="spinner">
@ -94,7 +39,7 @@
:class="listingViewMode + ' file-icons'" :class="listingViewMode + ' file-icons'"
> >
<div> <div>
<div class="header"> <div class="header" :class="{ 'dark-mode-item-header': isDarkMode }" >
<p <p
:class="{ active: nameSorted }" :class="{ active: nameSorted }"
class="name" class="name"
@ -209,13 +154,7 @@
</div> </div>
</template> </template>
<style>
.header-items {
width: 100% !important;
max-width: 100% !important;
justify-content: center;
}
</style>
<script> <script>
import { files as api } from "@/api"; import { files as api } from "@/api";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
@ -224,12 +163,10 @@ import throttle from "@/utils/throttle";
import { state, mutations, getters } from "@/store"; import { state, mutations, getters } from "@/store";
import { showError } from "@/notify"; import { showError } from "@/notify";
import Action from "@/components/header/Action.vue";
import Item from "@/components/files/ListingItem.vue"; import Item from "@/components/files/ListingItem.vue";
export default { export default {
name: "listingView", name: "listingView",
components: { components: {
Action,
Item, Item,
}, },
data() { data() {
@ -241,6 +178,9 @@ export default {
}; };
}, },
computed: { computed: {
isDarkMode() {
return state.user?.darkMode;
},
getMultiple() { getMultiple() {
return state.multiple; return state.multiple;
}, },
@ -321,18 +261,7 @@ export default {
listingViewMode() { listingViewMode() {
return state.user?.viewMode; 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() { selectedCount() {
return state.selected.length; return state.selected.length;
}, },
@ -637,38 +566,12 @@ export default {
openSearch() { openSearch() {
this.currentPrompt = "search"; this.currentPrompt = "search";
}, },
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.closeHovers();
},
windowsResize: throttle(function () { windowsResize: throttle(function () {
this.colunmsResize(); this.colunmsResize();
this.width = window.innerWidth; this.width = window.innerWidth;
// Listing element is not displayed // Listing element is not displayed
if (this.$refs.listingView == null) return; if (this.$refs.listingView == null) return;
}, 100), }, 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() { upload() {
if ( if (
typeof window.DataTransferItem !== "undefined" && typeof window.DataTransferItem !== "undefined" &&
@ -682,3 +585,15 @@ export default {
}, },
}; };
</script> </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: { watch: {
$route() { $route() {
if (!getters.isLoggedIn()) {
return;
}
this.updatePreview(); this.updatePreview();
this.toggleNavigation(); this.toggleNavigation();
}, },

View File

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

View File

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

View File

@ -1,22 +1,19 @@
# Planned Roadmap # 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 - Theme configuration from settings
- Replace afero requests with std library
- Add Job status to the sidebar - Add Job status to the sidebar
- index status. - index status.
- new jobs as they come - new jobs as they come via pocketbase
Future releases: 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 - Allow multiple volumes to show up in the same filebrowser container. https://github.com/filebrowser/filebrowser/issues/2514
- enable/disable indexing for certain mounts - enable/disable indexing for certain mounts
- Add tools to sidebar - Add tools to sidebar
- duplicate file detector. - duplicate file detector.
- bulk rename https://github.com/filebrowser/filebrowser/issues/2473 - bulk rename https://github.com/filebrowser/filebrowser/issues/2473
- job manager - folder sync, copy, lifecycle operations - job manager - folder sync, copy, lifecycle operations
- metrics tracker - user access, file access, download count, last login, etc - 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 minio s3 and backblaze sources https://github.com/filebrowser/filebrowser/issues/2544