better explorer

This commit is contained in:
jr-k 2024-07-13 00:28:48 +02:00
parent 2855ba7133
commit 148a01079b
9 changed files with 219 additions and 62 deletions

File diff suppressed because one or more lines are too long

View File

@ -55,20 +55,47 @@ jQuery(function ($) {
initExplr(); initExplr();
}; };
$(document).on('click', '.explr-item-edit', function () { const selectEpxlrLink = function ($link) {
const $item = $('.explr-dirview .highlight-clicked'); $('a.explr-link').removeClass('highlight-clicked');
const is_folder = $item.attr('data-folder') === '1'; $('a.explr-link').parent().removeClass('highlight-clicked');
$('body').removeClass('explr-selection explr-selection-actionable explr-selection-entity explr-selection-folder');
if (is_folder) { if ($link.hasClass('explr-item-selectable')) {
$link.addClass('highlight-clicked');
$link.parent().addClass('highlight-clicked');
$('body').addClass('explr-selection');
if ($link.hasClass('explr-item-actionable')) {
$('body').addClass('explr-selection-actionable');
}
if ($link.hasClass('explr-item-entity')) {
$('body').addClass('explr-selection-entity');
}
if ($link.hasClass('explr-item-folder')) {
$('body').addClass('explr-selection-folder');
}
}
};
const getExplrSelection = function () {
return $('.explr-dirview .highlight-clicked');
};
const renameExplrItem = function($item) {
$('.dirview .renaming').removeClass('renaming');
$item.addClass('renaming'); $item.addClass('renaming');
$item.find('input').focus().select(); $item.find('input').focus().select();
} else {
document.location.href = $(this).attr('data-entity-route').replace('!c!', $item.attr('data-id'));
} }
$(document).on('click', '.explr-item-edit', function () {
document.location.href = $(this).attr('data-entity-route').replace('!c!', getExplrSelection().attr('data-id'));
});
$(document).on('click', '.explr-item-rename', function () {
renameExplrItem(getExplrSelection());
}); });
$(document).on('click', '.explr-item-delete', function () { $(document).on('click', '.explr-item-delete', function () {
const $item = $('.explr-dirview .highlight-clicked'); const $item = getExplrSelection();
const is_folder = $item.attr('data-folder') === '1'; const is_folder = $item.attr('data-folder') === '1';
let route; let route;
@ -84,10 +111,118 @@ jQuery(function ($) {
}); });
$(document).keyup(function (e) { $(document).keyup(function (e) {
const $selectedLink = $('.explr-item-selectable.highlight-clicked');
const $selectedLi = $selectedLink.parents('li:eq(0)');
if (e.key === "Escape") { if (e.key === "Escape") {
$('.dirview .new-folder').addClass('hidden'); $('.dirview .new-folder').addClass('hidden');
$('.dirview .renaming').removeClass('renaming');
} else if (e.code === "Space") {
renameExplrItem($selectedLi);
} else if ($selectedLink.length) {
const $prevLi = $selectedLi.prev('li:visible');
const $nextLi = $selectedLi.next('li:visible');
const verticalNeighbors = getAboveBelowElement($selectedLi);
if (e.key === "Enter") {
$selectedLink.trigger('dblclick');
} else if (e.key === "ArrowLeft" && $prevLi.length) {
selectEpxlrLink($prevLi.find('.explr-link'));
} else if (e.key === "ArrowRight" && $nextLi.length) {
selectEpxlrLink($nextLi.find('.explr-link'));
} else if (e.key === "ArrowUp" && verticalNeighbors.above) {
selectEpxlrLink(verticalNeighbors.above.find('.explr-link'));
} else if (e.key === "ArrowDown" && verticalNeighbors.below) {
selectEpxlrLink(verticalNeighbors.below.find('.explr-link'));
}
} }
}); });
// Explorer item selection
$(document).on('click', 'a.explr-link', function (event) {
event.preventDefault();
selectEpxlrLink($(this));
});
$(document).on('dblclick', 'a.explr-link', function (event) {
event.preventDefault();
$(this).off('click');
const href = $(this).attr('href');
if ($(this).attr('target') === '_blank') {
window.open(href);
} else {
window.location.href = href;
}
});
$(document).on('click', function (event) {
const $parentClickable = $(event.target).parents('a, button');
if ($parentClickable.length === 0) {
$('a.explr-link').removeClass('highlight-clicked');
$('a.explr-link').parent().removeClass('highlight-clicked');
$('body').removeClass('explr-selection explr-selection-entity explr-selection-folder explr-selection-actionable');
}
});
const getAboveBelowElement = function ($elem) {
const $liElements = $elem.parents('ul:eq(0)').find('li');
const positions = [];
// Get the Y positions of each element
$liElements.each(function () {
const $this = $(this);
positions.push({
element: $this,
y: $this.offset().top,
x: $this.offset().left
});
});
// Group elements by their Y position
const groupedByY = positions.reduce((acc, pos) => {
if (!acc[pos.y]) {
acc[pos.y] = [];
}
acc[pos.y].push(pos);
return acc;
}, {});
// Convert groupedByY to an array of arrays
const rows = Object.values(groupedByY);
let targetRowIndex = -1;
let targetColIndex = -1;
// Find the row and column index of the target element
rows.forEach((row, rowIndex) => {
row.forEach((pos, colIndex) => {
if (pos.element.is($elem)) {
targetRowIndex = rowIndex;
targetColIndex = colIndex;
}
});
});
const result = {
above: null,
below: null
};
if (targetRowIndex > 0) {
const aboveRow = rows[targetRowIndex - 1];
if (targetColIndex < aboveRow.length) {
result.above = aboveRow[targetColIndex].element;
}
}
if (targetRowIndex < rows.length - 1) {
const belowRow = rows[targetRowIndex + 1];
if (targetColIndex < belowRow.length) {
result.below = belowRow[targetColIndex].element;
}
}
return result;
}
main(); main();
}); });

