Skip to content

Commit 5e84db7

Browse files
committed
v0.6.5
1 parent fd9eeb4 commit 5e84db7

7 files changed

Lines changed: 124 additions & 32 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ jobs:
6666
libglib2.0-dev \
6767
fuse libfuse2
6868
69+
# ── Generate app icons ───────────────────────────────────────────────────
70+
- name: Generate icons
71+
run: python make_icon.py
72+
6973
# ── Build with PyInstaller ───────────────────────────────────────────────
7074
- name: Build (Linux / macOS)
7175
if: matrix.platform != 'windows'

assets/icon.icns

3.86 KB
Binary file not shown.

assets/icon.ico

3.99 KB
Binary file not shown.

assets/icon.png

3.86 KB
Loading

build.spec

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ from PyInstaller.utils.hooks import collect_all
1111
block_cipher = None
1212
HERE = Path(SPECPATH) # noqa: F821 — injected by PyInstaller
1313

14+
# Platform-specific icon
15+
if sys.platform == "win32":
16+
_icon = str(HERE / "assets" / "icon.ico")
17+
elif sys.platform == "darwin":
18+
_icon = str(HERE / "assets" / "icon.icns")
19+
else:
20+
_icon = str(HERE / "assets" / "icon.png")
21+
1422
# Collect all data/binaries from flet packages
1523
# flet_desktop: bundled Flutter client binary
1624
# flet + flet_core: icons.json and other package data files
@@ -72,7 +80,7 @@ exe = EXE( # noqa: F821
7280
target_arch=None,
7381
codesign_identity=None,
7482
entitlements_file=None,
75-
icon=None, # add icon path here if available
83+
icon=_icon,
7684
)
7785

7886
coll = COLLECT( # noqa: F821

gui.py

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,45 @@
3232
"generate": PROJECT_DIR / "note_generation.py",
3333
}
3434

35-
COURSES: dict[int, str] = {
36-
85367: "CS2101 Effective Communication",
37-
85377: "CS2103/T Software Engineering",
38-
85397: "CS2105 Computer Networks",
39-
85427: "CS3210 Parallel Computing",
40-
}
35+
COURSES: dict[int, str] = {} # populated from Canvas API after token is entered
36+
37+
_SKIP_KEYWORDS = [
38+
"training", "pdp", "rmcpdp", "osa", "soct", "travel",
39+
"essentials", "respect", "consent",
40+
]
41+
42+
43+
def _load_courses_from_canvas() -> None:
44+
"""Fetch active courses from Canvas and update the global COURSES dict."""
45+
COURSES.clear()
46+
token_file = PROJECT_DIR / "canvas_token.txt"
47+
config_file = PROJECT_DIR / "config.json"
48+
token = token_file.read_text().strip() if token_file.exists() else ""
49+
if not token:
50+
return
51+
cfg = json.load(open(config_file)) if config_file.exists() else {}
52+
canvas_url = cfg.get("CANVAS_URL", "").rstrip("/")
53+
if not canvas_url:
54+
return
55+
try:
56+
import requests
57+
resp = requests.get(
58+
f"{canvas_url}/api/v1/courses",
59+
headers={"Authorization": f"Bearer {token}"},
60+
params={"enrollment_state": "active", "enrollment_type[]": "student",
61+
"per_page": 100},
62+
timeout=10,
63+
)
64+
resp.raise_for_status()
65+
for c in resp.json():
66+
name = c.get("name") or c.get("course_code") or ""
67+
if not name:
68+
continue
69+
if any(kw in name.lower() for kw in _SKIP_KEYWORDS):
70+
continue
71+
COURSES[c["id"]] = name
72+
except Exception:
73+
pass # silently leave COURSES empty; user sees the empty-state UI
4174

4275
# ── Palette ───────────────────────────────────────────────────────────────────
4376

