import quick_view


quick_view.launch()
__main__.py
Nyre221/dolphin-quick-view
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QListWidget, QHBoxLayout, QVBoxLayout, QListWidgetItem, QFrame
from PyQt5.QtGui import QPixmap, QFont, QIcon, QFontMetrics
from PyQt5.QtCore import Qt
from datetime import datetime
import sys
import zipfile
import os
from glob import glob
import tarfile
import rarfile
from threading import Thread
from random import randint

from translation_manager import Translator


class ContainerViewer(QWidget):

    def __init__(self, parent=None):
        super(ContainerViewer, self).__init__(parent)
        #5GB. is used to determine whether the archive should be opened and its contents shown.
        self.maximum_archive_size = 5368709120
        #to get translations
        self.translator = Translator()
        # used to determine if threads are still needed.
        self.is_active_viewer = False
        # is used to discard a thread no longer needed.
        self.current_thread_id = 0
        #the size to use for the image.
        self.logo_size = QApplication.primaryScreen().size()*0.125
        # preloads the icons to use in the listwidget.
        self.__preload_icons__()
        # file type logo/img
        self.logo_img = QLabel()

        # set the size of the logo if the class is opened by its own script (for testing).
        if parent is None:
            self.logo_img.setPixmap(QPixmap("/usr/share/icons/breeze-dark/mimetypes/64/application-zip.svg").scaled(self.logo_size, Qt.KeepAspectRatioByExpanding))

        # fonts
        # name font
        self.font_label_name = QFont()
        self.font_label_name.setPointSize(14)
        self.font_label_name.setBold(True)
        # size_text/modified_text,etc
        self.font_info_section = QFont()
        self.font_info_section.setPointSize(11)
        self.font_info_section.setBold(True)
        # size_value/modified_value,etc
        self.font_info_value = QFont()
        self.font_info_value.setPointSize(10)
        self.font_info_value.setUnderline(True)

        # name label
        self.label_name = QLabel()
        self.label_name.setText("Name")
        self.label_name.setFont(self.font_label_name)
        self.label_name.setAlignment(Qt.AlignCenter)
        # needed to elide the text of the name
        self.name_font_metrics = QFontMetrics(self.font_label_name)

        # modified date label (section)
        label_modified_date_text = QLabel()
        label_modified_date_text.setFont(self.font_info_section)
        label_modified_date_text.setText(self.translator.get_translation("last_modified"))
        label_modified_date_text.setAlignment(Qt.AlignCenter)
        # modified date label (value)
        self.label_modified_date_value = QLabel()
        self.label_modified_date_value.setFont(self.font_info_value)
        self.label_modified_date_value.setAlignment(Qt.AlignCenter)

        # size label (section)
        label_size_text = QLabel()
        label_size_text.setText(self.translator.get_translation("size"))
        label_size_text.setFont(self.font_info_section)
        label_size_text.setAlignment(Qt.AlignCenter)
        # size label (value)
        self.label_size_value = QLabel()
        self.label_size_value.setFont(self.font_info_value)
        self.label_size_value.setAlignment(Qt.AlignCenter)
        # files count label (section)
        label_files_count_text = QLabel()
        label_files_count_text.setText(self.translator.get_translation("files"))
        label_files_count_text.setFont(self.font_info_section)
        label_files_count_text.setAlignment(Qt.AlignCenter)
        # files count label (value)
        self.label_files_count_value = QLabel()
        self.label_files_count_value.setFont(self.font_info_value)
        self.label_files_count_value.setAlignment(Qt.AlignCenter)
        # separator
        separator_line = QFrame()
        separator_line.setFrameShape(QFrame.HLine)

        # widgets
        self.list_widget = QListWidget(self)

        # layouts
        self.main_layout = QHBoxLayout()
        layout_info = QVBoxLayout()
        layout_labels_container = QVBoxLayout()

        # setting main layout
        self.setLayout(self.main_layout)

        # adding to layouts:

        layout_labels_container.addWidget(label_files_count_text)
        layout_labels_container.addWidget(self.label_files_count_value)
        layout_labels_container.addWidget(label_size_text)
        layout_labels_container.addWidget(self.label_size_value)
        layout_labels_container.addWidget(label_modified_date_text)
        layout_labels_container.addWidget(self.label_modified_date_value)

        layout_info.addWidget(self.logo_img)
        layout_info.addWidget(self.label_name)
        layout_info.addWidget(separator_line)
        layout_info.addLayout(layout_labels_container)
        layout_info.insertStretch(-1)

        self.main_layout.addLayout(layout_info)
        self.main_layout.addWidget(self.list_widget)
        self.__set_placeholder_text__()

    def load_file(self, path):
        # to stop the thread if it is no longer the active viewer.
        self.is_active_viewer = True
        th = None
        # so the arrow keys can scroll through the items in the list.
        self.list_widget.setFocus()

        # get a random id for thread
        random_id = randint(0, 1000)
        # check that the id is not the same as the previous one.
        while random_id == self.current_thread_id:
            random_id = randint(0, 1000)

        # makes the id global so it can be accessed from anywhere.
        self.current_thread_id = random_id
        # load the placeholder text as it may take time to load the new data.
        self.__set_placeholder_text__()

        
        # use the correct function for the file type.
        extension = os.path.splitext(path.lower())[-1]
        if os.path.isdir(path):
            # change the logo image to match the file type.
            self.__set_logo_icon__("/usr/share/icons/breeze/places/96/folder.svg")
            # prepare to start the function in another thread to avoid slowdowns.
            th = Thread(target=self.__load_folder__, args=([path]))
        elif extension in ".zip":
            # change the logo image to match the file type.
            self.__set_logo_icon__(
                "/usr/share/icons/breeze-dark/mimetypes/64/application-zip.svg")
            # prepare to start the function in another thread to avoid slowdowns.
            th = Thread(target=self.__load_zip__, args=([path]))
        elif extension in ".rar":
            # change the logo image to match the file type.
            self.__set_logo_icon__(
                "/usr/share/icons/breeze-dark/mimetypes/64/application-zip.svg")
            # prepare to start the function in another thread to avoid slowdowns.
            th = Thread(target=self.__load_rar__, args=([path]))
        elif extension in [".gz", ".xz"]:
            # change the logo image to match the file type.
            self.__set_logo_icon__(
                "/usr/share/icons/breeze-dark/mimetypes/64/application-zip.svg")
            # prepare to start the function in another thread to avoid slowdowns.
            th = Thread(target=self.__load_tar__, args=([path]))
        else:
            print("container viewer:File not supported")
            return

        

        # allows closing the application even if the thread has not finished
        th.setDaemon(True)
        #start the thread
        th.start()
        # uses another thread to set basic information about the file.
        # This makes things faster.
        th_2 = Thread(target=self.__set_path_info__, args=([path]))
        # allows closing the application even if the thread has not finished
        th_2.setDaemon(True)
        th_2.start()