View File

@ -112,40 +112,6 @@ jQuery(document).ready(function ($) {
$('#entity-utrack-updated-at').val(prettyTimestamp(entity.updated_at * 1000)); $('#entity-utrack-updated-at').val(prettyTimestamp(entity.updated_at * 1000));
}); });
// Explorer item selection
$(document).on('click', 'a.explr-link', function (event) {
event.preventDefault();
$('a.explr-link').removeClass('highlight-clicked');
$('a.explr-link').parent().removeClass('highlight-clicked');
$('body').removeClass('explr-selection');
if ($(this).hasClass('explr-item-selectable')) {
$(this).addClass('highlight-clicked');
$(this).parent().addClass('highlight-clicked');
$('body').addClass('explr-selection');
}
});
$(document).on('dblclick', 'a.explr-link', function (event) {
event.preventDefault();
$(this).off('click');
const href = $(this).attr('href');
if ($(this).attr('target') === '_blank') {
window.open(href);
} else {
window.location.href = href;
}
});
$(document).on('click', function (event) {
const $parentClickable = $(event.target).parents('a, button');
if ($parentClickable.length === 0) {
$('a.explr-link').removeClass('highlight-clicked');
$('a.explr-link').parent().removeClass('highlight-clicked');
$('body').removeClass('explr-selection');
}
});
setTimeout(function() { setTimeout(function() {
$('.alert-timeout').remove(); $('.alert-timeout').remove();
}, 3000); }, 3000);

View File

