Skip to content

Commit 9b11bfa

Browse files
committed
Add bare gdrive_app
[skip ci]
1 parent 402eb99 commit 9b11bfa

3 files changed

Lines changed: 288 additions & 15 deletions

File tree

‎scripts/gdrive_app.py‎

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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())

‎scripts/gdrive_cache.py‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,6 @@ def find_nonwebsite_tag_files() -> list[dict]:
269269
random.shuffle(ret)
270270
return ret
271271

272-
@backup_level(54, 'eks', "The Ezra Klein Show Archive")
273-
def find_eks_files() -> list[str]:
274-
return gdrive.gcache.parent_sql_query(
275-
"parent.parent_id = '1_HQsNoi2teB7SzbFX7vMniL01ZGttHuF'"
276-
)
277-
278272
@backup_level(57, "academia.edu", "unsorted PDFs from Academia.edu")
279273
def find_academia_edu_pdfs() -> list[dict]:
280274
return query_parent_name(BULK_PDF_FOLDER_NAMES[BulkPDFType.ACADEMIA_EDU])
@@ -549,9 +543,6 @@ def backup_main(from_level: int=0, new_max_level: int | None=None, parallelism:
549543
run_backup_level(level, parallelism=parallelism)
550544
print(f"All files with priority <= {max_level} are now saved locally!")
551545

552-
def format_cache_percentage(dl_size: int, total_size: int) -> str:
553-
return f"{(float(dl_size)/total_size):.1%} ({format_size(dl_size)}/{format_size(total_size)})"
554-
555546
def print_backup_levels_list(statistics: bool=False):
556547
"""`statistics` replaces the generic description with current fill level stats"""
557548
print("\033[1mGoogle Drive Backup Levels\033[0m")
@@ -569,7 +560,7 @@ def print_backup_levels_list(statistics: bool=False):
569560
cum_sum_dl_size = 0
570561

571562
if statistics:
572-
print(f"\033[4m Lvl: {'Level Name':<16}{'This Level':^25} {'Cummulative':^24}\033[0m")
563+
print(f"\033[4m Lvl: {'Level Name':<16}{'This Level':^27} {'Cummulative':^27}\033[0m")
573564
else:
574565
print(f"\033[4m Lvl: {'Level Name':<16}{'Est. Size':>9} - {'Description'}\033[0m")
575566
for lvl, bl in BACKUP_LEVELS.items():
@@ -599,7 +590,17 @@ def print_backup_levels_list(statistics: bool=False):
599590
cum_sum_dl_size += this_level_inc_dl_size
600591
if statistics:
601592
if this_level_inc_size+this_level_overlap_size > 0:
602-
description = f"{format_cache_percentage(this_level_inc_dl_size+this_level_overlap_dl_size, this_level_inc_size+this_level_overlap_size):<25} {format_cache_percentage(cum_sum_dl_size, cum_sum_size):^24}"
593+
this_level_dl_size = float(this_level_inc_dl_size+this_level_overlap_dl_size)
594+
this_level_total_size = this_level_inc_size+this_level_overlap_size
595+
this_level_percent = f"{this_level_dl_size/this_level_total_size:.1%}"
596+
this_level_col = f"{this_level_percent} ({format_size(this_level_dl_size)}✓"
597+
this_level_missing = this_level_total_size-this_level_dl_size
598+
if this_level_missing > 10000:
599+
this_level_col += f" {format_size(this_level_missing)}𐄂"
600+
this_level_col += ")"
601+
cum_percent = f"{cum_sum_dl_size/cum_sum_size:.1%}"
602+
cum_col = f"{cum_percent} @ +{format_size(this_level_inc_dl_size)} / {format_size(this_level_inc_size)}"
603+
description = f"{this_level_col:<27} {cum_col:<27}"
603604
else:
604605
description = f" [N/A]"
605606
else:

‎scripts/strutils.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,15 @@ def get_file_sizes(files):
480480
def format_size(size_in_bytes):
481481
"""Convert size in bytes to human readable format"""
482482
if size_in_bytes < 1000:
483-
return f"{int(size_in_bytes)} B"
483+
return f"{int(size_in_bytes)}b"
484484
size_in_bytes /= 1024
485-
for unit in ['KB', 'MB', 'GB', 'TB']:
485+
# Play around with spacing and uppercase
486+
# to give the bigger units a slightly bigger feeling
487+
for unit in ['kb', 'MB', ' GB', ' TiB']:
486488
if size_in_bytes < 1000:
487-
return f"{size_in_bytes:.1f} {unit}"
489+
return f"{size_in_bytes:.1f}{unit}"
488490
size_in_bytes /= 1024
489-
return f"{size_in_bytes:.1f} PB"
491+
return f"{size_in_bytes:.1f} PetaBytes"
490492

491493
def write_frontmatter_key(path: Path, key: str, value, insert_after_key=None):
492494
"""Takes a markdown file and top-level frontmatter key and sets it to value

0 commit comments

Comments
 (0)