#

    def __load_folder__(self, path):
        # get the current thread id
        thread_id = self.current_thread_id
        items = []
        files = []
        folders_in_list = []
        files_in_list = []
        ordered_files = []

        # gets a list of the files.
        files = glob(f"{path}/*")
        
        # sorting: alphabetical order and folders first.
        folders_in_list = [x for x in files if os.path.isdir(x)]
        files_in_list =  [x for x in files if os.path.isfile(x)]

        folders_in_list = sorted(folders_in_list,key=str.lower)
        files_in_list = sorted(files_in_list,key=str.lower)

        ordered_files.extend(folders_in_list)
        ordered_files.extend(files_in_list)

        # create the object for qlistwidget.
        for file in ordered_files:
            if not self.__continue_thread_execution__(thread_id):
               # not needed, exit
                exit()
            # gets the icon appropriate for the file type.
            icon = self.__list_icon_chooser__(file, is_folder=os.path.isdir(file))
            # creates the object for qlistwidget and appends it to the list.
            items.append(QListWidgetItem(icon, os.path.basename(file)))

        # sets the file count in the label.
        self.__set_labels_text__(file_count=len(files))
        # adds items to qlistwidget.
        self.__set_list_widget_items__(items)

    def __load_zip__(self, path):
        # get the current thread id
        thread_id = self.current_thread_id
        items = []
        files_list = []

        # avoid opening the archive if it is too large.
        if os.stat(path).st_size > self.maximum_archive_size:
            # adds an item to let the user know that the content will not be shown.
            self.__show_warning_file_too_big__()
            exit()
        # check if the thread is still needed.
        if not self.__continue_thread_execution__(thread_id):
                    #not needed,exit
                    exit()
        with zipfile.ZipFile(path, "r") as zip_ref:
            zip_infolist = zip_ref.infolist()
        # gets a list of the files and create the object for qlistwidget.
        for file in  zip_infolist:
            if not self.__continue_thread_execution__(thread_id):
                # not needed, exit
                exit()
            if not file.is_dir():
                # gets the icon appropriate for the file type.
                icon = self.__list_icon_chooser__(file.filename, is_folder=False)
                # creates the object for qlistwidget and appends it to the list.
                items.append(QListWidgetItem(icon, file.filename))
                # used later to count the number of files.
                files_list.append(file.filename)
        # sets the file count in the label.
        self.__set_labels_text__(file_count=len(files_list))
        # adds items to qlistwidget.
        self.__set_list_widget_items__(items)

    def __load_rar__(self, path):
        # get the current thread id
        thread_id = self.current_thread_id
        items = []
        files_list = []

        # avoid opening the archive if it is too large.
        if os.stat(path).st_size > self.maximum_archive_size:
            # adds an item to let the user know that the content will not be shown.
            self.__show_warning_file_too_big__()
            exit()
        # check if the thread is still needed.
        if not self.__continue_thread_execution__(thread_id):
                    #not needed,exit
                    exit()

        with rarfile.RarFile(path, "r") as rar_ref:
            rar_infolist = rar_ref.infolist()
        # gets a list of the files and create the object for qlistwidget.
        for file in rar_infolist:
            if not self.__continue_thread_execution__(thread_id):
                # not needed, exit
                exit()
            if not file.is_dir():
                # gets the icon appropriate for the file type.
                icon = self.__list_icon_chooser__(file.filename, is_folder=False)
                # creates the object for qlistwidget and appends it to the list.
                items.append(QListWidgetItem(icon, file.filename))
                # used later to count the number of files.
                files_list.append(file.filename)
        # sets the file count in the label.
        self.__set_labels_text__(file_count=len(files_list))
        # adds items to qlistwidget.
        self.__set_list_widget_items__(items)

    def __load_tar__(self, path):
        # get the current thread id
        thread_id = self.current_thread_id
        items = []
        files_list = []

        # avoid opening the archive if it is too large.
        if os.stat(path).st_size > self.maximum_archive_size:
            # adds an item to let the user know that the content will not be shown.
            self.__show_warning_file_too_big__()
            exit()
        # check if the thread is still needed.
        if not self.__continue_thread_execution__(thread_id):
                    #not needed,exit
                    exit()

        with tarfile.open(path,"r") as tar_ref:
            tar_infolist = tar_ref.getmembers()
        # gets a list of the files and create the object for qlistwidget.
        for file in tar_infolist:
            if not self.__continue_thread_execution__(thread_id):
                # not needed, exit
                exit()
            if not file.isdir():
                # gets the icon appropriate for the file type.
                icon = self.__list_icon_chooser__(file.name, is_folder=False)
                # creates the object for qlistwidget and appends it to the list.
                items.append(QListWidgetItem(icon, file.name))
                # used later to count the number of files.
                files_list.append(file.name)
        # sets the file count in the label.
        self.__set_labels_text__(file_count=len(files_list))
        # adds items to qlistwidget.
        self.__set_list_widget_items__(items)

    def __set_list_widget_items__(self, items):
        self.list_widget.clear()
        for i in items:
            self.list_widget.addItem(i)

    def __preload_icons__(self):
        # these icons are the ones that will be used in qlistwidget.
        # default icon:
        self.unknown_icon = QIcon(
            "/usr/share/icons/breeze-dark/mimetypes/64/unknown.svg")
        # icons
        self.txt_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/text-x-log.svg")
        self.folder_icon = QIcon(
            "/usr/share/icons/breeze/places/16/folder-blue.svg")
        self.archive_icon = QIcon(
            "/usr/share/icons/breeze-dark/mimetypes/16/application-zip.svg")
        self.pdf_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/application-pdf.svg")
        self.img_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/image-png.svg")
        self.video_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/video-mp4.svg")
        self.audio_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/audio-mp3.svg")
        self.shell_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/application-x-shellscript.svg")
        self.python_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/text-x-python.svg")
        self.c_language_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/text-x-c++src.svg")
        self.html_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/text-html.svg")
        self.document_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/application-vnd.oasis.opendocument.text.svg")
        self.iso_image_icon = QIcon(
            "/usr/share/icons/breeze/mimetypes/16/application-x-iso.svg")

    def __list_icon_chooser__(self, path, is_folder=False):
        # returns the icon based on the file type.
        icon = None
        extension = os.path.splitext(path.lower())[-1]

        if is_folder:
            icon = self.folder_icon
        elif extension in [".zip", ".gz", ".rar", ".pyz"]:
            icon = self.archive_icon
        elif extension in [".pdf"]:
            icon = self.pdf_icon
        elif extension in [".png", ".jpg", ".jpeg", ".webp", ".svg", "svgz"]:
            icon = self.img_icon
        elif extension in [".mp4"]:
            icon = self.video_icon
        elif extension in [".mp3"]:
            icon = self.audio_icon
        elif extension in [".sh"]:
            icon = self.shell_icon
        elif extension in [".py"]:
            icon = self.python_icon
        elif extension in [".ccp"]:
            icon = self.c_language_icon
        elif extension in [".txt"]:
            icon = self.txt_icon
        elif extension in [".html", ".htm"]:
            icon = self.html_icon
        elif extension in [".doc", ".docx", ".odt", ".ods", ".xlsx", ".xls", ".csv"]:
            icon = self.document_icon
        elif extension in [".iso", ".img"]:
            icon = self.iso_image_icon
        else:
            icon = self.unknown_icon
        return icon

    def __set_logo_icon__(self, path):
        icon = QPixmap(path).scaled(self.logo_size, Qt.KeepAspectRatioByExpanding)
        self.logo_img.setPixmap(icon)

    def __set_labels_text__(self, name=None, last_modified=None, size=None, size_unit="?", file_count=None):
        if name:
            # sets the text and elides it if necessary.
            self.label_name.setText(self.name_font_metrics.elidedText(
                name, Qt.ElideMiddle,int(self.logo_size.width())))
        if last_modified:
            self.label_modified_date_value.setText(last_modified)
        if size is not None:
            self.label_size_value.setText(f"{size} {size_unit}")
        if file_count is not None:
            self.label_files_count_value.setText(f"{file_count}")

    def __set_path_info__(self, path):
        # set name
        self.__set_labels_text__(name=os.path.basename(path))
        # get and set modification date
        last_modified_date = datetime.fromtimestamp(os.path.getmtime(path))
        last_modified_date = last_modified_date.replace(microsecond=0)
        last_modified_date = str(last_modified_date)
        self.__set_labels_text__(last_modified=last_modified_date)
        # get and set size
        size, unit = self.get_size(path)
        self.__set_labels_text__(size=size, size_unit=unit)

    def get_size(self, path):
        # get the current thread id
        thread_id = self.current_thread_id
        size = 0
        if os.path.isdir(path):
            subfile_path = ""
            try:
                # adds the size of each file to the size variable
                for subfolder_path, dirs, files in os.walk(path):
                    for f in files:
                        if thread_id is not self.current_thread_id or not self.is_active_viewer:
                            # exits if the id has changed (this happens when the user changes files).
                            exit()
                        subfile_path = os.path.join(subfolder_path, f)
                        # .st_size doesn't work on links.
                        if not os.path.islink(subfile_path):
                            size += os.stat(subfile_path).st_size
            except FileNotFoundError:
                print(
                    "get_size(): file not found during size calculation:" + str(subfile_path))

        elif os.path.isfile(path) and not os.path.islink(path):
            # if it is a generic file.
            size = os.stat(path).st_size
        # returns the converted value.
        return self.convert_size(size)

    def convert_size(self, size):
        # convert bytes to KiB
        size = size/1024
        # starting unit
        unit = "KiB"

        if size > 1024:
            # convert KiB to MiB if more than 1024 KiB
            size = size/1024
            unit = "MiB"

        if size > 1024:
            # convert MiB to GiB if more than 1024 MiB
            size = size/1024
            unit = "GiB"

        # round the number
        size = round(size, 1)
        return [size, unit]

    def __set_placeholder_text__(self):
        self.list_widget.clear()
        # ph = placeholder
        ph = self.translator.get_translation("loading_placeholder")
        self.__set_labels_text__(name=ph, last_modified=ph,
                             size=ph, size_unit="", file_count=ph)
        # adds an element to qlistwidget to indicate that the files are loading.
        self.list_widget.addItem(ph)
        


    def hide(self) -> None:
        # to stop the thread if it is no longer the active viewer.
        self.is_active_viewer = False
        return super().hide()

    def __continue_thread_execution__(self,thread_id):
        # check if the thread is still needed.
        if thread_id is self.current_thread_id and self.is_active_viewer:
            #needed, returns true.
            return True
        else:
            return False

    def __show_warning_file_too_big__(self):
        # adds an item to qlistwidget.
        self.__set_list_widget_items__([QListWidgetItem(self.translator.get_translation("container_file_too_big"))])
        # set file count text to "unknown"
        self.__set_labels_text__(file_count=self.translator.get_translation("unknown"))

         
