mirror of
https://github.com/netinvent/npbackup.git
synced 2025-10-06 19:46:58 +08:00
WIP: Refactor UI
This commit is contained in:
parent
9ddd676af3
commit
1ef8887b8b
5 changed files with 189 additions and 148 deletions
|
@ -14,12 +14,13 @@ from typing import List
|
|||
import os
|
||||
from logging import getLogger
|
||||
import PySimpleGUI as sg
|
||||
import textwrap
|
||||
from ruamel.yaml.comments import CommentedMap
|
||||
import npbackup.configuration as configuration
|
||||
from ofunctions.misc import get_key_from_value
|
||||
from npbackup.core.i18n_helper import _t
|
||||
from npbackup.path_helper import CURRENT_EXECUTABLE
|
||||
from npbackup.customization import INHERITANCE_ICON
|
||||
from npbackup.customization import INHERITANCE_ICON, FILE_ICON, FOLDER_ICON
|
||||
|
||||
if os.name == "nt":
|
||||
from npbackup.windows.task import create_scheduled_task
|
||||
|
@ -27,6 +28,29 @@ if os.name == "nt":
|
|||
logger = getLogger()
|
||||
|
||||
|
||||
# Monkeypatching PySimpleGUI
|
||||
def delete(self, key):
|
||||
if key == '':
|
||||
return False
|
||||
try:
|
||||
node = self.tree_dict[key]
|
||||
key_list = [key, ]
|
||||
parent_node = self.tree_dict[node.parent]
|
||||
parent_node.children.remove(node)
|
||||
while key_list != []:
|
||||
temp = []
|
||||
for item in key_list:
|
||||
temp += self.tree_dict[item].children
|
||||
del self.tree_dict[item]
|
||||
key_list = temp
|
||||
return True
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
sg.TreeData.delete = delete
|
||||
|
||||
|
||||
def ask_manager_password(manager_password: str) -> bool:
|
||||
if manager_password:
|
||||
if sg.PopupGetText(
|
||||
|
@ -433,47 +457,14 @@ def config_gui(full_config: dict, config_file: str):
|
|||
Returns the GUI layout depending on the object type
|
||||
"""
|
||||
backup_col = [
|
||||
[
|
||||
sg.Text(_t("config_gui.compression"), size=(40, 1)),
|
||||
sg.pin(
|
||||
sg.Image(
|
||||
INHERITANCE_ICON,
|
||||
key="inherited.backup_opts.compression",
|
||||
tooltip=_t("config_gui.group_inherited"),
|
||||
)
|
||||
),
|
||||
sg.Combo(
|
||||
list(combo_boxes["compression"].values()),
|
||||
key="backup_opts.compression",
|
||||
size=(48, 1),
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Text(
|
||||
f"{_t('config_gui.backup_paths')}\n({_t('config_gui.one_per_line')})",
|
||||
size=(40, 2),
|
||||
textwrap.fill(f"{_t('config_gui.backup_paths')}"),
|
||||
size=(None, None), expand_x=True,
|
||||
),
|
||||
sg.pin(
|
||||
sg.Image(
|
||||
INHERITANCE_ICON,
|
||||
expand_x=True,
|
||||
expand_y=True,
|
||||
key="inherited.backup_opts.paths",
|
||||
tooltip=_t("config_gui.group_inherited"),
|
||||
)
|
||||
),
|
||||
sg.Multiline(key="backup_opts.paths", size=(48, 4)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.source_type"), size=(40, 1)),
|
||||
sg.pin(
|
||||
sg.Image(
|
||||
INHERITANCE_ICON,
|
||||
expand_x=True,
|
||||
expand_y=True,
|
||||
key="inherited.backup_opts.source_type",
|
||||
tooltip=_t("config_gui.group_inherited"),
|
||||
)
|
||||
sg.Text(
|
||||
textwrap.fill(f"{_t('config_gui.source_type')}"),
|
||||
size=(None, None), expand_x=True, justification='R'
|
||||
),
|
||||
sg.Combo(
|
||||
list(combo_boxes["source_type"].values()),
|
||||
|
@ -482,32 +473,67 @@ def config_gui(full_config: dict, config_file: str):
|
|||
),
|
||||
],
|
||||
[
|
||||
sg.Tree(sg.TreeData(), key="inherited.backup_opts.path", headings=[], expand_x=True, expand_y=True)
|
||||
],
|
||||
[
|
||||
sg.Input(visible=False, key="--PATHS-ADD-FILE--", enable_events=True),
|
||||
sg.FilesBrowse(_t("generic.add_files"), target="--PATHS-ADD-FILE--"),
|
||||
sg.Input(visible=False, key="--PATHS-ADD-FOLDER--", enable_events=True),
|
||||
sg.FolderBrowse(_t("generic.add_folder"), target="--PATHS-ADD-FOLDER--"),
|
||||
sg.Button(_t("generic.remove_selected"), key="--REMOVE-SELECTED-BACKUP-PATHS--")
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.compression"), size=(20, None)),
|
||||
sg.Combo(list(combo_boxes["compression"].values()), key="backup_opts.compression", size=(20, 1)),
|
||||
sg.pin(sg.Image(INHERITANCE_ICON, key="inherited.backup_opts.compression", tooltip=_t("config_gui.group_inherited"))),
|
||||
sg.Text(_t("config_gui.backup_priority"), size=(20, 1)),
|
||||
sg.Combo(
|
||||
list(combo_boxes["priority"].values()),
|
||||
key="backup_opts.priority",
|
||||
size=(20, 1),
|
||||
),
|
||||
sg.pin(sg.Image(INHERITANCE_ICON, key="inherited.backup_opts.backup_priority", tooltip=_t("config_gui.group_inherited")))
|
||||
],
|
||||
[
|
||||
sg.Checkbox("", key="backup_opts.use_fs_snapshot", size=(1, 1)),
|
||||
sg.Text(
|
||||
"{}\n({})".format(
|
||||
_t("config_gui.use_fs_snapshot"), _t("config_gui.windows_only")
|
||||
),
|
||||
size=(40, 2),
|
||||
textwrap.fill(f'{_t("config_gui.use_fs_snapshot")} ({_t("config_gui.windows_only")})', width=34),
|
||||
size=(34, 2),
|
||||
),
|
||||
sg.pin(
|
||||
sg.Image(
|
||||
INHERITANCE_ICON,
|
||||
expand_x=True,
|
||||
expand_y=True,
|
||||
key="inherited.backup_opts.use_fs_snapshot",
|
||||
tooltip=_t("config_gui.group_inherited"),
|
||||
)
|
||||
),
|
||||
sg.Checkbox("", key="backup_opts.use_fs_snapshot", size=(41, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.minimum_backup_size_error"), size=(40, 2)),
|
||||
sg.Input(key="backup_opts.minimum_backup_size_error", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(
|
||||
"{}\n({})".format(
|
||||
_t("config_gui.ignore_cloud_files"),
|
||||
_t("config_gui.windows_only"),
|
||||
),
|
||||
f"{_t('config_gui.tags')}\n({_t('config_gui.one_per_line')})",
|
||||
size=(40, 2),
|
||||
),
|
||||
sg.Checkbox("", key="backup_opts.ignore_cloud_files", size=(41, 1)),
|
||||
sg.Multiline(key="backup_opts.tags", size=(48, 4)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.additional_parameters"), size=(40, 1)),
|
||||
sg.Input(key="backup_opts.additional_parameters", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(
|
||||
_t("config_gui.additional_backup_only_parameters"), size=(40, 1)
|
||||
),
|
||||
sg.Input(
|
||||
key="backup_opts.additional_backup_only_parameters", size=(50, 1)
|
||||
),
|
||||
],
|
||||
|
||||
]
|
||||
|
||||
exclusions_col = [
|
||||
[
|
||||
sg.Checkbox("", key="backup_opts.ignore_cloud_files", size=(1, 1)),
|
||||
sg.Text(
|
||||
textwrap.fill(f'{_t("config_gui.ignore_cloud_files")} ({_t("config_gui.windows_only")})', width=34),
|
||||
size=(34, 2),
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Text(
|
||||
|
@ -548,11 +574,10 @@ def config_gui(full_config: dict, config_file: str):
|
|||
sg.Text(_t("config_gui.one_file_system"), size=(40, 1)),
|
||||
sg.Checkbox("", key="backup_opts.one_file_system", size=(41, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.minimum_backup_size_error"), size=(40, 2)),
|
||||
sg.Input(key="backup_opts.minimum_backup_size_error", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
]
|
||||
|
||||
pre_post_col = [
|
||||
[
|
||||
sg.Text(
|
||||
f"{_t('config_gui.pre_exec_commands')}\n({_t('config_gui.one_per_line')})",
|
||||
size=(40, 2),
|
||||
|
@ -594,33 +619,6 @@ def config_gui(full_config: dict, config_file: str):
|
|||
size=(41, 1),
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Text(
|
||||
f"{_t('config_gui.tags')}\n({_t('config_gui.one_per_line')})",
|
||||
size=(40, 2),
|
||||
),
|
||||
sg.Multiline(key="backup_opts.tags", size=(48, 4)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.backup_priority"), size=(40, 1)),
|
||||
sg.Combo(
|
||||
list(combo_boxes["priority"].values()),
|
||||
key="backup_opts.priority",
|
||||
size=(48, 1),
|
||||
),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.additional_parameters"), size=(40, 1)),
|
||||
sg.Input(key="backup_opts.additional_parameters", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(
|
||||
_t("config_gui.additional_backup_only_parameters"), size=(40, 1)
|
||||
),
|
||||
sg.Input(
|
||||
key="backup_opts.additional_backup_only_parameters", size=(50, 1)
|
||||
),
|
||||
],
|
||||
]
|
||||
|
||||
repo_col = [
|
||||
|
@ -628,6 +626,14 @@ def config_gui(full_config: dict, config_file: str):
|
|||
sg.Text(_t("config_gui.backup_repo_uri"), size=(40, 1)),
|
||||
sg.Input(key="repo_uri", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.backup_repo_password"), size=(40, 1)),
|
||||
sg.Input(key="repo_opts.repo_password", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.backup_repo_password_command"), size=(40, 1)),
|
||||
sg.Input(key="repo_opts.repo_password_command", size=(50, 1)),
|
||||
],
|
||||
[sg.Button(_t("config_gui.set_permissions"), key="--SET-PERMISSIONS--")],
|
||||
[
|
||||
sg.Text(_t("config_gui.repo_group"), size=(40, 1)),
|
||||
|
@ -642,14 +648,6 @@ def config_gui(full_config: dict, config_file: str):
|
|||
),
|
||||
sg.Input(key="repo_opts.minimum_backup_age", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.backup_repo_password"), size=(40, 1)),
|
||||
sg.Input(key="repo_opts.repo_password", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.backup_repo_password_command"), size=(40, 1)),
|
||||
sg.Input(key="repo_opts.repo_password_command", size=(50, 1)),
|
||||
],
|
||||
[
|
||||
sg.Text(_t("config_gui.upload_speed"), size=(40, 1)),
|
||||
sg.Input(key="repo_opts.upload_speed", size=(50, 1)),
|
||||
|
@ -774,37 +772,41 @@ def config_gui(full_config: dict, config_file: str):
|
|||
[
|
||||
sg.Tab(
|
||||
_t("config_gui.backup"),
|
||||
[
|
||||
[
|
||||
sg.Column(
|
||||
backup_col,
|
||||
scrollable=True,
|
||||
vertical_scroll_only=True,
|
||||
size=(700, 450),
|
||||
)
|
||||
]
|
||||
],
|
||||
backup_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-backup--",
|
||||
element_justification="L",
|
||||
#element_justification="L",
|
||||
expand_x=True,
|
||||
expand_y=True
|
||||
)
|
||||
],
|
||||
[
|
||||
sg.Tab(
|
||||
_t("config_gui.backup_destination"),
|
||||
[
|
||||
[
|
||||
sg.Column(
|
||||
repo_col,
|
||||
scrollable=True,
|
||||
vertical_scroll_only=True,
|
||||
size=(700, 450),
|
||||
)
|
||||
]
|
||||
],
|
||||
repo_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-repo--",
|
||||
element_justification="L",
|
||||
expand_x=True, expand_y=True,
|
||||
)
|
||||
],
|
||||
[
|
||||
sg.Tab(
|
||||
_t("config_gui.exclusions"),
|
||||
exclusions_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-exclusions--",
|
||||
expand_x=True,
|
||||
expand_y=True
|
||||
)
|
||||
],
|
||||
[
|
||||
sg.Tab(
|
||||
_t("config_gui.pre_post"),
|
||||
pre_post_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-hooks--",
|
||||
expand_x=True,
|
||||
expand_y=True
|
||||
)
|
||||
],
|
||||
[
|
||||
|
@ -813,7 +815,7 @@ def config_gui(full_config: dict, config_file: str):
|
|||
prometheus_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-prometheus--",
|
||||
element_justification="L",
|
||||
expand_x=True, expand_y=True,
|
||||
)
|
||||
],
|
||||
[
|
||||
|
@ -822,13 +824,15 @@ def config_gui(full_config: dict, config_file: str):
|
|||
env_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-env--",
|
||||
element_justification="L",
|
||||
expand_x=True, expand_y=True,
|
||||
)
|
||||
],
|
||||
]
|
||||
|
||||
_layout = [
|
||||
[sg.Column(object_selector, element_justification="L")],
|
||||
[sg.Column(object_selector,
|
||||
#element_justification="L"
|
||||
)],
|
||||
[
|
||||
sg.TabGroup(
|
||||
tab_group_layout, enable_events=True, key="--object-tabgroup--"
|
||||
|
@ -906,7 +910,7 @@ def config_gui(full_config: dict, config_file: str):
|
|||
identity_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-global-identification--",
|
||||
element_justification="L",
|
||||
#element_justification="L",
|
||||
)
|
||||
],
|
||||
[
|
||||
|
@ -915,7 +919,7 @@ def config_gui(full_config: dict, config_file: str):
|
|||
global_options_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-global-options--",
|
||||
element_justification="L",
|
||||
#element_justification="L",
|
||||
)
|
||||
],
|
||||
[
|
||||
|
@ -924,7 +928,7 @@ def config_gui(full_config: dict, config_file: str):
|
|||
scheduled_task_col,
|
||||
font="helvetica 16",
|
||||
key="--tab-global-scheduled_task--",
|
||||
element_justification="L",
|
||||
#element_justification="L",
|
||||
)
|
||||
],
|
||||
]
|
||||
|
@ -943,13 +947,13 @@ def config_gui(full_config: dict, config_file: str):
|
|||
[
|
||||
sg.Push(),
|
||||
sg.Button(
|
||||
_t("config_gui.create_object"), key="-OBJECT-CREATE-", size=(30, 1)
|
||||
_t("config_gui.create_object"), key="-OBJECT-CREATE-", size=(28, 1)
|
||||
),
|
||||
sg.Button(
|
||||
_t("config_gui.delete_object"), key="-OBJECT-DELETE-", size=(30, 1)
|
||||
_t("config_gui.delete_object"), key="-OBJECT-DELETE-", size=(28, 1)
|
||||
),
|
||||
sg.Button(_t("generic.cancel"), key="--CANCEL--", size=(15, 1)),
|
||||
sg.Button(_t("generic.accept"), key="--ACCEPT--", size=(15, 1)),
|
||||
sg.Button(_t("generic.cancel"), key="--CANCEL--", size=(13, 1)),
|
||||
sg.Button(_t("generic.accept"), key="--ACCEPT--", size=(13, 1)),
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -959,6 +963,8 @@ def config_gui(full_config: dict, config_file: str):
|
|||
_t("config_gui.repo_group_config"),
|
||||
object_layout(),
|
||||
key="--repo-group-config--",
|
||||
expand_x=True,
|
||||
expand_y=True
|
||||
)
|
||||
],
|
||||
[
|
||||
|
@ -966,6 +972,8 @@ def config_gui(full_config: dict, config_file: str):
|
|||
_t("config_gui.global_config"),
|
||||
global_options_layout(),
|
||||
key="--global-config--",
|
||||
expand_x=True,
|
||||
expand_y=True
|
||||
)
|
||||
],
|
||||
]
|
||||
|
@ -973,10 +981,11 @@ def config_gui(full_config: dict, config_file: str):
|
|||
_global_layout = [
|
||||
[
|
||||
sg.TabGroup(
|
||||
tab_group_layout, enable_events=True, key="--configtabgroup--"
|
||||
tab_group_layout, enable_events=True, key="--configtabgroup--", expand_x=True, expand_y=True,
|
||||
)
|
||||
],
|
||||
[sg.Push(), sg.Column(buttons, element_justification="L")],
|
||||
[sg.Push(), sg.Column(buttons,
|
||||
)],
|
||||
]
|
||||
return _global_layout
|
||||
|
||||
|
@ -984,8 +993,7 @@ def config_gui(full_config: dict, config_file: str):
|
|||
window = sg.Window(
|
||||
"Configuration",
|
||||
config_layout(),
|
||||
size=(800, 600),
|
||||
text_justification="C",
|
||||
#size=(800, 650),
|
||||
auto_size_text=True,
|
||||
auto_size_buttons=False,
|
||||
no_titlebar=False,
|
||||
|
@ -997,6 +1005,8 @@ def config_gui(full_config: dict, config_file: str):
|
|||
finalize=True,
|
||||
)
|
||||
|
||||
backup_paths_tree = sg.TreeData()
|
||||
|
||||
# Update gui with first default object (repo or group)
|
||||
update_object_gui(get_objects()[0], unencrypted=False)
|
||||
update_global_gui(full_config, unencrypted=False)
|
||||
|
@ -1028,6 +1038,20 @@ def config_gui(full_config: dict, config_file: str):
|
|||
if ask_manager_password(manager_password):
|
||||
full_config = set_permissions(full_config, values["-OBJECT-SELECT-"])
|
||||
continue
|
||||
if event in ("--PATHS-ADD-FILE--", '--PATHS-ADD-FOLDER--'):
|
||||
if event == "--PATHS-ADD-FILE--":
|
||||
node = values["--PATHS-ADD-FILE--"]
|
||||
icon = FILE_ICON
|
||||
elif event == '--PATHS-ADD-FOLDER--':
|
||||
node = values['--PATHS-ADD-FOLDER--']
|
||||
icon = FOLDER_ICON
|
||||
backup_paths_tree.insert('', node, node, node, icon=icon)
|
||||
window['inherited.backup_opts.path'].update(values=backup_paths_tree)
|
||||
if event == "--REMOVE-SELECTED-BACKUP-PATHS--":
|
||||
# TODO: prevent removing inherited values
|
||||
for key in values['inherited.backup_opts.path']:
|
||||
backup_paths_tree.delete(key)
|
||||
window['inherited.backup_opts.path'].update(values=backup_paths_tree)
|
||||
if event == "--ACCEPT--":
|
||||
if (
|
||||
not values["repo_opts.repo_password"]
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
en:
|
||||
encrypted_data: Encrypted Data
|
||||
# tabs
|
||||
backup: Backup
|
||||
backup_destination: Destination
|
||||
exclusions: Exclusions
|
||||
pre_post: Pre/Post exec
|
||||
|
||||
encrypted_data: Encrypted Data
|
||||
compression: Compression
|
||||
backup_paths: Backup paths
|
||||
use_fs_snapshot: Use VSS snapshots
|
||||
|
@ -24,7 +29,6 @@ en:
|
|||
additional_parameters: Additional parameters
|
||||
additional_backup_only_parameters: Additional backup only parmas
|
||||
|
||||
backup_destination: Backup destination
|
||||
minimum_backup_age: Minimum delay between two backups
|
||||
backup_repo_uri: backup repo URI / path
|
||||
backup_repo_password: Backup repo encryption password
|
||||
|
@ -98,10 +102,10 @@ en:
|
|||
|
||||
# source types
|
||||
source_type: Sources type
|
||||
folder_list: Folder list
|
||||
files_from: Files from list
|
||||
files_from_verbatim: Files from verbatim list
|
||||
files_from_raw: Files from raw list
|
||||
folder_list: Folder / file list
|
||||
files_from: From file
|
||||
files_from_verbatim: From verbatim
|
||||
files_from_raw: From raw
|
||||
|
||||
# retention policiy
|
||||
keep: Keep
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
fr:
|
||||
encrypted_data: Donnée Chiffrée
|
||||
# tabs
|
||||
backup: Sauvegarde
|
||||
backup_destination: Destination
|
||||
exclusions: Exclusions
|
||||
pre_post: Pré/Post exec
|
||||
|
||||
|
||||
encrypted_data: Donnée Chiffrée
|
||||
compression: Compression
|
||||
backup_paths: Chemins à sauvegarder
|
||||
use_fs_snapshot: Utiliser les instantanés VSS
|
||||
|
@ -24,7 +30,6 @@ fr:
|
|||
additional_parameters: Paramètres supplémentaires
|
||||
additional_backup_only_parameters: Paramètres supp. sauvegarde
|
||||
|
||||
backup_destination: Destination de sauvegarde
|
||||
minimum_backup_age: Délai minimal entre deux sauvegardes
|
||||
backup_repo_uri: URL / chemin local dépot de sauvegarde
|
||||
backup_repo_password: Mot de passe (chiffrement) dépot de sauvegarde
|
||||
|
@ -98,10 +103,10 @@ fr:
|
|||
|
||||
# source types
|
||||
source_type: Type de sources
|
||||
folder_list: Liste de dossiers
|
||||
files_from: Liste fichiers depuis un fichier
|
||||
files_from_verbatim: Liste fichiers depuis un fichier "exact"
|
||||
files_from_raw: Liste fichiers depuis un fichier "raw"
|
||||
folder_list: Liste de dossiers / fichiers
|
||||
files_from: Liste depuis un fichier
|
||||
files_from_verbatim: Liste depuis un fichier "exact"
|
||||
files_from_raw: Liste depuis un fichier "raw"
|
||||
|
||||
# retention policies
|
||||
retention_policy: Politique de conservation
|
||||
|
|
|
@ -62,4 +62,8 @@ en:
|
|||
please_wait: Please wait
|
||||
|
||||
bad_file: Bad file
|
||||
file_does_not_exist: File does not exist
|
||||
file_does_not_exist: File does not exist
|
||||
|
||||
add_files: Add files
|
||||
add_folder: Add folder
|
||||
remove_selected: Remove selected
|
|
@ -62,4 +62,8 @@ fr:
|
|||
please_wait: Merci de patienter
|
||||
|
||||
bad_file: Fichier erroné
|
||||
file_does_not_exist: Fichier inexistant
|
||||
file_does_not_exist: Fichier inexistant
|
||||
|
||||
add_files: Ajouter fichiers
|
||||
add_folder: Ajouter dossier
|
||||
remove_selected: Enlever la sélection
|
Loading…
Add table
Reference in a new issue