|
| 1 | +import sys |
| 2 | +import os |
| 3 | +import subprocess |
| 4 | +from typing import List, Dict, Any, Optional |
| 5 | + |
| 6 | +from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, |
| 7 | + QHBoxLayout, QListWidget, QListWidgetItem, |
| 8 | + QPushButton, QLineEdit, QSplitter, |
| 9 | + QListWidget, QListView) |
| 10 | +from PySide6.QtCore import Qt, QSize |
| 11 | +from PySide6.QtGui import QIcon, QPixmap |
| 12 | + |
| 13 | +import pytablericons |
| 14 | +from pytablericons.outline_icon import OutlineIcon |
| 15 | +from pytablericons.filled_icon import FilledIcon |
| 16 | + |
| 17 | +from gdrive import gcache |
| 18 | + |
| 19 | +from PySide6.QtSvg import QSvgRenderer |
| 20 | +from PySide6.QtCore import QByteArray, Qt |
| 21 | +from PySide6.QtGui import QPainter |
| 22 | + |
| 23 | +def get_icon(icon_enum, is_filled=False, color: Optional[str] = None) -> QIcon: |
| 24 | + if color is None: |
| 25 | + if QApplication.instance(): |
| 26 | + color = QApplication.palette().windowText().color().name() |
| 27 | + else: |
| 28 | + color = "#000000" |
| 29 | + |
| 30 | + icon_type = 'filled' if is_filled else 'outline' |
| 31 | + svg_path = os.path.join(pytablericons.__path__[0], 'icons', icon_type, icon_enum.value) |
| 32 | + |
| 33 | + with open(svg_path, 'r', encoding='utf-8') as f: |
| 34 | + svg_content = f.read() |
| 35 | + |
| 36 | + if is_filled: |
| 37 | + svg_content = svg_content.replace('fill="currentColor"', f'fill="{color}"') |
| 38 | + else: |
| 39 | + svg_content = svg_content.replace('stroke="currentColor"', f'stroke="{color}"') |
| 40 | + |
| 41 | + renderer = QSvgRenderer(QByteArray(svg_content.encode('utf-8'))) |
| 42 | + |
| 43 | + size = 128 |
| 44 | + pixmap = QPixmap(size, size) |
| 45 | + pixmap.fill(Qt.transparent) |
| 46 | + |
| 47 | + painter = QPainter(pixmap) |
| 48 | + painter.setRenderHint(QPainter.Antialiasing) |
| 49 | + renderer.render(painter) |
| 50 | + painter.end() |
| 51 | + |
| 52 | + return QIcon(pixmap) |
| 53 | + |
| 54 | +def get_mime_icon(mime_type: str) -> QIcon: |
| 55 | + color = QApplication.palette().windowText().color().name() |
| 56 | + if mime_type == 'application/vnd.google-apps.folder': |
| 57 | + return get_icon(FilledIcon.FOLDER, is_filled=True, color=color) |
| 58 | + elif mime_type == 'application/vnd.google-apps.document': |
| 59 | + return get_icon(OutlineIcon.FILE_TEXT, color="#4285F4") |
| 60 | + elif mime_type == 'application/vnd.google-apps.spreadsheet': |
| 61 | + return get_icon(OutlineIcon.FILE_SPREADSHEET, color="#0F9D58") |
| 62 | + elif mime_type == 'application/vnd.google-apps.presentation': |
| 63 | + return get_icon(OutlineIcon.FILE_PRESENTATION, color="#F4B400") |
| 64 | + elif mime_type == 'application/pdf': |
| 65 | + return get_icon(OutlineIcon.FILE_TYPE_PDF, color="#DB4437") |
| 66 | + elif mime_type.startswith('image/'): |
| 67 | + return get_icon(OutlineIcon.PHOTO, color="#DB4437") |
| 68 | + elif mime_type.startswith('audio/'): |
| 69 | + return get_icon(OutlineIcon.FILE_MUSIC, color="#E91E63") |
| 70 | + elif mime_type.startswith('video/'): |
| 71 | + return get_icon(OutlineIcon.MOVIE, color="#FF9800") |
| 72 | + elif 'epub' in mime_type or 'ebook' in mime_type: |
| 73 | + return get_icon(OutlineIcon.BOOK_2, color="#4CAF50") |
| 74 | + elif mime_type == 'application/zip': |
| 75 | + return get_icon(OutlineIcon.FILE_ZIP, color="#9C27B0") |
| 76 | + else: |
| 77 | + return get_icon(OutlineIcon.FILE, color=color) |
| 78 | + |
| 79 | +class GDriveApp(QMainWindow): |
| 80 | + def __init__(self): |
| 81 | + super().__init__() |
| 82 | + self.setWindowTitle("Google Drive Explorer") |
| 83 | + self.resize(1000, 600) |
| 84 | + |
| 85 | + self.history = [] |
| 86 | + self.history_index = -1 |
| 87 | + self.current_folder_id = None |
| 88 | + |
| 89 | + self.init_ui() |
| 90 | + self.load_root("my_drive") |
| 91 | + |
| 92 | + def init_ui(self): |
| 93 | + central_widget = QSplitter(Qt.Horizontal) |
| 94 | + self.setCentralWidget(central_widget) |
| 95 | + |
| 96 | + # Left Panel |
| 97 | + left_panel = QWidget() |
| 98 | + left_layout = QVBoxLayout(left_panel) |
| 99 | + left_layout.setContentsMargins(0, 0, 0, 0) |
| 100 | + |
| 101 | + self.nav_list = QListWidget() |
| 102 | + self.nav_list.setIconSize(QSize(24, 24)) |
| 103 | + my_drive_item = QListWidgetItem("My Drive") |
| 104 | + my_drive_item.setIcon(get_icon(OutlineIcon.BRAND_GOOGLE_DRIVE)) |
| 105 | + my_drive_item.setData(Qt.UserRole, "my_drive") |
| 106 | + self.nav_list.addItem(my_drive_item) |
| 107 | + |
| 108 | + shared_item = QListWidgetItem("Shared with me") |
| 109 | + shared_item.setIcon(get_icon(OutlineIcon.USERS)) |
| 110 | + shared_item.setData(Qt.UserRole, "shared_with_me") |
| 111 | + self.nav_list.addItem(shared_item) |
| 112 | + |
| 113 | + self.nav_list.itemClicked.connect(self.on_nav_clicked) |
| 114 | + left_layout.addWidget(self.nav_list) |
| 115 | + |
| 116 | + # Right Panel |
| 117 | + right_panel = QWidget() |
| 118 | + right_layout = QVBoxLayout(right_panel) |
| 119 | + right_layout.setContentsMargins(0, 0, 0, 0) |
| 120 | + |
| 121 | + # Top Bar |
| 122 | + top_bar = QHBoxLayout() |
| 123 | + self.back_btn = QPushButton() |
| 124 | + self.back_btn.setIcon(get_icon(OutlineIcon.ARROW_LEFT)) |
| 125 | + self.back_btn.clicked.connect(self.go_back) |
| 126 | + |
| 127 | + self.fwd_btn = QPushButton() |
| 128 | + self.fwd_btn.setIcon(get_icon(OutlineIcon.ARROW_RIGHT)) |
| 129 | + self.fwd_btn.clicked.connect(self.go_forward) |
| 130 | + |
| 131 | + self.address_bar = QLineEdit() |
| 132 | + self.address_bar.setReadOnly(True) |
| 133 | + |
| 134 | + top_bar.addWidget(self.back_btn) |
| 135 | + top_bar.addWidget(self.fwd_btn) |
| 136 | + top_bar.addWidget(self.address_bar) |
| 137 | + |
| 138 | + right_layout.addLayout(top_bar) |
| 139 | + |
| 140 | + # Main File View |
| 141 | + self.file_view = QListWidget() |
| 142 | + self.file_view.setViewMode(QListView.IconMode) |
| 143 | + self.file_view.setIconSize(QSize(64, 64)) |
| 144 | + self.file_view.setResizeMode(QListView.Adjust) |
| 145 | + self.file_view.setSpacing(10) |
| 146 | + self.file_view.setWordWrap(True) |
| 147 | + self.file_view.itemDoubleClicked.connect(self.on_item_double_clicked) |
| 148 | + |
| 149 | + right_layout.addWidget(self.file_view) |
| 150 | + |
| 151 | + central_widget.addWidget(left_panel) |
| 152 | + central_widget.addWidget(right_panel) |
| 153 | + central_widget.setSizes([200, 800]) |
| 154 | + |
| 155 | + self.update_nav_buttons() |
| 156 | + |
| 157 | + def load_root(self, root_type: str, add_history=True): |
| 158 | + if root_type == "my_drive": |
| 159 | + items = gcache.get_root_my_drive_children() |
| 160 | + self.address_bar.setText("My Drive") |
| 161 | + else: |
| 162 | + items = gcache.get_root_shared_with_me_items() |
| 163 | + self.address_bar.setText("Shared with me") |
| 164 | + |
| 165 | + self.current_folder_id = root_type |
| 166 | + if add_history: |
| 167 | + self.add_to_history(root_type) |
| 168 | + self.populate_files(items) |
| 169 | + |
| 170 | + def load_folder(self, folder_id: str, folder_name: str, add_history=True): |
| 171 | + items = gcache.get_children(folder_id) |
| 172 | + self.current_folder_id = folder_id |
| 173 | + self.address_bar.setText(folder_name) |
| 174 | + if add_history: |
| 175 | + self.add_to_history(folder_id) |
| 176 | + self.populate_files(items) |
| 177 | + |
| 178 | + def add_to_history(self, folder_id: str): |
| 179 | + # Trim future history if we navigated back then clicked a new folder |
| 180 | + self.history = self.history[:self.history_index + 1] |
| 181 | + self.history.append((folder_id, self.address_bar.text())) |
| 182 | + self.history_index += 1 |
| 183 | + self.update_nav_buttons() |
| 184 | + |
| 185 | + def go_back(self): |
| 186 | + if self.history_index > 0: |
| 187 | + self.history_index -= 1 |
| 188 | + folder_id, name = self.history[self.history_index] |
| 189 | + self._load_from_history(folder_id, name) |
| 190 | + |
| 191 | + def go_forward(self): |
| 192 | + if self.history_index < len(self.history) - 1: |
| 193 | + self.history_index += 1 |
| 194 | + folder_id, name = self.history[self.history_index] |
| 195 | + self._load_from_history(folder_id, name) |
| 196 | + |
| 197 | + def _load_from_history(self, folder_id: str, name: str): |
| 198 | + if folder_id in ["my_drive", "shared_with_me"]: |
| 199 | + self.load_root(folder_id, add_history=False) |
| 200 | + else: |
| 201 | + self.load_folder(folder_id, name, add_history=False) |
| 202 | + self.update_nav_buttons() |
| 203 | + |
| 204 | + def update_nav_buttons(self): |
| 205 | + self.back_btn.setEnabled(self.history_index > 0) |
| 206 | + self.fwd_btn.setEnabled(self.history_index < len(self.history) - 1) |
| 207 | + |
| 208 | + def on_nav_clicked(self, item: QListWidgetItem): |
| 209 | + root_type = item.data(Qt.UserRole) |
| 210 | + self.load_root(root_type) |
| 211 | + |
| 212 | + def populate_files(self, items: List[Dict[str, Any]]): |
| 213 | + self.file_view.clear() |
| 214 | + |
| 215 | + # Sort folders first, then files, both alphabetically |
| 216 | + folders = [] |
| 217 | + files = [] |
| 218 | + for item in items: |
| 219 | + mime = item.get('mimeType', '') |
| 220 | + if mime == 'application/vnd.google-apps.folder': |
| 221 | + folders.append(item) |
| 222 | + else: |
| 223 | + files.append(item) |
| 224 | + |
| 225 | + folders.sort(key=lambda x: x.get('name', '').lower()) |
| 226 | + files.sort(key=lambda x: x.get('name', '').lower()) |
| 227 | + |
| 228 | + for item in folders + files: |
| 229 | + name = item.get('name', 'Unknown') |
| 230 | + mime = item.get('mimeType', '') |
| 231 | + |
| 232 | + list_item = QListWidgetItem(name) |
| 233 | + list_item.setIcon(get_mime_icon(mime)) |
| 234 | + list_item.setData(Qt.UserRole, item) |
| 235 | + self.file_view.addItem(list_item) |
| 236 | + |
| 237 | + def on_item_double_clicked(self, item: QListWidgetItem): |
| 238 | + file_data = item.data(Qt.UserRole) |
| 239 | + mime = file_data.get('mimeType', '') |
| 240 | + |
| 241 | + if mime == 'application/vnd.google-apps.folder': |
| 242 | + self.load_folder(file_data['id'], file_data['name']) |
| 243 | + else: |
| 244 | + self.open_file(file_data) |
| 245 | + |
| 246 | + def open_file(self, file_data: Dict[str, Any]): |
| 247 | + cache_path = gcache.get_cache_path_for_file(file_data) |
| 248 | + if cache_path and cache_path.exists(): |
| 249 | + # Open with default app |
| 250 | + if sys.platform.startswith('linux'): |
| 251 | + subprocess.Popen(['xdg-open', str(cache_path)]) |
| 252 | + elif sys.platform == 'darwin': |
| 253 | + subprocess.Popen(['open', str(cache_path)]) |
| 254 | + else: |
| 255 | + os.startfile(str(cache_path)) |
| 256 | + else: |
| 257 | + # Placeholder for download |
| 258 | + from PySide6.QtWidgets import QMessageBox |
| 259 | + QMessageBox.information(self, "Not in Cache", |
| 260 | + f"'{file_data.get('name')}' is not downloaded yet.\nDownloading will be implemented later.") |
| 261 | + |
| 262 | +if __name__ == "__main__": |
| 263 | + app = QApplication(sys.argv) |
| 264 | + |
| 265 | + # Try to set a modern style |
| 266 | + app.setStyle("Fusion") |
| 267 | + |
| 268 | + window = GDriveApp() |
| 269 | + window.show() |
| 270 | + sys.exit(app.exec()) |
0 commit comments