@ -45,12 +45,36 @@ ul.explr-tree {
margin-right: 10px; margin-right: 10px;
border-right: 1px solid #222; border-right: 1px solid #222;
padding-right: 20px; padding-right: 20px;
button {
display: none;
}
} }
.explr-selection .explr-selection-actions { body.explr-selection-actionable {
.explr-selection-actions {
display: flex; display: flex;
} }
&.explr-selection-folder {
.explr-selection-actions {
button.explr-selection-folder {
display: flex;
}
}
}
&.explr-selection-entity {
.explr-selection-actions {
button.explr-selection-entity {
display: flex;
}
}
}
}
ul.explr-dirview { ul.explr-dirview {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -21,6 +21,7 @@ class ContentController(ObController):
self._app.add_url_rule('/slideshow/content/edit/<content_id>', 'slideshow_content_edit', self._auth(self.slideshow_content_edit), methods=['GET']) self._app.add_url_rule('/slideshow/content/edit/<content_id>', 'slideshow_content_edit', self._auth(self.slideshow_content_edit), methods=['GET'])
self._app.add_url_rule('/slideshow/content/save/<content_id>', 'slideshow_content_save', self._auth(self.slideshow_content_save), methods=['POST']) self._app.add_url_rule('/slideshow/content/save/<content_id>', 'slideshow_content_save', self._auth(self.slideshow_content_save), methods=['POST'])
self._app.add_url_rule('/slideshow/content/delete', 'slideshow_content_delete', self._auth(self.slideshow_content_delete), methods=['GET']) self._app.add_url_rule('/slideshow/content/delete', 'slideshow_content_delete', self._auth(self.slideshow_content_delete), methods=['GET'])
self._app.add_url_rule('/slideshow/content/rename', 'slideshow_content_rename', self._auth(self.slideshow_content_rename), methods=['POST'])
self._app.add_url_rule('/slideshow/content/cd', 'slideshow_content_cd', self._auth(self.slideshow_content_cd), methods=['GET']) self._app.add_url_rule('/slideshow/content/cd', 'slideshow_content_cd', self._auth(self.slideshow_content_cd), methods=['GET'])
self._app.add_url_rule('/slideshow/content/add-folder', 'slideshow_content_folder_add', self._auth(self.slideshow_content_folder_add), methods=['POST']) self._app.add_url_rule('/slideshow/content/add-folder', 'slideshow_content_folder_add', self._auth(self.slideshow_content_folder_add), methods=['POST'])
self._app.add_url_rule('/slideshow/content/move-folder', 'slideshow_content_folder_move', self._auth(self.slideshow_content_folder_move), methods=['POST']) self._app.add_url_rule('/slideshow/content/move-folder', 'slideshow_content_folder_move', self._auth(self.slideshow_content_folder_move), methods=['POST'])
@ -105,6 +106,14 @@ class ContentController(ObController):
self._post_update() self._post_update()
return redirect(url_for('slideshow_content_list')) return redirect(url_for('slideshow_content_list'))
def slideshow_content_rename(self):
self._model_store.content().update_form(
id=request.form['id'],
name=request.form['name'],
)
return redirect(url_for('slideshow_content_list'))
def slideshow_content_cd(self): def slideshow_content_cd(self):
path = request.args.get('path') path = request.args.get('path')

View File

@ -25,6 +25,7 @@ class FleetNodePlayerController(ObController):
self._app.add_url_rule('/fleet/node-player/edit/<node_player_id>', 'fleet_node_player_edit', self._auth(self.fleet_node_player_edit), methods=['GET']) self._app.add_url_rule('/fleet/node-player/edit/<node_player_id>', 'fleet_node_player_edit', self._auth(self.fleet_node_player_edit), methods=['GET'])
self._app.add_url_rule('/fleet/node-player/save/<node_player_id>', 'fleet_node_player_save', self._auth(self.fleet_node_player_save), methods=['POST']) self._app.add_url_rule('/fleet/node-player/save/<node_player_id>', 'fleet_node_player_save', self._auth(self.fleet_node_player_save), methods=['POST'])
self._app.add_url_rule('/fleet/node-player/delete', 'fleet_node_player_delete', self.guard_fleet(self._auth(self.fleet_node_player_delete)), methods=['GET']) self._app.add_url_rule('/fleet/node-player/delete', 'fleet_node_player_delete', self.guard_fleet(self._auth(self.fleet_node_player_delete)), methods=['GET'])
self._app.add_url_rule('/fleet/node-player/rename', 'fleet_node_player_rename', self.guard_fleet(self._auth(self.fleet_node_player_rename)), methods=['POST'])
self._app.add_url_rule('/fleet/node-player/cd', 'fleet_node_player_cd', self._auth(self.fleet_node_player_cd), methods=['GET']) self._app.add_url_rule('/fleet/node-player/cd', 'fleet_node_player_cd', self._auth(self.fleet_node_player_cd), methods=['GET'])
self._app.add_url_rule('/fleet/node-player/add-folder', 'fleet_node_player_folder_add', self._auth(self.fleet_node_player_folder_add), methods=['POST']) self._app.add_url_rule('/fleet/node-player/add-folder', 'fleet_node_player_folder_add', self._auth(self.fleet_node_player_folder_add), methods=['POST'])
self._app.add_url_rule('/fleet/node-player/move-folder', 'fleet_node_player_folder_move', self._auth(self.fleet_node_player_folder_move), methods=['POST']) self._app.add_url_rule('/fleet/node-player/move-folder', 'fleet_node_player_folder_move', self._auth(self.fleet_node_player_folder_move), methods=['POST'])
@ -103,6 +104,14 @@ class FleetNodePlayerController(ObController):
self._post_update() self._post_update()
return redirect(url_for('fleet_node_player_list')) return redirect(url_for('fleet_node_player_list'))
def fleet_node_player_rename(self):
self._model_store.node_player().update_form(
id=request.form['id'],
name=request.form['name'],
)
return redirect(url_for('fleet_node_player_list'))
def fleet_node_player_cd(self): def fleet_node_player_cd(self):
path = request.args.get('path') path = request.args.get('path')

View File

@ -113,7 +113,7 @@ class NodePlayerManager(ModelManager):
def post_delete(self, node_player_id: str) -> str: def post_delete(self, node_player_id: str) -> str:
return node_player_id return node_player_id
def update_form(self, id: int, name: str, host: str, operating_system: Optional[OperatingSystem] = None, group_id: Optional[int] = None) -> NodePlayer: def update_form(self, id: int, name: str, host: Optional[str] = None, operating_system: Optional[OperatingSystem] = None, group_id: Optional[int] = None) -> NodePlayer:
node_player = self.get(id) node_player = self.get(id)
if not node_player: if not node_player:
@ -121,9 +121,9 @@ class NodePlayerManager(ModelManager):
form = { form = {
"name": name, "name": name,
"host": host, "host": host if host else node_player.host,
"operating_system": operating_system.value if operating_system else None, "operating_system": operating_system.value if operating_system else node_player.operating_system.value,
"group_id": group_id if group_id else None "group_id": group_id if group_id else node_player.group_id
} }
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form)) self._db.update_by_id(self.TABLE_NAME, id, self.pre_update(form))