if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = ContainerViewer()
    widget.resize(640, 480)
    widget.show()
    sys.exit(app.exec_())

container_viewer.py
Nyre221/dolphin-quick-view
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
import sys

# Adhere to naming conventions: https://visualgit.readthedocs.io/en/latest/pages/naming_convention.html

# Name the module like this: name_viewer.py (all lowercase)
# I chose this way of naming the modules to avoid using a name already in use.

class ClassExample(QWidget):

    def __init__(self, parent=None):
        super(ClassExample, self).__init__(parent)

        # buttons
        self.button_1 = QPushButton()
        self.button_1.setText("Button 1")
        self.button_2 = QPushButton()
        self.button_2.setText("Button 2")

        # label
        self.example_label = QLabel()
        self.example_label.setText("Label Text")

        # main layout
        self.main_layout = QVBoxLayout()
        self.main_layout.addWidget(self.example_label)
        self.main_layout.addWidget(self.button_1)
        self.main_layout.addWidget(self.button_2)
        self.setLayout(self.main_layout)



# load_file() is the class method that will be called by quick_view.py
# Use this method to receive the path of the file to show.
    def load_file(self,file_path):
        pass





if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = ClassExample()
    widget.resize(640, 480)
    widget.show()
    sys.exit(app.exec_())

example_module.py
Nyre221/dolphin-quick-view
import os
import sys
import pyexcel
from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QWidget, QApplication
import xlrd
from random import randint
from threading import Thread

# from pyexcel import xlsx


