v0.2.7 update
This commit is contained in:
parent
d8085c1f1b
commit
fa3ed6b948
|
@ -10,6 +10,7 @@ rice-box.go
|
|||
/frontend/package-lock.json
|
||||
/backend/vendor
|
||||
/backend/*.cov
|
||||
/backend/test_config.yaml
|
||||
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
|
|
@ -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.
|
||||
|
|
77
README.md
77
README.md
|
@ -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,16 +53,17 @@ 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">
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -23,4 +23,6 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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!
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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)));
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -155,6 +155,9 @@ export default {
|
|||
},
|
||||
watch: {
|
||||
$route() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
this.updatePreview();
|
||||
this.toggleNavigation();
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
5
makefile
5
makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue