diff --git a/res/styles/device-list.qss b/res/styles/device-list.qss new file mode 100644 index 0000000..045f80e --- /dev/null +++ b/res/styles/device-list.qss @@ -0,0 +1,17 @@ +QListView { + outline: 0; + background-color: #FAFAFA; +} + +QListView::item{ + background-color: #E6E6E6; +} + +QListView::item:hover{ + background-color: #D6D6D6; +} + +QListView::item:active:selected{ + color: black; + background-color: #C1C1C1; +} \ No newline at end of file diff --git a/res/styles/file-list.qss b/res/styles/file-list.qss new file mode 100644 index 0000000..e1f4873 --- /dev/null +++ b/res/styles/file-list.qss @@ -0,0 +1,22 @@ +QListView { + outline: 0; + background-color: #FAFAFA; +} + +QListView::item{ + background-color: #E6E6E6; +} + +QListView::item:hover{ + background-color: #D6D6D6; +} + +QListView::item:active:selected{ + color: black; + background-color: #C1C1C1; + border: 1px solid #666666; +} + +QListView::item:active:!selected{ + border: 1px solid #AAAAAA; +} \ No newline at end of file diff --git a/res/styles/window.qss b/res/styles/window.qss index e38a9c8..309bf96 100644 --- a/res/styles/window.qss +++ b/res/styles/window.qss @@ -1,3 +1,3 @@ QWidget { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-family: url("-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"); } \ No newline at end of file diff --git a/src/app.py b/src/app.py index d18de98..62304ca 100644 --- a/src/app.py +++ b/src/app.py @@ -21,6 +21,7 @@ from core.configurations import Resources from core.main import Adb from gui.window import MainWindow +from helpers.tools import read_string_from_file if __name__ == '__main__': adb = Adb() @@ -30,7 +31,7 @@ app = QApplication(sys.argv) window = MainWindow() - window.setStyleSheet(Resources.read_string_from_file(Resources.style_window)) + window.setStyleSheet(read_string_from_file(Resources.style_window)) window.show() sys.exit(app.exec_()) diff --git a/src/core/configurations.py b/src/core/configurations.py index 5825c85..319893f 100644 --- a/src/core/configurations.py +++ b/src/core/configurations.py @@ -18,19 +18,14 @@ import pathlib import platform -from PyQt5.QtCore import QFile, QIODevice, QTextStream - from data.models import Device from helpers.tools import Singleton class Application: - __version__ = '1.1.0' + __version__ = '1.2.0' __metaclass__ = Singleton - def __init__(self): - print(Application.NOTICE) - NOTICE = f"""\033[0;32m ADB File Explorer v{__version__} Copyright (C) 2022 Azat Aldeshov Platform {platform.platform()} @@ -71,6 +66,8 @@ class Resources: path = os.path.join(Application.PATH, 'res') style_window = os.path.join(path, 'styles', 'window.qss') + style_file_list = os.path.join(path, 'styles', 'file-list.qss') + style_device_list = os.path.join(path, 'styles', 'device-list.qss') style_notification_button = os.path.join(path, 'styles', 'notification-button.qss') icon_logo = os.path.join(path, 'icons', 'logo.svg') @@ -93,12 +90,3 @@ class Resources: icon_folder_create = os.path.join(path, 'icons', 'files', 'actions', 'folder_create.svg') anim_loading = os.path.join(path, 'anim', 'loading.gif') - - @staticmethod - def read_string_from_file(path: str): - file = QFile(path) - if file.open(QIODevice.ReadOnly | QIODevice.Text): - text = QTextStream(file).readAll() - file.close() - return text - return '' diff --git a/src/gui/abstract/__init__.py b/src/gui/abstract/__init__.py deleted file mode 100644 index 8449e1e..0000000 --- a/src/gui/abstract/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# ADB File Explorer `tool` -# Copyright (C) 2022 Azat Aldeshov azata1919@gmail.com -# -# 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 . diff --git a/src/gui/abstract/base.py b/src/gui/abstract/base.py deleted file mode 100644 index a8ee683..0000000 --- a/src/gui/abstract/base.py +++ /dev/null @@ -1,205 +0,0 @@ -# ADB File Explorer `tool` -# Copyright (C) 2022 Azat Aldeshov azata1919@gmail.com -# -# 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 . - -from PyQt5 import QtCore, QtGui -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtGui import QPaintEvent, QPainter, QPixmap, QMovie -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QStyleOption, QStyle, QSizePolicy, QVBoxLayout, QGridLayout, \ - QLineEdit - -from core.configurations import Resources - - -class BaseIconWidget(QLabel): - def __init__(self, path, width=32, height=32, context: QWidget = None): - super(BaseIconWidget, self).__init__() - self.setParent(context) - self.pixmap = QPixmap(path) - self.pixmap = self.pixmap.scaled(width, height, Qt.KeepAspectRatio) - self.setPixmap(self.pixmap) - - -class BaseListHeaderWidget(QWidget): - def __init__(self): - super().__init__() - self.layout = QHBoxLayout() - self.layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.layout) - - @staticmethod - def property(label, **kwargs): - return BaseListItemWidget.property( - label, - font_style=kwargs.get("font_style"), - alignment=kwargs.get("alignment"), - width_policy=kwargs.get("width_policy"), - height_policy=kwargs.get("height_policy"), - stretch=kwargs.get("stretch") - ) - - -class BaseListItemWidget(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.setMinimumHeight(40) - self.setObjectName("item") - self.installEventFilter(self) - self.setStyleSheet("background-color: #E6E6E6;") - - self.layout = QHBoxLayout() - self.layout.setContentsMargins(0, 0, 0, 0) - self.layout.setSpacing(0) - self.setLayout(self.layout) - - def enterEvent(self, event: QtCore.QEvent): - self.setStyleSheet("background-color: #D6D6D6;") - - def leaveEvent(self, event: QtCore.QEvent): - self.setStyleSheet("background-color: #E6E6E6;") - - def mousePressEvent(self, event: QtGui.QMouseEvent): - self.setStyleSheet("background-color: #C1C1C1;") - - def mouseReleaseEvent(self, event: QtGui.QMouseEvent): - self.setStyleSheet("background-color: #D6D6D6;") - - def paintEvent(self, event: QPaintEvent): - option = QStyleOption() - option.initFrom(self) - painter = QPainter(self) - self.style().drawPrimitive(QStyle.PrimitiveElement(), option, painter, self) - super().paintEvent(event) - - @staticmethod - def icon(path: str, **kwargs): - icon = BaseIconWidget(path, width=kwargs.get("width") or 28, height=kwargs.get("height") or 28) - icon.setContentsMargins(kwargs.get("margin") or 5, 0, 0, 0) - policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - policy.setHorizontalStretch(1) - icon.setSizePolicy(policy) - return icon - - @staticmethod - def name(label: str, **kwargs): - name = QLabel(label) - name.setContentsMargins(kwargs.get("margin") or 10, 0, 0, 0) - policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) - policy.setHorizontalStretch(kwargs.get("stretch") or 4) - name.setSizePolicy(policy) - return name - - @staticmethod - def editable_name(label: str, **kwargs): - edit = QLineEdit() - edit.setStyleSheet("QLineEdit { background: white; padding 0; }") - policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) - policy.setHorizontalStretch(kwargs.get("stretch") or 4) - edit.setSizePolicy(policy) - edit.setVisible(False) - edit.setText(label) - return edit - - @staticmethod - def property(label, **kwargs): - prop = QLabel(label) - if kwargs.get("font_style"): - prop.setStyleSheet(kwargs.get("font_style")) - if kwargs.get("alignment"): - prop.setAlignment(kwargs.get("alignment")) - - policy = QSizePolicy( - kwargs.get("width_policy") or QSizePolicy.Ignored, - kwargs.get("height_policy") or QSizePolicy.Preferred - ) - policy.setHorizontalStretch(kwargs.get("stretch") or 2) - prop.setSizePolicy(policy) - return prop - - @staticmethod - def separator(width=1): - item = QLabel() - item.setStyleSheet("QLabel { background-color: #CACACA }") - item.setMaximumWidth(width) - return item - - -class BaseListWidget(QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.widgets = [] - self.loading_widget = None - - self.layout = QVBoxLayout() - self.layout.setSpacing(0) - self.layout.addStretch() - self.setLayout(self.layout) - - def load(self, widgets, empty_message="Empty", empty_full=True): - self.clear() - - if not widgets: - self.empty(empty_message, empty_full) - else: - for widget in widgets: - self.widgets.append(widget) - self.layout.insertWidget(self.layout.count() - 1, widget) - - def loading(self): - self.clear() - gif = QLabel(self) - movie = QMovie(Resources.anim_loading) - movie.setScaledSize(QSize(48, 48)) - gif.setAlignment(Qt.AlignCenter) - gif.setMovie(movie) - - box = QGridLayout() - box.addWidget(gif, 1, 0) - box.setAlignment(Qt.AlignCenter) - - self.loading_widget = QWidget() - self.loading_widget.setLayout(box) - self.widgets.append(self.loading_widget) - self.layout.addWidget(self.loading_widget, 1) - movie.start() - - def clear(self): - if self.loading_widget: - self.layout.removeWidget(self.loading_widget) - self.loading_widget.close() - self.loading_widget = None - for widget in self.widgets: - self.layout.removeWidget(widget) - widget.close() - widget.deleteLater() - self.widgets.clear() - - def empty(self, msg, full): - if full: - label = QLabel(msg) - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("color: #969696") - box = QVBoxLayout() - box.addWidget(label) - box.addStretch() - - widget = QWidget() - widget.setLayout(box) - self.widgets.append(widget) - self.layout.addWidget(widget) - else: - label = QLabel(msg) - label.setStyleSheet("color: #969696") - self.layout.insertWidget(self.layout.count() - 1, label) diff --git a/src/gui/explorer/__init__.py b/src/gui/explorer/__init__.py index 8449e1e..970dbd1 100644 --- a/src/gui/explorer/__init__.py +++ b/src/gui/explorer/__init__.py @@ -13,3 +13,40 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . + +from PyQt5.QtWidgets import QWidget, QVBoxLayout + +from core.main import Adb +from core.managers import Global +from gui.explorer.devices import DeviceExplorerWidget +from gui.explorer.files import FileExplorerWidget + + +class MainExplorer(QWidget): + def __init__(self, parent=None): + super(MainExplorer, self).__init__(parent) + self.body = QWidget(self) + self.setLayout(QVBoxLayout(self)) + + Global().communicate.files.connect(self.files) + Global().communicate.devices.connect(self.devices) + + def files(self): + self.clear() + + self.body = FileExplorerWidget(self) + self.layout().addWidget(self.body) + self.body.update() + + def devices(self): + self.clear() + Adb.manager().clear_device() + + self.body = DeviceExplorerWidget(self) + self.layout().addWidget(self.body) + self.body.update() + + def clear(self): + self.layout().removeWidget(self.body) + self.body.close() + self.body.deleteLater() diff --git a/src/gui/explorer/devices.py b/src/gui/explorer/devices.py index d9ea61f..4b723c3 100644 --- a/src/gui/explorer/devices.py +++ b/src/gui/explorer/devices.py @@ -14,43 +14,154 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from PyQt5.QtCore import Qt +from typing import List, Any + +from PyQt5 import QtGui, QtCore +from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex, QRect, QVariant, QSize +from PyQt5.QtGui import QPalette, QPixmap, QMovie +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QStyledItemDelegate, QStyleOptionViewItem, QApplication, \ + QStyle, QListView from core.configurations import Resources from core.main import Adb from core.managers import Global from data.models import Device, DeviceType, MessageData from data.repositories import DeviceRepository -from gui.abstract.base import BaseListWidget, BaseListItemWidget, BaseListHeaderWidget -from helpers.tools import AsyncRepositoryWorker +from helpers.tools import AsyncRepositoryWorker, read_string_from_file + + +class DeviceItemDelegate(QStyledItemDelegate): + def sizeHint(self, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtCore.QSize: + result = super(DeviceItemDelegate, self).sizeHint(option, index) + result.setHeight(40) + return result + + def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): + if not index.data(): + return super(DeviceItemDelegate, self).paint(painter, option, index) + top = option.rect.top() + bottom = option.rect.height() + first_start = option.rect.left() + 50 + second_start = option.rect.left() + int(option.rect.width() / 2) + end = option.rect.width() + option.rect.left() -class DeviceHeaderWidget(BaseListHeaderWidget): - def __init__(self): - super(DeviceHeaderWidget, self).__init__() + self.initStyleOption(option, index) + style = option.widget.style() if option.widget else QApplication.style() + style.drawControl(QStyle.CE_ItemViewItem, option, painter, option.widget) + painter.setPen(option.palette.color(QPalette.Normal, QPalette.Text)) - self.layout.addWidget( - self.property('Connected devices', alignment=Qt.AlignCenter) + painter.drawText( + QRect(first_start, top, second_start - first_start - 4, bottom), + option.displayAlignment, index.data().name ) + painter.drawText( + QRect(second_start, top, end - second_start, bottom), + Qt.AlignCenter | option.displayAlignment, index.data().id + ) + + +class DeviceListModel(QAbstractListModel): + def __init__(self, parent=None): + super().__init__(parent) + self.items: List[Device] = [] + + def clear(self): + self.beginResetModel() + self.items.clear() + self.endResetModel() -class DeviceListWidget(BaseListWidget): + def populate(self, devices: list): + self.beginResetModel() + self.items.clear() + self.items = devices + self.endResetModel() + + def rowCount(self, parent: QModelIndex = ...) -> int: + return len(self.items) + + def icon_path(self, index: QModelIndex = ...): + return Resources.icon_phone if self.items[index.row()].type == DeviceType.DEVICE \ + else Resources.icon_phone_unknown + + def data(self, index: QModelIndex, role: int = ...) -> Any: + if not index.isValid(): + return QVariant() + + if role == Qt.DisplayRole: + return self.items[index.row()] + elif role == Qt.DecorationRole: + return QPixmap(self.icon_path(index)).scaled(32, 32, Qt.KeepAspectRatio) + return QVariant() + + +class DeviceExplorerWidget(QWidget): DEVICES_WORKER_ID = 200 - def __init__(self, parent): - super(DeviceListWidget, self).__init__(parent) + def __init__(self, parent=None): + super(DeviceExplorerWidget, self).__init__(parent) + self.setLayout(QVBoxLayout(self)) + + self.header = QLabel('Connected devices', self) + self.header.setAlignment(Qt.AlignCenter) + self.layout().addWidget(self.header) + + self.list = QListView(self) + self.model = DeviceListModel(self.list) + + self.list.setSpacing(1) + self.list.setModel(self.model) + self.list.clicked.connect(self.open) + self.list.setItemDelegate(DeviceItemDelegate(self.list)) + self.list.setStyleSheet(read_string_from_file(Resources.style_device_list)) + self.layout().addWidget(self.list) + + self.loading = QLabel(self) + self.loading.setAlignment(Qt.AlignCenter) + self.loading_movie = QMovie(Resources.anim_loading, parent=self.loading) + self.loading_movie.setScaledSize(QSize(48, 48)) + self.loading.setMovie(self.loading_movie) + self.layout().addWidget(self.loading) + + self.empty_label = QLabel("No connected devices", self) + self.empty_label.setAlignment(Qt.AlignTop) + self.empty_label.setContentsMargins(15, 10, 0, 0) + self.empty_label.setStyleSheet("color: #969696; border: 1px solid #969696") + self.layout().addWidget(self.empty_label) + + self.layout().setStretch(self.layout().count() - 1, 1) + self.layout().setStretch(self.layout().count() - 2, 1) + + def update(self): + super(DeviceExplorerWidget, self).update() worker = AsyncRepositoryWorker( - worker_id=self.DEVICES_WORKER_ID, name="Devices", + worker_id=self.DEVICES_WORKER_ID, repository_method=DeviceRepository.devices, arguments=(), - response_callback=self.__async_response + response_callback=self._async_response ) if Adb.worker().work(worker): - self.loading() + # First Setup loading view + self.model.clear() + self.list.setHidden(True) + self.loading.setHidden(False) + self.empty_label.setHidden(True) + self.loading_movie.start() + + # Then start async worker worker.start() - def __async_response(self, devices, error): + @property + def device(self): + if self.list and self.list.currentIndex() is not None: + return self.model.items[self.list.currentIndex().row()] + + def _async_response(self, devices, error): + self.loading_movie.stop() + self.loading.setHidden(True) + if error: Global().communicate.notification.emit( MessageData( @@ -59,30 +170,14 @@ def __async_response(self, devices, error): body=f" {error} " ) ) - - widgets = [] - for device in devices: - item = DeviceItemWidget(device) - widgets.append(item) - self.load(widgets, "No connected devices", False) - - -class DeviceItemWidget(BaseListItemWidget): - def __init__(self, device: Device): - super(DeviceItemWidget, self).__init__() - self.device = device - if device.type == DeviceType.DEVICE: - self.layout.addWidget(self.icon(Resources.icon_phone, width=32, height=32)) + if not devices: + self.empty_label.setHidden(False) else: - self.layout.addWidget(self.icon(Resources.icon_phone_unknown, width=32, height=32)) - - self.layout.addWidget(self.name(device.name)) - self.layout.addWidget(self.property(device.id)) - - def mouseReleaseEvent(self, event): - super(DeviceItemWidget, self).mouseReleaseEvent(event) + self.list.setHidden(False) + self.model.populate(devices) - if event.button() == Qt.LeftButton and self.device.type == DeviceType.DEVICE: + def open(self): + if self.device.type == DeviceType.DEVICE: if Adb.manager().set_device(self.device): Global().communicate.files.emit() else: diff --git a/src/gui/explorer/files.py b/src/gui/explorer/files.py index c80e13e..485d72a 100644 --- a/src/gui/explorer/files.py +++ b/src/gui/explorer/files.py @@ -15,148 +15,330 @@ # along with this program. If not, see . import sys +from typing import List, Any -from PyQt5 import QtCore -from PyQt5.QtCore import Qt, QPoint -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QFileDialog +from PyQt5 import QtCore, QtGui +from PyQt5.QtCore import Qt, QPoint, QModelIndex, QAbstractListModel, QVariant, QRect, QSize, QEvent, QObject +from PyQt5.QtGui import QPixmap, QColor, QPalette, QMovie, QKeySequence +from PyQt5.QtWidgets import QMenu, QAction, QMessageBox, QFileDialog, QStyle, QWidget, QStyledItemDelegate, \ + QStyleOptionViewItem, QApplication, QListView, QVBoxLayout, QLabel, QSizePolicy, QHBoxLayout from core.configurations import Resources from core.main import Adb from core.managers import Global from data.models import File, FileType, MessageData, MessageType from data.repositories import FileRepository -from gui.abstract.base import BaseListItemWidget, BaseListWidget, BaseListHeaderWidget -from helpers.tools import AsyncRepositoryWorker, ProgressCallbackHelper +from gui.explorer.toolbar import ParentButton, UploadTools, PathBar +from helpers.tools import AsyncRepositoryWorker, ProgressCallbackHelper, read_string_from_file + + +class FileHeaderWidget(QWidget): + def __init__(self, parent=None): + super(FileHeaderWidget, self).__init__(parent) + self.setLayout(QHBoxLayout(self)) + policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) + + self.file = QLabel('File', self) + self.file.setContentsMargins(45, 0, 0, 0) + policy.setHorizontalStretch(39) + self.file.setSizePolicy(policy) + self.layout().addWidget(self.file) + + self.permissions = QLabel('Permissions', self) + self.permissions.setAlignment(Qt.AlignCenter) + policy.setHorizontalStretch(18) + self.permissions.setSizePolicy(policy) + self.layout().addWidget(self.permissions) + + self.size = QLabel('Size', self) + self.size.setAlignment(Qt.AlignCenter) + policy.setHorizontalStretch(21) + self.size.setSizePolicy(policy) + self.layout().addWidget(self.size) + + self.date = QLabel('Date', self) + self.date.setAlignment(Qt.AlignCenter) + policy.setHorizontalStretch(22) + self.date.setSizePolicy(policy) + self.layout().addWidget(self.date) + + self.setStyleSheet("background: #E5E5E5; font-weight: 500;") + + +class FileExplorerToolbar(QWidget): + def __init__(self, parent=None): + super(FileExplorerToolbar, self).__init__(parent) + self.setLayout(QHBoxLayout(self)) + + self.upload_tools = UploadTools(self) + self.upload_tools.setFixedHeight(32) + policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) + policy.setHorizontalStretch(1) + self.upload_tools.setSizePolicy(policy) + self.layout().addWidget(self.upload_tools) + + self.parent_button = ParentButton(self) + self.parent_button.setFixedHeight(32) + policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) + policy.setHorizontalStretch(1) + self.parent_button.setSizePolicy(policy) + self.layout().addWidget(self.parent_button) + + self.path_bar = PathBar(self) + policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred) + policy.setHorizontalStretch(8) + self.path_bar.setSizePolicy(policy) + self.layout().addWidget(self.path_bar) + + +class FileItemDelegate(QStyledItemDelegate): + def sizeHint(self, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtCore.QSize: + result = super(FileItemDelegate, self).sizeHint(option, index) + result.setHeight(40) + return result + + def setEditorData(self, editor: QWidget, index: QtCore.QModelIndex): + editor.setText(index.model().data(index, Qt.EditRole)) + + def updateEditorGeometry(self, editor: QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): + editor.setGeometry( + option.rect.left() + 50, option.rect.top(), int(option.rect.width() / 2.5) - 54, option.rect.height() + ) + + def setModelData(self, editor: QWidget, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex): + model.setData(index, editor.text(), Qt.EditRole) + + @staticmethod + def paint_line(painter: QtGui.QPainter, color: QColor, x, y, w, h): + painter.setPen(color) + painter.drawLine(x, y, w, h) + + @staticmethod + def paint_text(painter: QtGui.QPainter, text: str, color: QColor, options, x, y, w, h): + painter.setPen(color) + painter.drawText(QRect(x, y, w, h), options, text) + def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): + if not index.data(): + return super(FileItemDelegate, self).paint(painter, option, index) -class FileHeaderWidget(BaseListHeaderWidget): - def __init__(self): - super(FileHeaderWidget, self).__init__() + self.initStyleOption(option, index) + style = option.widget.style() if option.widget else QApplication.style() + style.drawControl(QStyle.CE_ItemViewItem, option, painter, option.widget) - self.layout.addWidget( - BaseListItemWidget.name('File', margin=48) + line_color = QColor("#CCCCCC") + text_color = option.palette.color(QPalette.Normal, QPalette.Text) + + top = option.rect.top() + bottom = option.rect.height() + + first_start = option.rect.left() + 50 + second_start = option.rect.left() + int(option.rect.width() / 2.5) + third_start = option.rect.left() + int(option.rect.width() / 1.75) + fourth_start = option.rect.left() + int(option.rect.width() / 1.25) + end = option.rect.width() + option.rect.left() + + self.paint_text( + painter, index.data().name, text_color, option.displayAlignment, + first_start, top, second_start - first_start - 4, bottom ) - self.layout.addWidget( - self.property('Permissions', alignment=Qt.AlignCenter) + self.paint_line(painter, line_color, second_start - 2, top, second_start - 1, bottom) + + self.paint_text( + painter, index.data().permissions, text_color, Qt.AlignCenter | option.displayAlignment, + second_start, top, third_start - second_start - 4, bottom ) - self.layout.addWidget( - self.property('Size', alignment=Qt.AlignCenter) + self.paint_line(painter, line_color, third_start - 2, top, third_start - 1, bottom) + + self.paint_text( + painter, index.data().size, text_color, Qt.AlignCenter | option.displayAlignment, + third_start, top, fourth_start - third_start - 4, bottom ) - self.layout.addWidget( - self.property('Date', alignment=Qt.AlignCenter, stretch=3) + self.paint_line(painter, line_color, fourth_start - 2, top, fourth_start - 1, bottom) + + self.paint_text( + painter, index.data().date, text_color, Qt.AlignCenter | option.displayAlignment, + fourth_start, top, end - fourth_start, bottom ) -class FileListWidget(BaseListWidget): - FILES_WORKER_ID = 300 +class FileListModel(QAbstractListModel): + def __init__(self, parent=None): + super().__init__(parent) + self.items: List[File] = [] - def __init__(self, parent): - super(FileListWidget, self).__init__(parent) + def clear(self): + self.beginResetModel() + self.items.clear() + self.endResetModel() - def update(self): - super(FileListWidget, self).update() - worker = AsyncRepositoryWorker( - worker_id=self.FILES_WORKER_ID, - name="Files", - repository_method=FileRepository.files, - response_callback=self.__async_response, - arguments=() - ) - if Adb.worker().work(worker): - self.loading() - worker.start() + def populate(self, files: list): + self.beginResetModel() + self.items.clear() + self.items = files + self.endResetModel() - def __async_response(self, files, error): - if error: - print(error, file=sys.stderr) - if error and not files: - Global().communicate.notification.emit( - MessageData( - title='Files', - timeout=15000, - body=f" {error} " + def rowCount(self, parent: QModelIndex = ...) -> int: + return len(self.items) + + def icon_path(self, index: QModelIndex = ...): + file_type = self.items[index.row()].type + if file_type == FileType.DIRECTORY: + return Resources.icon_folder + elif file_type == FileType.FILE: + return Resources.icon_file + elif file_type == FileType.LINK: + link_type = self.items[index.row()].link_type + if link_type == FileType.DIRECTORY: + return Resources.icon_link_folder + elif link_type == FileType.FILE: + return Resources.icon_link_file + return Resources.icon_link_file_unknown + return Resources.icon_file_unknown + + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + if not index.isValid(): + return Qt.NoItemFlags + + return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable + + def setData(self, index: QModelIndex, value: Any, role: int = ...) -> bool: + if role == Qt.EditRole and value: + data, error = FileRepository.rename(self.items[index.row()], value) + if error: + Global().communicate.notification.emit( + MessageData( + timeout=10000, + title="Rename", + body=f" {error} ", + ) ) - ) + Global.communicate.files__refresh.emit() + return super(FileListModel, self).setData(index, value, role) - widgets = [] - for file in files: - item = FileItemWidget(self, file) - widgets.append(item) - self.load(widgets, "Folder is empty") - Global().communicate.path_toolbar__refresh.emit() + def data(self, index: QModelIndex, role: int = ...) -> Any: + if not index.isValid(): + return QVariant() + if role == Qt.DisplayRole: + return self.items[index.row()] + elif role == Qt.EditRole: + return self.items[index.row()].name + elif role == Qt.DecorationRole: + return QPixmap(self.icon_path(index)).scaled(32, 32, Qt.KeepAspectRatio) + return QVariant() -class FileItemWidget(BaseListItemWidget): + +class FileExplorerWidget(QWidget): + FILES_WORKER_ID = 300 DOWNLOAD_WORKER_ID = 399 - def __init__(self, parent, file: File): - super(FileItemWidget, self).__init__(parent) - self.file = file - self.name_widget = self.name(self.file.name) - self.name_edit_widget = self.editable_name(self.file.name) - self.name_edit_widget.installEventFilter(self) + def __init__(self, parent=None): + super(FileExplorerWidget, self).__init__(parent) + self.setLayout(QVBoxLayout(self)) + self.layout().setSpacing(5) - self.setContextMenuPolicy(Qt.CustomContextMenu) - self.customContextMenuRequested.connect(self.context_menu) + self.toolbar = FileExplorerToolbar(self) + self.layout().addWidget(self.toolbar) - self.layout.addWidget( - self.icon(self.icon_path) - ) + self.header = FileHeaderWidget(self) + self.layout().addWidget(self.header) - self.layout.addWidget( - self.name_widget - ) + self.list = QListView(self) + self.model = FileListModel(self.list) - self.layout.addWidget( - self.name_edit_widget - ) + self.list.setSpacing(1) + self.list.setModel(self.model) + self.list.installEventFilter(self) + self.list.doubleClicked.connect(self.open) + self.list.setItemDelegate(FileItemDelegate(self.list)) + self.list.setContextMenuPolicy(Qt.CustomContextMenu) + self.list.customContextMenuRequested.connect(self.context_menu) + self.list.setStyleSheet(read_string_from_file(Resources.style_file_list)) + self.layout().addWidget(self.list) - self.layout.addWidget(self.separator()) + self.loading = QLabel(self) + self.loading.setAlignment(Qt.AlignCenter) + self.loading_movie = QMovie(Resources.anim_loading, parent=self.loading) + self.loading_movie.setScaledSize(QSize(48, 48)) + self.loading.setMovie(self.loading_movie) + self.layout().addWidget(self.loading) - self.layout.addWidget( - self.property(self.file.permissions, font_style="italic", alignment=Qt.AlignCenter) - ) + self.empty_label = QLabel("Folder is empty", self) + self.empty_label.setAlignment(Qt.AlignCenter) + self.empty_label.setStyleSheet("color: #969696; border: 1px solid #969696") + self.layout().addWidget(self.empty_label) - self.layout.addWidget(self.separator()) + self.layout().setStretch(self.layout().count() - 1, 1) + self.layout().setStretch(self.layout().count() - 2, 1) - self.layout.addWidget( - self.property(self.file.size, alignment=Qt.AlignCenter) - ) + Global().communicate.files__refresh.connect(self.update) - self.layout.addWidget(self.separator()) + @property + def file(self): + if self.list and self.list.currentIndex(): + return self.model.items[self.list.currentIndex().row()] - self.layout.addWidget( - self.property(self.file.date, alignment=Qt.AlignCenter, stretch=3) + def update(self): + super(FileExplorerWidget, self).update() + worker = AsyncRepositoryWorker( + name="Files", + worker_id=self.FILES_WORKER_ID, + repository_method=FileRepository.files, + response_callback=self._async_response, + arguments=() ) + if Adb.worker().work(worker): + # First Setup loading view + self.model.clear() + self.list.setHidden(True) + self.loading.setHidden(False) + self.empty_label.setHidden(True) + self.loading_movie.start() + + # Then start async worker + worker.start() + Global().communicate.path_toolbar__refresh.emit() - self.setToolTip(self.file.name) - if self.file.type == FileType.LINK: - self.setToolTip(self.file.link) - - @property - def icon_path(self): - if self.file.type == FileType.DIRECTORY: - return Resources.icon_folder - elif self.file.type == FileType.FILE: - return Resources.icon_file - elif self.file.type == FileType.LINK: - if self.file.link_type == FileType.DIRECTORY: - return Resources.icon_link_folder - elif self.file.link_type == FileType.FILE: - return Resources.icon_link_file - return Resources.icon_link_file_unknown - return Resources.icon_file_unknown + def close(self) -> bool: + Global().communicate.files__refresh.disconnect() + return super(FileExplorerWidget, self).close() - def mouseReleaseEvent(self, event): - super(FileItemWidget, self).mouseReleaseEvent(event) + def _async_response(self, files: list, error: str): + self.loading_movie.stop() + self.loading.setHidden(True) - if event.button() == Qt.LeftButton: - if Adb.manager().open(self.file): - Global().communicate.files__refresh.emit() + if error: + print(error, file=sys.stderr) + if not files: + Global().communicate.notification.emit( + MessageData( + title='Files', + timeout=15000, + body=f" {error} " + ) + ) + if not files: + self.empty_label.setHidden(False) + else: + self.list.setHidden(False) + self.model.populate(files) + self.list.setFocus() + + def eventFilter(self, obj: 'QObject', event: 'QEvent') -> bool: + if obj == self.list and \ + event.type() == QEvent.KeyPress and \ + event.matches(QKeySequence.InsertParagraphSeparator) and \ + not self.list.isPersistentEditorOpen(self.list.currentIndex()): + self.open(self.list.currentIndex()) + return super(FileExplorerWidget, self).eventFilter(obj, event) + + def open(self, index: QModelIndex = ...): + if Adb.manager().open(self.model.items[index.row()]): + Global().communicate.files__refresh.emit() def context_menu(self, pos: QPoint): menu = QMenu() @@ -171,7 +353,7 @@ def context_menu(self, pos: QPoint): menu.addAction(action_move) action_rename = QAction('Rename', self) - action_rename.triggered.connect(self.open_rename) + action_rename.triggered.connect(self.rename) menu.addAction(action_rename) action_delete = QAction('Delete', self) @@ -195,7 +377,7 @@ def context_menu(self, pos: QPoint): menu.exec(self.mapToGlobal(pos)) @staticmethod - def __async_response(data, error): + def default_response(data, error): if error: Global().communicate.notification.emit( MessageData( @@ -213,22 +395,8 @@ def __async_response(data, error): ) ) - def open_rename(self): - self.name_widget.setVisible(False) - self.name_edit_widget.setVisible(True) - self.name_edit_widget.setText(self.name_widget.text()) - self.name_edit_widget.returnPressed.connect(self.rename) - self.name_edit_widget.setFocus() - - def close_rename(self): - self.name_widget.setVisible(True) - self.name_edit_widget.setVisible(False) - self.name_edit_widget.disconnect() - - def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: - if event.type() == event.FocusOut and obj == self.name_edit_widget: - self.close_rename() - return super(FileItemWidget, self).eventFilter(obj, event) + def rename(self): + self.list.edit(self.list.currentIndex()) def delete(self): reply = QMessageBox.critical( @@ -258,26 +426,13 @@ def delete(self): ) Global.communicate.files__refresh.emit() - def rename(self): - if self.name_edit_widget.text(): - data, error = FileRepository.rename(self.file, self.name_edit_widget.text()) - if error: - Global().communicate.notification.emit( - MessageData( - timeout=10000, - title="Rename", - body=f" {error} ", - ) - ) - Global.communicate.files__refresh.emit() - def download(self): helper = ProgressCallbackHelper() worker = AsyncRepositoryWorker( worker_id=self.DOWNLOAD_WORKER_ID, name="Download", repository_method=FileRepository.download, - response_callback=FileItemWidget.__async_response, + response_callback=self.default_response, arguments=(helper.progress_callback.emit, self.file.path) ) if Adb.worker().work(worker): @@ -299,8 +454,10 @@ def download_to(self): worker_id=self.DOWNLOAD_WORKER_ID, name="Download", repository_method=FileRepository.download_to, - response_callback=FileItemWidget.__async_response, - arguments=(helper.progress_callback.emit, self.file.path, dir_name) + response_callback=self.default_response, + arguments=( + helper.progress_callback.emit, self.file.path, dir_name + ) ) if Adb.worker().work(worker): Global().communicate.notification.emit( @@ -315,8 +472,8 @@ def download_to(self): def file_properties(self): file, error = FileRepository.file(self.file.path) - if file: - self.file = file + file = file if file else self.file + if error: Global().communicate.notification.emit( MessageData( @@ -326,20 +483,23 @@ def file_properties(self): ) ) - info = f"
{str(self.file)}
" - info += f"
Name:        {self.file.name or '-'}
" - info += f"
Owner:       {self.file.owner or '-'}
" - info += f"
Group:       {self.file.group or '-'}
" - info += f"
Size:        {self.file.size or '-'}
" - info += f"
Permissions: {self.file.permissions or '-'}
" - info += f"
Date:        {self.file.date__raw or '-'}
" - info += f"
Type:        {self.file.type or '-'}
" + info = f"
{str(file)}
" + info += f"
Name:        {file.name or '-'}
" + info += f"
Owner:       {file.owner or '-'}
" + info += f"
Group:       {file.group or '-'}
" + info += f"
Size:        {file.size or '-'}
" + info += f"
Permissions: {file.permissions or '-'}
" + info += f"
Date:        {file.date__raw or '-'}
" + info += f"
Type:        {file.type or '-'}
" - if self.file.type == FileType.LINK: - info += f"
Links to:    {self.file.link or '-'}
" + if file.type == FileType.LINK: + info += f"
Links to:    {file.link or '-'}
" properties = QMessageBox(self) - properties.setIconPixmap(QPixmap(self.icon_path).scaled(128, 128, Qt.KeepAspectRatio)) + properties.setStyleSheet("background-color: #DDDDDD") + properties.setIconPixmap( + QPixmap(self.model.icon_path(self.list.currentIndex())).scaled(128, 128, Qt.KeepAspectRatio) + ) properties.setWindowTitle('Properties') properties.setInformativeText(info) properties.exec_() diff --git a/src/gui/explorer/main.py b/src/gui/explorer/main.py deleted file mode 100644 index 50c47c7..0000000 --- a/src/gui/explorer/main.py +++ /dev/null @@ -1,121 +0,0 @@ -# ADB File Explorer `tool` -# Copyright (C) 2022 Azat Aldeshov azata1919@gmail.com -# -# 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 . - -from PyQt5.QtWidgets import QWidget, QScrollArea, QVBoxLayout, QHBoxLayout, QSizePolicy - -from core.main import Adb -from core.managers import Global -from gui.explorer.devices import DeviceHeaderWidget, DeviceListWidget -from gui.explorer.files import FileHeaderWidget, FileListWidget -from gui.explorer.toolbar import UploadTools, ParentButton, PathBar - - -class FileExplorerToolbar(QWidget): - def __init__(self, parent): - super(FileExplorerToolbar, self).__init__(parent) - self.explorer = parent - self.layout = QHBoxLayout() - self.setLayout(self.layout) - - self.upload = UploadTools(self) - self.upload.setFixedHeight(32) - policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) - policy.setHorizontalStretch(1) - self.upload.setSizePolicy(policy) - self.layout.addWidget(self.upload) - - self.parent_dir = ParentButton(self) - self.parent_dir.setFixedHeight(32) - policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) - policy.setHorizontalStretch(1) - self.parent_dir.setSizePolicy(policy) - self.layout.addWidget(self.parent_dir) - - self.path_bar = PathBar(self) - policy = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) - policy.setHorizontalStretch(8) - self.path_bar.setSizePolicy(policy) - self.layout.addWidget(self.path_bar) - - -class Explorer(QWidget): - def __init__(self, main_window): - super().__init__() - self.main_window = main_window - self.layout = QVBoxLayout() - self.setLayout(self.layout) - - self.body = QWidget() - self.header = QWidget() - self.toolbar = QWidget() - self.scroll = QScrollArea(self) - self.scroll.setWidgetResizable(True) - - Global().communicate.files.connect(self.files) - Global().communicate.devices.connect(self.devices) - - def __update_files(self): - self.scroll.widget().update() - Global().communicate.path_toolbar__refresh.emit() - - def files(self): - self.clear() - self.toolbar = FileExplorerToolbar(self) - self.layout.addWidget(self.toolbar) - - self.header = FileHeaderWidget() - self.layout.addWidget(self.header) - - self.body = FileListWidget(self) - self.scroll.setWidget(self.body) - self.layout.addWidget(self.scroll) - - self.scroll.widget().update() - - Global().communicate.files__refresh.connect(print) - Global().communicate.files__refresh.disconnect() - Global().communicate.files__refresh.connect(self.__update_files) - - def devices(self): - self.clear() - Adb.manager().clear_device() - self.toolbar = QWidget() - - self.header = DeviceHeaderWidget() - self.layout.addWidget(self.header) - - self.body = DeviceListWidget(self) - self.scroll.setWidget(self.body) - self.layout.addWidget(self.scroll) - - def clear(self): - self.toolbar.close() - self.toolbar.deleteLater() - self.layout.removeWidget(self.toolbar) - del self.toolbar - - self.header.close() - self.header.deleteLater() - self.layout.removeWidget(self.header) - del self.header - - self.body.close() - self.body.deleteLater() - self.layout.removeWidget(self.body) - del self.body - - self.layout.removeWidget(self.scroll) - self.scroll.setWidget(None) diff --git a/src/gui/explorer/toolbar.py b/src/gui/explorer/toolbar.py index 0def9d8..54db106 100644 --- a/src/gui/explorer/toolbar.py +++ b/src/gui/explorer/toolbar.py @@ -22,7 +22,6 @@ from core.managers import Global from data.models import MessageData, MessageType from data.repositories import FileRepository -from gui.explorer.files import FileListWidget from helpers.tools import AsyncRepositoryWorker, ProgressCallbackHelper @@ -145,7 +144,7 @@ def __init__(self, parent): @staticmethod def __action__(): - if Adb.worker().check(FileListWidget.FILES_WORKER_ID) and Adb.manager().up(): + if Adb.worker().check(300) and Adb.manager().up(): Global().communicate.files__refresh.emit() diff --git a/src/gui/others/help.py b/src/gui/help/__init__.py similarity index 91% rename from src/gui/others/help.py rename to src/gui/help/__init__.py index d82d7ab..a9827f0 100644 --- a/src/gui/others/help.py +++ b/src/gui/help/__init__.py @@ -15,17 +15,17 @@ # along with this program. If not, see . from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtWidgets import QWidget, QLabel, QApplication -from gui.abstract.base import BaseIconWidget from core.configurations import Resources, Application class About(QWidget): def __init__(self): super(QWidget, self).__init__() - icon = BaseIconWidget(Resources.icon_logo, width=64, height=64, context=self) + icon = QLabel(self) + icon.setPixmap(QPixmap(Resources.icon_logo).scaled(64, 64, Qt.KeepAspectRatio)) icon.move(168, 40) about_text = "

" about_text += "ADB File Explorer
" @@ -47,4 +47,4 @@ def __init__(self): self.setFixedWidth(400) center = QApplication.desktop().availableGeometry(self).center() - self.move(int(center.x() - self.width() * 0.5), int(center.y() - self.height() * 0.5)) + self.move(int(center.x() - self.width() * 0.5), int(center.y() - self.height() * 0.5)) \ No newline at end of file diff --git a/src/gui/others/additional.py b/src/gui/others/additional.py index 7d7a7af..4f5bdc9 100644 --- a/src/gui/others/additional.py +++ b/src/gui/others/additional.py @@ -41,8 +41,7 @@ def __init__(self, parent, text): self.text.setAlignment(Qt.AlignCenter) self.layout.addWidget(self.text) - self.setMinimumWidth(192) - self.setMinimumHeight(192) + self.setMinimumSize(192, 192) self.setWindowModality(Qt.WindowModal) self.setWindowFlags(QtCore.Qt.Dialog | Qt.Window | Qt.CustomizeWindowHint) diff --git a/src/gui/others/notification.py b/src/gui/others/notification.py index c88fe29..ad572f2 100644 --- a/src/gui/others/notification.py +++ b/src/gui/others/notification.py @@ -24,6 +24,7 @@ from core.configurations import Resources from data.models import MessageType +from helpers.tools import read_string_from_file class BaseMessage(QWidget): @@ -103,7 +104,7 @@ def create_close(self): button.setIcon(QIcon(Resources.icon_close)) button.setFixedSize(32, 32) button.setIconSize(QSize(10, 10)) - button.setStyleSheet(Resources.read_string_from_file(Resources.style_notification_button)) + button.setStyleSheet(read_string_from_file(Resources.style_notification_button)) button.clicked.connect(lambda: self.close() or None) self.header.addWidget(button) diff --git a/src/gui/window.py b/src/gui/window.py index af347ea..dc64f02 100644 --- a/src/gui/window.py +++ b/src/gui/window.py @@ -22,8 +22,8 @@ from core.managers import Global from data.models import MessageData, MessageType from data.repositories import DeviceRepository -from gui.explorer.main import Explorer -from gui.others.help import About +from gui.explorer import MainExplorer +from gui.help import About from gui.others.notification import NotificationCenter from helpers.tools import AsyncRepositoryWorker @@ -154,14 +154,12 @@ def __init__(self): super(MainWindow, self).__init__() self.setMenuBar(MenuBar(self)) - self.setCentralWidget(Explorer(self)) + self.setCentralWidget(MainExplorer(self)) - self.move(300, 300) self.resize(640, 480) - self.setMinimumWidth(480) - self.setMinimumHeight(360) - self.setWindowIcon(QIcon(Resources.icon_logo)) + self.setMinimumSize(480, 360) self.setWindowTitle('ADB File Explorer') + self.setWindowIcon(QIcon(Resources.icon_logo)) # Show Devices Widget Global().communicate.devices.emit() diff --git a/src/helpers/tools.py b/src/helpers/tools.py index c0deb74..4205cc6 100644 --- a/src/helpers/tools.py +++ b/src/helpers/tools.py @@ -19,7 +19,7 @@ import subprocess from PyQt5 import QtCore -from PyQt5.QtCore import QThread, QObject +from PyQt5.QtCore import QThread, QObject, QFile, QIODevice, QTextStream from PyQt5.QtWidgets import QWidget from adb_shell.auth.keygen import keygen from adb_shell.auth.sign_pythonrsa import PythonRSASigner @@ -131,3 +131,12 @@ def get_python_rsa_keys_signer(rerun=True) -> PythonRSASigner: os.mkdir(path) keygen(key) return get_python_rsa_keys_signer(False) + + +def read_string_from_file(path: str): + file = QFile(path) + if file.open(QIODevice.ReadOnly | QIODevice.Text): + text = QTextStream(file).readAll() + file.close() + return text + return str()