Skip to content

Commit f6c3d4b

Browse files
committed
App: Add address bar typeahead
[skip ci]
1 parent 00b5199 commit f6c3d4b

4 files changed

Lines changed: 112 additions & 44 deletions

File tree

scripts/gdrive.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -92,50 +92,54 @@ def FOLDERS_DATA() -> dict[str, dict[str, str]]:
9292
FOLDERS_DATA.ret = json.loads(FOLDERS_DATA_FILE.read_text())
9393
return FOLDERS_DATA.ret
9494

95-
def course_input_completer_factory() -> Callable[[str, int], str]:
96-
gfolders: dict[str, dict[str, str]]
95+
def get_course_suggestions(so_far: str) -> list[str]:
9796
gfolders = FOLDERS_DATA()
98-
suggestions_cache: dict[str, list[str]]
99-
suggestions_cache = dict()
100-
subfolders_cache = dict()
101-
def _ret(so_far: str, suggestion_idx: int) -> str:
97+
if '/' not in so_far:
98+
return [
99+
course_name for course_name in gfolders.keys()
100+
if course_name and course_name.startswith(so_far)
101+
]
102+
103+
parts = so_far.split('/')
104+
course = parts[0]
105+
if course not in gfolders:
106+
return []
107+
108+
links = gfolders[course]
109+
flink = links['private'] or links['public']
110+
fid = folderlink_to_id(flink)
111+
pidx = 1
112+
prefix = course
113+
while True:
114+
subfolders = gcache.get_subfolders(fid)
115+
q = parts[pidx].lower()
116+
matches = [f for f in subfolders if q in f['name'].lower()]
117+
118+
if len(parts) <= pidx + 1:
119+
# We are at the last part, return matches
120+
ret = []
121+
for f in matches:
122+
# Check if it has subfolders to add trailing slash
123+
has_children = len(gcache.get_subfolders(f['id'])) > 0
124+
ret.append(f"{prefix}/{f['name']}{'/' if has_children else ''}")
125+
return ret
126+
127+
if len(matches) != 1:
128+
return []
129+
130+
fid = matches[0]['id']
131+
prefix = f"{prefix}/{matches[0]['name']}"
132+
pidx += 1
133+
134+
def course_input_completer_factory() -> Callable[[str, int], str | None]:
135+
suggestions_cache: dict[str, list[str]] = {}
136+
def _ret(so_far: str, suggestion_idx: int) -> str | None:
102137
if so_far not in suggestions_cache:
103-
if '/' not in so_far:
104-
suggestions_cache[so_far] = [
105-
course_name for course_name in gfolders.keys()
106-
if course_name and course_name.startswith(so_far)
107-
]
108-
else:
109-
parts = so_far.split('/')
110-
course = parts[0]
111-
if course not in gfolders:
112-
suggestions_cache[so_far] = []
113-
else:
114-
links = gfolders[course]
115-
flink = links['private'] or links['public']
116-
fid = folderlink_to_id(flink)
117-
pidx = 1
118-
prefix = course
119-
while True:
120-
if fid in subfolders_cache:
121-
subfolders = subfolders_cache[fid]
122-
else:
123-
subfolders = gcache.get_subfolders(fid)
124-
subfolders_cache[fid] = subfolders
125-
matches = [f for f in subfolders if parts[pidx].lower() in f['name'].lower()]
126-
for f in matches:
127-
if f['id'] not in subfolders_cache:
128-
subfolders_cache[f['id']] = gcache.get_subfolders(f['id'])
129-
if len(parts) <= pidx + 1:
130-
suggestions_cache[so_far] = [f"{prefix}/{f['name']}{'/' if subfolders_cache[f['id']] else ''}" for f in matches]
131-
break
132-
if len(matches) != 1: # Don't know which, so we better run
133-
suggestions_cache[so_far] = []
134-
break
135-
fid = matches[0]['id']
136-
prefix = f"{prefix}/{matches[0]['name']}"
137-
pidx += 1
138-
return suggestions_cache[so_far][suggestion_idx]
138+
suggestions_cache[so_far] = get_course_suggestions(so_far)
139+
140+
if suggestion_idx < len(suggestions_cache[so_far]):
141+
return suggestions_cache[so_far][suggestion_idx]
142+
return None
139143

140144
return _ret
141145

scripts/gdrive_app.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
99
QHBoxLayout, QListWidget, QListWidgetItem,
1010
QPushButton, QLineEdit, QSplitter, QMessageBox,
11-
QListView, QMenu, QProgressDialog)
11+
QListView, QMenu, QProgressDialog, QCompleter)
1212
from PySide6.QtCore import Qt, QSize
1313
from PySide6.QtGui import QIcon, QPixmap, QShortcut, QKeySequence
1414

@@ -18,7 +18,7 @@
1818

1919

2020
from PySide6.QtSvg import QSvgRenderer
21-
from PySide6.QtCore import QByteArray, Qt, QRunnable, Signal, QThreadPool, Slot, QTimer, QThread
21+
from PySide6.QtCore import QByteArray, Qt, QRunnable, Signal, QThreadPool, Slot, QTimer, QThread, QStringListModel
2222
from PySide6.QtGui import QPainter, QImage
2323