class TableViewer(QTableWidget):

    def __init__(self, parent=None):
        super(TableViewer, self).__init__(parent)

        # used to discard a thread if the user changes documents
        self.current_thread_id = 0

        self.is_active_viewer = False

        # makes the table read only.
        self.setEditTriggers(QTableWidget.NoEditTriggers)
        # for some reason when I create the quick view executable with zipapp,
        # pyexcel gives an error when I try to open an xlsx:
        # /quick_view/quick_view.pyz/xlrd/xlsx.py", line 266, in process_stream
        # AttributeError: 'ElementTree' object has no attribute 'getiterator'
        # this seems to have to do with some deprecated function that for some
        # reason gets triggered when I add pyexcel into the executable
        # (the module versions are the same as the system ones)
        # this solves the problem for now:
        # (version 1.2.0 is included in the executable)
        if xlrd.__version__ == "1.2.0":
            xlrd.xlsx.ensure_elementtree_imported(False, None)
            xlrd.xlsx.Element_has_iter = True

    def load_file(self, file_path):
        self.__load_placeholder__()
        self.is_active_viewer = True

        # choose an id for the thread.
        # in this way is possible to close the thread if it is no longer needed.
        random_id = randint(0, 1000)
        while random_id == self.current_thread_id:
            random_id = randint(0, 1000)

        self.current_thread_id = random_id

        # prepare the thread
        self.th = Thread(target=self.__load_sheet__, args=([file_path]))
        # #allows to close the application if there is a thread still running.
        self.th.setDaemon(True)
        self.th.start()

    def __load_placeholder__(self):
        # set placeholder text
        self.clear()
        self.setColumnCount(1)
        self.setRowCount(1)
        self.setItem(0, 0, QTableWidgetItem("Loading"))

    def __load_sheet__(self, path):
        # to check if the id has changed.
        thread_id = self.current_thread_id

        if os.path.splitext(path.lower())[-1] == ".csv":
            # for csv files read_only is an invalid parameter.
            sheet = pyexcel.get_sheet(
                file_name=path, encoding="windows-1252", row_limit=50)
        else:
            sheet = pyexcel.get_sheet(
                file_name=path, encoding="windows-1252", read_only=True, row_limit=50)

        if thread_id is not self.current_thread_id or not self.is_active_viewer:
            sys.exit()

        self.__set_table__(sheet)
        sys.exit()

    def __set_table__(self, sheet):

        # convert everything to strings otherwise QTableWidget can crash.
        sheet.format(str)

        self.setColumnCount(sheet.number_of_columns())
        self.setRowCount(sheet.number_of_rows())

        # sets the items in the table.
        for r in range(sheet.number_of_rows()):
            for c in range(sheet.number_of_columns()):
                self.setItem(r, c, QTableWidgetItem(sheet[r, c]))

    def hide(self) -> None:
        # clears the table.
        self.__load_placeholder__()
        pyexcel.free_resources()
        self.is_active_viewer = False
        return super().hide()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = TableViewer()
    widget.resize(640, 480)
    widget.show()
    sys.exit(app.exec_())

extras/deprecated/table_viewer.py
Nyre221/dolphin-quick-view
import qpageview
import shutil
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtCore import Qt, pyqtSignal
from translation_manager import Translator
import sys
import subprocess
from threading import Thread
import tempfile
import shlex
import os
import signal
import time
from queue import Queue


class FallbackViewer(QWidget):


    def __init__(self, parent=None):
        super(FallbackViewer, self).__init__(parent)
        # to get translations
        self.translator = Translator()
        # name font
        self.font_label_message_name = QFont()
        self.font_label_message_name.setPointSize(14)
        self.font_label_message_name.setBold(True)
        # header font
        self.font_label_message = QFont()
        self.font_label_message.setPointSize(11)
        self.font_label_message.setBold(True)

        # img
        self.logo_img = QLabel()
        self.logo_img.setAlignment(Qt.AlignCenter)
        # sets the image size based on the screen size
        self.logo_size = QApplication.primaryScreen().size()*0.135
        self.logo_img.setPixmap(QPixmap("/usr/share/icons/breeze-dark/mimetypes/64/unknown.svg").scaled(
            self.logo_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation))

        # labels
        self.label_message_name = QLabel()
        self.label_message_name.setAlignment(Qt.AlignCenter)
        self.label_message_name.setFont(self.font_label_message_name)
        self.label_message = QLabel()
        self.label_message.setFont(self.font_label_message)
        self.label_message.setAlignment(Qt.AlignCenter)

        # layouts
        # main
        self.layout_main = QVBoxLayout()
        self.setLayout(self.layout_main)

        # adding to layout
        self.layout_main.addWidget(self.logo_img)
        self.layout_main.addWidget(self.label_message_name)
        self.layout_main.addWidget(self.label_message)
        self.layout_main.insertStretch(-1)

        # placeholder
        self.__set_placeholder_text__()

    def load_file(self, path):
        self.label_message_name.setText(path.split("/")[-1])

    def __set_placeholder_text__(self):
        self.label_message_name.setText("Name")
        self.label_message.setText(self.translator.get_translation("unsupported_file_type"))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = FallbackViewer()
    widget.resize(640, 480)
    widget.show()
    sys.exit(app.exec_())

fallback_viewer.py
Nyre221/dolphin-quick-view
from random import randint
import qpageview
import shutil
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel
from PyQt5.QtGui import QPixmap, QFont, QCloseEvent
from PyQt5.QtCore import Qt, pyqtSignal
from translation_manager import Translator
import sys
import subprocess
from threading import Thread
import tempfile
import shlex
import os
import signal
import time
from queue import Queue


