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.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.AddGroupDot = translate('OpenLP.Ui', 'Add group.')
|
||||
self.Advanced = translate('OpenLP.Ui', 'Advanced')
|
||||
|
@ -278,6 +278,11 @@ class Settings(QtCore.QSettings):
|
||||
'media/vlc arguments': '',
|
||||
'media/live volume': 50,
|
||||
'media/preview volume': 0,
|
||||
'media/db type': 'sqlite',
|
||||
'media/db username': '',
|
||||
'media/db password': '',
|
||||
'media/db hostname': '',
|
||||
'media/db database': '',
|
||||
'players/background color': '#000000',
|
||||
'planningcenter/status': PluginStatus.Inactive,
|
||||
'planningcenter/application_id': '',
|
||||
@ -296,6 +301,11 @@ class Settings(QtCore.QSettings):
|
||||
'presentations/powerpoint control window': QtCore.Qt.Unchecked,
|
||||
'presentations/impress use display setting': QtCore.Qt.Unchecked,
|
||||
'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 file': None,
|
||||
'servicemanager/service theme': None,
|
||||
|
@ -30,10 +30,11 @@ from urllib.parse import quote_plus as urlquote
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
from sqlalchemy import Column, MetaData, Table, UnicodeText, create_engine, types
|
||||
from sqlalchemy.engine.url import make_url, URL
|
||||
from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table, Unicode, UnicodeText, create_engine, types
|
||||
from sqlalchemy.engine.url import URL, make_url
|
||||
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 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.lib.ui import critical_error_message_box
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -224,6 +224,49 @@ def get_upgrade_op(session):
|
||||
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):
|
||||
"""
|
||||
BaseModel provides a base object with a set of generic functions
|
||||
@ -494,16 +537,19 @@ class Manager(object):
|
||||
if try_count >= 2:
|
||||
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
|
||||
|
||||
:param object_class: The type of object to return
|
||||
: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):
|
||||
try:
|
||||
return self.session.query(object_class).filter(filter_clause).first()
|
||||
return query.first()
|
||||
except OperationalError as oe:
|
||||
# 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
|
||||
|
@ -21,13 +21,12 @@
|
||||
"""
|
||||
Provides the generic functions for interfacing plugins with the Media Manager.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
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.lib import ServiceItemContext
|
||||
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.views import ListWidgetWithDnD
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
class MediaManagerItem(QtWidgets.QWidget, RegistryProperties, LogMixin):
|
||||
"""
|
||||
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
|
||||
(treat the filename as an image).
|
||||
"""
|
||||
log.info('Media Item loaded')
|
||||
|
||||
def __init__(self, parent=None, plugin=None):
|
||||
"""
|
||||
@ -212,12 +208,16 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
self.add_actionlist_to_toolbar(toolbar_actions)
|
||||
|
||||
def add_actionlist_to_toolbar(self, toolbar_actions):
|
||||
for action in toolbar_actions:
|
||||
self.toolbar.add_toolbar_action('{name}{action}Action'.format(name=self.plugin.name, action=action[0]),
|
||||
text=self.plugin.get_string(action[1])['title'],
|
||||
icon=action[2],
|
||||
tooltip=self.plugin.get_string(action[1])['tooltip'],
|
||||
triggers=action[3])
|
||||
added_actions = []
|
||||
for action, plugin, icon, triggers in toolbar_actions:
|
||||
added_actions.append(self.toolbar.add_toolbar_action(
|
||||
'{name}{action}Action'.format(name=self.plugin.name, action=action),
|
||||
text=self.plugin.get_string(plugin)['title'],
|
||||
icon=icon,
|
||||
tooltip=self.plugin.get_string(plugin)['tooltip'],
|
||||
triggers=triggers
|
||||
))
|
||||
return added_actions
|
||||
|
||||
def add_list_view_to_toolbar(self):
|
||||
"""
|
||||
@ -350,7 +350,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
self, self.on_new_prompt,
|
||||
self.settings.value(self.settings_section + '/last directory'),
|
||||
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:
|
||||
self.application.set_busy_cursor()
|
||||
self.validate_and_load(file_paths)
|
||||
@ -441,7 +441,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
file_paths = []
|
||||
for index in range(self.list_view.count()):
|
||||
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)
|
||||
return file_paths
|
||||
|
||||
@ -523,7 +523,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
translate('OpenLP.MediaManagerItem',
|
||||
'You must select one or more items to preview.'))
|
||||
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)
|
||||
service_item = self.build_service_item()
|
||||
if service_item:
|
||||
@ -558,7 +558,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
:param item_id: item to make live
|
||||
: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
|
||||
if 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
|
||||
# multiple service items?
|
||||
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)
|
||||
else:
|
||||
items = self.list_view.selectedIndexes()
|
||||
@ -634,7 +634,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
translate('OpenLP.MediaManagerItem',
|
||||
'You must select one or more items.'))
|
||||
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()
|
||||
if not service_item:
|
||||
QtWidgets.QMessageBox.information(self, UiStrings().NISs,
|
||||
|
@ -36,10 +36,10 @@ The Projector table keeps track of entries for controlled projectors.
|
||||
import logging
|
||||
|
||||
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 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.constants import PJLINK_DEFAULT_CODES
|
||||
|
||||
@ -51,17 +51,6 @@ log.debug('projector.lib.db module loaded')
|
||||
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):
|
||||
"""
|
||||
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'},
|
||||
'exception': {'icon': 'fa.times-circle'},
|
||||
'exit': {'icon': 'fa.sign-out'},
|
||||
'folder': {'icon': 'fa.folder'},
|
||||
'group': {'icon': 'fa.object-group'},
|
||||
'inactive': {'icon': 'fa.child', 'attr': 'lightGray'},
|
||||
'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:
|
||||
self.drop_position = get_parent_item_data(item) - 1
|
||||
Registry().execute('{plugin}_add_service_item'.format(plugin=plugin), replace)
|
||||
else:
|
||||
self.log_warning('Unrecognised item')
|
||||
|
||||
def update_theme_list(self, theme_list):
|
||||
"""
|
||||
|
@ -18,6 +18,7 @@
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import gc
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
@ -141,7 +142,9 @@ class BibleManager(LogMixin, RegistryProperties):
|
||||
name = bible.get_name()
|
||||
# Remove corrupted files.
|
||||
if name is None:
|
||||
bible.session.close_all()
|
||||
bible.session.close()
|
||||
bible.session = None
|
||||
gc.collect()
|
||||
delete_file(self.path / file_path)
|
||||
continue
|
||||
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))
|
||||
bible = self.db_cache[name]
|
||||
bible.session.close_all()
|
||||
bible.session.close()
|
||||
bible.session = None
|
||||
gc.collect()
|
||||
return delete_file(bible.path / '{name}{suffix}'.format(name=name, suffix=self.suffix))
|
||||
|
||||
def get_bibles(self):
|
||||
|
@ -97,7 +97,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
"""
|
||||
Set which icons the media manager tab should show.
|
||||
"""
|
||||
MediaManagerItem.required_icons(self)
|
||||
super().required_icons()
|
||||
self.has_file_icon = True
|
||||
self.has_new_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 os
|
||||
from pathlib import Path
|
||||
|
||||
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.i18n import UiStrings, get_natural_key, translate
|
||||
from openlp.core.common.mixins import RegistryProperties
|
||||
from openlp.core.common.path import create_paths, path_to_str
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
from openlp.core.common.path import create_paths
|
||||
from openlp.core.common.registry import Registry
|
||||
from openlp.core.lib import MediaType, ServiceItemContext, check_item_selected
|
||||
from openlp.core.lib.mediamanageritem import MediaManagerItem
|
||||
from openlp.core.lib import MediaType, ServiceItemContext
|
||||
from openlp.core.lib.plugin import StringContent
|
||||
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.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.vlcplayer import get_vlc
|
||||
|
||||
from openlp.plugins.media.lib.db import Folder, Item
|
||||
|
||||
if get_vlc() is not None:
|
||||
from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm
|
||||
from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm
|
||||
@ -47,7 +52,7 @@ if get_vlc() is not None:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
class MediaMediaItem(FolderLibraryItem):
|
||||
"""
|
||||
This is the custom media manager item for Media Slides.
|
||||
"""
|
||||
@ -57,7 +62,28 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
|
||||
def __init__(self, parent, plugin):
|
||||
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):
|
||||
"""
|
||||
@ -69,33 +95,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
self.error_icon = UiIcons().delete
|
||||
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):
|
||||
"""
|
||||
Set which icons the media manager tab should show
|
||||
"""
|
||||
MediaManagerItem.required_icons(self)
|
||||
super().required_icons()
|
||||
self.has_file_icon = True
|
||||
self.has_new_icon = False
|
||||
self.has_edit_icon = False
|
||||
@ -106,37 +110,126 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
if State().check_preconditions('media_live'):
|
||||
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):
|
||||
"""
|
||||
Creates the main widget for listing items.
|
||||
"""
|
||||
MediaManagerItem.add_list_view_to_toolbar(self)
|
||||
# self.list_view.addAction(self.replace_action)
|
||||
super().add_list_view_to_toolbar()
|
||||
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'):
|
||||
optical_button_text = 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,
|
||||
text=optical_button_text,
|
||||
tooltip=optical_button_tooltip,
|
||||
triggers=self.on_load_optical)
|
||||
self.load_optical = create_action(self, 'load_optical',
|
||||
icon=UiIcons().optical,
|
||||
text=optical_button_text,
|
||||
tooltip=optical_button_tooltip,
|
||||
triggers=self.on_load_optical)
|
||||
device_stream_button_text = 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,
|
||||
text=device_stream_button_text,
|
||||
tooltip=device_stream_button_tooltip,
|
||||
triggers=self.on_open_device_stream)
|
||||
self.open_stream = create_action(self, 'open_device_stream',
|
||||
icon=UiIcons().device_stream,
|
||||
text=device_stream_button_text,
|
||||
tooltip=device_stream_button_tooltip,
|
||||
triggers=self.on_open_device_stream)
|
||||
network_stream_button_text = 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',
|
||||
icon=UiIcons().network_stream,
|
||||
text=network_stream_button_text,
|
||||
tooltip=network_stream_button_tooltip,
|
||||
triggers=self.on_open_network_stream)
|
||||
self.open_network_stream = create_action(self, 'open_network_stream',
|
||||
icon=UiIcons().network_stream,
|
||||
text=network_stream_button_text,
|
||||
tooltip=network_stream_button_tooltip,
|
||||
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,
|
||||
**kwargs):
|
||||
@ -153,7 +246,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
item = self.list_view.currentItem()
|
||||
if item is None:
|
||||
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
|
||||
if filename.startswith('optical:'):
|
||||
(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()
|
||||
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):
|
||||
"""
|
||||
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),
|
||||
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,
|
||||
translate('MediaPlugin.MediaItem', 'You must select a media file to delete.')):
|
||||
row_list = [item.row() for item in self.list_view.selectedIndexes()]
|
||||
row_list.sort(reverse=True)
|
||||
for row in row_list:
|
||||
self.list_view.takeItem(row)
|
||||
self.settings.setValue('media/media files', self.get_file_list())
|
||||
if isinstance(filename, Path):
|
||||
name = filename.name
|
||||
filename = str(filename)
|
||||
elif filename.startswith('optical:'):
|
||||
# Handle optical based item
|
||||
_, _, _, _, _, _, name = parse_optical_path(filename)
|
||||
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
|
||||
:param target_group:
|
||||
def delete_item(self, item):
|
||||
"""
|
||||
media.sort(key=lambda file_path: get_natural_key(os.path.split(str(file_path))[1]))
|
||||
for track in media:
|
||||
track_str = str(track)
|
||||
track_info = QtCore.QFileInfo(track_str)
|
||||
item_name = 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)
|
||||
item_name = QtWidgets.QListWidgetItem(clip_name)
|
||||
item_name.setIcon(UiIcons().optical)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip('{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)
|
||||
item_name = QtWidgets.QListWidgetItem(name)
|
||||
if track_str.startswith('devicestream:'):
|
||||
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)
|
||||
Delete any and all thumbnails and things associated with this media item
|
||||
"""
|
||||
delete_file(self.service_path / Path(item.file_path).name)
|
||||
|
||||
def format_search_result(self, item):
|
||||
"""
|
||||
Format an item for the search results
|
||||
|
||||
: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
|
||||
"""
|
||||
if Path(item.file_path).exists():
|
||||
return [item.file_path, Path(item.file_path).name]
|
||||
elif item.file_path.startswith('device'):
|
||||
(name, _, _) = parse_stream_path(item.file_path)
|
||||
return [item.file_path, name]
|
||||
else:
|
||||
return super().format_search_result(item)
|
||||
|
||||
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.
|
||||
: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:
|
||||
extension = AUDIO_EXT
|
||||
extensions = AUDIO_EXT
|
||||
else:
|
||||
extension = VIDEO_EXT
|
||||
extension = [x[1:] for x in extension]
|
||||
media = [x for x in media_file_paths if os.path.splitext(x)[1] in extension]
|
||||
return media
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
extensions = VIDEO_EXT
|
||||
clauses = []
|
||||
for extension in extensions:
|
||||
# Drop the initial * and add to the list of clauses
|
||||
clauses.append(Item.file_path.endswith(extension[1:]))
|
||||
items = self.manager.get_all_objects(Item, or_(*clauses))
|
||||
return [Path(item.file_path) for item in items]
|
||||
|
||||
def on_load_optical(self):
|
||||
"""
|
||||
@ -349,16 +436,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
|
||||
:param optical: The clip to add.
|
||||
"""
|
||||
file_paths = self.get_file_list()
|
||||
# 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)
|
||||
self.validate_and_load([str(optical)])
|
||||
|
||||
def on_open_device_stream(self):
|
||||
"""
|
||||
@ -378,16 +456,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
|
||||
:param stream: The clip to add.
|
||||
"""
|
||||
file_paths = self.get_file_list()
|
||||
# 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)
|
||||
self.validate_and_load([str(stream)])
|
||||
|
||||
def on_open_network_stream(self):
|
||||
"""
|
||||
@ -407,13 +476,4 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
|
||||
:param stream: The clip to add.
|
||||
"""
|
||||
file_paths = self.get_file_list()
|
||||
# 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)
|
||||
self.validate_and_load([str(stream)])
|
||||
|
@ -22,12 +22,19 @@
|
||||
The Media plugin
|
||||
"""
|
||||
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.ui.icons import UiIcons
|
||||
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.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
|
||||
|
||||
|
||||
@ -43,7 +50,8 @@ class MediaPlugin(Plugin):
|
||||
log.info('{name} MediaPlugin loaded'.format(name=__name__))
|
||||
|
||||
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.icon_path = UiIcons().video
|
||||
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
|
||||
"""
|
||||
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()
|
||||
|
||||
def app_startup(self):
|
||||
"""
|
||||
Override app_startup() in order to do nothing
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_pre_conditions(self):
|
||||
"""
|
||||
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 openlp.core.common import sha256_file_hash
|
||||
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
|
||||
from openlp.core.common.path import path_to_str
|
||||
from openlp.core.common.i18n import UiStrings, translate
|
||||
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.mediamanageritem import MediaManagerItem
|
||||
from openlp.core.lib import ServiceItemContext, build_icon, create_thumb, validate_thumb
|
||||
from openlp.core.ui.library import FolderLibraryItem
|
||||
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.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.pdfcontroller import PDF_CONTROLLER_FILETYPES
|
||||
|
||||
@ -41,7 +42,7 @@ from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETY
|
||||
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
|
||||
Powerpoint
|
||||
@ -56,12 +57,13 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
"""
|
||||
self.icon_path = 'presentations/presentation'
|
||||
self.controllers = controllers
|
||||
super(PresentationMediaItem, self).__init__(parent, plugin)
|
||||
super(PresentationMediaItem, self).__init__(parent, plugin, Folder, Item)
|
||||
|
||||
def retranslate_ui(self):
|
||||
"""
|
||||
The name of the plugin media displayed in UI
|
||||
"""
|
||||
super().retranslate_ui()
|
||||
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
|
||||
self.automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
|
||||
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
|
||||
@ -80,35 +82,31 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
# Allow DnD from the desktop
|
||||
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):
|
||||
"""
|
||||
Set which icons the media manager tab should show.
|
||||
"""
|
||||
MediaManagerItem.required_icons(self)
|
||||
super().required_icons()
|
||||
self.has_file_icon = True
|
||||
self.has_new_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):
|
||||
"""
|
||||
Display custom media manager items for presentations.
|
||||
"""
|
||||
super().add_middle_header_bar()
|
||||
self.presentation_widget = QtWidgets.QWidget(self)
|
||||
self.presentation_widget.setObjectName('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.
|
||||
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_paths = self.settings.value('presentations/presentations files')
|
||||
self.load_list(file_paths, initial_load=True)
|
||||
self.populate_display_types()
|
||||
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 populate_display_types(self):
|
||||
"""
|
||||
@ -154,96 +159,57 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
else:
|
||||
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
|
||||
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
|
||||
Given an item object, return a QTreeWidgetItem
|
||||
"""
|
||||
current_paths = self.get_file_list()
|
||||
titles = [file_path.name for file_path in current_paths]
|
||||
self.application.set_busy_cursor()
|
||||
if not initial_load:
|
||||
self.main_window.display_progress_bar(len(file_paths))
|
||||
# Sort the presentations by its filename considering language specific characters.
|
||||
file_paths.sort(key=lambda file_path: get_natural_key(file_path.name))
|
||||
for file_path in file_paths:
|
||||
if not initial_load:
|
||||
self.main_window.increment_progress_bar()
|
||||
if current_paths.count(file_path) > 0:
|
||||
continue
|
||||
file_name = file_path.name
|
||||
if not file_path.exists():
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
item_name.setIcon(UiIcons().delete)
|
||||
item_name.setData(QtCore.Qt.UserRole, file_path)
|
||||
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'
|
||||
tree_item = None
|
||||
file_path = Path(item.file_path)
|
||||
file_name = file_path.name
|
||||
if not file_path.exists():
|
||||
tree_item = QtWidgets.QTreeWidgetItem([file_name])
|
||||
tree_item.setIcon(0, UiIcons().delete)
|
||||
tree_item.setData(0, QtCore.Qt.UserRole, item)
|
||||
tree_item.setToolTip(0, str(file_path))
|
||||
else:
|
||||
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)
|
||||
if not preview_path and not is_initial_load:
|
||||
doc.load_presentation()
|
||||
preview_path = doc.get_thumbnail_path(1, True)
|
||||
if not preview_path and not initial_load:
|
||||
doc.load_presentation()
|
||||
preview_path = doc.get_thumbnail_path(1, True)
|
||||
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)
|
||||
doc.close_presentation()
|
||||
if not (preview_path and preview_path.exists()):
|
||||
icon = UiIcons().delete
|
||||
else:
|
||||
if initial_load:
|
||||
icon = UiIcons().delete
|
||||
if validate_thumb(preview_path, thumbnail_path):
|
||||
icon = build_icon(thumbnail_path)
|
||||
else:
|
||||
critical_error_message_box(UiStrings().UnsupportedFile,
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'This type of presentation is not supported.'))
|
||||
continue
|
||||
item_name = QtWidgets.QListWidgetItem(file_name)
|
||||
item_name.setData(QtCore.Qt.UserRole, file_path)
|
||||
item_name.setIcon(icon)
|
||||
item_name.setToolTip(str(file_path))
|
||||
self.list_view.addItem(item_name)
|
||||
if not initial_load:
|
||||
self.main_window.finished_progress_bar()
|
||||
self.application.set_normal_cursor()
|
||||
icon = create_thumb(preview_path, thumbnail_path)
|
||||
else:
|
||||
if is_initial_load:
|
||||
icon = UiIcons().delete
|
||||
else:
|
||||
critical_error_message_box(UiStrings().UnsupportedFile,
|
||||
translate('PresentationPlugin.MediaItem',
|
||||
'This type of presentation is not supported.'))
|
||||
return None
|
||||
tree_item = QtWidgets.QTreeWidgetItem([file_name])
|
||||
tree_item.setData(0, QtCore.Qt.UserRole, item)
|
||||
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.
|
||||
"""
|
||||
if check_item_selected(self.list_view, UiStrings().SelectDelete):
|
||||
items = self.list_view.selectedIndexes()
|
||||
row_list = [item.row() for item in items]
|
||||
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()
|
||||
file_path = Path(item.file_path)
|
||||
if file_path.exists():
|
||||
self.clean_up_thumbnails(file_path)
|
||||
|
||||
def clean_up_thumbnails(self, file_path, clean_for_update=False):
|
||||
"""
|
||||
@ -318,8 +284,10 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
items = self.list_view.selectedItems()
|
||||
if len(items) > 1:
|
||||
return False
|
||||
items = [self.list_view.itemFromIndex(item) if isinstance(item, QtCore.QModelIndex) else item
|
||||
for item in items]
|
||||
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:]
|
||||
if not self.display_type_combo_box.currentText():
|
||||
return False
|
||||
@ -335,7 +303,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
service_item.theme = -1
|
||||
for bitem in items:
|
||||
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
|
||||
service_item.title = file_name
|
||||
if file_path.exists():
|
||||
@ -372,7 +340,7 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
service_item.processor = self.display_type_combo_box.currentText()
|
||||
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
|
||||
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
|
||||
service_item.title = file_name
|
||||
if file_path.exists():
|
||||
@ -448,20 +416,3 @@ class PresentationMediaItem(MediaManagerItem):
|
||||
if file_type in self.controllers[controller].also_supports:
|
||||
return controller
|
||||
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
|
||||
|
||||
|
||||
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.lib import build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.plugin import Plugin, StringContent
|
||||
from openlp.core.state import State
|
||||
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.mediaitem import PresentationMediaItem
|
||||
from openlp.plugins.presentations.lib.presentationtab import PresentationTab
|
||||
@ -51,12 +54,13 @@ class PresentationPlugin(Plugin):
|
||||
"""
|
||||
PluginPresentation constructor.
|
||||
"""
|
||||
log.debug('Initialised')
|
||||
self.controllers = {}
|
||||
Plugin.__init__(self, 'presentations', None)
|
||||
super().__init__('presentations', PresentationMediaItem)
|
||||
self.manager = Manager(plugin_name='media', init_schema=init_schema)
|
||||
self.weight = -8
|
||||
self.icon_path = UiIcons().presentation
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.controllers = {}
|
||||
self.dnd_id = 'Presentations'
|
||||
State().add_service('presentation', self.weight, is_plugin=True)
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
if self.controllers[controller].enabled():
|
||||
try:
|
||||
@ -128,26 +150,6 @@ class PresentationPlugin(Plugin):
|
||||
self.register_controllers(controller)
|
||||
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
|
||||
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:
|
||||
instance = BibleManager(MagicMock())
|
||||
# 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'),
|
||||
**{'session.close_all': mocked_close_all})
|
||||
**{'session.close': mocked_close})
|
||||
instance.db_cache = {'KJV': mocked_bible}
|
||||
|
||||
# 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
|
||||
# the deletion returned.
|
||||
assert result is True
|
||||
mocked_close_all.assert_called_once_with()
|
||||
mocked_close.assert_called_once_with()
|
||||
assert mocked_bible.session is None
|
||||
mocked_delete_file.assert_called_once_with(Path('bibles') / 'KJV.sqlite')
|
||||
|
@ -23,9 +23,11 @@ Test the media plugin
|
||||
"""
|
||||
import pytest
|
||||
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.ui.media import AUDIO_EXT, VIDEO_EXT
|
||||
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('live_controller', 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'):
|
||||
m_item = MediaMediaItem(None, mocked_plugin)
|
||||
m_item.manager = MagicMock()
|
||||
return m_item
|
||||
|
||||
|
||||
@ -48,21 +51,11 @@ def test_search_found(media_item):
|
||||
Media Remote Search Successful find
|
||||
"""
|
||||
# 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
|
||||
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
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
result = media_item.search('test.mpx', False)
|
||||
|
||||
# THEN: a file should be found
|
||||
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
|
||||
def media_item(settings):
|
||||
def media_item(settings, mock_plugin):
|
||||
"""Local test setup"""
|
||||
Registry().register('service_manager', 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'):
|
||||
m_item = PresentationMediaItem(None, MagicMock, MagicMock())
|
||||
m_item = PresentationMediaItem(None, mock_plugin, MagicMock())
|
||||
m_item.settings_section = 'media'
|
||||
return m_item
|
||||
|
||||
@ -44,4 +44,5 @@ def media_item(settings):
|
||||
def mock_plugin(temp_folder):
|
||||
m_plugin = MagicMock()
|
||||
m_plugin.settings_section = temp_folder
|
||||
m_plugin.manager = MagicMock()
|
||||
yield m_plugin
|
||||
|
@ -24,7 +24,6 @@ This module contains tests for the lib submodule of the Presentations plugin.
|
||||
from pathlib import Path
|
||||
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.serviceitem import ItemCapabilities
|
||||
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)
|
||||
|
||||
|
||||
@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')
|
||||
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,
|
||||
# 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)
|
||||
# GIVEN: The Mediaitem set up a list of presentations
|
||||
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
|
||||
results = media_item.search('IMPRE', False)
|
||||
# WHEN: Retrieving the test file
|
||||
result = media_item.search('test.odp', False)
|
||||
|
||||
# THEN: The first two results should have been returned
|
||||
assert results == [[str(path_1), 'Impress_file_1'], [str(path_2), 'impress_file_2']]
|
||||
# THEN: a file should be found
|
||||
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