diff --git a/assets/file.png b/assets/file.png new file mode 100644 index 0000000..9c9f83e Binary files /dev/null and b/assets/file.png differ diff --git a/assets/folder.png b/assets/folder.png new file mode 100644 index 0000000..0099fbb Binary files /dev/null and b/assets/folder.png differ diff --git a/desktop_client/file_widgets.py b/desktop_client/file_widgets.py index 5cdd512..b23bf10 100644 --- a/desktop_client/file_widgets.py +++ b/desktop_client/file_widgets.py @@ -3,13 +3,19 @@ from __future__ import annotations import datetime import os import uuid -from typing import Protocol +from typing import Protocol, Self import httpx import pydantic import state from PyQt6.QtCore import QPoint, Qt -from PyQt6.QtGui import QAction, QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon +from PyQt6.QtGui import ( + QAction, + QDragEnterEvent, + QDragMoveEvent, + QDropEvent, + QIcon, +) from PyQt6.QtWidgets import ( QHBoxLayout, QListWidget, @@ -19,6 +25,8 @@ from PyQt6.QtWidgets import ( QPushButton, QVBoxLayout, QWidget, + QLabel, + QFileDialog, ) from request_client import RequestClient @@ -30,7 +38,13 @@ class DisplayProtocol(Protocol): def delete(self) -> None: raise NotImplementedError - def details(self) -> QWidget: + def details(self, list: FileListWidget) -> QWidget: + raise NotImplementedError + + def icon(self) -> QIcon: + raise NotImplementedError + + def double_click(self, list: FileListWidget) -> None: raise NotImplementedError @@ -46,11 +60,51 @@ class File(pydantic.BaseModel): return self.file_name def delete(self) -> None: - RequestClient().client.delete("/files", params={"file_id": self.file_id}) + RequestClient().client.delete( + "/files", params={"file_id": self.file_id} + ) - def details_button(self) -> QPushButton: - # TODO - raise NotImplementedError + def details(self, list: FileListWidget) -> QWidget: + del list + details = ( + f"file id: {self.file_id}\nfile_name: {self.file_name}\n" + + f"file_size: {self._format_bytes(self.file_size)}\n" + + f"created at: {self.created_at}\nupdated at: {self.updated_at}" + ) + label = QLabel() + label.setWindowTitle("File info") + label.setText(details) + return label + + @staticmethod + def _format_bytes(size: int): + power = 2**10 + n = 0 + power_labels = {0: "", 1: "kilo", 2: "mega", 3: "giga", 4: "tera"} + while size > power and n < 4: + size /= power + n += 1 + return size, power_labels[n] + "bytes" + + def icon(self) -> QIcon: + return QIcon("assets/file.png") + + def double_click(self, list: FileListWidget) -> None: + location = QFileDialog.getExistingDirectory( + list, caption="Select save location" + ) + if not location: + return + with open( + os.path.join(location, self.file_name), "wb" + ) as f, RequestClient().client.stream( + "GET", "/files", params={"file_id": self.file_id} + ) as stream: + if not stream.is_success: + QMessageBox.warning(list, "Error downloading the file") + return + for data in stream.iter_bytes(): + f.write(data) class Folder(pydantic.BaseModel): @@ -63,12 +117,29 @@ class Folder(pydantic.BaseModel): return self.folder_name def delete(self) -> None: - RequestClient().client.delete("/folders", params={"folder_id": self.folder_id}) + RequestClient().client.delete( + "/folders", params={"folder_id": self.folder_id} + ) - def details_button(self) -> QPushButton: + def details(self, list: FileListWidget) -> QWidget: # TODO raise NotImplementedError + def icon(self) -> QIcon: + return QIcon("assets/folder.png") + + def double_click(self, list: FileListWidget) -> None: + list.responses.append(ListResponse.get(self.folder_id)) + list.update() + + +class ResponseProtocol(Protocol): + def items(self) -> list[DisplayProtocol]: + raise NotImplementedError + + def update(self) -> ResponseProtocol: + raise NotImplementedError + class ListResponse(pydantic.BaseModel): folder_id: uuid.UUID @@ -78,6 +149,18 @@ class ListResponse(pydantic.BaseModel): def items(self) -> list[DisplayProtocol]: return self.files + self.folders + @classmethod + def get(cls, folder_id: uuid.UUID | None = None) -> Self: + params = {} + if folder_id: + params["folder_id"] = folder_id + return ListResponse.model_validate_json( + RequestClient().client.get("/folders", params=params).text + ) + + def update(self) -> ResponseProtocol: + return self.get(self.folder_id) + class FileListWidget(QListWidget): def __init__(self, state: state.State): @@ -86,9 +169,16 @@ class FileListWidget(QListWidget): self.setAcceptDrops(True) self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.show_context_menu) + self.itemDoubleClicked.connect(self.show_item) + self.details_widget = None - self.responses: list[ListResponse] = [] - self.update_response() + self.responses: list[ResponseProtocol] = [ListResponse.get()] + self.update() + + def show_item(self, qitem: QListWidgetItem) -> None: + row = self.row(qitem) + item = self.current_response().items()[row] + item.double_click(self) def current_response(self) -> ListResponse: if not self.responses: @@ -96,17 +186,7 @@ class FileListWidget(QListWidget): return self.responses[-1] def update_response(self): - if not self.responses: - response = ListResponse.model_validate_json( - RequestClient().client.get("/folders").text - ) - self.responses.append(response) - print(response.files) - self.responses[-1] = ListResponse.model_validate_json( - RequestClient() - .client.get("/folders", params={"folder_id": self.responses[-1].folder_id}) - .text - ) + self.responses[-1] = self.responses[-1].update() self.update() def dragEnterEvent(self, event: QDragEnterEvent): @@ -133,7 +213,9 @@ class FileListWidget(QListWidget): response = RequestClient().client.post( "http://localhost:3000/files", files=files, - params={"parent_folder": self.current_response().folder_id}, + params={ + "parent_folder": self.current_response().folder_id + }, ) if response.is_success: QMessageBox.information( @@ -147,10 +229,10 @@ class FileListWidget(QListWidget): except httpx.HTTPError as e: QMessageBox.critical(self, "HTTP Error", str(e)) - def add_file_item(self, file_name): - item = QListWidgetItem(file_name) - item.setIcon(QIcon.fromTheme("text-x-generic")) # File icon - self.addItem(item) + def add_item(self, item: DisplayProtocol): + widget = QListWidgetItem(item.name()) + widget.setIcon(item.icon()) + self.addItem(widget) def show_context_menu(self, pos: QPoint): item = self.itemAt(pos) @@ -165,8 +247,10 @@ class FileListWidget(QListWidget): menu.exec(self.mapToGlobal(pos)) def show_details(self, item): - file_name = item.text() - QMessageBox.information(self, "Details", f"Details for {file_name}") + row = self.row(item) + item = self.current_response().items()[row] + self.details_widget = item.details(self) + self.details_widget.show() def delete_item(self, item): row = self.row(item) @@ -179,13 +263,14 @@ class FileListWidget(QListWidget): self.clear() last = self.responses[-1] for item in last.items(): - self.add_file_item(item.name()) + self.add_item(item) class Sidebar(QWidget): - def __init__(self, state: state.State): + def __init__(self, state: state.State, file_list: FileListWidget): super().__init__() self.state = state + self.file_list = file_list layout = QVBoxLayout() self.setLayout(layout) # Add your sidebar buttons here @@ -194,14 +279,27 @@ class Sidebar(QWidget): layout.addWidget(btn) layout.addStretch() + def get_user(self): ... + + def logout(self): ... + + def go_back(self): ... + + def go_root(self): ... + + def get_tlp(self): + """Get top level permitted folders""" + + def sync(self): ... + class MainFileWidget(QWidget): def __init__(self, state: state.State): super().__init__() self.state = state layout = QHBoxLayout() - self.sidebar = Sidebar(state) self.file_list = FileListWidget(state) + self.sidebar = Sidebar(state, self.file_list) layout.addWidget(self.sidebar) layout.addWidget(self.file_list) self.setLayout(layout)