explorer move & add ok

This commit is contained in:
jr-k 2024-07-10 14:21:34 +02:00
parent 3aff1b4c1f
commit 001ce36336
22 changed files with 19425 additions and 102 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -15,7 +15,7 @@ const hideDropdowns = function () {
};
jQuery(document).ready(function ($) {
$('.dropdown .trigger').on('click', function(event) {
$('.dropdown .trigger').on('click', function (event) {
event.stopPropagation();
var $dropdown = $(this).closest('.dropdown');
var $menu = $dropdown.find('ul.dropdown-menu');
@ -23,7 +23,7 @@ jQuery(document).ready(function ($) {
$('.dropdown').not($dropdown).removeClass('dropdown-show');
$dropdown.toggleClass('dropdown-show');
// Adjust dropdown position to prevent overflow
// Adjust dropdown position to prevent overflow
var triggerHeight = $(this).outerHeight() + 20;
var triggerOffset = $(this).offset();
var menuWidth = $menu.outerWidth();
@ -52,11 +52,11 @@ jQuery(document).ready(function ($) {
}
});
$(document).on('click', function() {
$(document).on('click', function () {
$('.dropdown').removeClass('dropdown-show');
});
$(window).on('resize', function() {
$(window).on('resize', function () {
$('.dropdown.dropdown-show .trigger').trigger('click');
});
@ -71,7 +71,8 @@ jQuery(document).ready(function ($) {
}
});
$(document).on('click', '.protected', function(e) {
// Link protection
$(document).on('click', '.protected', function (e) {
e.preventDefault();
e.stopPropagation();
@ -88,6 +89,7 @@ jQuery(document).ready(function ($) {
return false;
});
// Datetime and owner tracking
$(document).on('click', '.item-utrack', function () {
const entity = JSON.parse($(this).parents('tr:eq(0)').attr('data-entity'));
showModal('modal-entity-utrack');
@ -97,4 +99,32 @@ jQuery(document).ready(function ($) {
$('#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');
$(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');
if ($(this).attr('target') === '_blank') {
window.open($(this).attr('href'));
} else {
window.location.href = $(this).attr('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');
}
});
});

19070
data/www/js/lib/jquery-ui.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -29,18 +29,63 @@ jQuery(document).ready(function ($) {
;
};
const main = function () {
const a= $('.explr').explr({
const initExplr = function () {
$('.explr').explr({
classesPlus: 'fa fa-plus',
classesMinus: 'fa fa-minus',
onLoadFinish: function($tree) {
onLoadFinish: function ($tree) {
$tree.removeClass('hidden');
}
});
};
const initDrags = function () {
$(".draggable").draggable({
revert: "invalid",
});
$(".droppable").droppable({
accept: ".draggable",
over: function (event, ui) {
$(this).addClass("highlight-drop");
},
out: function (event, ui) {
$(this).removeClass("highlight-drop");
},
drop: function (event, ui) {
$(this).removeClass("highlight-drop");
const $form = $('#folder-move-form');
const $moved = ui.draggable;
const $target = $(this);
$form.find('[name=is_folder]').val($moved.attr('data-folder'))
$form.find('[name=entity_id]').val($moved.attr('data-id'))
$form.find('[name=new_folder_id]').val($target.attr('data-id'))
ui.draggable.position({
my: "center",
at: "center",
of: $(this),
using: function (pos) {
$(this).animate(pos, 50);
}
});
$form.submit();
}
});
};
const main = function () {
initExplr();
initDrags();
};
$(document).on('change', '#content-add-type', inputTypeUpdate);
$(document).on('click', '.folder-add', function () {
$('.dirview .new-folder').removeClass('hidden');
$('.page-content').animate({scrollTop: 0}, 0);
$('.dirview input').focus();
});
$(document).on('click', '.content-add', function () {
showModal('modal-content-add');
inputTypeUpdate();

View File

@ -1,36 +1,105 @@
.left-panel {
flex: 0.5;
overflow-y: auto;
padding: 0;
background: $layoutBackground;
box-shadow: 1px 1px .5px .5px inset rgba(0, 0, 0, 0.2);
border: 1px solid #222;
max-width: 250px;
ul.explr-tree {
height: 100% !important;
ul.explr-tree {
height: 100% !important;
li {
span {
color: #AAA;
font-size: 17px;
padding-left: 1px;
}
li {
span {
color: #AAA;
font-size: 17px;
padding-left: 1px;
a {
color: white;
padding-right: 80px;
&:hover {
color: white;
}
a {
color: white;
padding-right: 80px;
&.active {
background: rgba(255,255,255,.1);
border-radius: $baseRadius;
font-weight: bold;
text-decoration: underline;
margin-left: 35px;
padding-left: 5px;
margin-right: 10px;
}
&.active {
background: rgba(255, 255, 255, .1);
border-radius: $baseRadius;
font-weight: bold;
text-decoration: underline;
margin-left: 35px;
padding-left: 5px;
margin-right: 10px;
}
}
}
}
ul.explr-dirview {
display: flex;
flex-direction: row;
flex-wrap: wrap;
li {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
margin: 18px;
min-width: 90px;
min-height: 104px;
padding-top: 5px;
border: 1px solid transparent;
border-radius: $baseRadius;
&.new-folder {
a {
color: $seaBlue;
}
}
&.highlight-drop {
border: 1px dashed rgba($white, .2);
background: rgba($white, .1);
}
&.highlight-clicked {
border: 1px dashed rgba($seaBlue, .4);
background: rgba($seaBlue, .3);
}
a {
color: #BBB;
text-decoration: none;
flex: 1;
text-align: center;
font-size: 12px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
max-width: 84px;
i {
font-size: 64px;
margin-bottom: 12px;
border-radius: 8px;
}
input {
width: 100%;
&:focus {
outline: none;
}
}
&:hover {
opacity: 0.8;
}
}
}
}
.ui-draggable-dragging {
z-index: 20;
a {
opacity: 1 !important;
}
}

View File

@ -1,6 +1,6 @@
// Import utility styles
@import 'utils/variables';
@import 'utils/mixins';
@import 'utils/variables';
// Import base styles
@import 'base/fonts';

View File

@ -1,50 +1,25 @@
.view-content-list main .main-container {
.left-panel {
flex: 0.5;
overflow-y: auto;
padding: 0;
background: $layoutBackground;
box-shadow: 1px 1px .5px .5px inset rgba(0, 0, 0, 0.2);
border: 1px solid #222;
max-width: 250px;
}
.page-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
background: rgba(0,0,0,.9);
.dirview {
padding: 0 10px 40px 10px;
ul {
display: flex;
flex-direction: row;
flex-wrap: wrap;
li {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
margin: 18px;
a {
color: white;
text-decoration: none;
flex: 1;
text-align: center;
font-size: 12px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
i {
font-size: 64px;
margin-bottom: 12px;
}
&:hover {
color: #DDD;
}
}
}
}
}
}
}

View File

@ -28,4 +28,18 @@
@mixin pixel-box($pixelOffset: 1) {
box-shadow: #{$pixelOffset}px 0 0 #fff, 0 #{$pixelOffset}px 0 $limeGreen, -#{$pixelOffset}px 0 0 $seaBlue, 0 -#{$pixelOffset}px 0 $pinkyRed;
}
@mixin generate-color-classes($color-map) {
@each $name, $color in $color-map {
.#{"#{$name}"} {
color: $color;
}
.bg-#{"#{$name}"} {
background-color: $color;
}
.border-#{"#{$name}"} {
border-color: $color;
}
}
}

View File

@ -12,15 +12,27 @@ $neutralGrey: rgb(70, 70, 70);
$lightGrey: rgb(153, 153, 153);
$white: rgb(255, 255, 255);
$black: rgb(0, 0, 0);
$youtube: rgb(253, 60, 1);
// Type Colors
$info: $seaBlue;
$success: $limeGreen;
$danger: $pinkyRed;
$primary: $seaBlue;
// Common styles
$baseRadius: 4px;
$layoutBorder: 1px solid #222;
$layoutBackground: #111;
// Packs
$colors: (
info: $info,
success: $success,
danger: $danger,
purple: $sweetPurple,
youtube: $youtube,
);
// Classes
@include generate-color-classes($colors);

View File

@ -46,7 +46,7 @@
"js_slideshow_slide_delete_confirmation": "Are you sure?",
"slideshow_content_page_title": "Content Library",
"slideshow_content_button_add": "Add a content",
"slideshow_content_button_add": "New Content",
"slideshow_content_panel_active": "Content",
"slideshow_content_panel_empty": "Currently, there are no content. %link% now.",
"slideshow_content_panel_th_name": "Name",

View File

@ -46,7 +46,7 @@
"js_slideshow_slide_delete_confirmation": "¿Estás seguro?",
"slideshow_content_page_title": "Biblioteca de contenidos",
"slideshow_content_button_add": "Agregar un contenido",
"slideshow_content_button_add": "Nuevo Contenido",
"slideshow_content_panel_active": "Contenido",
"slideshow_content_panel_empty": "Actualmente, no hay contenido. %link% ahora.",
"slideshow_content_panel_th_name": "Nombre",

View File

@ -46,7 +46,7 @@
"js_slideshow_slide_delete_confirmation": "Êtes-vous sûr ?",
"slideshow_content_page_title": "Bibliothèque de contenus",
"slideshow_content_button_add": "Ajouter un contenu",
"slideshow_content_button_add": "Nouveau Contenu",
"slideshow_content_panel_active": "Contenus",
"slideshow_content_panel_empty": "Actuellement, il n'y a aucun contenu. %link% maintenant.",
"slideshow_content_panel_th_name": "Nom",

View File

@ -46,7 +46,7 @@
"js_slideshow_slide_delete_confirmation": "Sei sicuro?",
"slideshow_content_page_title": "Libreria dei contenuti",
"slideshow_content_button_add": "Aggiungi un contenuto",
"slideshow_content_button_add": "Nuovo Contenuto",
"slideshow_content_panel_active": "Contenuti",
"slideshow_content_panel_empty": "Attualmente non ci sono contenuti. %link% adesso.",
"slideshow_content_panel_th_name": "Nome",

View File

@ -6,6 +6,7 @@ from flask import Flask, render_template, redirect, request, url_for, send_from_
from werkzeug.utils import secure_filename
from src.service.ModelStore import ModelStore
from src.model.entity.Content import Content
from src.model.entity.Folder import Folder
from src.model.enum.ContentType import ContentType
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH
from src.interface.ObController import ObController
@ -17,6 +18,8 @@ class ContentController(ObController):
def register(self):
self._app.add_url_rule('/slideshow/content', 'slideshow_content_list', self._auth(self.slideshow_content_list), 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/move-folder', 'slideshow_content_folder_move', self._auth(self.slideshow_content_folder_move), methods=['POST'])
self._app.add_url_rule('/slideshow/content/add', 'slideshow_content_add', self._auth(self.slideshow_content_add), methods=['POST'])
self._app.add_url_rule('/slideshow/content/edit', 'slideshow_content_edit', self._auth(self.slideshow_content_edit), methods=['POST'])
self._app.add_url_rule('/slideshow/content/delete', 'slideshow_content_delete', self._auth(self.slideshow_content_delete), methods=['DELETE'])
@ -34,9 +37,27 @@ class ContentController(ObController):
working_folder_path=working_folder_path,
working_folder=working_folder,
working_folder_children=self._model_store.folder().get_children(working_folder),
enum_content_type=ContentType
enum_content_type=ContentType,
enum_folder_entity=FolderEntity,
)
def slideshow_content_folder_add(self):
self._model_store.folder().add_folder(
entity=FolderEntity.CONTENT,
name=request.form['name'],
)
return redirect(url_for('slideshow_content_list'))
def slideshow_content_folder_move(self):
self._model_store.folder().move_to_folder(
entity_id=request.form['entity_id'],
folder_id=request.form['new_folder_id'],
entity_is_folder=True if request.form['is_folder'] == '1' else False,
)
return redirect(url_for('slideshow_content_list'))
def slideshow_content_add(self):
self._model_store.content().add_form_raw(
name=request.form['name'],

View File

@ -3,6 +3,7 @@ from typing import Dict, Optional, List, Tuple, Union
from src.model.entity.Folder import Folder
from src.model.enum.FolderEntity import FolderEntity, FOLDER_ROOT_PATH, FOLDER_ROOT_NAME
from src.manager.DatabaseManager import DatabaseManager
from src.manager.ContentManager import ContentManager
from src.manager.LangManager import LangManager
from src.manager.UserManager import UserManager
from src.manager.VariableManager import VariableManager
@ -10,7 +11,6 @@ from src.service.ModelManager import ModelManager
class FolderManager(ModelManager):
TABLE_NAME = "folder"
TABLE_MODEL = [
"name CHAR(255)",
@ -23,7 +23,8 @@ class FolderManager(ModelManager):
"updated_at INTEGER"
]
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager, variable_manager: VariableManager):
def __init__(self, lang_manager: LangManager, database_manager: DatabaseManager, user_manager: UserManager,
variable_manager: VariableManager):
super().__init__(lang_manager, database_manager, user_manager, variable_manager)
self._db = database_manager.open(self.TABLE_NAME, self.TABLE_MODEL)
@ -54,9 +55,10 @@ class FolderManager(ModelManager):
def get_one_by_path(self, path: str, entity: FolderEntity) -> Folder:
parts = path[1:].split('/')
return self.get_one_by("name = '{}' and depth = {} and entity = '{}'".format(parts[-1], len(parts) - 1, entity.value))
return self.get_one_by(
"name = '{}' and depth = {} and entity = '{}'".format(parts[-1], len(parts) - 1, entity.value))
def hydrate_parents(self, folder: Optional[Folder]) -> Optional[Folder]:
def hydrate_parents(self, folder: Optional[Folder], deep=False) -> Optional[Folder]:
if not folder:
return None
@ -67,6 +69,12 @@ class FolderManager(ModelManager):
folder.set_previous(parent)
return self.hydrate_parents(parent)
def get_parent_folder(self, folder: Optional[Folder]) -> Optional[Folder]:
if not folder or not folder.parent_id:
return None
return self.get(folder.parent_id)
def get_one_by(self, query) -> Optional[Folder]:
object = self._db.get_one_by_query(self.TABLE_NAME, query=query)
@ -110,6 +118,50 @@ class FolderManager(ModelManager):
self._db.update_by_id(self.TABLE_NAME, id, self.pre_update({"name": name}))
self.post_update(id)
def move_to_folder(self, entity_id: int, folder_id: int, entity_is_folder=False) -> None:
folder = self.get(folder_id)
if not folder:
return
if entity_is_folder:
return self._db.execute_write_query(
query="UPDATE {} set parent_id = ?, depth = ? WHERE id = ?".format(self.TABLE_NAME),
params=(folder_id, folder.depth + 1, entity_id)
)
elif folder.entity == FolderEntity.CONTENT:
return self._db.execute_write_query(
query="UPDATE {} set folder_id = ? WHERE id = ?".format(ContentManager.TABLE_NAME),
params=(folder_id, entity_id)
)
def get_working_folder(self, entity: FolderEntity) -> str:
var_name = None
if entity == FolderEntity.CONTENT:
var_name = "last_folder_content"
if not var_name:
raise Error("No variable for entity {}".format(entity.value))
return self.variable_manager.get_one_by_name(var_name).as_string()
def add_folder(self, entity: FolderEntity, name: str) -> Folder:
working_folder_path = self.get_working_folder(entity)
working_folder = self.get_one_by_path(path=working_folder_path, entity=FolderEntity.CONTENT)
folder_path = "{}/{}".format(working_folder_path, name)
parts = folder_path[1:].split('/')
depth = len(parts) - 1
folder = Folder(
entity=entity,
name=name,
depth=depth,
parent_id=working_folder.id if working_folder else 1
)
self.add_form(folder)
return folder
def add_form(self, folder: Union[Folder, Dict]) -> None:
form = folder

View File

@ -96,7 +96,7 @@ class Folder:
self._depth = value
def __str__(self) -> str:
return f"NodePlayer(" \
return f"Folder(" \
f"id='{self.id}',\n" \
f"name='{self.name}',\n" \
f"parent_id='{self.parent_id}',\n" \

View File

@ -10,7 +10,7 @@ from src.model.hook.HookRegistration import HookRegistration
from src.model.hook.StaticHookRegistration import StaticHookRegistration
from src.model.hook.FunctionalHookRegistration import FunctionalHookRegistration
from src.constant.WebDirConstant import WebDirConstant
from src.util.utils import get_safe_cron_descriptor, is_valid_cron_date_time, seconds_to_hhmmss, am_i_in_docker
from src.util.utils import get_safe_cron_descriptor, is_valid_cron_date_time, seconds_to_hhmmss, am_i_in_docker, truncate
class TemplateRenderer:
@ -41,6 +41,7 @@ class TemplateRenderer:
seconds_to_hhmmss=seconds_to_hhmmss,
is_valid_cron_date_time=is_valid_cron_date_time,
json_dumps=json.dumps,
truncate=truncate,
l=self._model_store.lang().map(),
t=self._model_store.lang().translate,
)

View File

@ -240,3 +240,10 @@ def restart(debug=False) -> None:
except subprocess.CalledProcessError:
pass
def truncate(s, length, ellipsis=None):
if ellipsis and len(s) > length:
return s[:length].strip() + ellipsis
return s[:length]

View File

@ -13,6 +13,8 @@
{% block add_js %}
<script src="{{ STATIC_PREFIX }}js/lib/jquery-explr-1.4.js"></script>
<script src="{{ STATIC_PREFIX }}js/slideshow/contents.js"></script>
<script src="{{ STATIC_PREFIX }}js/lib/jquery-ui.min.js"></script>
{{ HOOK(H_SLIDESHOW_CONTENT_JAVASCRIPT) }}
{% endblock %}
@ -50,12 +52,12 @@
{% if loop.last %}
<span>
<i class="explr-icon explr-icon-folder"></i>
{{ dir }}
{{ truncate(dir, 25, '...') }}
</span>
{% else %}
<a href="{{ url_for('slideshow_content_cd', path=ns.breadpath) }}">
<i class="explr-icon explr-icon-folder"></i>
{{ dir }}
{{ truncate(dir, 25, '...') }}
</a>
{% endif %}
@ -112,16 +114,40 @@
</ul>
</div>
<form id="folder-move-form" action="{{ url_for('slideshow_content_folder_move') }}" class="hidden" method="POST">
<input type="text" name="entity_id" />
<input type="text" name="new_folder_id" />
<input type="text" name="is_folder" />
</form>
<div class="page-content">
<div class="dirview">
<ul>
{% for folder in working_folder_children %}
<li>
<a href="{{ url_for('slideshow_content_cd', path=working_folder_path~'/'~folder.name) }}">
<ul class="explr-dirview">
<li class="new-folder hidden">
<a href="javascript:void(0);">
<i class="fa fa-folder"></i>
<form action="{{ url_for('slideshow_content_folder_add') }}" method="POST">
<input type="text" name="name" />
</form>
</a>
</li>
{% set parent_path = '/'.join(working_folder_path.rstrip('/').split('/')[:-1]) %}
{% if parent_path %}
<li class="previous-folder droppable" data-path="{{ parent_path }}" data-id="{{ working_folder.parent_id }}">
<a href="{{ url_for('slideshow_content_cd', path=parent_path) }}" class="explr-link">
<i class="fa fa-folder"></i>
{{ folder.name }}
..
</a>
</li>
{% endif %}
{% for folder in working_folder_children %}
{% set folder_path = working_folder_path ~ '/' ~ folder.name %}
<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">
<i class="fa fa-folder"></i>
{{ truncate(folder.name, 25, '...') }}
</a>
</li>
{% endfor %}
@ -129,21 +155,22 @@
{% for content in contents[working_folder.id]|default([]) %}
{% set icon = 'fa-file' %}
{% if content.type.value == 'picture' %}
{% set icon = 'fa-image' %}
{% set icon = 'fa-regular fa-image info' %}
{% elif content.type.value == 'video' %}
{% set icon = 'fa-film' %}
{% set icon = 'fa-video-camera success' %}
{% elif content.type.value == 'url' %}
{% set icon = 'fa-globe' %}
{% set icon = 'fa-link danger' %}
{% elif content.type.value == 'youtube' %}
{% set icon = 'fa-video' %}
{% set icon = 'fa-brands fa-youtube youtube' %}
{% endif %}
<li>
<a href="{{ url_for('slideshow_content_show', content_id=content.id) }}" target="_blank">
<li class="draggable" data-path="{{ working_folder_path }}" data-id="{{ content.id }}" data-folder="0">
<a href="{{ url_for('slideshow_content_show', content_id=content.id) }}" target="_blank" class="explr-link">
<i class="fa {{ icon }}"></i>
{{ content.name }}
{{ truncate(content.name, 25, '...') }}
</a>
</li>
{% endfor %}
</ul>
</div>