2424
from collections import OrderedDict
@@ -303,6 +303,15 @@ def init_ui(self):
303303
self.address_bar = QLineEdit()
304304
self.address_bar.returnPressed.connect(self.on_address_bar_return)
305305

306+
self.completer = QCompleter()
307+
self.completer_model = QStringListModel()
308+
self.completer.setModel(self.completer_model)
309+
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
310+
self.completer.setCompletionMode(QCompleter.PopupCompletion)
311+
self.completer.setFilterMode(Qt.MatchContains)
312+
self.address_bar.setCompleter(self.completer)
313+
self.address_bar.textEdited.connect(self.update_completer)
314+
306315
top_bar.addWidget(self.back_btn)
307316
top_bar.addWidget(self.fwd_btn)
308317
top_bar.addWidget(self.address_bar)
@@ -463,6 +472,13 @@ def on_address_bar_return(self):
463472
except Exception as e:
464473
QMessageBox.critical(self, "Error", f"An error occurred: {e}")
465474

475+
def update_completer(self, text):
476+
if not text or not self.gcache:
477+
return
478+
import gdrive
479+
suggestions = gdrive.get_course_suggestions(text)
480+
self.completer_model.setStringList(suggestions)
481+
466482
def populate_files(self, items: List[Dict[str, Any]]):
467483
if hasattr(self, 'current_cancel_flag'):
468484
self.current_cancel_flag[0] = True

scripts/test_fixtures/drive.sqlite

0 Bytes
Binary file not shown.

scripts/test_gdrive.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,54 @@ def test_handle_close_pair_decision_same_tag_folder(test_db):
605605
args, _ = mock_move.call_args
606606
assert args[0] == file_b['id']
607607

608+
def test_get_course_suggestions_top_level():
609+
mock_folders = {
610+
'course1': {'public': 'link1', 'private': 'link2'},
611+
'course2': {'public': 'link3', 'private': 'link4'},
612+
}
613+
with mock.patch('gdrive.FOLDERS_DATA', return_value=mock_folders):
614+
suggestions = gdrive.get_course_suggestions('course')
615+
assert set(suggestions) == {'course1', 'course2'}
616+
617+
suggestions = gdrive.get_course_suggestions('course1')
618+
assert suggestions == ['course1']
619+
620+
def test_get_course_suggestions_subfolders(test_db):
621+
mock_folders = {
622+
'course1': {'public': 'pub1', 'private': 'priv1'},
623+
}
624+
625+
# Mock folderlink_to_id
626+
with mock.patch('gdrive.folderlink_to_id', return_value='priv1_id'), \
627+
mock.patch('gdrive.FOLDERS_DATA', return_value=mock_folders), \
628+
mock.patch('gdrive.gcache', test_db):
629+
630+
# Inject items into test_db
631+
with test_db._lock:
632+
sql = "INSERT INTO drive_items (id, version, name, mime_type, parent_id, modified_time, owner) VALUES (?, ?, ?, ?, ?, ?, 1)"
633+
now = '2024-01-01T00:00:00Z'
634+
test_db.cursor.execute(sql, ('priv1_id', 1, 'Course 1', 'application/vnd.google-apps.folder', 'root', now))
635+
test_db.cursor.execute(sql, ('sub1_id', 1, 'Sub Folder 1', 'application/vnd.google-apps.folder', 'priv1_id', now))
636+
test_db.cursor.execute(sql, ('sub2_id', 1, 'Another Sub', 'application/vnd.google-apps.folder', 'priv1_id', now))
637+
test_db.cursor.execute(sql, ('subsub1_id', 1, 'Nested One', 'application/vnd.google-apps.folder', 'sub1_id', now))
638+
test_db.conn.commit()
639+
640+
# course1/ -> matches subfolders of priv1_id
641+
suggestions = gdrive.get_course_suggestions('course1/')
642+
assert 'course1/Sub Folder 1/' in suggestions
643+
assert 'course1/Another Sub' in suggestions
644+
645+
# course1/sub -> matches Sub Folder 1 (Another Sub doesn't contain "sub")
646+
# Wait, q in f['name'].lower()
647+
# "Sub Folder 1" contains "sub"
648+
# "Another Sub" contains "sub"
649+
suggestions = gdrive.get_course_suggestions('course1/sub')
650+
assert set(suggestions) == {'course1/Sub Folder 1/', 'course1/Another Sub'}
651+
652+
# course1/Sub Folder 1/n -> matches Nested One
653+
suggestions = gdrive.get_course_suggestions('course1/Sub Folder 1/n')
654+
assert suggestions == ['course1/Sub Folder 1/Nested One']
655+
608656
if __name__ == "__main__":
609657
if len(sys.argv) > 2 and sys.argv[1] == "extract":
610658
extract_to_test_db(sys.argv[2:])

0 commit comments

Comments
 (0)