1717from gdrive import gcache
1818
1919from PySide6 .QtSvg import QSvgRenderer
20- from PySide6 .QtCore import QByteArray , Qt
21- from PySide6 .QtGui import QPainter
20+ from PySide6 .QtCore import QByteArray , Qt , QRunnable , QObject , Signal , QThreadPool , Slot
21+ from PySide6 .QtGui import QPainter , QImage
2222
23+ from collections import OrderedDict
24+ from functools import lru_cache
25+ import pdfutils as pdfutils
26+ import gdrive_base as gdrive_base
27+ from strutils import thumbnail_path_for_file
28+
29+ class ThumbnailSignals (QObject ):
30+ result = Signal (str , QImage )
31+
32+ class ThumbnailWorker (QRunnable ):
33+ def __init__ (self , file_data ):
34+ super ().__init__ ()
35+ self .file_data = file_data
36+ self .signals = ThumbnailSignals ()
37+
38+ @Slot ()
39+ def run (self ):
40+ file_id = self .file_data .get ('id' , '' )
41+ mime = self .file_data .get ('mimeType' , '' )
42+ img = None
43+
44+ try :
45+ from gdrive import gcache
46+ cache_path = gcache .get_cache_path_for_file (self .file_data )
47+ if cache_path and cache_path .exists ():
48+ if mime == 'application/pdf' :
49+ thumbnail_bytes = pdfutils .get_cached_pdf_thumbnail (cache_path , size = 'large' )
50+ img = QImage ()
51+ img .loadFromData (thumbnail_bytes )
52+ else :
53+ thumb_path = thumbnail_path_for_file (f"gdrive_{ file_id } " , size = 'large' )
54+ if thumb_path .exists ():
55+ img = QImage (str (thumb_path ))
56+ else :
57+ if mime == 'application/pdf' :
58+ thumbnail_bytes = gdrive_base .fetch_preview_image (file_id , size = 256 )
59+ if thumbnail_bytes :
60+ img = QImage ()
61+ img .loadFromData (thumbnail_bytes )
62+ thumb_path .parent .mkdir (parents = True , exist_ok = True )
63+ thumb_path .write_bytes (thumbnail_bytes )
64+ except Exception as e :
65+ print (f"Error fetching thumbnail for { file_id } : { e } " )
66+
67+ if img and not img .isNull ():
68+ self .signals .result .emit (file_id , img )
69+
70+ class ThumbnailKickoffWorker (QRunnable ):
71+ def __init__ (self , items , callback ):
72+ super ().__init__ ()
73+ self .items = items
74+ self .callback = callback
75+
76+ @Slot ()
77+ def run (self ):
78+ pool = QThreadPool .globalInstance ()
79+ for item in self .items :
80+ worker = ThumbnailWorker (item )
81+ worker .signals .result .connect (self .callback )
82+ pool .start (worker )
83+
84+ class LRUCache :
85+ def __init__ (self , capacity : int ):
86+ self .cache = OrderedDict ()
87+ self .capacity = capacity
88+
89+ def get (self , key : str ):
90+ if key not in self .cache :
91+ return None
92+ self .cache .move_to_end (key )
93+ return self .cache [key ]
94+
95+ def put (self , key : str , value : Any ):
96+ self .cache [key ] = value
97+ self .cache .move_to_end (key )
98+ if len (self .cache ) > self .capacity :
99+ self .cache .popitem (last = False )
100+
101+ @lru_cache (maxsize = None )
23102def get_icon (icon_enum , is_filled = False , color : Optional [str ] = None ) -> QIcon :
24103 if color is None :
25104 if QApplication .instance ():
@@ -86,6 +165,11 @@ def __init__(self):
86165 self .history_index = - 1
87166 self .current_folder_id = None
88167
168+ self .threadpool = QThreadPool .globalInstance ()
169+ self .threadpool .setMaxThreadCount (12 )
170+ self .thumbnail_cache = LRUCache (500 )
171+ self .item_mapping = {}
172+
89173 self .init_ui ()
90174 self .load_root ("my_drive" )
91175
@@ -212,10 +296,12 @@ def on_nav_clicked(self, item: QListWidgetItem):
212296
213297 def populate_files (self , items : List [Dict [str , Any ]]):
214298 self .file_view .clear ()
299+ self .item_mapping .clear ()
215300
216301 # Sort folders first, then files, both alphabetically
217302 folders = []
218303 files = []
304+ items_needing_thumbnails = []
219305 for item in items :
220306 mime = item .get ('mimeType' , '' )
221307 if mime == 'application/vnd.google-apps.folder' :
@@ -229,11 +315,33 @@ def populate_files(self, items: List[Dict[str, Any]]):
229315 for item in folders + files :
230316 name = item .get ('name' , 'Unknown' )
231317 mime = item .get ('mimeType' , '' )
318+ file_id = item .get ('id' , '' )
232319
233320 list_item = QListWidgetItem (name )
234- list_item .setIcon (get_mime_icon (mime ))
321+
322+ cached_pixmap = self .thumbnail_cache .get (file_id )
323+ if cached_pixmap :
324+ list_item .setIcon (QIcon (cached_pixmap ))
325+ else :
326+ list_item .setIcon (get_mime_icon (mime ))
327+ if mime == 'application/pdf' :
328+ items_needing_thumbnails .append (item )
329+
235330 list_item .setData (Qt .UserRole , item )
236331 self .file_view .addItem (list_item )
332+ self .item_mapping [file_id ] = list_item
333+
334+ if items_needing_thumbnails :
335+ kickoff_worker = ThumbnailKickoffWorker (items_needing_thumbnails , self .on_thumbnail_loaded )
336+ self .threadpool .start (kickoff_worker )
337+
338+ def on_thumbnail_loaded (self , file_id : str , img : QImage ):
339+ pixmap = QPixmap .fromImage (img )
340+ self .thumbnail_cache .put (file_id , pixmap )
341+ if file_id in self .item_mapping :
342+ item = self .item_mapping [file_id ]
343+ if item .listWidget () == self .file_view :
344+ item .setIcon (QIcon (pixmap ))
237345
238346 def on_item_activated (self , item : QListWidgetItem ):
239347 file_data = item .data (Qt .UserRole )
0 commit comments