View File

@ -30,10 +30,13 @@
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }} {{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
<div class="explr-selection-actions"> <div class="explr-selection-actions">
<button class="explr-item-edit btn-info" data-entity-route="{{ url_for('fleet_node_player_edit', node_player_id='!c!') }}"> <button class="explr-item-edit explr-selection-entity btn-info" data-entity-route="{{ url_for('fleet_node_player_edit', node_player_id='!c!') }}">
<i class="fa fa-eye"></i>
</button>
<button class="explr-item-rename explr-selection-entity explr-selection-folder btn-info">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>
<button class="explr-item-delete btn-danger-alt" data-folder-route="{{ url_for('fleet_node_player_folder_delete') }}" data-entity-route="{{ url_for('fleet_node_player_delete') }}"> <button class="explr-item-delete explr-selection-entity explr-selection-folder btn-danger-alt" data-folder-route="{{ url_for('fleet_node_player_folder_delete') }}" data-entity-route="{{ url_for('fleet_node_player_delete') }}">
<i class="fa fa-trash-alt"></i> <i class="fa fa-trash-alt"></i>
</button> </button>
</div> </div>
@ -148,7 +151,7 @@
{% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %} {% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %}
{% if parent_path %} {% if parent_path %}
<li class="previous-folder droppable" data-path="{{ parent_path }}" data-id="{{ working_folder.parent_id }}" data-folder="1"> <li class="previous-folder droppable" data-path="{{ parent_path }}" data-id="{{ working_folder.parent_id }}" data-folder="1">
<a href="{{ url_for('fleet_node_player_cd', path=parent_path) }}" class="explr-link"> <a href="{{ url_for('fleet_node_player_cd', path=parent_path) }}" class="explr-link explr-item-selectable explr-item-folder">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
.. ..
</a> </a>
@ -158,7 +161,7 @@
{% for folder in working_folder_children %} {% for folder in working_folder_children %}
{% set folder_path = working_folder_path ~ '/' ~ folder.name %} {% set folder_path = working_folder_path ~ '/' ~ folder.name %}
<li class="draggable droppable" data-path="{{ folder_path }}" data-id="{{ folder.id }}" data-folder="1"> <li class="draggable droppable" data-path="{{ folder_path }}" data-id="{{ folder.id }}" data-folder="1">
<a href="{{ url_for('fleet_node_player_cd', path=folder_path) }}" class="explr-link explr-item-selectable"> <a href="{{ url_for('fleet_node_player_cd', path=folder_path) }}" class="explr-link explr-item-selectable explr-item-actionable explr-item-folder">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
<span>{{ truncate(folder.name, 25, '...') }}</span> <span>{{ truncate(folder.name, 25, '...') }}</span>
<form action="{{ url_for('fleet_node_player_folder_rename') }}" method="POST"> <form action="{{ url_for('fleet_node_player_folder_rename') }}" method="POST">
@ -174,9 +177,13 @@
{% set icon = enum_operating_system.get_fa_icon(node_player.operating_system) %} {% set icon = enum_operating_system.get_fa_icon(node_player.operating_system) %}
{% set color = node_player.operating_system.value %} {% set color = node_player.operating_system.value %}
<li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ node_player.id }}" data-folder="0"> <li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ node_player.id }}" data-folder="0">
<a href="{{ url_for('fleet_node_player_edit', node_player_id=node_player.id) }}" target="_blank" class="explr-link explr-item-selectable"> <a href="{{ url_for('fleet_node_player_edit', node_player_id=node_player.id) }}" class="explr-link explr-item-selectable explr-item-actionable explr-item-entity">
<i class="fa {{ icon }} {{ color }}"></i> <i class="fa {{ icon }} {{ color }}"></i>
{{ truncate(node_player.name, 25, '...') }} <span>{{ truncate(node_player.name, 25, '...') }}</span>
<form action="{{ url_for('fleet_node_player_rename') }}" method="POST">
<input type="text" name="name" value="{{ node_player.name }}" autocomplete="off" />
<input type="hidden" name="id" value="{{ node_player.id }}" />
</form>
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -30,10 +30,13 @@
{{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }} {{ HOOK(H_SLIDESHOW_CONTENT_TOOLBAR_ACTIONS_START) }}
<div class="explr-selection-actions"> <div class="explr-selection-actions">
<button class="explr-item-edit btn-info" data-entity-route="{{ url_for('slideshow_content_edit', content_id='!c!') }}"> <button class="explr-item-edit explr-selection-entity btn-info" data-entity-route="{{ url_for('slideshow_content_edit', content_id='!c!') }}">
<i class="fa fa-eye"></i>
</button>
<button class="explr-item-rename explr-selection-entity explr-selection-folder btn-info">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>
<button class="explr-item-delete btn-danger-alt" data-folder-route="{{ url_for('slideshow_content_folder_delete') }}" data-entity-route="{{ url_for('slideshow_content_delete') }}"> <button class="explr-item-delete explr-selection-entity explr-selection-folder btn-danger-alt" data-folder-route="{{ url_for('slideshow_content_folder_delete') }}" data-entity-route="{{ url_for('slideshow_content_delete') }}">
<i class="fa fa-trash-alt"></i> <i class="fa fa-trash-alt"></i>
</button> </button>
</div> </div>
@ -148,7 +151,7 @@
{% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %} {% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %}
{% if parent_path %} {% if parent_path %}
<li class="previous-folder droppable" data-path="{{ parent_path }}" data-id="{{ working_folder.parent_id }}" data-folder="1"> <li class="previous-folder droppable" data-path="{{ parent_path }}" data-id="{{ working_folder.parent_id }}" data-folder="1">
<a href="{{ url_for('slideshow_content_cd', path=parent_path) }}" class="explr-link"> <a href="{{ url_for('slideshow_content_cd', path=parent_path) }}" class="explr-link explr-item-selectable explr-item-folder">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
.. ..
</a> </a>
@ -158,7 +161,7 @@
{% for folder in working_folder_children %} {% for folder in working_folder_children %}
{% set folder_path = working_folder_path ~ '/' ~ folder.name %} {% set folder_path = working_folder_path ~ '/' ~ folder.name %}
<li class="draggable droppable" data-path="{{ folder_path }}" data-id="{{ folder.id }}" data-folder="1"> <li class="draggable droppable" data-path="{{ folder_path }}" data-id="{{ folder.id }}" data-folder="1">
<a href="{{ url_for('slideshow_content_cd', path=folder_path) }}" class="explr-link explr-item-selectable"> <a href="{{ url_for('slideshow_content_cd', path=folder_path) }}" class="explr-link explr-item-selectable explr-item-actionable explr-item-folder">
<i class="fa fa-folder"></i> <i class="fa fa-folder"></i>
<span>{{ truncate(folder.name, 25, '...') }}</span> <span>{{ truncate(folder.name, 25, '...') }}</span>
<form action="{{ url_for('slideshow_content_folder_rename') }}" method="POST"> <form action="{{ url_for('slideshow_content_folder_rename') }}" method="POST">
@ -175,9 +178,13 @@
{% set color = enum_content_type.get_color_icon(content.type) %} {% set color = enum_content_type.get_color_icon(content.type) %}
<li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ content.id }}" data-folder="0"> <li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ content.id }}" data-folder="0">
<a href="{{ url_for('slideshow_content_edit', content_id=content.id) }}" class="explr-link explr-item-selectable"> <a href="{{ url_for('slideshow_content_edit', content_id=content.id) }}" class="explr-link explr-item-selectable explr-item-actionable explr-item-entity">
<i class="fa {{ icon }} {{ color }}"></i> <i class="fa {{ icon }} {{ color }}"></i>
{{ truncate(content.name, 25, '...') }} <span>{{ truncate(content.name, 25, '...') }}</span>
<form action="{{ url_for('slideshow_content_rename') }}" method="POST">
<input type="text" name="name" value="{{ content.name }}" autocomplete="off" />
<input type="hidden" name="id" value="{{ content.id }}" />
</form>
</a> </a>
</li> </li>
{% endfor %} {% endfor %}