class PageViewer(QWidget):
    # used to trigger a function in the main thread from the secondary thread.
    # Qt does not allow calling some widget functions from a secondary thread.
    signLoadingArrested = pyqtSignal(bool, bool, bool, str)
    signConversionFinished = pyqtSignal(str)

    def __init__(self, parent=None, app=None):
        super(PageViewer, self).__init__(parent)
        app.aboutToQuit.connect(self.__app_is_closing__)
        self.temp_dir = None
        # a precaution to make sure that the file selected PREVIOUSLY
        #  is not loaded at the end of the conversion to pdf.
        self.current_thread_id = 0
        self.conversion_process = None
        self.is_active_viewer = False
        self.libreoffice_command = self.__get_libreoffice_command__()
        # the page to see if they have problems with libreoffice.
        self.libreoffice_help_page = "https://github.com/Nyre221/dolphin-quick-view/tree/main/extras/Libreoffice%20troubleshooting"
        # to get translations
        self.translator = Translator()
        # name font
        self.font_label_message_name = QFont()
        self.font_label_message_name.setPointSize(14)
        self.font_label_message_name.setBold(True)
        # header font
        self.font_label_message_header = QFont()
        self.font_label_message_header.setPointSize(11)
        self.font_label_message_header.setBold(True)
        # message font
        self.font_label_message = QFont()
        self.font_label_message.setPointSize(10)

        # qpageview
        self.qpage = qpageview.View()
        # set scroll speed
        self.qpage.verticalScrollBar().setSingleStep(50)

        # img
        self.logo_img = QLabel()
        self.logo_img.setAlignment(Qt.AlignCenter)
        # sets the image size based on the screen size
        self.logo_size = QApplication.primaryScreen().size()*0.135
        self.logo_img.setPixmap(QPixmap("/usr/share/icons/breeze-dark/mimetypes/64/x-office-document.svg").scaled(
            self.logo_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation))

        # labels
        self.label_message_name = QLabel()
        self.label_message_name.setAlignment(Qt.AlignCenter)
        self.label_message_name.setFont(self.font_label_message_name)
        self.label_message_header = QLabel()
        self.label_message_header.setFont(self.font_label_message_header)
        self.label_message_header.setAlignment(Qt.AlignCenter)
        self.label_message = QLabel()
        self.label_message.setFont(self.font_label_message)
        self.label_message.setAlignment(Qt.AlignCenter)
        # allows to open external links.
        self.label_message.setWordWrap(True)
        self.label_message.setTextFormat(Qt.RichText)
        self.label_message.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.label_message.setOpenExternalLinks(True)
        # layouts
        # main
        self.layout_main = QVBoxLayout()
        self.setLayout(self.layout_main)
        # message layout
        self.layout_message = QVBoxLayout()

        # labels container
        self.container_message = QWidget()
        self.container_message.setLayout(self.layout_message)

        # adding to layout
        self.layout_main.addWidget(self.container_message)
        self.layout_main.addWidget(self.qpage)
        self.layout_message.addWidget(self.logo_img)
        self.layout_message.addWidget(self.label_message_name)
        self.layout_message.addWidget(self.label_message_header)
        self.layout_message.addWidget(self.label_message)
        self.layout_message.insertStretch(-1)

        # connecting signals
        # used to trigger a function in the main thread from the secondary thread.
        self.signLoadingArrested.connect(self.__loading_arrested__)
        self.signConversionFinished.connect(self.__conversion_finished__)
        # placeholder
        self.__set_placeholder_text__()

    def load_file(self, path, extension):
        # loading message
        self.container_message.show()
        self.__set_placeholder_text__()
        self.label_message_name.setText(path.split("/")[-1])
        # set the focus so you can scroll through the pages with the arrow keys.
        self.qpage.setFocus()
        # gives space to the loading screen.
        self.qpage.hide()
        # removes the previously viewed document.
        self.qpage.clear()
        # It is used to kill the process if the viewer has been hidden.
        self.is_active_viewer = True

        # get a random id for thread
        # used to prevent the converted pdf file from being loaded if it is no longer the selected one.
        random_id = randint(0, 1000)
        # check that the id is not the same as the previous one.
        while random_id == self.current_thread_id:
            random_id = randint(0, 1000)
        # killing the process would have been more logical,
        # but I noticed that it can behave strangely sometimes and I don't want to risk anything.
        # this problem happens when you view a pdf after viewing a file that requires conversion.
        self.current_thread_id = random_id

        # load and set the display mode based on the file.
        if extension == ".pdf":
            self.qpage.loadPdf(path)
            self.qpage.setViewMode(qpageview.FitWidth)
            self.__file_loaded__()
        elif extension in [".png", ".jpeg", ".jpg", ".webp"]:
            self.qpage.loadImages([path])
            self.qpage.setViewMode(qpageview.FitBoth)
            self.__file_loaded__()
        elif extension in [".svg"]:
            self.qpage.loadSvgs([path])
            self.qpage.setViewMode(qpageview.FitBoth)
            self.__file_loaded__()
        elif extension in [".doc", ".docx", ".odt", ".ods", ".xlsx", ".xls", ".csv", ".odp", ".ppt", ".pptx"]:
            # converts to pdf.
            self.__convert_document__(path=path)

    def __file_loaded__(self):
        # hides the messages/loading screen and shows qpage.
        self.container_message.hide()
        self.qpage.show()
        # resets the page position to 0.
        # this is necessary because qpageview does not set the position to 0
        #  if the viewing mode has been changed to a different one.
        self.qpage.setPosition((0, 0, 0), False)

    def hide(self) -> None:
        # clears qpageview screen to save memory(doesn't seem to work).
        self.qpage.clear()
        self.is_active_viewer = False
        return super().hide()

    def __convert_document__(self, path):
        # shows a message and stops the function if libreoffice was not found.
        if self.libreoffice_command is None:
            self.__loading_arrested__(loffice_not_found=True)
            print("libreoffice not found")
            return

        # create a temporary folder to put the pdfs in.
        if self.temp_dir is None:
            self.temp_dir = tempfile.mkdtemp()

        # use another thread to avoid blocking the interface.
        th = Thread(target=self.__convert_document_thread__,
                    args=(path, self.temp_dir))
        th.setDaemon(True)
        th.start()

    def __convert_document_thread__(self, path, temp_dir):
        # get the thread id
        thread_id = self.current_thread_id
        # kills the process group if the old process is still running.
        self.__kill_conversion_process__(self.conversion_process)

        # libreoffice shell command
        commands = f"{self.libreoffice_command} --headless --nolockcheck --norestore --convert-to pdf '{path}' --outdir {temp_dir} "
        args = shlex.split(commands)
        # preexec_fn=os.setsid is used to create a process group.
        # this is necessary to terminate libreoffice and reopen it later.
        self.conversion_process = subprocess.Popen(
            args=args, preexec_fn=os.setsid, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        # the global variable is used to kill the old process when a new thread starts,
        #  the local one is used to get the return code and information without risking taking those of the new process.
        local_process = self.conversion_process
        # to kill the process after X sec
        # I don't want to make the user wait forever if the file takes too long to load.
        start = time.time()
        end = start + 10
        # checks if the maximum time limit is reached.
        while True:
            if local_process.poll() is not None:
                # exits the loop if the process has finished.
                break

            if time.time() >= end:
                # kill the process
                os.killpg(local_process.pid, signal.SIGTERM)
                # display the message
                self.signLoadingArrested.emit(True, False, False, "")
                # exit
                break

            time.sleep(0.1)

        # store the return code
        return_code = local_process.returncode
        # send the path of the pdf if the return code is 0, the viewer is active and the thread id is the current one
        if thread_id == self.current_thread_id and return_code == 0 and self.is_active_viewer:
            output = local_process.communicate()[0]
            try:
                # get the path
                pdf_path = output.decode(
                    "utf-8").split(">")[1].split("using filter")[0].strip()

                if not os.path.exists(pdf_path):
                    # shows an error if for some reason libreoffice fails to give a path.
                    # True:path error
                    self.signLoadingArrested.emit(False, False, True, "")
                    # kills the process group
                    self.__kill_conversion_process__(local_process)
                    return

                # send
                self.signConversionFinished.emit(pdf_path)
                # kills the process group
                self.__kill_conversion_process__(local_process)
            except IndexError:
                print("IndexError")
                process_error = local_process.communicate()[1]
                # shows an error if for some reason libreoffice fails to give a path.
                self.signLoadingArrested.emit(
                    False, False, False, process_error.decode("utf-8"))
                # kills the process group
                self.__kill_conversion_process__(local_process)
                return

    def __conversion_finished__(self, path: str):
        # calls the function with the path of the new pdf
        self.load_file(path=path, extension=".pdf")
        self.qpage.setFocus()

    def __get_libreoffice_command__(self):
        command = None
        # check if libreoffice is installed.
        if shutil.which('libreoffice'):
            command = "libreoffice"
        elif shutil.which('flatpak'):
            # check if libreoffice is installed with flatpak
            if subprocess.run(["bash", "-c", "flatpak list | grep -i libreoffice"]).returncode == 0:
                command = "flatpak run org.libreoffice.LibreOffice"
        else:
            command = None

        # return the command to use
        return command

    def __loading_arrested__(self, timeout=False, loffice_not_found=False, path_error=False, custom_message=""):
        # sets the information in the labels.
        self.label_message_header.setText(
            self.translator.get_translation("libreoffice_preview_unavailable"))
        html = f"<a href=\"{self.libreoffice_help_page}\">{self.translator.get_translation('click_here')}</a>"
        if timeout:
            self.label_message.setText(
                self.translator.get_translation("libreoffice_timeout"))
        elif loffice_not_found:
            self.label_message.setText(self.translator.get_translation(
                "libreoffice_not_found")+" "+html)
        elif path_error:
            self.label_message.setText(self.translator.get_translation(
                "libreoffice_file_not_found")+" "+html)
        if custom_message != "":

            self.label_message.setText(custom_message + " : " + html)

    def __set_placeholder_text__(self):
        self.label_message_header.setText(
            self.translator.get_translation("loading_placeholder"))
        # there is no need to translate it because the name is set immediately.
        self.label_message_name.setText("Name")
        self.label_message.setText("")

    def __kill_conversion_process__(self, process):
        # terminate the process used for PDF conversion if it is alive.
        if process is not None and process.poll() is None:
            os.killpg(process.pid, signal.SIGTERM)

    def __app_is_closing__(self):
        print("closing pageviewer")
        # closes the libreoffice process if it is still active.
        # This is because the process may remain active and slow down quickview the next time it is started.
        self.__kill_conversion_process__(self.conversion_process)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = PageViewer()
    widget.resize(640, 480)
    widget.show()
    sys.exit(app.exec_())

page_viewer.py
Nyre221/dolphin-quick-view
#!/bin/python3

# import QT
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QStyle, \
    QSizePolicy, QShortcut
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
# import viewers
from page_viewer import PageViewer
from text_viewer import TextViewer
from video_viewer import VideoViewer
from container_viewer import ContainerViewer
from fallback_viewer import FallbackViewer
# other
from glob import glob
import subprocess
import sys
import os
import shutil


class Main(QMainWindow):

    def __init__(self, app):
        super().__init__()
        self.app = app
        # gets the active dolphin window before quick view becomes the active window.
        self.dolphin_window = self.get_dolphin_window()
        # a way to access and check if the widget has been created
        self.added_viewers = []
        self.page_viewer = None
        self.fallback_viewer = None
        self.video_viewer = None
        self.text_viewer = None
        self.container_viewer = None
        # used to manage the index indicator in the title bar.
        self.is_parent_in_list = False
        # sets the files and folders to display.
        self.set_directory()

        # setting up ui
        self.setWindowIcon(self.style().standardIcon(
            QStyle.SP_FileDialogContentsView))
        screen_size = app.primaryScreen().size()
        self.resize(int(screen_size.width() * 0.5),
                    int(screen_size.height() * 0.55))
        self.create_ui()
        self.set_shortcut()
        self.setFocus()
        self.load_file_at_index(self.current_index)

    def create_ui(self):
        # central widget
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        # main layout
        self.main_layout = QVBoxLayout(self.central_widget)
        self.main_layout.setContentsMargins(0, 0, 0, 0)

        # controls layout
        controls_layout = QHBoxLayout()
        controls_layout.setContentsMargins(0, 0, 0, 0)

        # viewer_container and layout
        self.viewer_container = QWidget()
        self.viewer_container_layout = QVBoxLayout(self.viewer_container)
        self.viewer_container_layout.setContentsMargins(0, 0, 0, 0)

        # back button
        self.back_button = QPushButton()
        self.back_button.setFixedHeight(24)
        self.back_button.setMinimumWidth(80)
        self.back_button.setIcon(
            self.style().standardIcon(QStyle.SP_ArrowLeft))

        # forward button
        self.forward_button = QPushButton()
        self.forward_button.setFixedHeight(24)
        self.forward_button.setMinimumWidth(80)
        self.forward_button.setIcon(
            self.style().standardIcon(QStyle.SP_ArrowRight))

        # open button
        self.open_button = QPushButton()
        self.open_button.setFixedHeight(24)
        self.open_button.setMinimumWidth(80)
        self.open_button.setSizePolicy(QSizePolicy(
            QSizePolicy.Maximum, QSizePolicy.Fixed))
        self.open_button.setIcon(
            self.style().standardIcon(QStyle.SP_DialogOpenButton))

        # button signal
        self.back_button.pressed.connect(self.back)
        self.forward_button.pressed.connect(self.forward)
        self.open_button.pressed.connect(self.open_with_app)

        # adding to layouts
        controls_layout.addWidget(self.back_button)
        controls_layout.addWidget(self.open_button)
        controls_layout.addWidget(self.forward_button)
        self.main_layout.addWidget(self.viewer_container)
        self.main_layout.addLayout(controls_layout)

    def load_file_at_index(self, index):
        # hides the viewer shown previously.
        self.hide_widgets()

        # resets some variables
        is_folder = False
        extension = None

        # retrieves the path of the current file
        self.current_file = self.files[index]
        # set the window title
        file_name = self.current_file.split("/")[-1]
        if not self.is_parent_in_list:
            self.setWindowTitle(f"[{index+1}/{len(self.files)}]  {file_name}")
        else:
            self.setWindowTitle(f"[{index}/{len(self.files)-1}]  {file_name}")

        # avoid extracting the extension if it is a folder.
        if os.path.isdir(self.current_file):
            # used to determine whether the container viewer should be used.
            is_folder = True
        elif os.path.splitext(self.current_file.lower())[-1]:
            extension = os.path.splitext(self.current_file.lower())[-1]

        # load viewer based on extension or file type
        if is_folder or extension in [".zip", ".gz", ".xz", ".rar"]:
            self.load_container_viewer(self.current_file)

        elif extension in [".pdf", ".png", ".jpeg", ".jpg", ".webp", ".doc", ".docx", ".odt", ".ods", ".xlsx", ".xls", ".csv", ".odp", ".ppt", ".pptx",".svg", ".svgz"]:
            self.load_page_viewer(self.current_file, extension)


        elif extension in [".mp4", ".mp3"]:
            self.load_video_viewer(self.current_file)

        elif extension in [".md"]:
            with open(self.current_file, "r") as f:
                self.load_text_viewer(f.read(), markdown=True)

        # for simple text (.sh,.txt,.xml,.html,etc) which may or may not have an extension.
        elif "text/" in subprocess.run(["file", "--mime-type", self.current_file],
                                       stdout=subprocess.PIPE).stdout.decode("utf-8"):
            with open(self.current_file, "r") as f:
                self.load_text_viewer(f.read())

        else:
            self.load_fallback_viewer(path=self.current_file)

    def load_container_viewer(self, path):
        if self.container_viewer is None:
            self.container_viewer = ContainerViewer(self)
            # adds the widget to its container.
            self.add_widget(self.container_viewer)

        self.container_viewer.show()
        self.container_viewer.load_file(path)

    def load_text_viewer(self, text, markdown=False):
        if self.text_viewer is None:
            self.text_viewer = TextViewer(self)
            # adds the widget to its container.
            self.add_widget(self.text_viewer)

            font = QFont("Monospace")
            self.text_viewer.setFont(font)
            self.text_viewer.zoomIn(3)

        # here I remove the previously set text to avoid showing the contents of another file if something wrong happens.
        self.text_viewer.clear()

        if markdown:
            self.text_viewer.setMarkdown(text)
        else:
            self.text_viewer.setText(text)

        self.text_viewer.show()
        self.text_viewer.setFocus()

    def load_video_viewer(self, path):
        if self.video_viewer is None:
            self.video_viewer = VideoViewer(self)
            # adds the widget to its container.
            self.add_widget(self.video_viewer)

        self.video_viewer.open(path)
        self.video_viewer.show()

    def load_page_viewer(self, path, extension):
        if self.page_viewer is None:
            # self.app is used to connect the signal "aboutToQuit"
            self.page_viewer = PageViewer(self, self.app)
            # adds the widget to its container.
            self.add_widget(self.page_viewer)

        self.page_viewer.load_file(path, extension)
        self.page_viewer.show()

    def load_fallback_viewer(self, path):
        if self.fallback_viewer is None:
            self.fallback_viewer = FallbackViewer(self)
            # adds the widget to its container.
            self.add_widget(self.fallback_viewer)

        self.fallback_viewer.load_file(path)
        self.fallback_viewer.show()

    def set_directory(self):
        # gets the directory from the system arguments.

        args_count = len(sys.argv)
        if args_count < 2:
            print("No path or file given")
            exit()
        if not os.path.exists(sys.argv[1]):
            print("Invalid path:", sys.argv[1])
            exit()

        argv_path = os.path.abspath(sys.argv[1])

        # if something (folder or file) was selected: "-s"
        if args_count > 2 and sys.argv[-1] == "-s":
            # is a file or folder was selected
            # current file is selected file
            self.current_file = argv_path
            # current path is the parent folder
            self.current_path = os.path.dirname(self.current_file)
            # search for files in the folder
            self.files = glob(f"{self.current_path}/*")
            self.files = self.sort_files(self.files)
            # set the index at the current file
            self.current_index = self.files.index(self.current_file)

        # elif args_count > 2 and sys.argv[-1] == "-a":
        # NOT NEEDED ANYMORE: the action in the dolphin menu calls the shortcut and no longer quickview.
        #     # activated by the dophin action in the dropdown menu
        #     # this is necessary because the actions in the dolphin menu have no way of knowing
        #     # whether the selected one is the parent folder or a subfolder.
        #     if os.path.isfile(argv_path):
        #         # if a file was selected
        #         # current file is selected file
        #         self.current_file = argv_path
        #         # current path is the parent folder
        #         self.current_path = os.path.dirname(self.current_file)
        #         # search for files
        #         self.files = glob(f"{self.current_path}/*")
        #         self.files = self.sort_files(self.files)
        #         # set the index at the current file
        #         self.current_index = self.files.index(self.current_file)
        #     elif os.path.isdir(argv_path):
        #         # if a folder was selected
        #         # current path is the selected folder
        #         self.current_path = argv_path
        #         # search for files in the selected folder
        #         self.files = glob(f"{self.current_path}/*")
        #         self.files = self.sort_files(self.files)
        #         # choose a random file or folder.
        #         self.current_file = self.files[0]
        #         self.current_index = 0
        else:
            # if the shortcut was used and no file was selected.
            # current file is the parent folder
            self.current_file = argv_path
            # current path is the parent folder
            self.current_path = argv_path
            # search for files in the folder
            self.files = glob(f"{self.current_path}/*")
            self.files = self.sort_files(self.files)
            # add the parent directory to the files list
            # this is done so as not to break the back and forward function
            self.files.insert(0, self.current_file)
            # used to manage the index indicator in the title bar.
            self.is_parent_in_list = True
            # set the index at the current fie
            self.current_index = self.files.index(self.current_file)

    def add_widget(self, widget):
        # It is used to add new viewers to the default container.
        self.viewer_container_layout.addWidget(widget)
        # Adds the viewer to the list of added ones.
        # This is done to use hide() on all of them.
        self.added_viewers.append(widget)

    def open_with_app(self):
        # opens the folder in the active dolphin window
        if os.path.isdir(self.current_file):
            if self.dolphin_window is None:
                print("dolphin window not found")
                return
            else:
                qdbus_command = 'qdbus-qt5' if shutil.which(
                    'qdbus-qt5') else 'qdbus'
                args = f"{qdbus_command} {self.dolphin_window} /dolphin/Dolphin_1  org.kde.dolphin.MainWindow.openDirectories 'file://{self.current_file}' false"
                subprocess.run(["bash", "-c", args], stdout=subprocess.PIPE)
                exit()

        else:  # for any other file
            subprocess.run(["xdg-open", f"{self.current_file}"])
            exit()

    def get_dolphin_window(self):
        # It is used to get the dolphin dbus address.
        dolphin_window = None
        # the qdbus command can change name depending on the distro
        qdbus_command = 'qdbus-qt5' if shutil.which('qdbus-qt5') else 'qdbus'
        # get dolphin windows
        windows = subprocess.run(
            ["bash", "-c", f"{qdbus_command} | grep -i dolphin"], stdout=subprocess.PIPE)
        # convert string to list
        windows = windows.stdout.decode("utf-8").split()

        # controls which dolphin window is active
        for w in windows:
            is_active_window = subprocess.run(
                ["bash", "-c", f"dbus-send --session --print-reply --type=method_call --dest={w} /dolphin/Dolphin_1 org.qtproject.Qt.QWidget.isActiveWindow"], stdout=subprocess.PIPE)
            if "true" in is_active_window.stdout.decode("utf-8").split():
                dolphin_window = w

        # return the active window
        return dolphin_window

    def back(self):
        if self.current_index == 0:
            self.current_index = self.current_index = len(self.files) - 1
        else:
            self.current_index = self.current_index - 1
        self.load_file_at_index(self.current_index)

    def forward(self):

        if len(self.files) - 1 == self.current_index:
            self.current_index = 0
        else:
            self.current_index = self.current_index + 1
        self.load_file_at_index(self.current_index)

    def hide_widgets(self):
        for w in self.added_viewers:
            w.hide()

    def sort_files(self, content_list):
        ordered_files = []
        # sorting: alphabetical order and folders first.
        folders_in_list = [x for x in content_list if os.path.isdir(x)]
        files_in_list = [x for x in content_list if os.path.isfile(x)]

        folders_in_list = sorted(folders_in_list, key=str.lower)
        files_in_list = sorted(files_in_list, key=str.lower)

        ordered_files.extend(folders_in_list)
        ordered_files.extend(files_in_list)
        return ordered_files

    def set_shortcut(self):
        for shortcut_close in ["q", Qt.Key_Escape, Qt.Key_Space]:
            QShortcut(shortcut_close, self).activated.connect(self.app.quit)

        for shortcut_back in ["a", Qt.Key_Left]:
            QShortcut(shortcut_back, self).activated.connect(self.back)

        for shortcut_forward in ["d", Qt.Key_Right]:
            QShortcut(shortcut_forward, self).activated.connect(self.forward)

        for shortcut_open in ["w", Qt.Key_Return]:
            QShortcut(shortcut_open, self).activated.connect(
                self.open_with_app)


def launch():
    app = QApplication(sys.argv)
    window = Main(app)
    window.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    launch()

quick_view.py
Nyre221/dolphin-quick-view
from PyQt5.QtWidgets import QTextBrowser

class TextViewer(QTextBrowser):
    def __init__(self, parent=None):
        super(TextViewer,self).__init__(parent)
        self.verticalScrollBar().setSingleStep(30)

        

text_viewer.py
Nyre221/dolphin-quick-view
from translations import available_placeholder_translations
import os

class Translator:

    def __init__(self):
        self.system_language = os.getenv("LANG").split(".")[0]
        # self.system_language = "en_US"

    def get_translation(self, string: str):
        # get the dictionary of the current locale if exist or return the default one otherwise
        locale_dictionary = available_placeholder_translations.get(self.system_language, available_placeholder_translations["en_US"])
        # return the translated string or the default one in case the string doesn't exist.
        translated_string = locale_dictionary.get(string, available_placeholder_translations["en_US"][string])
        return translated_string


translation_manager.py
Nyre221/dolphin-quick-view
from translations import en_US
from translations import it_IT


available_placeholder_translations = {
    "it_IT":it_IT.dictionary,
    "en_US":en_US.dictionary,
}
translations/__init__.py
Nyre221/dolphin-quick-view
dictionary = dict(
    loading_placeholder="Loading...",
    size="Size",
    last_modified="Last modified",
    files="Files",
    unknown="Unknown",
    container_file_too_big="Preview not available: the file is too big.",
    click_here="Click Here!",
    libreoffice_preview_unavailable="Preview not available.",
    libreoffice_timeout="The document took too long to load.",
    libreoffice_not_found="Libreoffice is not installed:",
    libreoffice_file_not_found="File not found: Flatpak may not be configured correctly:",
    unsupported_file_type="Unsupported file type.",
)

translations/en_US.py
Nyre221/dolphin-quick-view
dictionary = dict(
    loading_placeholder="Caricamento...",
    size="Dimensione",
    last_modified="Ultima modifica",
    files="File",
    unknown="Sconosciuto",
    container_file_too_big="Anteprima non disponibile: il file è troppo grande.",
    click_here="Clicca Qui!",
    libreoffice_preview_unavailable="Anteprima non disponibile",
    libreoffice_timeout="Il documento ha impiegato troppo tempo a caricare.",
    libreoffice_not_found="Libreoffice non è installato:",
    libreoffice_file_not_found="File non trovato: Flatpak potrebbe non essere configurato correttamente:",
    unsupported_file_type="Tipo di file non supportato.",

)

translations/it_IT.py
Nyre221/dolphin-quick-view
from PyQt5.QtWidgets import (QHBoxLayout,
        QPushButton, QSlider, QStyle, QVBoxLayout, QWidget, QShortcut)

from PyQt5.QtCore import Qt,QUrl
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtMultimediaWidgets import QVideoWidget

class VideoViewer(QWidget):

    def __init__(self, parent=None):
        super(VideoViewer, self).__init__(parent)
        self.setFocus()
        self.media_player = QMediaPlayer(None, QMediaPlayer.VideoSurface)
        self.videoWidget = QVideoWidget()
        self.current_file = None
        #play button
        self.play_button = QPushButton()
        self.play_button.setFixedHeight(24)
        self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
        self.play_button.pressed.connect(self.play)

        #position slider
        self.position_slider = QSlider(Qt.Horizontal)
        self.position_slider.setRange(0, 0)
        #in order to avoid glitch the media is paused when the slider is moved 
        self.position_slider.sliderPressed.connect(lambda: self.media_player.pause())
        self.position_slider.sliderReleased.connect(self.setPosition)


        controls_layout = QHBoxLayout()
        controls_layout.setContentsMargins(0, 0, 0, 0)
        controls_layout.addWidget(self.play_button)
        controls_layout.addWidget(self.position_slider)


        main_layout = QVBoxLayout()
        main_layout.addWidget(self.videoWidget)
        main_layout.addLayout(controls_layout)
        self.setLayout(main_layout)
        
        self.media_player.setVideoOutput(self.videoWidget)
        self.media_player.stateChanged.connect(self.mediaStateChanged)
        self.media_player.positionChanged.connect(self.positionChanged)
        self.media_player.durationChanged.connect(self.durationChanged)

        self.set_shortcut()


    def open(self,path):
        self.current_file = path
        self.videoWidget.setFocus()
        self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(path)))
        self.play()


    def play(self):
        if self.media_player.state() == QMediaPlayer.PlayingState:
            self.media_player.pause()
        elif self.media_player.state() == QMediaPlayer.PausedState:
            self.media_player.play()
        elif self.media_player.state() == QMediaPlayer.StoppedState:
            self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(self.current_file)))
            self.media_player.play()


    def mediaStateChanged(self, state):
        if self.media_player.state() == QMediaPlayer.PlayingState:
            self.play_button.setIcon(
                    self.style().standardIcon(QStyle.SP_MediaPause))
        else:
            self.play_button.setIcon(
                    self.style().standardIcon(QStyle.SP_MediaPlay))


    def positionChanged(self, position):
        self.position_slider.setValue(position)


    def durationChanged(self, duration):
        self.position_slider.setRange(0, duration)
    

    def setPosition(self):
        self.media_player.setPosition(self.position_slider.value())
        self.play()



    def time_back(self):
        #milliseconds: 10 sec
        mseconds_back = 10000
        current_pos = self.media_player.position()
        if current_pos -  mseconds_back > 0:
            self.media_player.setPosition(current_pos-mseconds_back)
        else:
            self.media_player.setPosition(0)



    def time_forward(self):
        total_time = self.media_player.duration()
        #milliseconds: 10 sec
        mseconds_forward= 10000
        current_pos = self.media_player.position()
        if mseconds_forward + current_pos < total_time:
            self.media_player.setPosition( mseconds_forward + current_pos)
        else:
            # reaches end of video.
            # -100 is used to trigger the "mediaStateChanged" signal.
            self.media_player.setPosition(total_time-100)

    def set_shortcut(self):
        QShortcut(Qt.Key_Down, self).activated.connect(self.time_back)
        QShortcut(Qt.Key_Up, self).activated.connect(self.time_forward)



    def hide(self) -> None:
        self.media_player.stop()
        return super().hide()

video_viewer.py
Nyre221/dolphin-quick-view