@@ -385,17 +418,20 @@ def _course_dropdown(value: str, on_select: callable,
385418
options.append(ft.dropdown.Option(key="0", text="All courses"))
386419
for cid, name in COURSES.items():
387420
options.append(ft.dropdown.Option(key=str(cid), text=f"{name} ({cid})"))
421+
if not options:
422+
options.append(ft.dropdown.Option(
423+
key="", text="— no courses, add Canvas token in Settings —"))
388424
return ft.Dropdown(
389425
options=options,
390-
value=value,
426+
value=value if COURSES else None,
391427
on_select=on_select,
392428
bgcolor=C_SURFACE,
393429
border_color=ft.Colors.with_opacity(0.25, ft.Colors.WHITE),
394430
focused_border_color=C_PRIMARY,
395431
color=ft.Colors.WHITE,
396432
label="Course",
397433
label_style=ft.TextStyle(color=C_PRIMARY),
398-
expand=True, # responsive: fills available width
434+
expand=True,
399435
)
400436

401437
def _text_field(label: str, value: str = "", hint: str = "",
@@ -516,16 +552,33 @@ def _go(_):
516552
on_click=_go,
517553
)
518554

555+
if COURSES:
556+
items = list(COURSES.items())
557+
course_rows = [
558+
ft.Row(controls=[_course_card(cid, name)
559+
for cid, name in items[i:i+2]], spacing=12)
560+
for i in range(0, len(items), 2)
561+
]
562+
else:
563+
course_rows = [_card(ft.Column(controls=[
564+
ft.Container(height=8),
565+
ft.Icon(ft.Icons.SCHOOL_OUTLINED, size=52,
566+
color=ft.Colors.with_opacity(0.25, ft.Colors.WHITE)),
567+
ft.Text("No courses loaded", size=15,
568+
color=ft.Colors.with_opacity(0.45, ft.Colors.WHITE),
569+
weight=ft.FontWeight.W_500),
570+
ft.Text(
571+
"Go to Settings → enter your Canvas URL and API token,\n"
572+
"then save to load your courses automatically.",
573+
size=12, text_align=ft.TextAlign.CENTER,
574+
color=ft.Colors.with_opacity(0.35, ft.Colors.WHITE),
575+
),
576+
ft.Container(height=8),
577+
], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=10))]
578+
519579
scroll_content = [
520580
_section_title("Course Overview", ft.Icons.DASHBOARD_OUTLINED),
521-
ft.Row(
522-
controls=[_course_card(cid, name) for cid, name in list(COURSES.items())[:2]],
523-
spacing=12,
524-
),
525-
ft.Row(
526-
controls=[_course_card(cid, name) for cid, name in list(COURSES.items())[2:]],
527-
spacing=12,
528-
),
581+
*course_rows,
529582
ft.Container(height=4),
530583
_section_title("Quick Actions", ft.Icons.BOLT_OUTLINED),
531584
ft.Row(controls=[
@@ -541,7 +594,7 @@ def _go(_):
541594
# ── Page: Full Pipeline ───────────────────────────────────────────────────────
542595

543596
def build_pipeline(page: ft.Page, console: OutputConsole) -> ft.Column:
544-
course_val = {"v": str(list(COURSES.keys())[0])}
597+
course_val = {"v": str(next(iter(COURSES), ""))}
545598

546599
course_dd = _course_dropdown(
547600
value=course_val["v"],
@@ -858,11 +911,11 @@ def _run_manual(_) -> None:
858911
# ── Page: Generate Notes ──────────────────────────────────────────────────────
859912

860913
def build_generate(page: ft.Page, console: OutputConsole) -> ft.Column:
861-
default_cid = list(COURSES.keys())[0]
862-
course_val = {"v": str(default_cid)}
914+
default_cid = next(iter(COURSES), None)
915+
course_val = {"v": str(default_cid) if default_cid else ""}
863916

864917
course_name_f = _text_field("Course name",
865-
value=_course_name_from_notes(default_cid))
918+
value=_course_name_from_notes(default_cid) if default_cid else "")
866919

867920
def _on_course_select(e) -> None:
868921
# Bug fix: use e.data
@@ -971,7 +1024,8 @@ def _run(_) -> None:
9711024

9721025
# ── Page: Settings ────────────────────────────────────────────────────────────
9731026

974-
def build_settings(page: ft.Page) -> ft.Column:
1027+
def build_settings(page: ft.Page,
1028+
on_courses_changed: callable | None = None) -> ft.Column:
9751029

9761030
def _snack(msg: str, ok: bool = True) -> None:
9771031
page.snack_bar = ft.SnackBar(
@@ -1020,6 +1074,8 @@ def _save_canvas_url(_):
10201074
try:
10211075
_save_config("CANVAS_URL", tf_canvas_url.value.strip())
10221076
_snack("Canvas URL saved.")
1077+
if on_courses_changed:
1078+
on_courses_changed()
10231079
except Exception as e:
10241080
_snack(f"Error: {e}", ok=False)
10251081

@@ -1075,7 +1131,9 @@ def _save_panopto(_):
10751131
def _save_canvas(_):
10761132
try:
10771133
canvas_file.write_text(tf_canvas.value.strip())
1078-
_snack("Canvas token saved to canvas_token.txt.")
1134+
_snack("Canvas token saved.")
1135+
if on_courses_changed:
1136+
on_courses_changed()
10791137
except Exception as e:
10801138
_snack(f"Error: {e}", ok=False)
10811139

@@ -1320,15 +1378,25 @@ def navigate(idx: int) -> None:
13201378
if _nav_target[0]:
13211379
_nav_target[0](idx)
13221380

1323-
pages = [
1324-
build_dashboard(page, console, navigate=navigate),
1325-
build_pipeline(page, console),
1326-
build_download(page, console),
1327-
build_transcribe(page, console),
1328-
build_align(page, console),
1329-
build_generate(page, console),
1330-
build_settings(page),
1331-
]
1381+
# Mutable ref so _rebuild can be passed to build_settings before it's defined
1382+
_rebuild_ref: list[callable | None] = [None]
1383+
1384+
def _build_pages() -> list:
1385+
return [
1386+
build_dashboard(page, console, navigate=navigate),
1387+
build_pipeline(page, console),
1388+
build_download(page, console),
1389+
build_transcribe(page, console),
1390+
build_align(page, console),
1391+
build_generate(page, console),
1392+
build_settings(page,
1393+
on_courses_changed=lambda: _rebuild_ref[0] and _rebuild_ref[0]()),
1394+
]
1395+
1396+
# Try to populate courses immediately if credentials are already on disk
1397+
_load_courses_from_canvas()
1398+
1399+
pages = _build_pages()
13321400

13331401
# page_content swaps between tab pages; console stays fixed at the bottom
13341402
page_content = ft.Container(
@@ -1362,6 +1430,17 @@ def _navigate(idx: int) -> None:
13621430

13631431
_nav_target[0] = _navigate
13641432

1433+
def _rebuild() -> None:
1434+
"""Reload courses from Canvas and rebuild all course-dependent pages."""
1435+
_load_courses_from_canvas()
1436+
new = _build_pages()
1437+
pages.clear()
1438+
pages.extend(new)
1439+
page_content.content = pages[rail.selected_index]
1440+
page.update()
1441+
1442+
_rebuild_ref[0] = _rebuild
1443+
13651444
rail = ft.NavigationRail(
13661445
selected_index=0,
13671446
destinations=[

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pyinstaller
33
openai
44
httpx
55
anthropic
6+
pillow

0 commit comments

Comments
 (0)