Split up into multiple files
This commit is contained in:
		@@ -1,424 +1,17 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from typing import Protocol, Self
 | 
			
		||||
import datetime
 | 
			
		||||
import uuid
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
import dotenv
 | 
			
		||||
import httpx
 | 
			
		||||
import keyring
 | 
			
		||||
import pydantic
 | 
			
		||||
from PyQt6.QtCore import Qt, QPoint
 | 
			
		||||
from PyQt6.QtGui import (
 | 
			
		||||
    QAction,
 | 
			
		||||
    QDragEnterEvent,
 | 
			
		||||
    QDragMoveEvent,
 | 
			
		||||
    QDropEvent,
 | 
			
		||||
    QIcon,
 | 
			
		||||
)
 | 
			
		||||
from PyQt6.QtWidgets import (
 | 
			
		||||
    QApplication,
 | 
			
		||||
    QHBoxLayout,
 | 
			
		||||
    QLabel,
 | 
			
		||||
    QLineEdit,
 | 
			
		||||
    QListWidget,
 | 
			
		||||
    QListWidgetItem,
 | 
			
		||||
    QMainWindow,
 | 
			
		||||
    QMenu,
 | 
			
		||||
    QMessageBox,
 | 
			
		||||
    QPushButton,
 | 
			
		||||
    QStackedWidget,
 | 
			
		||||
    QVBoxLayout,
 | 
			
		||||
    QWidget,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RequestClient:
 | 
			
		||||
    _client: httpx.Client = None
 | 
			
		||||
 | 
			
		||||
    def __new__(cls) -> Self:
 | 
			
		||||
        if cls._client is None:
 | 
			
		||||
            url = os.environ.get("DRIVE_HOST_URL").strip()
 | 
			
		||||
            if not url:
 | 
			
		||||
                url = "localhost:3000"
 | 
			
		||||
            cls._client = httpx.Client(base_url=url)
 | 
			
		||||
        return super().__new__(cls)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def set_token(cls, token: str):
 | 
			
		||||
        cls._client.headers = {"Authorization": f"Bearer {token}"}
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def client(self) -> httpx.Client:
 | 
			
		||||
        return self._client
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RegisterWidget(QWidget):
 | 
			
		||||
    def __init__(self, switcher: State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        self.switcher = switcher
 | 
			
		||||
 | 
			
		||||
        self.username_label = QLabel("Username:")
 | 
			
		||||
        self.username_input = QLineEdit()
 | 
			
		||||
 | 
			
		||||
        self.email_label = QLabel("Email:")
 | 
			
		||||
        self.email_input = QLineEdit()
 | 
			
		||||
 | 
			
		||||
        self.password_label = QLabel("Password:")
 | 
			
		||||
        self.password_input = QLineEdit()
 | 
			
		||||
        self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
 | 
			
		||||
 | 
			
		||||
        self.register_button = QPushButton("Register")
 | 
			
		||||
        self.register_button.clicked.connect(self.register)
 | 
			
		||||
 | 
			
		||||
        self.login_button = QPushButton("Switch to Login")
 | 
			
		||||
        self.login_button.clicked.connect(self.switcher.switch_to_login)
 | 
			
		||||
 | 
			
		||||
        layout = QVBoxLayout()
 | 
			
		||||
        layout.addWidget(self.username_label)
 | 
			
		||||
        layout.addWidget(self.username_input)
 | 
			
		||||
        layout.addWidget(self.email_label)
 | 
			
		||||
        layout.addWidget(self.email_input)
 | 
			
		||||
        layout.addWidget(self.password_label)
 | 
			
		||||
        layout.addWidget(self.password_input)
 | 
			
		||||
        layout.addWidget(self.register_button)
 | 
			
		||||
        layout.addWidget(self.login_button)
 | 
			
		||||
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
 | 
			
		||||
    def register(self):
 | 
			
		||||
        username = self.username_input.text()
 | 
			
		||||
        email = self.email_input.text()
 | 
			
		||||
        password = self.password_input.text()
 | 
			
		||||
 | 
			
		||||
        if not username or not email or not password:
 | 
			
		||||
            QMessageBox.warning(self, "Input Error", "All fields are required")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            response = RequestClient().client.post(
 | 
			
		||||
                "http://localhost:3000/users/register",
 | 
			
		||||
                data={
 | 
			
		||||
                    "username": username,
 | 
			
		||||
                    "email": email,
 | 
			
		||||
                    "password": password,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            if response.is_success:
 | 
			
		||||
                token = response.json().get("access_token")
 | 
			
		||||
                if not token:
 | 
			
		||||
                    QMessageBox.critical(
 | 
			
		||||
                        self,
 | 
			
		||||
                        "Error getting the token",
 | 
			
		||||
                        "Error getting the token",
 | 
			
		||||
                    )
 | 
			
		||||
                self.switcher.login(token)
 | 
			
		||||
            else:
 | 
			
		||||
                QMessageBox.warning(
 | 
			
		||||
                    self, "Error", f"Registration failed: {response.text}"
 | 
			
		||||
                )
 | 
			
		||||
        except httpx.HTTPError as e:
 | 
			
		||||
            QMessageBox.critical(self, "HTTP Error", str(e))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginWidget(QWidget):
 | 
			
		||||
    def __init__(self, switcher: State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        self.switcher = switcher
 | 
			
		||||
 | 
			
		||||
        self.username_label = QLabel("Username:")
 | 
			
		||||
        self.username_input = QLineEdit()
 | 
			
		||||
 | 
			
		||||
        self.password_label = QLabel("Password:")
 | 
			
		||||
        self.password_input = QLineEdit()
 | 
			
		||||
        self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
 | 
			
		||||
 | 
			
		||||
        self.login_button = QPushButton("Login")
 | 
			
		||||
        self.login_button.clicked.connect(self.login)
 | 
			
		||||
 | 
			
		||||
        self.register_button = QPushButton("Switch to Register")
 | 
			
		||||
        self.register_button.clicked.connect(self.switcher.switch_to_register)
 | 
			
		||||
 | 
			
		||||
        layout = QVBoxLayout()
 | 
			
		||||
        layout.addWidget(self.username_label)
 | 
			
		||||
        layout.addWidget(self.username_input)
 | 
			
		||||
        layout.addWidget(self.password_label)
 | 
			
		||||
        layout.addWidget(self.password_input)
 | 
			
		||||
        layout.addWidget(self.login_button)
 | 
			
		||||
        layout.addWidget(self.register_button)
 | 
			
		||||
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
 | 
			
		||||
    def login(self):
 | 
			
		||||
        username = self.username_input.text()
 | 
			
		||||
        password = self.password_input.text()
 | 
			
		||||
 | 
			
		||||
        if not username or not password:
 | 
			
		||||
            QMessageBox.warning(
 | 
			
		||||
                self, "Input Error", "Email and Password are required"
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            response = RequestClient().client.post(
 | 
			
		||||
                "http://localhost:3000/users/authorize",
 | 
			
		||||
                data={"username": username, "password": password},
 | 
			
		||||
            )
 | 
			
		||||
            if response.is_success:
 | 
			
		||||
                access_token = response.json().get("access_token")
 | 
			
		||||
                if access_token:
 | 
			
		||||
                    self.switcher.login(access_token)
 | 
			
		||||
                else:
 | 
			
		||||
                    QMessageBox.warning(
 | 
			
		||||
                        self, "Error", "No access token received"
 | 
			
		||||
                    )
 | 
			
		||||
            else:
 | 
			
		||||
                QMessageBox.warning(
 | 
			
		||||
                    self, "Error", f"Login failed: {response.text}"
 | 
			
		||||
                )
 | 
			
		||||
        except httpx.HTTPError as e:
 | 
			
		||||
            QMessageBox.critical(self, "HTTP Error", str(e))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FileListWidget(QListWidget):
 | 
			
		||||
    def __init__(self, state: State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.state = state
 | 
			
		||||
        self.setAcceptDrops(True)
 | 
			
		||||
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
 | 
			
		||||
        self.customContextMenuRequested.connect(self.show_context_menu)
 | 
			
		||||
 | 
			
		||||
        self.responses: list[ListResponse] = []
 | 
			
		||||
        self.update_response()
 | 
			
		||||
 | 
			
		||||
    def current_response(self) -> ListResponse:
 | 
			
		||||
        if not self.responses:
 | 
			
		||||
            self.update_response()
 | 
			
		||||
        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.update()
 | 
			
		||||
 | 
			
		||||
    def dragEnterEvent(self, event: QDragEnterEvent):
 | 
			
		||||
        if event.mimeData().hasUrls():
 | 
			
		||||
            event.accept()
 | 
			
		||||
        else:
 | 
			
		||||
            event.ignore()
 | 
			
		||||
 | 
			
		||||
    def dragMoveEvent(self, e: QDragMoveEvent | None) -> None:
 | 
			
		||||
        return self.dragEnterEvent(e)
 | 
			
		||||
 | 
			
		||||
    def dropEvent(self, event: QDropEvent):
 | 
			
		||||
        event.accept()
 | 
			
		||||
        print("hi")
 | 
			
		||||
        for url in event.mimeData().urls():
 | 
			
		||||
            file_path = url.toLocalFile()
 | 
			
		||||
            self.upload_file(file_path)
 | 
			
		||||
 | 
			
		||||
    def upload_file(self, file_path):
 | 
			
		||||
        file_name = os.path.basename(file_path)
 | 
			
		||||
        try:
 | 
			
		||||
            with open(file_path, "rb") as f:
 | 
			
		||||
                files = {"file": (file_name, f)}
 | 
			
		||||
                response = RequestClient().client.post(
 | 
			
		||||
                    "http://localhost:3000/files",
 | 
			
		||||
                    files=files,
 | 
			
		||||
                    params={
 | 
			
		||||
                        "parent_folder": self.current_response().folder_id
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
                if response.is_success:
 | 
			
		||||
                    QMessageBox.information(
 | 
			
		||||
                        self, "Success", "File uploaded successfully"
 | 
			
		||||
                    )
 | 
			
		||||
                    self.update_response()
 | 
			
		||||
                else:
 | 
			
		||||
                    QMessageBox.warning(
 | 
			
		||||
                        self, "Error", f"Upload failed: {response.text}"
 | 
			
		||||
                    )
 | 
			
		||||
        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 show_context_menu(self, pos: QPoint):
 | 
			
		||||
        item = self.itemAt(pos)
 | 
			
		||||
        if item:
 | 
			
		||||
            menu = QMenu(self)
 | 
			
		||||
            details_action = QAction("Details", self)
 | 
			
		||||
            details_action.triggered.connect(lambda: self.show_details(item))
 | 
			
		||||
            delete_action = QAction("Delete", self)
 | 
			
		||||
            delete_action.triggered.connect(lambda: self.delete_item(item))
 | 
			
		||||
            menu.addAction(details_action)
 | 
			
		||||
            menu.addAction(delete_action)
 | 
			
		||||
            menu.exec(self.mapToGlobal(pos))
 | 
			
		||||
 | 
			
		||||
    def show_details(self, item):
 | 
			
		||||
        file_name = item.text()
 | 
			
		||||
        QMessageBox.information(self, "Details", f"Details for {file_name}")
 | 
			
		||||
 | 
			
		||||
    def delete_item(self, item):
 | 
			
		||||
        row = self.row(item)
 | 
			
		||||
        item = self.current_response().items()[row]
 | 
			
		||||
        item.delete()
 | 
			
		||||
        self.update_response()
 | 
			
		||||
        QMessageBox.information(self, "Delete", f"{item.name()} deleted")
 | 
			
		||||
 | 
			
		||||
    def update(self) -> None:
 | 
			
		||||
        self.clear()
 | 
			
		||||
        last = self.responses[-1]
 | 
			
		||||
        for item in last.items():
 | 
			
		||||
            self.add_file_item(item.name())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sidebar(QWidget):
 | 
			
		||||
    def __init__(self, state: State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.state = state
 | 
			
		||||
        layout = QVBoxLayout()
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
        # Add your sidebar buttons here
 | 
			
		||||
        for i in range(5):  # Example buttons
 | 
			
		||||
            btn = QPushButton(f"Button {i+1}")
 | 
			
		||||
            layout.addWidget(btn)
 | 
			
		||||
        layout.addStretch()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MainFileWidget(QWidget):
 | 
			
		||||
    def __init__(self, state: State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.state = state
 | 
			
		||||
        layout = QHBoxLayout()
 | 
			
		||||
        self.sidebar = Sidebar(state)
 | 
			
		||||
        self.file_list = FileListWidget(state)
 | 
			
		||||
        layout.addWidget(self.sidebar)
 | 
			
		||||
        layout.addWidget(self.file_list)
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DisplayProtocol(Protocol):
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def details(self) -> QWidget:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class File(pydantic.BaseModel):
 | 
			
		||||
    file_id: uuid.UUID
 | 
			
		||||
    file_name: str
 | 
			
		||||
    file_size: int
 | 
			
		||||
    sha512: str
 | 
			
		||||
    created_at: datetime.datetime
 | 
			
		||||
    updated_at: datetime.datetime
 | 
			
		||||
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        return self.file_name
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        RequestClient().client.delete(
 | 
			
		||||
            "/files", params={"file_id": self.file_id}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def details_button(self) -> QPushButton:
 | 
			
		||||
        # TODO
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Folder(pydantic.BaseModel):
 | 
			
		||||
    folder_id: uuid.UUID
 | 
			
		||||
    owner_id: int
 | 
			
		||||
    folder_name: str
 | 
			
		||||
    created_at: datetime.datetime
 | 
			
		||||
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        return self.folder_name
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        RequestClient().client.delete(
 | 
			
		||||
            "/folders", params={"folder_id": self.folder_id}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def details_button(self) -> QPushButton:
 | 
			
		||||
        # TODO
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ListResponse(pydantic.BaseModel):
 | 
			
		||||
    folder_id: uuid.UUID
 | 
			
		||||
    files: list[File]
 | 
			
		||||
    folders: list[Folder]
 | 
			
		||||
 | 
			
		||||
    def items(self) -> list[DisplayProtocol]:
 | 
			
		||||
        return self.files + self.folders
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class State(QMainWindow):
 | 
			
		||||
    def __init__(self, url: str):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        self.setWindowTitle("Auth App")
 | 
			
		||||
 | 
			
		||||
        self.stack = QStackedWidget()
 | 
			
		||||
        self.client = httpx.Client(base_url=url)
 | 
			
		||||
        self.register_widget = RegisterWidget(self)
 | 
			
		||||
        self.login_widget = LoginWidget(self)
 | 
			
		||||
 | 
			
		||||
        self.stack.addWidget(
 | 
			
		||||
            self.login_widget
 | 
			
		||||
        )  # Login widget is the first in the stack
 | 
			
		||||
        self.stack.addWidget(self.register_widget)
 | 
			
		||||
 | 
			
		||||
        self.setCentralWidget(self.stack)
 | 
			
		||||
 | 
			
		||||
        password = keyring.get_credential("auth_app", "access_token")
 | 
			
		||||
        if password is None:
 | 
			
		||||
            self.switch_to_login()  # Start with the login widget
 | 
			
		||||
 | 
			
		||||
        self.login(password.password)
 | 
			
		||||
 | 
			
		||||
    def switch_to_login(self):
 | 
			
		||||
        self.stack.setCurrentWidget(self.login_widget)
 | 
			
		||||
 | 
			
		||||
    def switch_to_register(self):
 | 
			
		||||
        self.stack.setCurrentWidget(self.register_widget)
 | 
			
		||||
 | 
			
		||||
    def login(self, token: str):
 | 
			
		||||
        keyring.set_password("auth_app", "access_token", token)
 | 
			
		||||
        RequestClient().set_token(token)
 | 
			
		||||
        self.file_widget = MainFileWidget(self)
 | 
			
		||||
        self.stack.addWidget(self.file_widget)
 | 
			
		||||
        self.stack.setCurrentWidget(self.file_widget)
 | 
			
		||||
 | 
			
		||||
from PyQt6.QtWidgets import QApplication
 | 
			
		||||
from state import State
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    dotenv.load_dotenv()
 | 
			
		||||
    url = os.environ.get("DRIVE_HOST_URL").strip()
 | 
			
		||||
    url = os.environ.get("DRIVE_HOST_URL")
 | 
			
		||||
    if not url:
 | 
			
		||||
        url = "localhost:3000"
 | 
			
		||||
    else:
 | 
			
		||||
        url = url.strip()
 | 
			
		||||
    app = QApplication(sys.argv)
 | 
			
		||||
    window = State(url)
 | 
			
		||||
    window.show()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										136
									
								
								desktop_client/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								desktop_client/auth.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import httpx
 | 
			
		||||
import state
 | 
			
		||||
from PyQt6.QtWidgets import (
 | 
			
		||||
    QLabel,
 | 
			
		||||
    QLineEdit,
 | 
			
		||||
    QMessageBox,
 | 
			
		||||
    QPushButton,
 | 
			
		||||
    QVBoxLayout,
 | 
			
		||||
    QWidget,
 | 
			
		||||
)
 | 
			
		||||
from request_client import RequestClient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RegisterWidget(QWidget):
 | 
			
		||||
    def __init__(self, switcher: state.State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        self.switcher = switcher
 | 
			
		||||
 | 
			
		||||
        self.username_label = QLabel("Username:")
 | 
			
		||||
        self.username_input = QLineEdit()
 | 
			
		||||
 | 
			
		||||
        self.email_label = QLabel("Email:")
 | 
			
		||||
        self.email_input = QLineEdit()
 | 
			
		||||
 | 
			
		||||
        self.password_label = QLabel("Password:")
 | 
			
		||||
        self.password_input = QLineEdit()
 | 
			
		||||
        self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
 | 
			
		||||
 | 
			
		||||
        self.register_button = QPushButton("Register")
 | 
			
		||||
        self.register_button.clicked.connect(self.register)
 | 
			
		||||
 | 
			
		||||
        self.login_button = QPushButton("Switch to Login")
 | 
			
		||||
        self.login_button.clicked.connect(self.switcher.switch_to_login)
 | 
			
		||||
 | 
			
		||||
        layout = QVBoxLayout()
 | 
			
		||||
        layout.addWidget(self.username_label)
 | 
			
		||||
        layout.addWidget(self.username_input)
 | 
			
		||||
        layout.addWidget(self.email_label)
 | 
			
		||||
        layout.addWidget(self.email_input)
 | 
			
		||||
        layout.addWidget(self.password_label)
 | 
			
		||||
        layout.addWidget(self.password_input)
 | 
			
		||||
        layout.addWidget(self.register_button)
 | 
			
		||||
        layout.addWidget(self.login_button)
 | 
			
		||||
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
 | 
			
		||||
    def register(self):
 | 
			
		||||
        username = self.username_input.text()
 | 
			
		||||
        email = self.email_input.text()
 | 
			
		||||
        password = self.password_input.text()
 | 
			
		||||
 | 
			
		||||
        if not username or not email or not password:
 | 
			
		||||
            QMessageBox.warning(self, "Input Error", "All fields are required")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            response = RequestClient().client.post(
 | 
			
		||||
                "http://localhost:3000/users/register",
 | 
			
		||||
                data={
 | 
			
		||||
                    "username": username,
 | 
			
		||||
                    "email": email,
 | 
			
		||||
                    "password": password,
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            if response.is_success:
 | 
			
		||||
                token = response.json().get("access_token")
 | 
			
		||||
                if not token:
 | 
			
		||||
                    QMessageBox.critical(
 | 
			
		||||
                        self,
 | 
			
		||||
                        "Error getting the token",
 | 
			
		||||
                        "Error getting the token",
 | 
			
		||||
                    )
 | 
			
		||||
                self.switcher.login(token)
 | 
			
		||||
            else:
 | 
			
		||||
                QMessageBox.warning(
 | 
			
		||||
                    self, "Error", f"Registration failed: {response.text}"
 | 
			
		||||
                )
 | 
			
		||||
        except httpx.HTTPError as e:
 | 
			
		||||
            QMessageBox.critical(self, "HTTP Error", str(e))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginWidget(QWidget):
 | 
			
		||||
    def __init__(self, switcher: State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        self.switcher = switcher
 | 
			
		||||
 | 
			
		||||
        self.username_label = QLabel("Username:")
 | 
			
		||||
        self.username_input = QLineEdit()
 | 
			
		||||
 | 
			
		||||
        self.password_label = QLabel("Password:")
 | 
			
		||||
        self.password_input = QLineEdit()
 | 
			
		||||
        self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
 | 
			
		||||
 | 
			
		||||
        self.login_button = QPushButton("Login")
 | 
			
		||||
        self.login_button.clicked.connect(self.login)
 | 
			
		||||
 | 
			
		||||
        self.register_button = QPushButton("Switch to Register")
 | 
			
		||||
        self.register_button.clicked.connect(self.switcher.switch_to_register)
 | 
			
		||||
 | 
			
		||||
        layout = QVBoxLayout()
 | 
			
		||||
        layout.addWidget(self.username_label)
 | 
			
		||||
        layout.addWidget(self.username_input)
 | 
			
		||||
        layout.addWidget(self.password_label)
 | 
			
		||||
        layout.addWidget(self.password_input)
 | 
			
		||||
        layout.addWidget(self.login_button)
 | 
			
		||||
        layout.addWidget(self.register_button)
 | 
			
		||||
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
 | 
			
		||||
    def login(self):
 | 
			
		||||
        username = self.username_input.text()
 | 
			
		||||
        password = self.password_input.text()
 | 
			
		||||
 | 
			
		||||
        if not username or not password:
 | 
			
		||||
            QMessageBox.warning(self, "Input Error", "Email and Password are required")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            response = RequestClient().client.post(
 | 
			
		||||
                "http://localhost:3000/users/authorize",
 | 
			
		||||
                data={"username": username, "password": password},
 | 
			
		||||
            )
 | 
			
		||||
            if response.is_success:
 | 
			
		||||
                access_token = response.json().get("access_token")
 | 
			
		||||
                if access_token:
 | 
			
		||||
                    self.switcher.login(access_token)
 | 
			
		||||
                else:
 | 
			
		||||
                    QMessageBox.warning(self, "Error", "No access token received")
 | 
			
		||||
            else:
 | 
			
		||||
                QMessageBox.warning(self, "Error", f"Login failed: {response.text}")
 | 
			
		||||
        except httpx.HTTPError as e:
 | 
			
		||||
            QMessageBox.critical(self, "HTTP Error", str(e))
 | 
			
		||||
							
								
								
									
										207
									
								
								desktop_client/file_widgets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								desktop_client/file_widgets.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import datetime
 | 
			
		||||
import os
 | 
			
		||||
import uuid
 | 
			
		||||
from typing import Protocol
 | 
			
		||||
 | 
			
		||||
import httpx
 | 
			
		||||
import pydantic
 | 
			
		||||
import state
 | 
			
		||||
from PyQt6.QtCore import QPoint, Qt
 | 
			
		||||
from PyQt6.QtGui import QAction, QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon
 | 
			
		||||
from PyQt6.QtWidgets import (
 | 
			
		||||
    QHBoxLayout,
 | 
			
		||||
    QListWidget,
 | 
			
		||||
    QListWidgetItem,
 | 
			
		||||
    QMenu,
 | 
			
		||||
    QMessageBox,
 | 
			
		||||
    QPushButton,
 | 
			
		||||
    QVBoxLayout,
 | 
			
		||||
    QWidget,
 | 
			
		||||
)
 | 
			
		||||
from request_client import RequestClient
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DisplayProtocol(Protocol):
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def details(self) -> QWidget:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class File(pydantic.BaseModel):
 | 
			
		||||
    file_id: uuid.UUID
 | 
			
		||||
    file_name: str
 | 
			
		||||
    file_size: int
 | 
			
		||||
    sha512: str
 | 
			
		||||
    created_at: datetime.datetime
 | 
			
		||||
    updated_at: datetime.datetime
 | 
			
		||||
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        return self.file_name
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        RequestClient().client.delete("/files", params={"file_id": self.file_id})
 | 
			
		||||
 | 
			
		||||
    def details_button(self) -> QPushButton:
 | 
			
		||||
        # TODO
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Folder(pydantic.BaseModel):
 | 
			
		||||
    folder_id: uuid.UUID
 | 
			
		||||
    owner_id: int
 | 
			
		||||
    folder_name: str
 | 
			
		||||
    created_at: datetime.datetime
 | 
			
		||||
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
        return self.folder_name
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        RequestClient().client.delete("/folders", params={"folder_id": self.folder_id})
 | 
			
		||||
 | 
			
		||||
    def details_button(self) -> QPushButton:
 | 
			
		||||
        # TODO
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ListResponse(pydantic.BaseModel):
 | 
			
		||||
    folder_id: uuid.UUID
 | 
			
		||||
    files: list[File]
 | 
			
		||||
    folders: list[Folder]
 | 
			
		||||
 | 
			
		||||
    def items(self) -> list[DisplayProtocol]:
 | 
			
		||||
        return self.files + self.folders
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FileListWidget(QListWidget):
 | 
			
		||||
    def __init__(self, state: state.State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.state = state
 | 
			
		||||
        self.setAcceptDrops(True)
 | 
			
		||||
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
 | 
			
		||||
        self.customContextMenuRequested.connect(self.show_context_menu)
 | 
			
		||||
 | 
			
		||||
        self.responses: list[ListResponse] = []
 | 
			
		||||
        self.update_response()
 | 
			
		||||
 | 
			
		||||
    def current_response(self) -> ListResponse:
 | 
			
		||||
        if not self.responses:
 | 
			
		||||
            self.update_response()
 | 
			
		||||
        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.update()
 | 
			
		||||
 | 
			
		||||
    def dragEnterEvent(self, event: QDragEnterEvent):
 | 
			
		||||
        if event.mimeData().hasUrls():
 | 
			
		||||
            event.accept()
 | 
			
		||||
        else:
 | 
			
		||||
            event.ignore()
 | 
			
		||||
 | 
			
		||||
    def dragMoveEvent(self, e: QDragMoveEvent | None) -> None:
 | 
			
		||||
        return self.dragEnterEvent(e)
 | 
			
		||||
 | 
			
		||||
    def dropEvent(self, event: QDropEvent):
 | 
			
		||||
        event.accept()
 | 
			
		||||
        print("hi")
 | 
			
		||||
        for url in event.mimeData().urls():
 | 
			
		||||
            file_path = url.toLocalFile()
 | 
			
		||||
            self.upload_file(file_path)
 | 
			
		||||
 | 
			
		||||
    def upload_file(self, file_path):
 | 
			
		||||
        file_name = os.path.basename(file_path)
 | 
			
		||||
        try:
 | 
			
		||||
            with open(file_path, "rb") as f:
 | 
			
		||||
                files = {"file": (file_name, f)}
 | 
			
		||||
                response = RequestClient().client.post(
 | 
			
		||||
                    "http://localhost:3000/files",
 | 
			
		||||
                    files=files,
 | 
			
		||||
                    params={"parent_folder": self.current_response().folder_id},
 | 
			
		||||
                )
 | 
			
		||||
                if response.is_success:
 | 
			
		||||
                    QMessageBox.information(
 | 
			
		||||
                        self, "Success", "File uploaded successfully"
 | 
			
		||||
                    )
 | 
			
		||||
                    self.update_response()
 | 
			
		||||
                else:
 | 
			
		||||
                    QMessageBox.warning(
 | 
			
		||||
                        self, "Error", f"Upload failed: {response.text}"
 | 
			
		||||
                    )
 | 
			
		||||
        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 show_context_menu(self, pos: QPoint):
 | 
			
		||||
        item = self.itemAt(pos)
 | 
			
		||||
        if item:
 | 
			
		||||
            menu = QMenu(self)
 | 
			
		||||
            details_action = QAction("Details", self)
 | 
			
		||||
            details_action.triggered.connect(lambda: self.show_details(item))
 | 
			
		||||
            delete_action = QAction("Delete", self)
 | 
			
		||||
            delete_action.triggered.connect(lambda: self.delete_item(item))
 | 
			
		||||
            menu.addAction(details_action)
 | 
			
		||||
            menu.addAction(delete_action)
 | 
			
		||||
            menu.exec(self.mapToGlobal(pos))
 | 
			
		||||
 | 
			
		||||
    def show_details(self, item):
 | 
			
		||||
        file_name = item.text()
 | 
			
		||||
        QMessageBox.information(self, "Details", f"Details for {file_name}")
 | 
			
		||||
 | 
			
		||||
    def delete_item(self, item):
 | 
			
		||||
        row = self.row(item)
 | 
			
		||||
        item = self.current_response().items()[row]
 | 
			
		||||
        item.delete()
 | 
			
		||||
        self.update_response()
 | 
			
		||||
        QMessageBox.information(self, "Delete", f"{item.name()} deleted")
 | 
			
		||||
 | 
			
		||||
    def update(self) -> None:
 | 
			
		||||
        self.clear()
 | 
			
		||||
        last = self.responses[-1]
 | 
			
		||||
        for item in last.items():
 | 
			
		||||
            self.add_file_item(item.name())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Sidebar(QWidget):
 | 
			
		||||
    def __init__(self, state: state.State):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.state = state
 | 
			
		||||
        layout = QVBoxLayout()
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
        # Add your sidebar buttons here
 | 
			
		||||
        for i in range(5):  # Example buttons
 | 
			
		||||
            btn = QPushButton(f"Button {i+1}")
 | 
			
		||||
            layout.addWidget(btn)
 | 
			
		||||
        layout.addStretch()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
        layout.addWidget(self.sidebar)
 | 
			
		||||
        layout.addWidget(self.file_list)
 | 
			
		||||
        self.setLayout(layout)
 | 
			
		||||
							
								
								
									
										24
									
								
								desktop_client/request_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								desktop_client/request_client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import os
 | 
			
		||||
from typing import Self
 | 
			
		||||
 | 
			
		||||
import httpx
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RequestClient:
 | 
			
		||||
    _client: httpx.Client = None
 | 
			
		||||
 | 
			
		||||
    def __new__(cls) -> Self:
 | 
			
		||||
        if cls._client is None:
 | 
			
		||||
            url = os.environ.get("DRIVE_HOST_URL").strip()
 | 
			
		||||
            if not url:
 | 
			
		||||
                url = "localhost:3000"
 | 
			
		||||
            cls._client = httpx.Client(base_url=url)
 | 
			
		||||
        return super().__new__(cls)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def set_token(cls, token: str):
 | 
			
		||||
        cls._client.headers = {"Authorization": f"Bearer {token}"}
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def client(self) -> httpx.Client:
 | 
			
		||||
        return self._client
 | 
			
		||||
							
								
								
									
										44
									
								
								desktop_client/state.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								desktop_client/state.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import auth
 | 
			
		||||
import file_widgets
 | 
			
		||||
import httpx
 | 
			
		||||
import keyring
 | 
			
		||||
import request_client
 | 
			
		||||
from PyQt6.QtWidgets import QMainWindow, QStackedWidget
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class State(QMainWindow):
 | 
			
		||||
    def __init__(self, url: str):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
        self.setWindowTitle("Auth App")
 | 
			
		||||
 | 
			
		||||
        self.stack = QStackedWidget()
 | 
			
		||||
        self.client = httpx.Client(base_url=url)
 | 
			
		||||
        self.register_widget = auth.RegisterWidget(self)
 | 
			
		||||
        self.login_widget = auth.LoginWidget(self)
 | 
			
		||||
 | 
			
		||||
        self.stack.addWidget(
 | 
			
		||||
            self.login_widget
 | 
			
		||||
        )  # Login widget is the first in the stack
 | 
			
		||||
        self.stack.addWidget(self.register_widget)
 | 
			
		||||
 | 
			
		||||
        self.setCentralWidget(self.stack)
 | 
			
		||||
 | 
			
		||||
        password = keyring.get_credential("auth_app", "access_token")
 | 
			
		||||
        if password is None:
 | 
			
		||||
            self.switch_to_login()  # Start with the login widget
 | 
			
		||||
 | 
			
		||||
        self.login(password.password)
 | 
			
		||||
 | 
			
		||||
    def switch_to_login(self):
 | 
			
		||||
        self.stack.setCurrentWidget(self.login_widget)
 | 
			
		||||
 | 
			
		||||
    def switch_to_register(self):
 | 
			
		||||
        self.stack.setCurrentWidget(self.register_widget)
 | 
			
		||||
 | 
			
		||||
    def login(self, token: str):
 | 
			
		||||
        keyring.set_password("auth_app", "access_token", token)
 | 
			
		||||
        request_client.RequestClient().set_token(token)
 | 
			
		||||
        self.file_widget = file_widgets.MainFileWidget(self)
 | 
			
		||||
        self.stack.addWidget(self.file_widget)
 | 
			
		||||
        self.stack.setCurrentWidget(self.file_widget)
 | 
			
		||||
		Reference in New Issue
	
	Block a user