forked from openlp/openlp
Add support for folders to plugins
- Make a derivative MediaManagerItem class for generic folder support - Make add and choose folder dialogs based on image plugin dialogs - Implement folder and item mixins to get db models "for free" - Implement database layer for media plugin - Implement database layer for presentations plugins - Refactor media plugin to inherit from FolderLibraryItem - Refactor presentations plugin to inherit from FolderLibraryItem - Migrate media files from settings to database - Migrate presentations files from settings to database - Convert the load icons in the media plugin into a dropdown - Add new tests for MediaMediaItem.get_list() - Closes #165 - Closes #223 - Closes #224 - Closes #582
This commit is contained in:
parent
b5a5cf82c4
commit
4d3ade20c7
@ -336,6 +336,8 @@ class UiStrings(metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
self.About = translate('OpenLP.Ui', 'About')
|
self.About = translate('OpenLP.Ui', 'About')
|
||||||
self.Add = translate('OpenLP.Ui', '&Add')
|
self.Add = translate('OpenLP.Ui', '&Add')
|
||||||
|
self.AddFolder = translate('OpenLP.Ui', 'Add folder')
|
||||||
|
self.AddFolderDot = translate('OpenLP.Ui', 'Add folder.')
|
||||||
self.AddGroup = translate('OpenLP.Ui', 'Add group')
|
self.AddGroup = translate('OpenLP.Ui', 'Add group')
|
||||||
self.AddGroupDot = translate('OpenLP.Ui', 'Add group.')
|
self.AddGroupDot = translate('OpenLP.Ui', 'Add group.')
|
||||||
self.Advanced = translate('OpenLP.Ui', 'Advanced')
|
self.Advanced = translate('OpenLP.Ui', 'Advanced')
|
||||||
|
@ -278,6 +278,11 @@ class Settings(QtCore.QSettings):
|
|||||||
'media/vlc arguments': '',
|
'media/vlc arguments': '',
|
||||||
'media/live volume': 50,
|
'media/live volume': 50,
|
||||||
'media/preview volume': 0,
|
'media/preview volume': 0,
|
||||||
|
'media/db type': 'sqlite',
|
||||||
|
'media/db username': '',
|
||||||
|
'media/db password': '',
|
||||||
|
'media/db hostname': '',
|
||||||
|
'media/db database': '',
|
||||||
'players/background color': '#000000',
|
'players/background color': '#000000',
|
||||||
'planningcenter/status': PluginStatus.Inactive,
|
'planningcenter/status': PluginStatus.Inactive,
|
||||||
'planningcenter/application_id': '',
|
'planningcenter/application_id': '',
|
||||||
@ -296,6 +301,11 @@ class Settings(QtCore.QSettings):
|
|||||||
'presentations/powerpoint control window': QtCore.Qt.Unchecked,
|
'presentations/powerpoint control window': QtCore.Qt.Unchecked,
|
||||||
'presentations/impress use display setting': QtCore.Qt.Unchecked,
|
'presentations/impress use display setting': QtCore.Qt.Unchecked,
|
||||||
'presentations/last directory': None,
|
'presentations/last directory': None,
|
||||||
|
'presentations/db type': 'sqlite',
|
||||||
|
'presentations/db username': '',
|
||||||
|
'presentations/db password': '',
|
||||||
|
'presentations/db hostname': '',
|
||||||
|
'presentations/db database': '',
|
||||||
'servicemanager/last directory': None,
|
'servicemanager/last directory': None,
|
||||||
'servicemanager/last file': None,
|
'servicemanager/last file': None,
|
||||||
'servicemanager/service theme': None,
|
'servicemanager/service theme': None,
|
||||||
|
@ -30,10 +30,11 @@ from urllib.parse import quote_plus as urlquote
|
|||||||
|
|
||||||
from alembic.migration import MigrationContext
|
from alembic.migration import MigrationContext
|
||||||
from alembic.operations import Operations
|
from alembic.operations import Operations
|
||||||
from sqlalchemy import Column, MetaData, Table, UnicodeText, create_engine, types
|
from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table, Unicode, UnicodeText, create_engine, types
|
||||||
from sqlalchemy.engine.url import make_url, URL
|
from sqlalchemy.engine.url import URL, make_url
|
||||||
from sqlalchemy.exc import DBAPIError, InvalidRequestError, OperationalError, ProgrammingError, SQLAlchemyError
|
from sqlalchemy.exc import DBAPIError, InvalidRequestError, OperationalError, ProgrammingError, SQLAlchemyError
|
||||||
from sqlalchemy.orm import mapper, scoped_session, sessionmaker
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
from sqlalchemy.orm import backref, mapper, relationship, scoped_session, sessionmaker
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
from openlp.core.common import delete_file
|
from openlp.core.common import delete_file
|
||||||
@ -43,7 +44,6 @@ from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
|
|||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -224,6 +224,49 @@ def get_upgrade_op(session):
|
|||||||
return Operations(context)
|
return Operations(context)
|
||||||
|
|
||||||
|
|
||||||
|
class CommonMixin(object):
|
||||||
|
"""
|
||||||
|
Base class to automate table name and ID column.
|
||||||
|
"""
|
||||||
|
@declared_attr
|
||||||
|
def __tablename__(self):
|
||||||
|
return self.__name__.lower()
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FolderMixin(CommonMixin):
|
||||||
|
"""
|
||||||
|
A mixin to provide most of the fields needed for folder support
|
||||||
|
"""
|
||||||
|
name = Column(Unicode(255), nullable=False, index=True)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def parent_id(self):
|
||||||
|
return Column(Integer(), ForeignKey('folder.id'))
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def folders(self):
|
||||||
|
return relationship('Folder', backref=backref('parent', remote_side='Folder.id'), order_by='Folder.name')
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def items(self):
|
||||||
|
return relationship('Item', backref='folder', order_by='Item.name')
|
||||||
|
|
||||||
|
|
||||||
|
class ItemMixin(CommonMixin):
|
||||||
|
"""
|
||||||
|
A mixin to provide most of the fields needed for folder support
|
||||||
|
"""
|
||||||
|
name = Column(Unicode(255), nullable=False, index=True)
|
||||||
|
file_path = Column(Unicode(255))
|
||||||
|
file_hash = Column(Unicode(255))
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def folder_id(self):
|
||||||
|
return Column(Integer(), ForeignKey('folder.id'))
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(object):
|
class BaseModel(object):
|
||||||
"""
|
"""
|
||||||
BaseModel provides a base object with a set of generic functions
|
BaseModel provides a base object with a set of generic functions
|
||||||
@ -494,16 +537,19 @@ class Manager(object):
|
|||||||
if try_count >= 2:
|
if try_count >= 2:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_object_filtered(self, object_class, filter_clause):
|
def get_object_filtered(self, object_class, *filter_clauses):
|
||||||
"""
|
"""
|
||||||
Returns an object matching specified criteria
|
Returns an object matching specified criteria
|
||||||
|
|
||||||
:param object_class: The type of object to return
|
:param object_class: The type of object to return
|
||||||
:param filter_clause: The criteria to select the object by
|
:param filter_clause: The criteria to select the object by
|
||||||
"""
|
"""
|
||||||
|
query = self.session.query(object_class)
|
||||||
|
for filter_clause in filter_clauses:
|
||||||
|
query = query.filter(filter_clause)
|
||||||
for try_count in range(3):
|
for try_count in range(3):
|
||||||
try:
|
try:
|
||||||
return self.session.query(object_class).filter(filter_clause).first()
|
return query.first()
|
||||||
except OperationalError as oe:
|
except OperationalError as oe:
|
||||||
# This exception clause is for users running MySQL which likes to terminate connections on its own
|
# This exception clause is for users running MySQL which likes to terminate connections on its own
|
||||||
# without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
|
# without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
|
||||||
|
@ -21,13 +21,12 @@
|
|||||||
"""
|
"""
|
||||||
Provides the generic functions for interfacing plugins with the Media Manager.
|
Provides the generic functions for interfacing plugins with the Media Manager.
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
from openlp.core.common.i18n import UiStrings, translate
|
from openlp.core.common.i18n import UiStrings, translate
|
||||||
from openlp.core.common.mixins import RegistryProperties
|
from openlp.core.common.mixins import LogMixin, RegistryProperties
|
||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
from openlp.core.lib import ServiceItemContext
|
from openlp.core.lib import ServiceItemContext
|
||||||
from openlp.core.lib.plugin import StringContent
|
from openlp.core.lib.plugin import StringContent
|
||||||
@ -39,10 +38,8 @@ from openlp.core.widgets.edits import SearchEdit
|
|||||||
from openlp.core.widgets.toolbar import OpenLPToolbar
|
from openlp.core.widgets.toolbar import OpenLPToolbar
|
||||||
from openlp.core.widgets.views import ListWidgetWithDnD
|
from openlp.core.widgets.views import ListWidgetWithDnD
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
class MediaManagerItem(QtWidgets.QWidget, RegistryProperties, LogMixin):
|
||||||
class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|
||||||
"""
|
"""
|
||||||
MediaManagerItem is a helper widget for plugins.
|
MediaManagerItem is a helper widget for plugins.
|
||||||
|
|
||||||
@ -75,7 +72,6 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
that is performed automatically by OpenLP when necessary. If this method is not defined, a default will be used
|
that is performed automatically by OpenLP when necessary. If this method is not defined, a default will be used
|
||||||
(treat the filename as an image).
|
(treat the filename as an image).
|
||||||
"""
|
"""
|
||||||
log.info('Media Item loaded')
|
|
||||||
|
|
||||||
def __init__(self, parent=None, plugin=None):
|
def __init__(self, parent=None, plugin=None):
|
||||||
"""
|
"""
|
||||||
@ -212,12 +208,16 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
self.add_actionlist_to_toolbar(toolbar_actions)
|
self.add_actionlist_to_toolbar(toolbar_actions)
|
||||||
|
|
||||||
def add_actionlist_to_toolbar(self, toolbar_actions):
|
def add_actionlist_to_toolbar(self, toolbar_actions):
|
||||||
for action in toolbar_actions:
|
added_actions = []
|
||||||
self.toolbar.add_toolbar_action('{name}{action}Action'.format(name=self.plugin.name, action=action[0]),
|
for action, plugin, icon, triggers in toolbar_actions:
|
||||||
text=self.plugin.get_string(action[1])['title'],
|
added_actions.append(self.toolbar.add_toolbar_action(
|
||||||
icon=action[2],
|
'{name}{action}Action'.format(name=self.plugin.name, action=action),
|
||||||
tooltip=self.plugin.get_string(action[1])['tooltip'],
|
text=self.plugin.get_string(plugin)['title'],
|
||||||
triggers=action[3])
|
icon=icon,
|
||||||
|
tooltip=self.plugin.get_string(plugin)['tooltip'],
|
||||||
|
triggers=triggers
|
||||||
|
))
|
||||||
|
return added_actions
|
||||||
|
|
||||||
def add_list_view_to_toolbar(self):
|
def add_list_view_to_toolbar(self):
|
||||||
"""
|
"""
|
||||||
@ -350,7 +350,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
self, self.on_new_prompt,
|
self, self.on_new_prompt,
|
||||||
self.settings.value(self.settings_section + '/last directory'),
|
self.settings.value(self.settings_section + '/last directory'),
|
||||||
self.on_new_file_masks)
|
self.on_new_file_masks)
|
||||||
log.info('New file(s) {file_paths}'.format(file_paths=file_paths))
|
self.log_info('New file(s) {file_paths}'.format(file_paths=file_paths))
|
||||||
if file_paths:
|
if file_paths:
|
||||||
self.application.set_busy_cursor()
|
self.application.set_busy_cursor()
|
||||||
self.validate_and_load(file_paths)
|
self.validate_and_load(file_paths)
|
||||||
@ -441,7 +441,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
file_paths = []
|
file_paths = []
|
||||||
for index in range(self.list_view.count()):
|
for index in range(self.list_view.count()):
|
||||||
list_item = self.list_view.item(index)
|
list_item = self.list_view.item(index)
|
||||||
file_path = list_item.data(QtCore.Qt.UserRole)
|
file_path = list_item.data(0, QtCore.Qt.UserRole)
|
||||||
file_paths.append(file_path)
|
file_paths.append(file_path)
|
||||||
return file_paths
|
return file_paths
|
||||||
|
|
||||||
@ -523,7 +523,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
translate('OpenLP.MediaManagerItem',
|
translate('OpenLP.MediaManagerItem',
|
||||||
'You must select one or more items to preview.'))
|
'You must select one or more items to preview.'))
|
||||||
else:
|
else:
|
||||||
log.debug('{plug} Preview requested'.format(plug=self.plugin.name))
|
self.log_debug('{plug} Preview requested'.format(plug=self.plugin.name))
|
||||||
Registry().set_flag('has doubleclick added item to service', False)
|
Registry().set_flag('has doubleclick added item to service', False)
|
||||||
service_item = self.build_service_item()
|
service_item = self.build_service_item()
|
||||||
if service_item:
|
if service_item:
|
||||||
@ -558,7 +558,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
:param item_id: item to make live
|
:param item_id: item to make live
|
||||||
:param remote: From Remote
|
:param remote: From Remote
|
||||||
"""
|
"""
|
||||||
log.debug('%s Live requested', self.plugin.name)
|
self.log_debug('{plugin} Live requested'.format(plugin=self.plugin.name))
|
||||||
item = None
|
item = None
|
||||||
if item_id:
|
if item_id:
|
||||||
item = self.create_item_from_id(item_id)
|
item = self.create_item_from_id(item_id)
|
||||||
@ -593,7 +593,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
# Is it possible to process multiple list items to generate
|
# Is it possible to process multiple list items to generate
|
||||||
# multiple service items?
|
# multiple service items?
|
||||||
if self.single_service_item:
|
if self.single_service_item:
|
||||||
log.debug('{plugin} Add requested'.format(plugin=self.plugin.name))
|
self.log_debug('{plugin} Add requested'.format(plugin=self.plugin.name))
|
||||||
self.add_to_service(replace=self.remote_triggered)
|
self.add_to_service(replace=self.remote_triggered)
|
||||||
else:
|
else:
|
||||||
items = self.list_view.selectedIndexes()
|
items = self.list_view.selectedIndexes()
|
||||||
@ -634,7 +634,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
|||||||
translate('OpenLP.MediaManagerItem',
|
translate('OpenLP.MediaManagerItem',
|
||||||
'You must select one or more items.'))
|
'You must select one or more items.'))
|
||||||
else:
|
else:
|
||||||
log.debug('{plugin} Add requested'.format(plugin=self.plugin.name))
|
self.log_debug('{plugin} Add requested'.format(plugin=self.plugin.name))
|
||||||
service_item = self.service_manager.get_service_item()
|
service_item = self.service_manager.get_service_item()
|
||||||
if not service_item:
|
if not service_item:
|
||||||
QtWidgets.QMessageBox.information(self, UiStrings().NISs,
|
QtWidgets.QMessageBox.information(self, UiStrings().NISs,
|
||||||
|
@ -36,10 +36,10 @@ The Projector table keeps track of entries for controlled projectors.
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
|
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
|
||||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from openlp.core.lib.db import Manager, init_db, init_url
|
from openlp.core.lib.db import CommonMixin, Manager, init_db, init_url
|
||||||
from openlp.core.projectors import upgrade
|
from openlp.core.projectors import upgrade
|
||||||
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
|
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
|
||||||
|
|
||||||
@ -51,17 +51,6 @@ log.debug('projector.lib.db module loaded')
|
|||||||
Base = declarative_base(MetaData())
|
Base = declarative_base(MetaData())
|
||||||
|
|
||||||
|
|
||||||
class CommonMixin(object):
|
|
||||||
"""
|
|
||||||
Base class to automate table name and ID column.
|
|
||||||
"""
|
|
||||||
@declared_attr
|
|
||||||
def __tablename__(self):
|
|
||||||
return self.__name__.lower()
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Manufacturer(Base, CommonMixin):
|
class Manufacturer(Base, CommonMixin):
|
||||||
"""
|
"""
|
||||||
Projector manufacturer table.
|
Projector manufacturer table.
|
||||||
|
302
openlp/core/ui/folders.py
Normal file
302
openlp/core/ui/folders.py
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2021 OpenLP Developers #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
|
from openlp.core.common.i18n import get_natural_key, translate
|
||||||
|
from openlp.core.lib.ui import create_button_box, critical_error_message_box
|
||||||
|
|
||||||
|
|
||||||
|
class FolderPopulateMixin(object):
|
||||||
|
"""
|
||||||
|
A mixin for common code between the two folder dialogs
|
||||||
|
"""
|
||||||
|
def populate_folders(self, parent_id=None, prefix=''):
|
||||||
|
"""
|
||||||
|
Recursively add folders to the combobox
|
||||||
|
|
||||||
|
:param folders: A dictionary object of the folders, in a tree.
|
||||||
|
:param parent_id: The ID of the parent folder.
|
||||||
|
:param prefix: A string containing the prefix that will be added in front of the folder name for each
|
||||||
|
level of the tree.
|
||||||
|
"""
|
||||||
|
if parent_id is None:
|
||||||
|
self.folder_combobox.clear()
|
||||||
|
# I'm not sure what this is here for, I'm just leaving it in for now.
|
||||||
|
self.folder_combobox.top_level_folder_added = False
|
||||||
|
folders = self.db_manager.get_all_objects(self.folder_class, self.folder_class.parent_id == parent_id)
|
||||||
|
folders.sort(key=lambda folder: get_natural_key(folder.name))
|
||||||
|
for folder in folders:
|
||||||
|
self.folder_combobox.addItem(prefix + folder.name, folder)
|
||||||
|
self.populate_folders(folder.id, prefix + ' ')
|
||||||
|
|
||||||
|
|
||||||
|
class AddFolderForm(QtWidgets.QDialog, FolderPopulateMixin):
|
||||||
|
"""
|
||||||
|
This class implements the 'Add folder' form for the plugins.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent=None, db_manager=None, folder_class=None):
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
"""
|
||||||
|
super().__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
|
||||||
|
QtCore.Qt.WindowCloseButtonHint)
|
||||||
|
self.setup_ui()
|
||||||
|
self.db_manager = db_manager
|
||||||
|
self.folder_class = folder_class
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.setObjectName('AddFolderDialog')
|
||||||
|
self.resize(300, 10)
|
||||||
|
self.dialog_layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
self.dialog_layout.setObjectName('dialog_layout')
|
||||||
|
self.name_layout = QtWidgets.QFormLayout()
|
||||||
|
self.name_layout.setObjectName('name_layout')
|
||||||
|
self.parent_folder_label = QtWidgets.QLabel(self)
|
||||||
|
self.parent_folder_label.setObjectName('parent_folder_label')
|
||||||
|
self.folder_combobox = QtWidgets.QComboBox(self)
|
||||||
|
self.folder_combobox.setObjectName('folder_combobox')
|
||||||
|
self.name_layout.addRow(self.parent_folder_label, self.folder_combobox)
|
||||||
|
self.name_label = QtWidgets.QLabel(self)
|
||||||
|
self.name_label.setObjectName('name_label')
|
||||||
|
self.name_edit = QtWidgets.QLineEdit(self)
|
||||||
|
self.name_edit.setObjectName('name_edit')
|
||||||
|
self.name_label.setBuddy(self.name_edit)
|
||||||
|
self.name_layout.addRow(self.name_label, self.name_edit)
|
||||||
|
self.dialog_layout.addLayout(self.name_layout)
|
||||||
|
self.button_box = create_button_box(self, 'button_box', ['cancel', 'save'])
|
||||||
|
self.dialog_layout.addWidget(self.button_box)
|
||||||
|
self.retranslate_ui()
|
||||||
|
self.setMaximumHeight(self.sizeHint().height())
|
||||||
|
|
||||||
|
def retranslate_ui(self):
|
||||||
|
self.setWindowTitle(translate('OpenLP.AddFolderForm', 'Add folder'))
|
||||||
|
self.parent_folder_label.setText(translate('OpenLP.AddFolderForm', 'Parent folder:'))
|
||||||
|
self.name_label.setText(translate('OpenLP.AddFolderForm', 'Folder name:'))
|
||||||
|
|
||||||
|
def exec(self, clear=True, show_top_level_folder=False, selected_folder=None):
|
||||||
|
"""
|
||||||
|
Show the form.
|
||||||
|
|
||||||
|
:param clear: Set to False if the text input box should not be cleared when showing the dialog (default: True).
|
||||||
|
:param show_top_level_folder: Set to True when "-- Top level folder --" should be showed as first item
|
||||||
|
(default: False).
|
||||||
|
:param selected_folder: The ID of the folder that should be selected by default when showing the dialog.
|
||||||
|
"""
|
||||||
|
self.populate_folders()
|
||||||
|
if clear:
|
||||||
|
self.name_edit.clear()
|
||||||
|
self.name_edit.setFocus()
|
||||||
|
if show_top_level_folder and not self.folder_combobox.top_level_folder_added:
|
||||||
|
self.folder_combobox.insertItem(0, translate('OpenLP.AddFolderForm', '-- Top-level folder --'), 0)
|
||||||
|
self.folder_combobox.top_level_folder_added = True
|
||||||
|
if selected_folder is not None:
|
||||||
|
for i in range(self.folder_combobox.count()):
|
||||||
|
if self.folder_combobox.itemData(i) == selected_folder:
|
||||||
|
self.folder_combobox.setCurrentIndex(i)
|
||||||
|
return QtWidgets.QDialog.exec(self)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""
|
||||||
|
Override the accept() method from QDialog to make sure something is entered in the text input box.
|
||||||
|
"""
|
||||||
|
if not self.name_edit.text():
|
||||||
|
critical_error_message_box(message=translate('OpenLP.AddFolderForm',
|
||||||
|
'You need to type in a folder name.'))
|
||||||
|
self.name_edit.setFocus()
|
||||||
|
return False
|
||||||
|
elif self.is_existing_folder():
|
||||||
|
critical_error_message_box(message=translate('OpenLP.AddFolderForm',
|
||||||
|
'This folder already exists, please use a different name.'))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return QtWidgets.QDialog.accept(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_id(self):
|
||||||
|
"""
|
||||||
|
A property to get the parent folder id
|
||||||
|
"""
|
||||||
|
if self.folder_combobox.currentIndex() == 0:
|
||||||
|
return None
|
||||||
|
return self.folder_combobox.itemData(self.folder_combobox.currentIndex(), QtCore.Qt.UserRole).id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def folder_name(self):
|
||||||
|
"""
|
||||||
|
A property to return the folder name
|
||||||
|
"""
|
||||||
|
return self.name_edit.text()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def new_folder(self):
|
||||||
|
"""
|
||||||
|
A Folder object property
|
||||||
|
"""
|
||||||
|
return self.folder_class(parent_id=self.parent_id, name=self.folder_name)
|
||||||
|
|
||||||
|
def is_existing_folder(self):
|
||||||
|
"""
|
||||||
|
Check if this folder already exists
|
||||||
|
"""
|
||||||
|
return self.db_manager.get_object_filtered(self.folder_class,
|
||||||
|
self.folder_class.parent_id == self.parent_id,
|
||||||
|
self.folder_class.name == self.folder_name) is not None
|
||||||
|
|
||||||
|
|
||||||
|
class ChooseFolderForm(QtWidgets.QDialog, FolderPopulateMixin):
|
||||||
|
"""
|
||||||
|
This class implements the 'Choose folder' form for the plugins.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent=None, db_manager=None, folder_class=None):
|
||||||
|
"""
|
||||||
|
Constructor
|
||||||
|
"""
|
||||||
|
super().__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
|
||||||
|
QtCore.Qt.WindowCloseButtonHint)
|
||||||
|
self.setup_ui()
|
||||||
|
self.db_manager = db_manager
|
||||||
|
self.folder_class = folder_class
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""
|
||||||
|
Set up the UI.
|
||||||
|
"""
|
||||||
|
self.setObjectName('ChooseFolderForm')
|
||||||
|
self.resize(399, 119)
|
||||||
|
self.choose_folder_layout = QtWidgets.QFormLayout(self)
|
||||||
|
self.choose_folder_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
|
||||||
|
self.choose_folder_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
self.choose_folder_layout.setSpacing(8)
|
||||||
|
self.choose_folder_layout.setLabelAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
|
||||||
|
self.choose_folder_layout.setObjectName('choose_folder_layout')
|
||||||
|
self.folder_question_label = QtWidgets.QLabel(self)
|
||||||
|
self.folder_question_label.setWordWrap(True)
|
||||||
|
self.folder_question_label.setObjectName('folder_question_label')
|
||||||
|
self.choose_folder_layout.setWidget(1, QtWidgets.QFormLayout.SpanningRole, self.folder_question_label)
|
||||||
|
self.nofolder_radio_button = QtWidgets.QRadioButton(self)
|
||||||
|
self.nofolder_radio_button.setChecked(True)
|
||||||
|
self.nofolder_radio_button.setObjectName('nofolder_radio_button')
|
||||||
|
self.choose_folder_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.nofolder_radio_button)
|
||||||
|
self.existing_radio_button = QtWidgets.QRadioButton(self)
|
||||||
|
self.existing_radio_button.setChecked(False)
|
||||||
|
self.existing_radio_button.setObjectName('existing_radio_button')
|
||||||
|
self.choose_folder_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.existing_radio_button)
|
||||||
|
self.folder_combobox = QtWidgets.QComboBox(self)
|
||||||
|
self.folder_combobox.setObjectName('folder_combobox')
|
||||||
|
self.folder_combobox.activated.connect(self.on_folder_combobox_selected)
|
||||||
|
self.choose_folder_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.folder_combobox)
|
||||||
|
self.new_radio_button = QtWidgets.QRadioButton(self)
|
||||||
|
self.new_radio_button.setChecked(False)
|
||||||
|
self.new_radio_button.setObjectName('new_radio_button')
|
||||||
|
self.choose_folder_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.new_radio_button)
|
||||||
|
self.new_folder_edit = QtWidgets.QLineEdit(self)
|
||||||
|
self.new_folder_edit.setObjectName('new_folder_edit')
|
||||||
|
self.new_folder_edit.textEdited.connect(self.on_new_folder_edit_changed)
|
||||||
|
self.choose_folder_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.new_folder_edit)
|
||||||
|
self.folder_button_box = create_button_box(self, 'buttonBox', ['ok'])
|
||||||
|
self.choose_folder_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.folder_button_box)
|
||||||
|
|
||||||
|
self.retranslate_ui()
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(self)
|
||||||
|
|
||||||
|
def retranslate_ui(self):
|
||||||
|
"""
|
||||||
|
Translate the UI on the fly.
|
||||||
|
|
||||||
|
:param self: The form object (not the class).
|
||||||
|
"""
|
||||||
|
self.setWindowTitle(translate('OpenLP.ChooseFolderForm', 'Select Folder'))
|
||||||
|
self.folder_question_label.setText(translate('OpenLP.ChooseFolderForm', 'Add items to folder:'))
|
||||||
|
self.nofolder_radio_button.setText(translate('OpenLP.ChooseFolderForm', 'No folder'))
|
||||||
|
self.existing_radio_button.setText(translate('OpenLP.ChooseFolderForm', 'Existing folder'))
|
||||||
|
self.new_radio_button.setText(translate('OpenLP.ChooseFolderForm', 'New folder'))
|
||||||
|
|
||||||
|
def exec(self, selected_folder=None):
|
||||||
|
"""
|
||||||
|
Show the form
|
||||||
|
|
||||||
|
:param selected_folder: The ID of the folder that should be selected by default when showing the dialog.
|
||||||
|
"""
|
||||||
|
is_disabled = self.db_manager.get_object_count(self.folder_class) == 0
|
||||||
|
self.existing_radio_button.setDisabled(is_disabled)
|
||||||
|
self.folder_combobox.setDisabled(is_disabled)
|
||||||
|
self.new_folder_edit.clear()
|
||||||
|
self.populate_folders()
|
||||||
|
if selected_folder is not None:
|
||||||
|
for index in range(self.folder_combobox.count()):
|
||||||
|
if self.folder_combobox.itemData(index) == selected_folder:
|
||||||
|
self.folder_combobox.setCurrentIndex(index)
|
||||||
|
self.existing_radio_button.setChecked(True)
|
||||||
|
return QtWidgets.QDialog.exec(self)
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
"""
|
||||||
|
Override the accept() method from QDialog to make sure something is entered in the text input box.
|
||||||
|
"""
|
||||||
|
if self.new_radio_button.isChecked() and not self.new_folder_edit.text():
|
||||||
|
critical_error_message_box(message=translate('OpenLP.ChooseFolderForm',
|
||||||
|
'You need to type in a folder name.'))
|
||||||
|
self.new_folder_edit.setFocus()
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return QtWidgets.QDialog.accept(self)
|
||||||
|
|
||||||
|
def on_folder_combobox_selected(self, index):
|
||||||
|
"""
|
||||||
|
Handles the activated signal from the existing folder combobox when the
|
||||||
|
user makes a selection
|
||||||
|
|
||||||
|
:param index: position of the selected item in the combobox
|
||||||
|
"""
|
||||||
|
self.existing_radio_button.setChecked(True)
|
||||||
|
self.folder_combobox.setFocus()
|
||||||
|
|
||||||
|
def on_new_folder_edit_changed(self, new_folder):
|
||||||
|
"""
|
||||||
|
Handles the textEdited signal from the new folder text input field
|
||||||
|
when the user enters a new folder name
|
||||||
|
|
||||||
|
:param new_folder: new text entered by the user
|
||||||
|
"""
|
||||||
|
self.new_radio_button.setChecked(True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def folder_name(self):
|
||||||
|
"""
|
||||||
|
A property to return the folder name
|
||||||
|
"""
|
||||||
|
return self.new_folder_edit.text()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def folder(self):
|
||||||
|
if self.existing_radio_button.isChecked() and self.folder_combobox.currentIndex() != -1:
|
||||||
|
return self.folder_combobox.currentData(QtCore.Qt.UserRole)
|
||||||
|
elif self.new_radio_button.isChecked() and self.new_folder_edit.text():
|
||||||
|
return self.folder_class(name=self.new_folder_edit.text())
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_new_folder(self):
|
||||||
|
"""
|
||||||
|
A property to indicate if the folder from the ``folder`` property is a new folder or an existing one
|
||||||
|
"""
|
||||||
|
return self.new_radio_button.isChecked()
|
@ -90,6 +90,7 @@ class UiIcons(metaclass=Singleton):
|
|||||||
'error': {'icon': 'fa.exclamation', 'attr': 'red'},
|
'error': {'icon': 'fa.exclamation', 'attr': 'red'},
|
||||||
'exception': {'icon': 'fa.times-circle'},
|
'exception': {'icon': 'fa.times-circle'},
|
||||||
'exit': {'icon': 'fa.sign-out'},
|
'exit': {'icon': 'fa.sign-out'},
|
||||||
|
'folder': {'icon': 'fa.folder'},
|
||||||
'group': {'icon': 'fa.object-group'},
|
'group': {'icon': 'fa.object-group'},
|
||||||
'inactive': {'icon': 'fa.child', 'attr': 'lightGray'},
|
'inactive': {'icon': 'fa.child', 'attr': 'lightGray'},
|
||||||
'info': {'icon': 'fa.info'},
|
'info': {'icon': 'fa.info'},
|
||||||
|
387
openlp/core/ui/library.py
Normal file
387
openlp/core/ui/library.py
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2021 OpenLP Developers #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
##########################################################################
|
||||||
|
"""
|
||||||
|
Provides additional classes for working in the library
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
|
from openlp.core.common import sha256_file_hash
|
||||||
|
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
||||||
|
from openlp.core.lib import check_item_selected
|
||||||
|
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
||||||
|
from openlp.core.lib.ui import critical_error_message_box
|
||||||
|
from openlp.core.ui.folders import AddFolderForm, ChooseFolderForm
|
||||||
|
from openlp.core.ui.icons import UiIcons
|
||||||
|
from openlp.core.widgets.views import TreeWidgetWithDnD
|
||||||
|
|
||||||
|
|
||||||
|
class FolderLibraryItem(MediaManagerItem):
|
||||||
|
"""
|
||||||
|
This is a custom MediaManagerItem subclass with support for folders
|
||||||
|
"""
|
||||||
|
def __init__(self, parent, plugin, folder_class, item_class):
|
||||||
|
super(FolderLibraryItem, self).__init__(parent, plugin)
|
||||||
|
self.manager = self.plugin.manager
|
||||||
|
self.choose_folder_form = ChooseFolderForm(self, self.manager, folder_class)
|
||||||
|
self.add_folder_form = AddFolderForm(self, self.manager, folder_class)
|
||||||
|
self.folder_class = folder_class
|
||||||
|
self.item_class = item_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_folder(self):
|
||||||
|
"""
|
||||||
|
Returns the currently active folder, or None
|
||||||
|
"""
|
||||||
|
selected_items = self.list_view.selectedItems()
|
||||||
|
selected_folder = None
|
||||||
|
if selected_items:
|
||||||
|
selected_item = selected_items[0]
|
||||||
|
if isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.item_class):
|
||||||
|
selected_item = selected_item.parent()
|
||||||
|
if isinstance(selected_item, QtWidgets.QTreeWidgetItem) and \
|
||||||
|
isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.folder_class):
|
||||||
|
selected_folder = selected_item.data(0, QtCore.Qt.UserRole)
|
||||||
|
return selected_folder
|
||||||
|
|
||||||
|
def retranslate_ui(self):
|
||||||
|
"""
|
||||||
|
This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
|
||||||
|
to another language.
|
||||||
|
"""
|
||||||
|
self.add_folder_action.setText(UiStrings().AddFolder)
|
||||||
|
self.add_folder_action.setToolTip(UiStrings().AddFolderDot)
|
||||||
|
|
||||||
|
def on_add_folder_click(self):
|
||||||
|
"""
|
||||||
|
Called to add a new folder
|
||||||
|
"""
|
||||||
|
# Syntactic sugar, plus a minor performance optimisation
|
||||||
|
Item = self.item_class
|
||||||
|
|
||||||
|
if self.add_folder_form.exec(show_top_level_folder=True, selected_folder=self.current_folder):
|
||||||
|
new_folder = self.add_folder_form.new_folder
|
||||||
|
if self.manager.save_object(new_folder):
|
||||||
|
self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path))
|
||||||
|
self.expand_folder(new_folder.id)
|
||||||
|
else:
|
||||||
|
critical_error_message_box(
|
||||||
|
message=translate('OpenLP.FolderLibraryItem', 'Could not add the new folder.'))
|
||||||
|
|
||||||
|
def on_delete_click(self):
|
||||||
|
"""
|
||||||
|
Remove an item from the list.
|
||||||
|
"""
|
||||||
|
# Syntactic sugar, plus a minor performance optimisation
|
||||||
|
Folder, Item = self.folder_class, self.item_class
|
||||||
|
|
||||||
|
# Turn off auto preview triggers.
|
||||||
|
self.list_view.blockSignals(True)
|
||||||
|
if check_item_selected(self.list_view,
|
||||||
|
translate('OpenLP.FolderLibraryItem', 'You must select an item or folder to delete.')):
|
||||||
|
tree_item_list = self.list_view.selectedItems()
|
||||||
|
self.application.set_busy_cursor()
|
||||||
|
self.main_window.display_progress_bar(len(tree_item_list))
|
||||||
|
for tree_item in tree_item_list:
|
||||||
|
if not tree_item:
|
||||||
|
self.main_window.increment_progress_bar()
|
||||||
|
continue
|
||||||
|
item = tree_item.data(0, QtCore.Qt.UserRole)
|
||||||
|
if isinstance(item, Item):
|
||||||
|
self.delete_item(item)
|
||||||
|
if not item.folder_id:
|
||||||
|
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
|
||||||
|
else:
|
||||||
|
tree_item.parent().removeChild(tree_item)
|
||||||
|
self.manager.delete_object(Item, item.id)
|
||||||
|
elif isinstance(item, Folder):
|
||||||
|
if QtWidgets.QMessageBox.question(
|
||||||
|
self.list_view.parent(),
|
||||||
|
translate('OpenLP.FolderLibraryItem', 'Remove folder'),
|
||||||
|
translate('OpenLP.FolderLibraryItem',
|
||||||
|
'Are you sure you want to remove "{name}" and everything in it?'
|
||||||
|
).format(name=item.name)
|
||||||
|
) == QtWidgets.QMessageBox.Yes:
|
||||||
|
self.recursively_delete_folder(item)
|
||||||
|
self.manager.delete_object(Folder, item.id)
|
||||||
|
if item.parent_id is None:
|
||||||
|
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
|
||||||
|
else:
|
||||||
|
tree_item.parent().removeChild(tree_item)
|
||||||
|
self.main_window.increment_progress_bar()
|
||||||
|
self.main_window.finished_progress_bar()
|
||||||
|
self.application.set_normal_cursor()
|
||||||
|
self.list_view.blockSignals(False)
|
||||||
|
|
||||||
|
def add_list_view_to_toolbar(self):
|
||||||
|
"""
|
||||||
|
Creates the main widget for listing items.
|
||||||
|
"""
|
||||||
|
# Add the List widget
|
||||||
|
self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
|
||||||
|
self.list_view.setObjectName('{name}TreeView'.format(name=self.plugin.name))
|
||||||
|
# Add to pageLayout
|
||||||
|
self.page_layout.addWidget(self.list_view)
|
||||||
|
# define and add the context menu
|
||||||
|
self.list_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||||
|
|
||||||
|
def add_middle_header_bar(self):
|
||||||
|
"""
|
||||||
|
Add buttons after the main buttons
|
||||||
|
"""
|
||||||
|
self.add_folder_action = self.toolbar.add_toolbar_action(
|
||||||
|
'add_folder_action', icon=UiIcons().folder, triggers=self.on_add_folder_click)
|
||||||
|
|
||||||
|
def add_sub_folders(self, folder_list, parent_id=None):
|
||||||
|
"""
|
||||||
|
Recursively add subfolders to the given parent folder in a QTreeWidget.
|
||||||
|
|
||||||
|
:param folder_list: The List object that contains all QTreeWidgetItems.
|
||||||
|
:param parent_folder_id: The ID of the folder that will be added recursively.
|
||||||
|
"""
|
||||||
|
# Syntactic sugar, plus a minor performance optimisation
|
||||||
|
Folder = self.folder_class
|
||||||
|
|
||||||
|
folders = self.manager.get_all_objects(Folder, Folder.parent_id == parent_id)
|
||||||
|
folders.sort(key=lambda folder_object: get_natural_key(folder_object.name))
|
||||||
|
folder_icon = UiIcons().folder
|
||||||
|
for folder in folders:
|
||||||
|
folder_item = QtWidgets.QTreeWidgetItem()
|
||||||
|
folder_item.setText(0, folder.name)
|
||||||
|
folder_item.setData(0, QtCore.Qt.UserRole, folder)
|
||||||
|
folder_item.setIcon(0, folder_icon)
|
||||||
|
if parent_id is None:
|
||||||
|
self.list_view.addTopLevelItem(folder_item)
|
||||||
|
else:
|
||||||
|
folder_list[parent_id].addChild(folder_item)
|
||||||
|
folder_list[folder.id] = folder_item
|
||||||
|
self.add_sub_folders(folder_list, folder.id)
|
||||||
|
|
||||||
|
def expand_folder(self, folder_id, root_item=None):
|
||||||
|
"""
|
||||||
|
Expand folders in the widget recursively.
|
||||||
|
|
||||||
|
:param folder_id: The ID of the folder that will be expanded.
|
||||||
|
:param root_item: This option is only used for recursion purposes.
|
||||||
|
"""
|
||||||
|
return_value = False
|
||||||
|
if root_item is None:
|
||||||
|
root_item = self.list_view.invisibleRootItem()
|
||||||
|
for i in range(root_item.childCount()):
|
||||||
|
child = root_item.child(i)
|
||||||
|
if self.expand_folder(folder_id, child):
|
||||||
|
child.setExpanded(True)
|
||||||
|
return_value = True
|
||||||
|
if isinstance(root_item.data(0, QtCore.Qt.UserRole), self.folder_class):
|
||||||
|
if root_item.data(0, QtCore.Qt.UserRole).id == folder_id:
|
||||||
|
return True
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
def recursively_delete_folder(self, folder):
|
||||||
|
"""
|
||||||
|
Recursively deletes a folder and all folders and items in it.
|
||||||
|
|
||||||
|
:param folder: The Folder instance of the folder that will be deleted.
|
||||||
|
"""
|
||||||
|
# Syntactic sugar, plus a minor performance optimisation
|
||||||
|
Folder, Item = self.folder_class, self.item_class
|
||||||
|
|
||||||
|
items = self.manager.get_all_objects(Item, Item.folder_id == folder.id)
|
||||||
|
for item in items:
|
||||||
|
self.delete_item(item)
|
||||||
|
self.manager.delete_object(Item, item.id)
|
||||||
|
folders = self.manager.get_all_objects(Folder, Folder.parent_id == folder.id)
|
||||||
|
for child in folders:
|
||||||
|
self.recursively_delete_folder(child)
|
||||||
|
self.manager.delete_object(Folder, child.id)
|
||||||
|
|
||||||
|
def file_to_item(self, filename):
|
||||||
|
"""
|
||||||
|
This method allows the media item to convert a string filename into an item class
|
||||||
|
|
||||||
|
Override this method to customise your plugin's loading method
|
||||||
|
"""
|
||||||
|
if isinstance(filename, Path):
|
||||||
|
name = filename.name
|
||||||
|
filename = str(filename)
|
||||||
|
else:
|
||||||
|
name = os.path.basename(filename)
|
||||||
|
item = self.item_class(name=name, file_path=filename)
|
||||||
|
self.manager.save_object(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def load_item(self, item, is_initial_load=False):
|
||||||
|
"""
|
||||||
|
This method allows the media item to set up the QTreeWidgetItem the way it wants
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('load_item needs to be implemented by the descendant class')
|
||||||
|
|
||||||
|
def delete_item(self, item):
|
||||||
|
"""
|
||||||
|
This method allows the media item to delete the Item
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('delete_item needs to be implemented by the descendant class')
|
||||||
|
|
||||||
|
def load_list(self, items, is_initial_load=False, target_folder=None):
|
||||||
|
"""
|
||||||
|
Load the list of items into the tree view
|
||||||
|
|
||||||
|
:param items: The items to load
|
||||||
|
:param target_folder: The folder to load the items into
|
||||||
|
"""
|
||||||
|
if not is_initial_load:
|
||||||
|
self.application.set_busy_cursor()
|
||||||
|
self.main_window.display_progress_bar(len(items))
|
||||||
|
self.list_view.clear()
|
||||||
|
# Load the list of folders and add them to the tree view
|
||||||
|
folder_items = {}
|
||||||
|
self.add_sub_folders(folder_items, parent_id=None)
|
||||||
|
if target_folder is not None:
|
||||||
|
self.expand_folder(target_folder.id)
|
||||||
|
# Convert any filenames to items
|
||||||
|
for counter, filename in enumerate(items):
|
||||||
|
if isinstance(filename, (Path, str)):
|
||||||
|
items[counter] = self.file_to_item(filename)
|
||||||
|
# Sort the files by the filename
|
||||||
|
items.sort(key=lambda item: get_natural_key(item if isinstance(item, str) else item.file_path))
|
||||||
|
for item in items:
|
||||||
|
self.log_debug('Loading item: {name}'.format(name=item.file_path))
|
||||||
|
tree_item = self.load_item(item, is_initial_load)
|
||||||
|
if not tree_item:
|
||||||
|
continue
|
||||||
|
elif not item.folder_id:
|
||||||
|
self.list_view.addTopLevelItem(tree_item)
|
||||||
|
else:
|
||||||
|
folder_items[item.folder_id].addChild(tree_item)
|
||||||
|
if not is_initial_load:
|
||||||
|
self.main_window.increment_progress_bar()
|
||||||
|
if not is_initial_load:
|
||||||
|
self.main_window.finished_progress_bar()
|
||||||
|
self.application.set_normal_cursor()
|
||||||
|
|
||||||
|
def format_search_result(self, item):
|
||||||
|
"""
|
||||||
|
Format an item for the search results. The default implementation simply returns
|
||||||
|
[item.file_path, item.file_path]
|
||||||
|
|
||||||
|
:param Item item: An Item to be formatted
|
||||||
|
:return list[str, str]: A list of two items containing the full path and a pretty name
|
||||||
|
"""
|
||||||
|
return [item.file_path, item.file_path]
|
||||||
|
|
||||||
|
def search(self, string, show_error):
|
||||||
|
"""
|
||||||
|
Performs a search for items containing ``string``
|
||||||
|
|
||||||
|
:param string: String to be displayed
|
||||||
|
:param show_error: Should the error be shown (True)
|
||||||
|
:return: The search result.
|
||||||
|
"""
|
||||||
|
string = string.lower()
|
||||||
|
items = self.manager.get_all_objects(self.item_class, self.item_class.file_path.match(string))
|
||||||
|
return [self.format_search_result(item) for item in items]
|
||||||
|
|
||||||
|
def validate_and_load(self, file_paths, target_folder=None):
|
||||||
|
"""
|
||||||
|
Process a list for files either from the File Dialog or from Drag and Drop.
|
||||||
|
This method is overloaded from MediaManagerItem.
|
||||||
|
|
||||||
|
:param list[Path] file_paths: A List of paths to be loaded
|
||||||
|
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
|
||||||
|
"""
|
||||||
|
self.application.set_normal_cursor()
|
||||||
|
if target_folder:
|
||||||
|
target_folder = target_folder.data(0, QtCore.Qt.UserRole)
|
||||||
|
elif self.current_folder:
|
||||||
|
target_folder = self.current_folder
|
||||||
|
if not target_folder and self.choose_folder_form.exec() == QtWidgets.QDialog.Accepted:
|
||||||
|
target_folder = self.choose_folder_form.folder
|
||||||
|
if self.choose_folder_form.is_new_folder:
|
||||||
|
self.manager.save_object(target_folder)
|
||||||
|
existing_files = [item.file_path for item in self.manager.get_all_objects(self.item_class)]
|
||||||
|
# Convert file paths to items
|
||||||
|
for file_path in file_paths:
|
||||||
|
if file_paths.count(file_path) > 1 or existing_files.count(file_path) > 0:
|
||||||
|
# If a file already exists in the items or has been selected twice, show an error message
|
||||||
|
critical_error_message_box(translate('OpenLP.FolderLibraryItem', 'File Exists'),
|
||||||
|
translate('OpenLP.FolderLibraryItem',
|
||||||
|
'An item with that filename already exists.'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.log_debug('Adding new item: {name}'.format(name=file_path))
|
||||||
|
item = self.item_class(name=str(file_path), file_path=str(file_path))
|
||||||
|
if isinstance(file_path, Path) and file_path.exists():
|
||||||
|
item.file_hash = sha256_file_hash(file_path)
|
||||||
|
if target_folder:
|
||||||
|
item.folder_id = target_folder.id
|
||||||
|
self.manager.save_object(item)
|
||||||
|
self.main_window.increment_progress_bar()
|
||||||
|
self.load_list(self.manager.get_all_objects(self.item_class, order_by_ref=self.item_class.file_path),
|
||||||
|
target_folder=target_folder)
|
||||||
|
last_dir = Path(file_paths[0]).parent
|
||||||
|
self.settings.setValue(self.settings_section + '/last directory', last_dir)
|
||||||
|
|
||||||
|
def dnd_move_internal(self, target):
|
||||||
|
"""
|
||||||
|
Handle drag-and-drop moving of items within the media manager
|
||||||
|
|
||||||
|
:param target: This contains the QTreeWidget that is the target of the DnD action
|
||||||
|
"""
|
||||||
|
items_to_move = self.list_view.selectedItems()
|
||||||
|
# Determine group to move images to
|
||||||
|
target_folder = target
|
||||||
|
if target_folder is not None and isinstance(target_folder.data(0, QtCore.Qt.UserRole), self.item_class):
|
||||||
|
target_folder = target.parent()
|
||||||
|
# Move to toplevel
|
||||||
|
if target_folder is None:
|
||||||
|
target_folder = self.list_view.invisibleRootItem()
|
||||||
|
target_folder.setData(0, QtCore.Qt.UserRole, self.folder_class())
|
||||||
|
target_folder.data(0, QtCore.Qt.UserRole).id = 0
|
||||||
|
# Move images in the treeview
|
||||||
|
items_to_save = []
|
||||||
|
for item in items_to_move:
|
||||||
|
if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
|
||||||
|
if isinstance(item.parent(), QtWidgets.QTreeWidgetItem):
|
||||||
|
item.parent().removeChild(item)
|
||||||
|
else:
|
||||||
|
self.list_view.invisibleRootItem().removeChild(item)
|
||||||
|
target_folder.addChild(item)
|
||||||
|
item.setSelected(True)
|
||||||
|
item_data = item.data(0, QtCore.Qt.UserRole)
|
||||||
|
item_data.folder_id = target_folder.data(0, QtCore.Qt.UserRole).id
|
||||||
|
items_to_save.append(item_data)
|
||||||
|
target_folder.setExpanded(True)
|
||||||
|
# Update the folder ID's of the items in the database
|
||||||
|
self.manager.save_objects(items_to_save)
|
||||||
|
# Sort the target folder
|
||||||
|
sort_folders = []
|
||||||
|
sort_items = []
|
||||||
|
for item in target_folder.takeChildren():
|
||||||
|
if isinstance(item.data(0, QtCore.Qt.UserRole), self.folder_class):
|
||||||
|
sort_folders.append(item)
|
||||||
|
if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
|
||||||
|
sort_items.append(item)
|
||||||
|
sort_folders.sort(key=lambda item: get_natural_key(item.text(0)))
|
||||||
|
target_folder.addChildren(sort_folders)
|
||||||
|
sort_items.sort(key=lambda item: get_natural_key(item.text(0)))
|
||||||
|
target_folder.addChildren(sort_items)
|
@ -1691,6 +1691,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
|
|||||||
else:
|
else:
|
||||||
self.drop_position = get_parent_item_data(item) - 1
|
self.drop_position = get_parent_item_data(item) - 1
|
||||||
Registry().execute('{plugin}_add_service_item'.format(plugin=plugin), replace)
|
Registry().execute('{plugin}_add_service_item'.format(plugin=plugin), replace)
|
||||||
|
else:
|
||||||
|
self.log_warning('Unrecognised item')
|
||||||
|
|
||||||
def update_theme_list(self, theme_list):
|
def update_theme_list(self, theme_list):
|
||||||
"""
|
"""
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License #
|
# You should have received a copy of the GNU General Public License #
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
import gc
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -141,7 +142,9 @@ class BibleManager(LogMixin, RegistryProperties):
|
|||||||
name = bible.get_name()
|
name = bible.get_name()
|
||||||
# Remove corrupted files.
|
# Remove corrupted files.
|
||||||
if name is None:
|
if name is None:
|
||||||
bible.session.close_all()
|
bible.session.close()
|
||||||
|
bible.session = None
|
||||||
|
gc.collect()
|
||||||
delete_file(self.path / file_path)
|
delete_file(self.path / file_path)
|
||||||
continue
|
continue
|
||||||
log.debug('Bible Name: "{name}"'.format(name=name))
|
log.debug('Bible Name: "{name}"'.format(name=name))
|
||||||
@ -185,8 +188,9 @@ class BibleManager(LogMixin, RegistryProperties):
|
|||||||
"""
|
"""
|
||||||
log.debug('BibleManager.delete_bible("{name}")'.format(name=name))
|
log.debug('BibleManager.delete_bible("{name}")'.format(name=name))
|
||||||
bible = self.db_cache[name]
|
bible = self.db_cache[name]
|
||||||
bible.session.close_all()
|
bible.session.close()
|
||||||
bible.session = None
|
bible.session = None
|
||||||
|
gc.collect()
|
||||||
return delete_file(bible.path / '{name}{suffix}'.format(name=name, suffix=self.suffix))
|
return delete_file(bible.path / '{name}{suffix}'.format(name=name, suffix=self.suffix))
|
||||||
|
|
||||||
def get_bibles(self):
|
def get_bibles(self):
|
||||||
|
@ -97,7 +97,7 @@ class ImageMediaItem(MediaManagerItem):
|
|||||||
"""
|
"""
|
||||||
Set which icons the media manager tab should show.
|
Set which icons the media manager tab should show.
|
||||||
"""
|
"""
|
||||||
MediaManagerItem.required_icons(self)
|
super().required_icons()
|
||||||
self.has_file_icon = True
|
self.has_file_icon = True
|
||||||
self.has_new_icon = False
|
self.has_new_icon = False
|
||||||
self.has_edit_icon = False
|
self.has_edit_icon = False
|
||||||
|
46
openlp/plugins/media/lib/db.py
Normal file
46
openlp/plugins/media/lib/db.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2021 OpenLP Developers #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
##########################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`~openlp.plugins.media.lib.db` module contains the database layer for the media plugin
|
||||||
|
"""
|
||||||
|
from sqlalchemy import MetaData
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from openlp.core.lib.db import FolderMixin, ItemMixin, init_db, init_url
|
||||||
|
|
||||||
|
Base = declarative_base(MetaData())
|
||||||
|
|
||||||
|
|
||||||
|
class Folder(Base, FolderMixin):
|
||||||
|
"""A folder holds items or other folders"""
|
||||||
|
|
||||||
|
|
||||||
|
class Item(Base, ItemMixin):
|
||||||
|
"""An item is something that can be contained within a folder"""
|
||||||
|
|
||||||
|
|
||||||
|
def init_schema(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Set up the media database and initialise the schema
|
||||||
|
"""
|
||||||
|
session, metadata = init_db(init_url('media'), base=Base)
|
||||||
|
metadata.create_all(checkfirst=True)
|
||||||
|
return session
|
@ -21,23 +21,28 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
|
from openlp.core.common import delete_file
|
||||||
from openlp.core.common.applocation import AppLocation
|
from openlp.core.common.applocation import AppLocation
|
||||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
from openlp.core.common.i18n import UiStrings, translate
|
||||||
from openlp.core.common.mixins import RegistryProperties
|
from openlp.core.common.path import create_paths
|
||||||
from openlp.core.common.path import create_paths, path_to_str
|
|
||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
from openlp.core.lib import MediaType, ServiceItemContext, check_item_selected
|
from openlp.core.lib import MediaType, ServiceItemContext
|
||||||
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
from openlp.core.lib.plugin import StringContent
|
||||||
from openlp.core.lib.serviceitem import ItemCapabilities
|
from openlp.core.lib.serviceitem import ItemCapabilities
|
||||||
from openlp.core.lib.ui import critical_error_message_box
|
from openlp.core.lib.ui import create_action, create_widget_action, critical_error_message_box
|
||||||
from openlp.core.state import State
|
from openlp.core.state import State
|
||||||
from openlp.core.ui.icons import UiIcons
|
from openlp.core.ui.icons import UiIcons
|
||||||
|
from openlp.core.ui.library import FolderLibraryItem
|
||||||
from openlp.core.ui.media import parse_optical_path, parse_stream_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT
|
from openlp.core.ui.media import parse_optical_path, parse_stream_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT
|
||||||
from openlp.core.ui.media.vlcplayer import get_vlc
|
from openlp.core.ui.media.vlcplayer import get_vlc
|
||||||
|
|
||||||
|
from openlp.plugins.media.lib.db import Folder, Item
|
||||||
|
|
||||||
if get_vlc() is not None:
|
if get_vlc() is not None:
|
||||||
from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm
|
from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm
|
||||||
from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm
|
from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm
|
||||||
@ -47,7 +52,7 @@ if get_vlc() is not None:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
class MediaMediaItem(FolderLibraryItem):
|
||||||
"""
|
"""
|
||||||
This is the custom media manager item for Media Slides.
|
This is the custom media manager item for Media Slides.
|
||||||
"""
|
"""
|
||||||
@ -57,7 +62,28 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
|
|
||||||
def __init__(self, parent, plugin):
|
def __init__(self, parent, plugin):
|
||||||
self.setup()
|
self.setup()
|
||||||
super(MediaMediaItem, self).__init__(parent, plugin)
|
super(MediaMediaItem, self).__init__(parent, plugin, Folder, Item)
|
||||||
|
|
||||||
|
def retranslate_ui(self):
|
||||||
|
"""
|
||||||
|
This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
|
||||||
|
to another language.
|
||||||
|
"""
|
||||||
|
super().retranslate_ui()
|
||||||
|
self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media')
|
||||||
|
|
||||||
|
def setup_item(self):
|
||||||
|
"""
|
||||||
|
Do some additional setup.
|
||||||
|
"""
|
||||||
|
self.media_go_live.connect(self.go_live_remote)
|
||||||
|
self.media_add_to_service.connect(self.add_to_service_remote)
|
||||||
|
self.single_service_item = False
|
||||||
|
self.has_search = True
|
||||||
|
self.media_object = None
|
||||||
|
Registry().register_function('mediaitem_media_rebuild', self.rebuild_players)
|
||||||
|
# Allow DnD from the desktop
|
||||||
|
self.list_view.activateDnD()
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""
|
"""
|
||||||
@ -69,33 +95,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
self.error_icon = UiIcons().delete
|
self.error_icon = UiIcons().delete
|
||||||
self.clapperboard = UiIcons().clapperboard
|
self.clapperboard = UiIcons().clapperboard
|
||||||
|
|
||||||
def setup_item(self):
|
|
||||||
"""
|
|
||||||
Do some additional setup.
|
|
||||||
"""
|
|
||||||
self.media_go_live.connect(self.go_live_remote)
|
|
||||||
self.media_add_to_service.connect(self.add_to_service_remote)
|
|
||||||
self.single_service_item = False
|
|
||||||
self.has_search = True
|
|
||||||
self.media_object = None
|
|
||||||
# self.display_controller = DisplayController(self.parent())
|
|
||||||
# Registry().register_function('video_background_replaced', self.video_background_replaced)
|
|
||||||
Registry().register_function('mediaitem_media_rebuild', self.rebuild_players)
|
|
||||||
# Allow DnD from the desktop
|
|
||||||
self.list_view.activateDnD()
|
|
||||||
|
|
||||||
def retranslate_ui(self):
|
|
||||||
"""
|
|
||||||
This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
|
|
||||||
to another language.
|
|
||||||
"""
|
|
||||||
self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media')
|
|
||||||
|
|
||||||
def required_icons(self):
|
def required_icons(self):
|
||||||
"""
|
"""
|
||||||
Set which icons the media manager tab should show
|
Set which icons the media manager tab should show
|
||||||
"""
|
"""
|
||||||
MediaManagerItem.required_icons(self)
|
super().required_icons()
|
||||||
self.has_file_icon = True
|
self.has_file_icon = True
|
||||||
self.has_new_icon = False
|
self.has_new_icon = False
|
||||||
self.has_edit_icon = False
|
self.has_edit_icon = False
|
||||||
@ -106,37 +110,126 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
if State().check_preconditions('media_live'):
|
if State().check_preconditions('media_live'):
|
||||||
self.can_make_live = True
|
self.can_make_live = True
|
||||||
|
|
||||||
|
def initialise(self):
|
||||||
|
"""
|
||||||
|
Initialize media item.
|
||||||
|
"""
|
||||||
|
self.log_debug('Initialise')
|
||||||
|
self.list_view.clear()
|
||||||
|
self.list_view.setIndentation(self.list_view.default_indentation)
|
||||||
|
self.list_view.allow_internal_dnd = True
|
||||||
|
self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
|
||||||
|
create_paths(self.service_path)
|
||||||
|
# Load images from the database
|
||||||
|
self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path), is_initial_load=True)
|
||||||
|
self.service_path = AppLocation.get_section_data_path('media') / 'thumbnails'
|
||||||
|
self.rebuild_players()
|
||||||
|
|
||||||
def add_list_view_to_toolbar(self):
|
def add_list_view_to_toolbar(self):
|
||||||
"""
|
"""
|
||||||
Creates the main widget for listing items.
|
Creates the main widget for listing items.
|
||||||
"""
|
"""
|
||||||
MediaManagerItem.add_list_view_to_toolbar(self)
|
super().add_list_view_to_toolbar()
|
||||||
# self.list_view.addAction(self.replace_action)
|
if self.has_edit_icon:
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
text=self.plugin.get_string(StringContent.Edit)['title'],
|
||||||
|
icon=UiIcons().edit,
|
||||||
|
triggers=self.on_edit_click)
|
||||||
|
create_widget_action(self.list_view, separator=True)
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
'listView{name}{preview}Item'.format(name=self.plugin.name.title(), preview=StringContent.Preview.title()),
|
||||||
|
text=self.plugin.get_string(StringContent.Preview)['title'],
|
||||||
|
icon=UiIcons().preview,
|
||||||
|
can_shortcuts=True,
|
||||||
|
triggers=self.on_preview_click)
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
'listView{name}{live}Item'.format(name=self.plugin.name.title(), live=StringContent.Live.title()),
|
||||||
|
text=self.plugin.get_string(StringContent.Live)['title'],
|
||||||
|
icon=UiIcons().live,
|
||||||
|
can_shortcuts=True,
|
||||||
|
triggers=self.on_live_click)
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
'listView{name}{service}Item'.format(name=self.plugin.name.title(), service=StringContent.Service.title()),
|
||||||
|
can_shortcuts=True,
|
||||||
|
text=self.plugin.get_string(StringContent.Service)['title'],
|
||||||
|
icon=UiIcons().add,
|
||||||
|
triggers=self.on_add_click)
|
||||||
|
if self.add_to_service_item:
|
||||||
|
create_widget_action(self.list_view, separator=True)
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
|
||||||
|
icon=UiIcons().add,
|
||||||
|
triggers=self.on_add_edit_click)
|
||||||
|
create_widget_action(self.list_view, separator=True)
|
||||||
|
if self.has_delete_icon:
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
'listView{name}{delete}Item'.format(name=self.plugin.name.title(), delete=StringContent.Delete.title()),
|
||||||
|
text=self.plugin.get_string(StringContent.Delete)['title'],
|
||||||
|
icon=UiIcons().delete,
|
||||||
|
can_shortcuts=True, triggers=self.on_delete_click)
|
||||||
|
self.add_custom_context_actions()
|
||||||
|
# Create the context menu and add all actions from the list_view.
|
||||||
|
self.menu = QtWidgets.QMenu()
|
||||||
|
self.menu.addActions(self.list_view.actions())
|
||||||
|
self.list_view.doubleClicked.connect(self.on_double_clicked)
|
||||||
|
self.list_view.itemSelectionChanged.connect(self.on_selection_change)
|
||||||
|
self.list_view.customContextMenuRequested.connect(self.context_menu)
|
||||||
|
|
||||||
def add_start_header_bar(self):
|
def add_custom_context_actions(self):
|
||||||
"""
|
"""
|
||||||
Adds buttons to the start of the header bar.
|
Add custom actions to the context menu.
|
||||||
"""
|
"""
|
||||||
|
create_widget_action(self.list_view, separator=True)
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
text=UiStrings().AddFolder, icon=UiIcons().folder, triggers=self.on_add_folder_click)
|
||||||
|
create_widget_action(
|
||||||
|
self.list_view,
|
||||||
|
text=translate('MediaPlugin', 'Add new media'),
|
||||||
|
icon=UiIcons().open, triggers=self.on_file_click)
|
||||||
|
create_widget_action(self.list_view, separator=True)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""
|
||||||
|
Re-implement to add a dropdown menu to the load icon
|
||||||
|
"""
|
||||||
|
super().setup_ui()
|
||||||
if State().check_preconditions('media'):
|
if State().check_preconditions('media'):
|
||||||
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||||
optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||||
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=UiIcons().optical,
|
self.load_optical = create_action(self, 'load_optical',
|
||||||
text=optical_button_text,
|
icon=UiIcons().optical,
|
||||||
tooltip=optical_button_tooltip,
|
text=optical_button_text,
|
||||||
triggers=self.on_load_optical)
|
tooltip=optical_button_tooltip,
|
||||||
|
triggers=self.on_load_optical)
|
||||||
device_stream_button_text = translate('MediaPlugin.MediaItem', 'Open device stream')
|
device_stream_button_text = translate('MediaPlugin.MediaItem', 'Open device stream')
|
||||||
device_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open device stream')
|
device_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open device stream')
|
||||||
self.open_stream = self.toolbar.add_toolbar_action('open_device_stream', icon=UiIcons().device_stream,
|
self.open_stream = create_action(self, 'open_device_stream',
|
||||||
text=device_stream_button_text,
|
icon=UiIcons().device_stream,
|
||||||
tooltip=device_stream_button_tooltip,
|
text=device_stream_button_text,
|
||||||
triggers=self.on_open_device_stream)
|
tooltip=device_stream_button_tooltip,
|
||||||
|
triggers=self.on_open_device_stream)
|
||||||
network_stream_button_text = translate('MediaPlugin.MediaItem', 'Open network stream')
|
network_stream_button_text = translate('MediaPlugin.MediaItem', 'Open network stream')
|
||||||
network_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open network stream')
|
network_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open network stream')
|
||||||
self.open_network_stream = self.toolbar.add_toolbar_action('open_network_stream',
|
self.open_network_stream = create_action(self, 'open_network_stream',
|
||||||
icon=UiIcons().network_stream,
|
icon=UiIcons().network_stream,
|
||||||
text=network_stream_button_text,
|
text=network_stream_button_text,
|
||||||
tooltip=network_stream_button_tooltip,
|
tooltip=network_stream_button_tooltip,
|
||||||
triggers=self.on_open_network_stream)
|
triggers=self.on_open_network_stream)
|
||||||
|
self.load_menu = QtWidgets.QMenu(self.toolbar)
|
||||||
|
self.load_menu.setObjectName('load_menu')
|
||||||
|
self.load_menu.addAction(self.load_optical)
|
||||||
|
self.load_menu.addAction(self.open_stream)
|
||||||
|
self.load_menu.addAction(self.open_network_stream)
|
||||||
|
self.toolbar.actions['mediaLoadAction'].setMenu(self.load_menu)
|
||||||
|
button = self.toolbar.widgetForAction(self.toolbar.actions['mediaLoadAction'])
|
||||||
|
button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
|
||||||
|
|
||||||
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
@ -153,7 +246,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
item = self.list_view.currentItem()
|
item = self.list_view.currentItem()
|
||||||
if item is None:
|
if item is None:
|
||||||
return False
|
return False
|
||||||
filename = str(item.data(QtCore.Qt.UserRole))
|
if isinstance(item, QtCore.QModelIndex):
|
||||||
|
item = self.list_view.itemFromIndex(item)
|
||||||
|
media_item = item.data(0, QtCore.Qt.UserRole)
|
||||||
|
if not isinstance(media_item, Item):
|
||||||
|
return False
|
||||||
|
filename = media_item.file_path
|
||||||
# Special handling if the filename is a optical clip
|
# Special handling if the filename is a optical clip
|
||||||
if filename.startswith('optical:'):
|
if filename.startswith('optical:'):
|
||||||
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
|
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
|
||||||
@ -205,16 +303,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
service_item.validate_item()
|
service_item.validate_item()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def initialise(self):
|
|
||||||
"""
|
|
||||||
Initialize media item.
|
|
||||||
"""
|
|
||||||
self.list_view.clear()
|
|
||||||
self.service_path = AppLocation.get_section_data_path('media') / 'thumbnails'
|
|
||||||
create_paths(self.service_path)
|
|
||||||
self.load_list([path_to_str(file) for file in self.settings.value('media/media files')])
|
|
||||||
self.rebuild_players()
|
|
||||||
|
|
||||||
def rebuild_players(self):
|
def rebuild_players(self):
|
||||||
"""
|
"""
|
||||||
Rebuild the tab in the media manager when changes are made in the settings.
|
Rebuild the tab in the media manager when changes are made in the settings.
|
||||||
@ -226,68 +314,91 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
audio=' '.join(AUDIO_EXT),
|
audio=' '.join(AUDIO_EXT),
|
||||||
files=UiStrings().AllFiles)
|
files=UiStrings().AllFiles)
|
||||||
|
|
||||||
def on_delete_click(self):
|
def file_to_item(self, filename):
|
||||||
"""
|
"""
|
||||||
Remove a media item from the list.
|
Override the inherited file_to_item() method to handle various tracks
|
||||||
"""
|
"""
|
||||||
if check_item_selected(self.list_view,
|
if isinstance(filename, Path):
|
||||||
translate('MediaPlugin.MediaItem', 'You must select a media file to delete.')):
|
name = filename.name
|
||||||
row_list = [item.row() for item in self.list_view.selectedIndexes()]
|
filename = str(filename)
|
||||||
row_list.sort(reverse=True)
|
elif filename.startswith('optical:'):
|
||||||
for row in row_list:
|
# Handle optical based item
|
||||||
self.list_view.takeItem(row)
|
_, _, _, _, _, _, name = parse_optical_path(filename)
|
||||||
self.settings.setValue('media/media files', self.get_file_list())
|
elif filename.startswith('devicestream:') or filename.startswith('networkstream:'):
|
||||||
|
name, _, _ = parse_stream_path(filename)
|
||||||
|
else:
|
||||||
|
name = os.path.basename(filename)
|
||||||
|
item = self.item_class(name=name, file_path=filename)
|
||||||
|
self.manager.save_object(item)
|
||||||
|
return item
|
||||||
|
|
||||||
def load_list(self, media, target_group=None):
|
def load_item(self, item, is_initial_load=False):
|
||||||
"""
|
"""
|
||||||
Load the media list
|
Given an item object, return a QTreeWidgetItem
|
||||||
|
"""
|
||||||
|
track_str = str(item.file_path)
|
||||||
|
track_info = QtCore.QFileInfo(track_str)
|
||||||
|
tree_item = None
|
||||||
|
if track_str.startswith('optical:'):
|
||||||
|
# Handle optical based item
|
||||||
|
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
|
||||||
|
tree_item = QtWidgets.QTreeWidgetItem([clip_name])
|
||||||
|
tree_item.setText(0, clip_name)
|
||||||
|
tree_item.setIcon(0, UiIcons().optical)
|
||||||
|
tree_item.setToolTip(0, '{name}@{start}-{end}'.format(name=file_name,
|
||||||
|
start=format_milliseconds(start),
|
||||||
|
end=format_milliseconds(end)))
|
||||||
|
elif track_str.startswith('devicestream:') or track_str.startswith('networkstream:'):
|
||||||
|
(name, mrl, options) = parse_stream_path(track_str)
|
||||||
|
tree_item = QtWidgets.QTreeWidgetItem([name])
|
||||||
|
tree_item.setText(0, name)
|
||||||
|
if track_str.startswith('devicestream:'):
|
||||||
|
tree_item.setIcon(0, UiIcons().device_stream)
|
||||||
|
else:
|
||||||
|
tree_item.setIcon(0, UiIcons().network_stream)
|
||||||
|
tree_item.setToolTip(0, mrl)
|
||||||
|
elif not Path(item.file_path).exists():
|
||||||
|
# File doesn't exist, mark as error.
|
||||||
|
file_name = Path(item.file_path).name
|
||||||
|
tree_item = QtWidgets.QTreeWidgetItem([file_name])
|
||||||
|
tree_item.setText(0, file_name)
|
||||||
|
tree_item.setIcon(0, UiIcons().error)
|
||||||
|
tree_item.setToolTip(0, track_str)
|
||||||
|
elif track_info.isFile():
|
||||||
|
# Normal media file handling.
|
||||||
|
file_name = Path(item.file_path).name
|
||||||
|
tree_item = QtWidgets.QTreeWidgetItem([file_name])
|
||||||
|
tree_item.setText(0, file_name)
|
||||||
|
search = "*." + file_name.split('.')[-1].lower()
|
||||||
|
if search in AUDIO_EXT:
|
||||||
|
tree_item.setIcon(0, UiIcons().audio)
|
||||||
|
else:
|
||||||
|
tree_item.setIcon(0, UiIcons().video)
|
||||||
|
tree_item.setToolTip(0, track_str)
|
||||||
|
if tree_item:
|
||||||
|
tree_item.setData(0, QtCore.Qt.UserRole, item)
|
||||||
|
return tree_item
|
||||||
|
|
||||||
:param media: The media
|
def delete_item(self, item):
|
||||||
:param target_group:
|
|
||||||
"""
|
"""
|
||||||
media.sort(key=lambda file_path: get_natural_key(os.path.split(str(file_path))[1]))
|
Delete any and all thumbnails and things associated with this media item
|
||||||
for track in media:
|
"""
|
||||||
track_str = str(track)
|
delete_file(self.service_path / Path(item.file_path).name)
|
||||||
track_info = QtCore.QFileInfo(track_str)
|
|
||||||
item_name = None
|
def format_search_result(self, item):
|
||||||
if track_str.startswith('optical:'):
|
"""
|
||||||
# Handle optical based item
|
Format an item for the search results
|
||||||
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
|
|
||||||
item_name = QtWidgets.QListWidgetItem(clip_name)
|
:param Item item: An Item to be formatted
|
||||||
item_name.setIcon(UiIcons().optical)
|
:return list[str, str]: A list of two items containing the full path and a pretty name
|
||||||
item_name.setData(QtCore.Qt.UserRole, track)
|
"""
|
||||||
item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name,
|
if Path(item.file_path).exists():
|
||||||
start=format_milliseconds(start),
|
return [item.file_path, Path(item.file_path).name]
|
||||||
end=format_milliseconds(end)))
|
elif item.file_path.startswith('device'):
|
||||||
elif track_str.startswith('devicestream:') or track_str.startswith('networkstream:'):
|
(name, _, _) = parse_stream_path(item.file_path)
|
||||||
(name, mrl, options) = parse_stream_path(track_str)
|
return [item.file_path, name]
|
||||||
item_name = QtWidgets.QListWidgetItem(name)
|
else:
|
||||||
if track_str.startswith('devicestream:'):
|
return super().format_search_result(item)
|
||||||
item_name.setIcon(UiIcons().device_stream)
|
|
||||||
else:
|
|
||||||
item_name.setIcon(UiIcons().network_stream)
|
|
||||||
item_name.setData(QtCore.Qt.UserRole, track)
|
|
||||||
item_name.setToolTip(mrl)
|
|
||||||
elif not os.path.exists(track):
|
|
||||||
# File doesn't exist, mark as error.
|
|
||||||
file_name = os.path.split(track_str)[1]
|
|
||||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
|
||||||
item_name.setIcon(UiIcons().error)
|
|
||||||
item_name.setData(QtCore.Qt.UserRole, track)
|
|
||||||
item_name.setToolTip(track_str)
|
|
||||||
elif track_info.isFile():
|
|
||||||
# Normal media file handling.
|
|
||||||
file_name = os.path.split(track_str)[1]
|
|
||||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
|
||||||
search = "*." + file_name.split('.')[-1].lower()
|
|
||||||
if search in AUDIO_EXT:
|
|
||||||
item_name.setIcon(UiIcons().audio)
|
|
||||||
else:
|
|
||||||
item_name.setIcon(UiIcons().video)
|
|
||||||
item_name.setData(QtCore.Qt.UserRole, track)
|
|
||||||
item_name.setToolTip(track_str)
|
|
||||||
if item_name:
|
|
||||||
self.list_view.addItem(item_name)
|
|
||||||
|
|
||||||
def get_list(self, media_type=MediaType.Audio):
|
def get_list(self, media_type=MediaType.Audio):
|
||||||
"""
|
"""
|
||||||
@ -296,40 +407,16 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
:param media_type: Type to get, defaults to audio.
|
:param media_type: Type to get, defaults to audio.
|
||||||
:return: The media list
|
:return: The media list
|
||||||
"""
|
"""
|
||||||
media_file_paths = self.settings.value('media/media files')
|
|
||||||
media_file_paths.sort(key=lambda file_path: get_natural_key(os.path.split(str(file_path))[1]))
|
|
||||||
if media_type == MediaType.Audio:
|
if media_type == MediaType.Audio:
|
||||||
extension = AUDIO_EXT
|
extensions = AUDIO_EXT
|
||||||
else:
|
else:
|
||||||
extension = VIDEO_EXT
|
extensions = VIDEO_EXT
|
||||||
extension = [x[1:] for x in extension]
|
clauses = []
|
||||||
media = [x for x in media_file_paths if os.path.splitext(x)[1] in extension]
|
for extension in extensions:
|
||||||
return media
|
# Drop the initial * and add to the list of clauses
|
||||||
|
clauses.append(Item.file_path.endswith(extension[1:]))
|
||||||
def search(self, string, show_error):
|
items = self.manager.get_all_objects(Item, or_(*clauses))
|
||||||
"""
|
return [Path(item.file_path) for item in items]
|
||||||
Performs a search for items containing ``string``
|
|
||||||
|
|
||||||
:param string: String to be displayed
|
|
||||||
:param show_error: Should the error be shown (True)
|
|
||||||
:return: The search result.
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
|
||||||
results = []
|
|
||||||
string = string.lower()
|
|
||||||
for file_path in self.settings.value('media/media files'):
|
|
||||||
if isinstance(file_path, Path):
|
|
||||||
file_name = file_path.name
|
|
||||||
if file_name.lower().find(string) > -1:
|
|
||||||
results.append([str(file_path), file_name])
|
|
||||||
else:
|
|
||||||
if file_path.lower().find(string) > -1:
|
|
||||||
if file_path.startswith('device'):
|
|
||||||
(name, _, _) = parse_stream_path(file_path)
|
|
||||||
results.append([str(file_path), name])
|
|
||||||
else:
|
|
||||||
results.append([str(file_path), file_path])
|
|
||||||
return results
|
|
||||||
|
|
||||||
def on_load_optical(self):
|
def on_load_optical(self):
|
||||||
"""
|
"""
|
||||||
@ -349,16 +436,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
|
|
||||||
:param optical: The clip to add.
|
:param optical: The clip to add.
|
||||||
"""
|
"""
|
||||||
file_paths = self.get_file_list()
|
self.validate_and_load([str(optical)])
|
||||||
# If the clip already is in the media list it isn't added and an error message is displayed.
|
|
||||||
if optical in file_paths:
|
|
||||||
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'),
|
|
||||||
translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved'))
|
|
||||||
return
|
|
||||||
# Append the optical string to the media list
|
|
||||||
file_paths.append(optical)
|
|
||||||
self.load_list([str(optical)])
|
|
||||||
self.settings.setValue('media/media files', file_paths)
|
|
||||||
|
|
||||||
def on_open_device_stream(self):
|
def on_open_device_stream(self):
|
||||||
"""
|
"""
|
||||||
@ -378,16 +456,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
|
|
||||||
:param stream: The clip to add.
|
:param stream: The clip to add.
|
||||||
"""
|
"""
|
||||||
file_paths = self.get_file_list()
|
self.validate_and_load([str(stream)])
|
||||||
# If the clip already is in the media list it isn't added and an error message is displayed.
|
|
||||||
if stream in file_paths:
|
|
||||||
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Stream already saved'),
|
|
||||||
translate('MediaPlugin.MediaItem', 'This stream has already been saved'))
|
|
||||||
return
|
|
||||||
# Append the device stream string to the media list
|
|
||||||
file_paths.append(stream)
|
|
||||||
self.load_list([str(stream)])
|
|
||||||
self.settings.setValue('media/media files', file_paths)
|
|
||||||
|
|
||||||
def on_open_network_stream(self):
|
def on_open_network_stream(self):
|
||||||
"""
|
"""
|
||||||
@ -407,13 +476,4 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
|||||||
|
|
||||||
:param stream: The clip to add.
|
:param stream: The clip to add.
|
||||||
"""
|
"""
|
||||||
file_paths = self.get_file_list()
|
self.validate_and_load([str(stream)])
|
||||||
# If the clip already is in the media list it isn't added and an error message is displayed.
|
|
||||||
if stream in file_paths:
|
|
||||||
critical_error_message_box(translate('MediaPlugin.MediaItem', 'Stream already saved'),
|
|
||||||
translate('MediaPlugin.MediaItem', 'This stream has already been saved'))
|
|
||||||
return
|
|
||||||
# Append the device stream string to the media list
|
|
||||||
file_paths.append(stream)
|
|
||||||
self.load_list([str(stream)])
|
|
||||||
self.settings.setValue('media/media files', file_paths)
|
|
||||||
|
@ -22,12 +22,19 @@
|
|||||||
The Media plugin
|
The Media plugin
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from openlp.core.state import State
|
from openlp.core.common import sha256_file_hash
|
||||||
from openlp.core.common.i18n import translate
|
from openlp.core.common.i18n import translate
|
||||||
from openlp.core.ui.icons import UiIcons
|
|
||||||
from openlp.core.lib import build_icon
|
from openlp.core.lib import build_icon
|
||||||
|
from openlp.core.lib.db import Manager
|
||||||
from openlp.core.lib.plugin import Plugin, StringContent
|
from openlp.core.lib.plugin import Plugin, StringContent
|
||||||
|
from openlp.core.state import State
|
||||||
|
from openlp.core.ui.icons import UiIcons
|
||||||
|
from openlp.core.ui.media import parse_optical_path, parse_stream_path
|
||||||
|
|
||||||
|
from openlp.plugins.media.lib.db import Item, init_schema
|
||||||
from openlp.plugins.media.lib.mediaitem import MediaMediaItem
|
from openlp.plugins.media.lib.mediaitem import MediaMediaItem
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +50,8 @@ class MediaPlugin(Plugin):
|
|||||||
log.info('{name} MediaPlugin loaded'.format(name=__name__))
|
log.info('{name} MediaPlugin loaded'.format(name=__name__))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MediaPlugin, self).__init__('media', MediaMediaItem)
|
super().__init__('media', MediaMediaItem)
|
||||||
|
self.manager = Manager(plugin_name='media', init_schema=init_schema)
|
||||||
self.weight = -6
|
self.weight = -6
|
||||||
self.icon_path = UiIcons().video
|
self.icon_path = UiIcons().video
|
||||||
self.icon = build_icon(self.icon_path)
|
self.icon = build_icon(self.icon_path)
|
||||||
@ -56,14 +64,29 @@ class MediaPlugin(Plugin):
|
|||||||
"""
|
"""
|
||||||
Override the inherited initialise() method in order to upgrade the media before trying to load it
|
Override the inherited initialise() method in order to upgrade the media before trying to load it
|
||||||
"""
|
"""
|
||||||
|
media_files = self.settings.value('media/media files') or []
|
||||||
|
for filename in media_files:
|
||||||
|
if not isinstance(filename, (Path, str)):
|
||||||
|
continue
|
||||||
|
if isinstance(filename, Path):
|
||||||
|
name = filename.name
|
||||||
|
filename = str(filename)
|
||||||
|
elif filename.startswith('devicestream:') or filename.startswith('networkstream:'):
|
||||||
|
name, _, _ = parse_stream_path(filename)
|
||||||
|
elif filename.startswith('optical:'):
|
||||||
|
_, _, _, _, _, _, name = parse_optical_path(filename)
|
||||||
|
else:
|
||||||
|
name = os.path.basename(filename)
|
||||||
|
item = Item(name=name, file_path=filename)
|
||||||
|
if not filename.startswith('devicestream:') and not filename.startswith('networkstream:') \
|
||||||
|
and not filename.startswith('optical:'):
|
||||||
|
file_path = Path(filename)
|
||||||
|
if file_path.is_file() and file_path.exists():
|
||||||
|
item.file_hash = sha256_file_hash(file_path)
|
||||||
|
self.manager.save_object(item)
|
||||||
|
self.settings.remove('media/media files')
|
||||||
super().initialise()
|
super().initialise()
|
||||||
|
|
||||||
def app_startup(self):
|
|
||||||
"""
|
|
||||||
Override app_startup() in order to do nothing
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def check_pre_conditions(self):
|
def check_pre_conditions(self):
|
||||||
"""
|
"""
|
||||||
Check the plugin can run and the media controller is available.
|
Check the plugin can run and the media controller is available.
|
||||||
|
46
openlp/plugins/presentations/lib/db.py
Normal file
46
openlp/plugins/presentations/lib/db.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2021 OpenLP Developers #
|
||||||
|
# ---------------------------------------------------------------------- #
|
||||||
|
# This program is free software: you can redistribute it and/or modify #
|
||||||
|
# it under the terms of the GNU General Public License as published by #
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or #
|
||||||
|
# (at your option) any later version. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, #
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||||
|
# GNU General Public License for more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License #
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||||
|
##########################################################################
|
||||||
|
"""
|
||||||
|
The :mod:`~openlp.plugins.presentations.lib.db` module contains the database layer for the presentations plugin
|
||||||
|
"""
|
||||||
|
from sqlalchemy import MetaData
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
from openlp.core.lib.db import FolderMixin, ItemMixin, init_db, init_url
|
||||||
|
|
||||||
|
Base = declarative_base(MetaData())
|
||||||
|
|
||||||
|
|
||||||
|
class Folder(Base, FolderMixin):
|
||||||
|
"""A folder holds items or other folders"""
|
||||||
|
|
||||||
|
|
||||||
|
class Item(Base, ItemMixin):
|
||||||
|
"""An item is something that can be contained within a folder"""
|
||||||
|
|
||||||
|
|
||||||
|
def init_schema(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Set up the media database and initialise the schema
|
||||||
|
"""
|
||||||
|
session, metadata = init_db(init_url('presentations'), base=Base)
|
||||||
|
metadata.create_all(checkfirst=True)
|
||||||
|
return session
|
@ -26,14 +26,15 @@ from PyQt5 import QtCore, QtWidgets
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openlp.core.common import sha256_file_hash
|
from openlp.core.common import sha256_file_hash
|
||||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
from openlp.core.common.i18n import UiStrings, translate
|
||||||
from openlp.core.common.path import path_to_str
|
|
||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
from openlp.core.lib import ServiceItemContext, build_icon, check_item_selected, create_thumb, validate_thumb
|
from openlp.core.lib import ServiceItemContext, build_icon, create_thumb, validate_thumb
|
||||||
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
from openlp.core.ui.library import FolderLibraryItem
|
||||||
from openlp.core.lib.serviceitem import ItemCapabilities
|
from openlp.core.lib.serviceitem import ItemCapabilities
|
||||||
from openlp.core.lib.ui import create_horizontal_adjusting_combo_box, critical_error_message_box
|
from openlp.core.lib.ui import create_horizontal_adjusting_combo_box, critical_error_message_box
|
||||||
from openlp.core.ui.icons import UiIcons
|
from openlp.core.ui.icons import UiIcons
|
||||||
|
|
||||||
|
from openlp.plugins.presentations.lib.db import Folder, Item
|
||||||
from openlp.plugins.presentations.lib.messagelistener import MessageListener
|
from openlp.plugins.presentations.lib.messagelistener import MessageListener
|
||||||
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
|
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETY
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PresentationMediaItem(MediaManagerItem):
|
class PresentationMediaItem(FolderLibraryItem):
|
||||||
"""
|
"""
|
||||||
This is the Presentation media manager item for Presentation Items. It can present files using Openoffice and
|
This is the Presentation media manager item for Presentation Items. It can present files using Openoffice and
|
||||||
Powerpoint
|
Powerpoint
|
||||||
@ -56,12 +57,13 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
"""
|
"""
|
||||||
self.icon_path = 'presentations/presentation'
|
self.icon_path = 'presentations/presentation'
|
||||||
self.controllers = controllers
|
self.controllers = controllers
|
||||||
super(PresentationMediaItem, self).__init__(parent, plugin)
|
super(PresentationMediaItem, self).__init__(parent, plugin, Folder, Item)
|
||||||
|
|
||||||
def retranslate_ui(self):
|
def retranslate_ui(self):
|
||||||
"""
|
"""
|
||||||
The name of the plugin media displayed in UI
|
The name of the plugin media displayed in UI
|
||||||
"""
|
"""
|
||||||
|
super().retranslate_ui()
|
||||||
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
|
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
|
||||||
self.automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
|
self.automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
|
||||||
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
|
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
|
||||||
@ -80,35 +82,31 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
# Allow DnD from the desktop
|
# Allow DnD from the desktop
|
||||||
self.list_view.activateDnD()
|
self.list_view.activateDnD()
|
||||||
|
|
||||||
def build_file_mask_string(self):
|
|
||||||
"""
|
|
||||||
Build the list of file extensions to be used in the Open file dialog.
|
|
||||||
"""
|
|
||||||
file_type_string = ''
|
|
||||||
for controller in self.controllers:
|
|
||||||
if self.controllers[controller].enabled():
|
|
||||||
file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
|
|
||||||
for file_type in file_types:
|
|
||||||
if file_type not in file_type_string:
|
|
||||||
file_type_string += '*.{text} '.format(text=file_type)
|
|
||||||
file_type_string = file_type_string.strip()
|
|
||||||
self.service_manager.supported_suffixes(file_type_string.split(' '))
|
|
||||||
self.on_new_file_masks = translate('PresentationPlugin.MediaItem',
|
|
||||||
'Presentations ({text})').format(text=file_type_string)
|
|
||||||
|
|
||||||
def required_icons(self):
|
def required_icons(self):
|
||||||
"""
|
"""
|
||||||
Set which icons the media manager tab should show.
|
Set which icons the media manager tab should show.
|
||||||
"""
|
"""
|
||||||
MediaManagerItem.required_icons(self)
|
super().required_icons()
|
||||||
self.has_file_icon = True
|
self.has_file_icon = True
|
||||||
self.has_new_icon = False
|
self.has_new_icon = False
|
||||||
self.has_edit_icon = False
|
self.has_edit_icon = False
|
||||||
|
|
||||||
|
def initialise(self):
|
||||||
|
"""
|
||||||
|
Populate the media manager tab
|
||||||
|
"""
|
||||||
|
self.list_view.clear()
|
||||||
|
self.list_view.setIndentation(self.list_view.default_indentation)
|
||||||
|
self.list_view.allow_internal_dnd = True
|
||||||
|
self.list_view.setIconSize(QtCore.QSize(88, 50))
|
||||||
|
self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path), is_initial_load=True)
|
||||||
|
self.populate_display_types()
|
||||||
|
|
||||||
def add_middle_header_bar(self):
|
def add_middle_header_bar(self):
|
||||||
"""
|
"""
|
||||||
Display custom media manager items for presentations.
|
Display custom media manager items for presentations.
|
||||||
"""
|
"""
|
||||||
|
super().add_middle_header_bar()
|
||||||
self.presentation_widget = QtWidgets.QWidget(self)
|
self.presentation_widget = QtWidgets.QWidget(self)
|
||||||
self.presentation_widget.setObjectName('presentation_widget')
|
self.presentation_widget.setObjectName('presentation_widget')
|
||||||
self.display_layout = QtWidgets.QFormLayout(self.presentation_widget)
|
self.display_layout = QtWidgets.QFormLayout(self.presentation_widget)
|
||||||
@ -124,14 +122,21 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
# Add the Presentation widget to the page layout.
|
# Add the Presentation widget to the page layout.
|
||||||
self.page_layout.addWidget(self.presentation_widget)
|
self.page_layout.addWidget(self.presentation_widget)
|
||||||
|
|
||||||
def initialise(self):
|
def build_file_mask_string(self):
|
||||||
"""
|
"""
|
||||||
Populate the media manager tab
|
Build the list of file extensions to be used in the Open file dialog.
|
||||||
"""
|
"""
|
||||||
self.list_view.setIconSize(QtCore.QSize(88, 50))
|
file_type_string = ''
|
||||||
file_paths = self.settings.value('presentations/presentations files')
|
for controller in self.controllers:
|
||||||
self.load_list(file_paths, initial_load=True)
|
if self.controllers[controller].enabled():
|
||||||
self.populate_display_types()
|
file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
|
||||||
|
for file_type in file_types:
|
||||||
|
if file_type not in file_type_string:
|
||||||
|
file_type_string += '*.{text} '.format(text=file_type)
|
||||||
|
file_type_string = file_type_string.strip()
|
||||||
|
self.service_manager.supported_suffixes(file_type_string.split(' '))
|
||||||
|
self.on_new_file_masks = translate('PresentationPlugin.MediaItem',
|
||||||
|
'Presentations ({text})').format(text=file_type_string)
|
||||||
|
|
||||||
def populate_display_types(self):
|
def populate_display_types(self):
|
||||||
"""
|
"""
|
||||||
@ -154,96 +159,57 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
else:
|
else:
|
||||||
self.presentation_widget.hide()
|
self.presentation_widget.hide()
|
||||||
|
|
||||||
def load_list(self, file_paths, target_group=None, initial_load=False):
|
def load_item(self, item, is_initial_load=False):
|
||||||
"""
|
"""
|
||||||
Add presentations into the media manager. This is called both on initial load of the plugin to populate with
|
Given an item object, return a QTreeWidgetItem
|
||||||
existing files, and when the user adds new files via the media manager.
|
|
||||||
|
|
||||||
:param list[pathlib.Path] file_paths: List of file paths to add to the media manager.
|
|
||||||
:param list[pathlib.Path] target_group: Group to load.
|
|
||||||
:param boolean initial_load: Is this the initial load of the list at start up
|
|
||||||
"""
|
"""
|
||||||
current_paths = self.get_file_list()
|
tree_item = None
|
||||||
titles = [file_path.name for file_path in current_paths]
|
file_path = Path(item.file_path)
|
||||||
self.application.set_busy_cursor()
|
file_name = file_path.name
|
||||||
if not initial_load:
|
if not file_path.exists():
|
||||||
self.main_window.display_progress_bar(len(file_paths))
|
tree_item = QtWidgets.QTreeWidgetItem([file_name])
|
||||||
# Sort the presentations by its filename considering language specific characters.
|
tree_item.setIcon(0, UiIcons().delete)
|
||||||
file_paths.sort(key=lambda file_path: get_natural_key(file_path.name))
|
tree_item.setData(0, QtCore.Qt.UserRole, item)
|
||||||
for file_path in file_paths:
|
tree_item.setToolTip(0, str(file_path))
|
||||||
if not initial_load:
|
else:
|
||||||
self.main_window.increment_progress_bar()
|
controller_name = self.find_controller_by_type(file_path)
|
||||||
if current_paths.count(file_path) > 0:
|
if controller_name:
|
||||||
continue
|
controller = self.controllers[controller_name]
|
||||||
file_name = file_path.name
|
doc = controller.add_document(file_path)
|
||||||
if not file_path.exists():
|
thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
|
||||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
preview_path = doc.get_thumbnail_path(1, True)
|
||||||
item_name.setIcon(UiIcons().delete)
|
if not preview_path and not is_initial_load:
|
||||||
item_name.setData(QtCore.Qt.UserRole, file_path)
|
doc.load_presentation()
|
||||||
item_name.setToolTip(str(file_path))
|
|
||||||
self.list_view.addItem(item_name)
|
|
||||||
else:
|
|
||||||
if titles.count(file_name) > 0:
|
|
||||||
if not initial_load:
|
|
||||||
critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
|
|
||||||
translate('PresentationPlugin.MediaItem',
|
|
||||||
'A presentation with that filename already exists.'))
|
|
||||||
continue
|
|
||||||
controller_name = self.find_controller_by_type(file_path)
|
|
||||||
if controller_name:
|
|
||||||
controller = self.controllers[controller_name]
|
|
||||||
doc = controller.add_document(file_path)
|
|
||||||
thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
|
|
||||||
preview_path = doc.get_thumbnail_path(1, True)
|
preview_path = doc.get_thumbnail_path(1, True)
|
||||||
if not preview_path and not initial_load:
|
doc.close_presentation()
|
||||||
doc.load_presentation()
|
if not (preview_path and preview_path.exists()):
|
||||||
preview_path = doc.get_thumbnail_path(1, True)
|
icon = UiIcons().delete
|
||||||
doc.close_presentation()
|
|
||||||
if not (preview_path and preview_path.exists()):
|
|
||||||
icon = UiIcons().delete
|
|
||||||
else:
|
|
||||||
if validate_thumb(preview_path, thumbnail_path):
|
|
||||||
icon = build_icon(thumbnail_path)
|
|
||||||
else:
|
|
||||||
icon = create_thumb(preview_path, thumbnail_path)
|
|
||||||
else:
|
else:
|
||||||
if initial_load:
|
if validate_thumb(preview_path, thumbnail_path):
|
||||||
icon = UiIcons().delete
|
icon = build_icon(thumbnail_path)
|
||||||
else:
|
else:
|
||||||
critical_error_message_box(UiStrings().UnsupportedFile,
|
icon = create_thumb(preview_path, thumbnail_path)
|
||||||
translate('PresentationPlugin.MediaItem',
|
else:
|
||||||
'This type of presentation is not supported.'))
|
if is_initial_load:
|
||||||
continue
|
icon = UiIcons().delete
|
||||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
else:
|
||||||
item_name.setData(QtCore.Qt.UserRole, file_path)
|
critical_error_message_box(UiStrings().UnsupportedFile,
|
||||||
item_name.setIcon(icon)
|
translate('PresentationPlugin.MediaItem',
|
||||||
item_name.setToolTip(str(file_path))
|
'This type of presentation is not supported.'))
|
||||||
self.list_view.addItem(item_name)
|
return None
|
||||||
if not initial_load:
|
tree_item = QtWidgets.QTreeWidgetItem([file_name])
|
||||||
self.main_window.finished_progress_bar()
|
tree_item.setData(0, QtCore.Qt.UserRole, item)
|
||||||
self.application.set_normal_cursor()
|
tree_item.setIcon(0, icon)
|
||||||
|
tree_item.setToolTip(0, str(file_path))
|
||||||
|
return tree_item
|
||||||
|
|
||||||
def on_delete_click(self):
|
def delete_item(self, item):
|
||||||
"""
|
"""
|
||||||
Remove a presentation item from the list.
|
Remove a presentation item from the list.
|
||||||
"""
|
"""
|
||||||
if check_item_selected(self.list_view, UiStrings().SelectDelete):
|
file_path = Path(item.file_path)
|
||||||
items = self.list_view.selectedIndexes()
|
if file_path.exists():
|
||||||
row_list = [item.row() for item in items]
|
self.clean_up_thumbnails(file_path)
|
||||||
row_list.sort(reverse=True)
|
|
||||||
self.application.set_busy_cursor()
|
|
||||||
self.main_window.display_progress_bar(len(row_list))
|
|
||||||
for item in items:
|
|
||||||
file_path = item.data(QtCore.Qt.UserRole)
|
|
||||||
# cleaning thumbnails depends on being able to calculate sha256 hash of the actual file
|
|
||||||
if file_path.exists():
|
|
||||||
self.clean_up_thumbnails(file_path)
|
|
||||||
self.main_window.increment_progress_bar()
|
|
||||||
self.main_window.finished_progress_bar()
|
|
||||||
for row in row_list:
|
|
||||||
self.list_view.takeItem(row)
|
|
||||||
self.settings.setValue('presentations/presentations files', self.get_file_list())
|
|
||||||
self.application.set_normal_cursor()
|
|
||||||
|
|
||||||
def clean_up_thumbnails(self, file_path, clean_for_update=False):
|
def clean_up_thumbnails(self, file_path, clean_for_update=False):
|
||||||
"""
|
"""
|
||||||
@ -318,8 +284,10 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
items = self.list_view.selectedItems()
|
items = self.list_view.selectedItems()
|
||||||
if len(items) > 1:
|
if len(items) > 1:
|
||||||
return False
|
return False
|
||||||
|
items = [self.list_view.itemFromIndex(item) if isinstance(item, QtCore.QModelIndex) else item
|
||||||
|
for item in items]
|
||||||
if file_path is None:
|
if file_path is None:
|
||||||
file_path = Path(items[0].data(QtCore.Qt.UserRole))
|
file_path = Path(items[0].data(0, QtCore.Qt.UserRole).file_path)
|
||||||
file_type = file_path.suffix.lower()[1:]
|
file_type = file_path.suffix.lower()[1:]
|
||||||
if not self.display_type_combo_box.currentText():
|
if not self.display_type_combo_box.currentText():
|
||||||
return False
|
return False
|
||||||
@ -335,7 +303,7 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
service_item.theme = -1
|
service_item.theme = -1
|
||||||
for bitem in items:
|
for bitem in items:
|
||||||
if file_path is None:
|
if file_path is None:
|
||||||
file_path = bitem.data(QtCore.Qt.UserRole)
|
file_path = Path(bitem.data(0, QtCore.Qt.UserRole).file_path)
|
||||||
path, file_name = file_path.parent, file_path.name
|
path, file_name = file_path.parent, file_path.name
|
||||||
service_item.title = file_name
|
service_item.title = file_name
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
@ -372,7 +340,7 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
service_item.processor = self.display_type_combo_box.currentText()
|
service_item.processor = self.display_type_combo_box.currentText()
|
||||||
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
||||||
for bitem in items:
|
for bitem in items:
|
||||||
file_path = Path(bitem.data(QtCore.Qt.UserRole))
|
file_path = Path(bitem.data(0, QtCore.Qt.UserRole).file_path)
|
||||||
path, file_name = file_path.parent, file_path.name
|
path, file_name = file_path.parent, file_path.name
|
||||||
service_item.title = file_name
|
service_item.title = file_name
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
@ -448,20 +416,3 @@ class PresentationMediaItem(MediaManagerItem):
|
|||||||
if file_type in self.controllers[controller].also_supports:
|
if file_type in self.controllers[controller].also_supports:
|
||||||
return controller
|
return controller
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def search(self, string, show_error):
|
|
||||||
"""
|
|
||||||
Search in files
|
|
||||||
|
|
||||||
:param string: name to be found
|
|
||||||
:param show_error: not used
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
file_paths = self.settings.value('presentations/presentations files')
|
|
||||||
results = []
|
|
||||||
string = string.lower()
|
|
||||||
for file_path in file_paths:
|
|
||||||
file_name = file_path.name
|
|
||||||
if file_name.lower().find(string) > -1:
|
|
||||||
results.append([path_to_str(file_path), file_name])
|
|
||||||
return results
|
|
||||||
|
@ -26,12 +26,15 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
from openlp.core.common import extension_loader
|
from openlp.core.common import extension_loader, sha256_file_hash
|
||||||
from openlp.core.common.i18n import translate
|
from openlp.core.common.i18n import translate
|
||||||
from openlp.core.lib import build_icon
|
from openlp.core.lib import build_icon
|
||||||
|
from openlp.core.lib.db import Manager
|
||||||
from openlp.core.lib.plugin import Plugin, StringContent
|
from openlp.core.lib.plugin import Plugin, StringContent
|
||||||
from openlp.core.state import State
|
from openlp.core.state import State
|
||||||
from openlp.core.ui.icons import UiIcons
|
from openlp.core.ui.icons import UiIcons
|
||||||
|
|
||||||
|
from openlp.plugins.presentations.lib.db import Item, init_schema
|
||||||
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
|
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
|
||||||
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
||||||
from openlp.plugins.presentations.lib.presentationtab import PresentationTab
|
from openlp.plugins.presentations.lib.presentationtab import PresentationTab
|
||||||
@ -51,12 +54,13 @@ class PresentationPlugin(Plugin):
|
|||||||
"""
|
"""
|
||||||
PluginPresentation constructor.
|
PluginPresentation constructor.
|
||||||
"""
|
"""
|
||||||
log.debug('Initialised')
|
super().__init__('presentations', PresentationMediaItem)
|
||||||
self.controllers = {}
|
self.manager = Manager(plugin_name='media', init_schema=init_schema)
|
||||||
Plugin.__init__(self, 'presentations', None)
|
|
||||||
self.weight = -8
|
self.weight = -8
|
||||||
self.icon_path = UiIcons().presentation
|
self.icon_path = UiIcons().presentation
|
||||||
self.icon = build_icon(self.icon_path)
|
self.icon = build_icon(self.icon_path)
|
||||||
|
self.controllers = {}
|
||||||
|
self.dnd_id = 'Presentations'
|
||||||
State().add_service('presentation', self.weight, is_plugin=True)
|
State().add_service('presentation', self.weight, is_plugin=True)
|
||||||
State().update_pre_conditions('presentation', self.check_pre_conditions())
|
State().update_pre_conditions('presentation', self.check_pre_conditions())
|
||||||
|
|
||||||
@ -73,7 +77,25 @@ class PresentationPlugin(Plugin):
|
|||||||
Initialise the plugin. Determine which controllers are enabled are start their processes.
|
Initialise the plugin. Determine which controllers are enabled are start their processes.
|
||||||
"""
|
"""
|
||||||
log.info('Presentations Initialising')
|
log.info('Presentations Initialising')
|
||||||
super(PresentationPlugin, self).initialise()
|
# Check if the thumbnail scheme needs to be updated
|
||||||
|
has_old_scheme = False
|
||||||
|
if self.settings.value('presentations/thumbnail_scheme') != 'sha256file':
|
||||||
|
self.settings.setValue('presentations/thumbnail_scheme', 'sha256file')
|
||||||
|
has_old_scheme = True
|
||||||
|
# Migrate each file
|
||||||
|
presentation_paths = self.settings.value('presentations/presentations files') or []
|
||||||
|
for path in presentation_paths:
|
||||||
|
# check to see if the file exists before trying to process it.
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
item = Item(name=path.name, file_path=str(path))
|
||||||
|
self.media_item.clean_up_thumbnails(path, clean_for_update=True)
|
||||||
|
item.file_hash = sha256_file_hash(path)
|
||||||
|
if has_old_scheme:
|
||||||
|
self.media_item.update_thumbnail_scheme(path)
|
||||||
|
self.manager.save_object(item)
|
||||||
|
self.settings.remove('presentations/presentations files')
|
||||||
|
super().initialise()
|
||||||
for controller in self.controllers:
|
for controller in self.controllers:
|
||||||
if self.controllers[controller].enabled():
|
if self.controllers[controller].enabled():
|
||||||
try:
|
try:
|
||||||
@ -128,26 +150,6 @@ class PresentationPlugin(Plugin):
|
|||||||
self.register_controllers(controller)
|
self.register_controllers(controller)
|
||||||
return bool(self.controllers)
|
return bool(self.controllers)
|
||||||
|
|
||||||
def app_startup(self):
|
|
||||||
"""
|
|
||||||
Perform tasks on application startup.
|
|
||||||
"""
|
|
||||||
# TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in
|
|
||||||
# PresentationDocument.get_thumbnail_folder and PresentationDocument.get_temp_folder is removed
|
|
||||||
super().app_startup()
|
|
||||||
presentation_paths = self.settings.value('presentations/presentations files')
|
|
||||||
for path in presentation_paths:
|
|
||||||
# check to see if the file exists before trying to process it.
|
|
||||||
if path.exists():
|
|
||||||
self.media_item.clean_up_thumbnails(path, clean_for_update=True)
|
|
||||||
self.media_item.list_view.clear()
|
|
||||||
# Update the thumbnail scheme if needed
|
|
||||||
if self.settings.value('presentations/thumbnail_scheme') != 'sha256file':
|
|
||||||
for path in presentation_paths:
|
|
||||||
self.media_item.update_thumbnail_scheme(path)
|
|
||||||
self.settings.setValue('presentations/thumbnail_scheme', 'sha256file')
|
|
||||||
self.media_item.validate_and_load(presentation_paths)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def about():
|
def about():
|
||||||
"""
|
"""
|
||||||
|
@ -36,9 +36,9 @@ def test_delete_bible(settings):
|
|||||||
patch('openlp.plugins.bibles.lib.manager.delete_file', return_value=True) as mocked_delete_file:
|
patch('openlp.plugins.bibles.lib.manager.delete_file', return_value=True) as mocked_delete_file:
|
||||||
instance = BibleManager(MagicMock())
|
instance = BibleManager(MagicMock())
|
||||||
# We need to keep a reference to the mock for close_all as it gets set to None later on!
|
# We need to keep a reference to the mock for close_all as it gets set to None later on!
|
||||||
mocked_close_all = MagicMock()
|
mocked_close = MagicMock()
|
||||||
mocked_bible = MagicMock(file_path='KJV.sqlite', path=Path('bibles'),
|
mocked_bible = MagicMock(file_path='KJV.sqlite', path=Path('bibles'),
|
||||||
**{'session.close_all': mocked_close_all})
|
**{'session.close': mocked_close})
|
||||||
instance.db_cache = {'KJV': mocked_bible}
|
instance.db_cache = {'KJV': mocked_bible}
|
||||||
|
|
||||||
# WHEN: Calling delete_bible with 'KJV'
|
# WHEN: Calling delete_bible with 'KJV'
|
||||||
@ -47,6 +47,6 @@ def test_delete_bible(settings):
|
|||||||
# THEN: The session should have been closed and set to None, the bible should be deleted, and the result of
|
# THEN: The session should have been closed and set to None, the bible should be deleted, and the result of
|
||||||
# the deletion returned.
|
# the deletion returned.
|
||||||
assert result is True
|
assert result is True
|
||||||
mocked_close_all.assert_called_once_with()
|
mocked_close.assert_called_once_with()
|
||||||
assert mocked_bible.session is None
|
assert mocked_bible.session is None
|
||||||
mocked_delete_file.assert_called_once_with(Path('bibles') / 'KJV.sqlite')
|
mocked_delete_file.assert_called_once_with(Path('bibles') / 'KJV.sqlite')
|
||||||
|
@ -23,9 +23,11 @@ Test the media plugin
|
|||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, call, patch
|
||||||
|
|
||||||
|
from openlp.core.lib import MediaType
|
||||||
from openlp.core.common.registry import Registry
|
from openlp.core.common.registry import Registry
|
||||||
|
from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT
|
||||||
from openlp.plugins.media.lib.mediaitem import MediaMediaItem
|
from openlp.plugins.media.lib.mediaitem import MediaMediaItem
|
||||||
|
|
||||||
|
|
||||||
@ -37,9 +39,10 @@ def media_item(settings):
|
|||||||
Registry().register('main_window', mocked_main_window)
|
Registry().register('main_window', mocked_main_window)
|
||||||
Registry().register('live_controller', MagicMock())
|
Registry().register('live_controller', MagicMock())
|
||||||
mocked_plugin = MagicMock()
|
mocked_plugin = MagicMock()
|
||||||
with patch('openlp.plugins.media.lib.mediaitem.MediaManagerItem._setup'), \
|
with patch('openlp.plugins.media.lib.mediaitem.FolderLibraryItem._setup'), \
|
||||||
patch('openlp.plugins.media.lib.mediaitem.MediaMediaItem.setup_item'):
|
patch('openlp.plugins.media.lib.mediaitem.MediaMediaItem.setup_item'):
|
||||||
m_item = MediaMediaItem(None, mocked_plugin)
|
m_item = MediaMediaItem(None, mocked_plugin)
|
||||||
|
m_item.manager = MagicMock()
|
||||||
return m_item
|
return m_item
|
||||||
|
|
||||||
|
|
||||||
@ -48,21 +51,11 @@ def test_search_found(media_item):
|
|||||||
Media Remote Search Successful find
|
Media Remote Search Successful find
|
||||||
"""
|
"""
|
||||||
# GIVEN: The Mediaitem set up a list of media
|
# GIVEN: The Mediaitem set up a list of media
|
||||||
media_item.settings.setValue('media/media files', [Path('test.mp3'), Path('test.mp4')])
|
media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test.mp4')]
|
||||||
|
|
||||||
# WHEN: Retrieving the test file
|
# WHEN: Retrieving the test file
|
||||||
result = media_item.search('test.mp4', False)
|
result = media_item.search('test.mp4', False)
|
||||||
# THEN: a file should be found
|
|
||||||
assert result == [['test.mp4', 'test.mp4']], 'The result file contain the file name'
|
|
||||||
|
|
||||||
|
|
||||||
def test_search_found_mixed(media_item):
|
|
||||||
"""
|
|
||||||
Media Remote Search Successful find with Paths and Strings
|
|
||||||
"""
|
|
||||||
# GIVEN: The Mediaitem set up a list of media
|
|
||||||
media_item.settings.setValue('media/media files', [Path('test.mp3'), 'test.mp4'])
|
|
||||||
# WHEN: Retrieving the test file
|
|
||||||
result = media_item.search('test.mp4', False)
|
|
||||||
# THEN: a file should be found
|
# THEN: a file should be found
|
||||||
assert result == [['test.mp4', 'test.mp4']], 'The result file contain the file name'
|
assert result == [['test.mp4', 'test.mp4']], 'The result file contain the file name'
|
||||||
|
|
||||||
@ -72,8 +65,91 @@ def test_search_not_found(media_item):
|
|||||||
Media Remote Search not find
|
Media Remote Search not find
|
||||||
"""
|
"""
|
||||||
# GIVEN: The Mediaitem set up a list of media
|
# GIVEN: The Mediaitem set up a list of media
|
||||||
media_item.settings.setValue('media/media files', [Path('test.mp3'), Path('test.mp4')])
|
media_item.manager.get_all_objects.return_value = []
|
||||||
|
|
||||||
# WHEN: Retrieving the test file
|
# WHEN: Retrieving the test file
|
||||||
result = media_item.search('test.mpx', False)
|
result = media_item.search('test.mpx', False)
|
||||||
|
|
||||||
# THEN: a file should be found
|
# THEN: a file should be found
|
||||||
assert result == [], 'The result file should be empty'
|
assert result == [], 'The result file should be empty'
|
||||||
|
|
||||||
|
|
||||||
|
@patch('openlp.plugins.media.lib.mediaitem.or_')
|
||||||
|
@patch('openlp.plugins.media.lib.mediaitem.Item')
|
||||||
|
def test_get_list_audio(MockItem, mocked_or, media_item):
|
||||||
|
"""
|
||||||
|
Test the ``MediaMediaItem.get_list()`` method to return all audio items
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked Item class and some mocked items to return
|
||||||
|
media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test1.mp3'),
|
||||||
|
MagicMock(file_path='test2.ogg')]
|
||||||
|
MockItem.file_path.endswith.side_effect = lambda ext: ext
|
||||||
|
mocked_or.side_effect = lambda *ext: list(ext)
|
||||||
|
|
||||||
|
# WHEN: get_list() is called
|
||||||
|
media_list = media_item.get_list(MediaType.Audio)
|
||||||
|
|
||||||
|
# THEN: All the clauses should have been created, and the right extensions used
|
||||||
|
expected_extensions = [ext[1:] for ext in AUDIO_EXT]
|
||||||
|
expected_calls = [call(ext) for ext in expected_extensions]
|
||||||
|
MockItem.file_path.endswith.assert_has_calls(expected_calls)
|
||||||
|
mocked_or.assert_called_once_with(*expected_extensions)
|
||||||
|
media_item.manager.get_all_objects.assert_called_once_with(MockItem, expected_extensions)
|
||||||
|
assert media_list == [Path('test1.mp3'), Path('test2.ogg')]
|
||||||
|
|
||||||
|
|
||||||
|
@patch('openlp.plugins.media.lib.mediaitem.or_')
|
||||||
|
@patch('openlp.plugins.media.lib.mediaitem.Item')
|
||||||
|
def test_get_list_video(MockItem, mocked_or, media_item):
|
||||||
|
"""
|
||||||
|
Test the ``MediaMediaItem.get_list()`` method to return all audio items
|
||||||
|
"""
|
||||||
|
# GIVEN: A mocked Item class and some mocked items to return
|
||||||
|
media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test1.mp4'),
|
||||||
|
MagicMock(file_path='test2.ogv')]
|
||||||
|
MockItem.file_path.endswith.side_effect = lambda ext: ext
|
||||||
|
mocked_or.side_effect = lambda *ext: list(ext)
|
||||||
|
|
||||||
|
# WHEN: get_list() is called
|
||||||
|
media_list = media_item.get_list(MediaType.Video)
|
||||||
|
|
||||||
|
# THEN: All the clauses should have been created, and the right extensions used
|
||||||
|
expected_extensions = [ext[1:] for ext in VIDEO_EXT]
|
||||||
|
expected_calls = [call(ext) for ext in expected_extensions]
|
||||||
|
MockItem.file_path.endswith.assert_has_calls(expected_calls)
|
||||||
|
mocked_or.assert_called_once_with(*expected_extensions)
|
||||||
|
media_item.manager.get_all_objects.assert_called_once_with(MockItem, expected_extensions)
|
||||||
|
assert media_list == [Path('test1.mp4'), Path('test2.ogv')]
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_optical_clip(media_item):
|
||||||
|
"""Test that the ``MediaMediaItem.add_optical_clip()`` method calls validate_and_load()"""
|
||||||
|
# GIVEN: A MediaMediaItem object, with a mocked out `validate_and_load` method
|
||||||
|
with patch.object(media_item, 'validate_and_load') as mocked_validate_and_load:
|
||||||
|
# WHEN: add_optical_clip is called
|
||||||
|
media_item.add_optical_clip('optical:/dev/sr0')
|
||||||
|
|
||||||
|
# THEN: The mocked method should have been called
|
||||||
|
mocked_validate_and_load.assert_called_once_with(['optical:/dev/sr0'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_device_stream(media_item):
|
||||||
|
"""Test that the ``MediaMediaItem.add_device_stream()`` method calls validate_and_load()"""
|
||||||
|
# GIVEN: A MediaMediaItem object, with a mocked out `validate_and_load` method
|
||||||
|
with patch.object(media_item, 'validate_and_load') as mocked_validate_and_load:
|
||||||
|
# WHEN: add_device_stream is called
|
||||||
|
media_item.add_device_stream('devicestream:/dev/v4l')
|
||||||
|
|
||||||
|
# THEN: The mocked method should have been called
|
||||||
|
mocked_validate_and_load.assert_called_once_with(['devicestream:/dev/v4l'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_network_stream(media_item):
|
||||||
|
"""Test that the ``MediaMediaItem.add_network_stream()`` method calls validate_and_load()"""
|
||||||
|
# GIVEN: A MediaMediaItem object, with a mocked out `validate_and_load` method
|
||||||
|
with patch.object(media_item, 'validate_and_load') as mocked_validate_and_load:
|
||||||
|
# WHEN: add_network_stream is called
|
||||||
|
media_item.add_network_stream('networkstream:rmtp://localhost')
|
||||||
|
|
||||||
|
# THEN: The mocked method should have been called
|
||||||
|
mocked_validate_and_load.assert_called_once_with(['networkstream:rmtp://localhost'])
|
||||||
|
@ -29,13 +29,13 @@ from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def media_item(settings):
|
def media_item(settings, mock_plugin):
|
||||||
"""Local test setup"""
|
"""Local test setup"""
|
||||||
Registry().register('service_manager', MagicMock())
|
Registry().register('service_manager', MagicMock())
|
||||||
Registry().register('main_window', MagicMock())
|
Registry().register('main_window', MagicMock())
|
||||||
with patch('openlp.plugins.presentations.lib.mediaitem.MediaManagerItem._setup'), \
|
with patch('openlp.plugins.presentations.lib.mediaitem.FolderLibraryItem._setup'), \
|
||||||
patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item'):
|
patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item'):
|
||||||
m_item = PresentationMediaItem(None, MagicMock, MagicMock())
|
m_item = PresentationMediaItem(None, mock_plugin, MagicMock())
|
||||||
m_item.settings_section = 'media'
|
m_item.settings_section = 'media'
|
||||||
return m_item
|
return m_item
|
||||||
|
|
||||||
@ -44,4 +44,5 @@ def media_item(settings):
|
|||||||
def mock_plugin(temp_folder):
|
def mock_plugin(temp_folder):
|
||||||
m_plugin = MagicMock()
|
m_plugin = MagicMock()
|
||||||
m_plugin.settings_section = temp_folder
|
m_plugin.settings_section = temp_folder
|
||||||
|
m_plugin.manager = MagicMock()
|
||||||
yield m_plugin
|
yield m_plugin
|
||||||
|
@ -24,7 +24,6 @@ This module contains tests for the lib submodule of the Presentations plugin.
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, PropertyMock, call, patch
|
from unittest.mock import MagicMock, PropertyMock, call, patch
|
||||||
|
|
||||||
from openlp.core.common.registry import Registry
|
|
||||||
from openlp.core.lib import ServiceItemContext
|
from openlp.core.lib import ServiceItemContext
|
||||||
from openlp.core.lib.serviceitem import ItemCapabilities
|
from openlp.core.lib.serviceitem import ItemCapabilities
|
||||||
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
|
||||||
@ -146,24 +145,37 @@ def test_pdf_generate_slide_data(media_item):
|
|||||||
mocked_service_item.add_capability.assert_any_call(ItemCapabilities.ProvidesOwnTheme)
|
mocked_service_item.add_capability.assert_any_call(ItemCapabilities.ProvidesOwnTheme)
|
||||||
|
|
||||||
|
|
||||||
@patch('openlp.plugins.presentations.lib.mediaitem.MediaManagerItem._setup')
|
@patch('openlp.plugins.presentations.lib.mediaitem.FolderLibraryItem._setup')
|
||||||
@patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item')
|
@patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item')
|
||||||
def test_search(mock_setup, mock_item, registry):
|
def test_search_found(mock_setup, mock_item, registry):
|
||||||
"""
|
"""
|
||||||
Test that the search method finds the correct results
|
Test that the search method works correctly
|
||||||
"""
|
"""
|
||||||
# GIVEN: A mocked Settings class which returns a list of Path objects,
|
# GIVEN: The Mediaitem set up a list of presentations
|
||||||
# and an instance of the PresentationMediaItem
|
|
||||||
path_1 = Path('some_dir', 'Impress_file_1')
|
|
||||||
path_2 = Path('some_other_dir', 'impress_file_2')
|
|
||||||
path_3 = Path('another_dir', 'ppt_file')
|
|
||||||
mocked_returned_settings = MagicMock()
|
|
||||||
mocked_returned_settings.value.return_value = [path_1, path_2, path_3]
|
|
||||||
Registry().register('settings', mocked_returned_settings)
|
|
||||||
media_item = PresentationMediaItem(None, MagicMock(), None)
|
media_item = PresentationMediaItem(None, MagicMock(), None)
|
||||||
|
media_item.manager = MagicMock()
|
||||||
|
media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test.odp')]
|
||||||
|
|
||||||
# WHEN: Calling search
|
# WHEN: Retrieving the test file
|
||||||
results = media_item.search('IMPRE', False)
|
result = media_item.search('test.odp', False)
|
||||||
|
|
||||||
# THEN: The first two results should have been returned
|
# THEN: a file should be found
|
||||||
assert results == [[str(path_1), 'Impress_file_1'], [str(path_2), 'impress_file_2']]
|
assert result == [['test.odp', 'test.odp']], 'The result file contain the file name'
|
||||||
|
|
||||||
|
|
||||||
|
@patch('openlp.plugins.presentations.lib.mediaitem.FolderLibraryItem._setup')
|
||||||
|
@patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item')
|
||||||
|
def test_search_not_found(mock_setup, mock_item, registry):
|
||||||
|
"""
|
||||||
|
Test that the search doesn't find anything
|
||||||
|
"""
|
||||||
|
# GIVEN: The Mediaitem set up a list of media
|
||||||
|
media_item = PresentationMediaItem(None, MagicMock(), None)
|
||||||
|
media_item.manager = MagicMock()
|
||||||
|
media_item.manager.get_all_objects.return_value = []
|
||||||
|
|
||||||
|
# WHEN: Retrieving the test file
|
||||||
|
result = media_item.search('test.pptx', False)
|
||||||
|
|
||||||
|
# THEN: a file should be found
|
||||||
|
assert result == [], 'The result file should be empty'
|
||||||
|
Loading…
Reference in New Issue
Block a user