@@ -1043,19 +1043,18 @@ def build_settings(page: ft.Page,
10431043 on_courses_changed : callable | None = None ) -> ft .Column :
10441044
10451045 def _snack (msg : str , ok : bool = True ) -> None :
1046- page .open ( ft .SnackBar (
1047- ft .Text (msg , color = ft .Colors .BLACK ),
1046+ page .snack_bar = ft .SnackBar (
1047+ content = ft .Text (msg , color = ft .Colors .BLACK ),
10481048 bgcolor = C_SUCCESS if ok else C_ERROR ,
10491049 duration = 2500 ,
1050- ))
1050+ open = True ,
1051+ )
1052+ page .update ()
10511053
1052- def _key_row (label : str , tf : ft .TextField , on_save ) -> ft .Row :
1054+ def _field_row (label : str , tf : ft .TextField ) -> ft .Row :
10531055 return ft .Row (controls = [
10541056 ft .Text (label , size = 12 , color = ft .Colors .WHITE , width = 140 ),
10551057 tf ,
1056- ft .FilledButton ("Save" , icon = ft .Icons .SAVE_OUTLINED ,
1057- on_click = on_save , style = ft .ButtonStyle (
1058- bgcolor = C_PRIMARY , color = ft .Colors .BLACK )),
10591058 ], spacing = 8 )
10601059
10611060 # ── Connection settings (config.json) ────────────────────────────────────
@@ -1065,9 +1064,9 @@ def _key_row(label: str, tf: ft.TextField, on_save) -> ft.Row:
10651064 def _load_config () -> dict :
10661065 return json .load (open (config_file )) if config_file .exists () else {}
10671066
1068- def _save_config ( key : str , value : str ) -> None :
1067+ def _save_config_all ( data : dict ) -> None :
10691068 cfg = _load_config ()
1070- cfg [ key ] = value
1069+ cfg . update ( data )
10711070 with open (config_file , "w" ) as f :
10721071 json .dump (cfg , f , indent = 2 )
10731072
@@ -1083,28 +1082,14 @@ def _save_config(key: str, value: str) -> None:
10831082 bgcolor = C_OUTPUT_BG , border_color = C_PRIMARY , text_size = 12 ,
10841083 )
10851084
1086- def _save_canvas_url (_ ):
1087- try :
1088- _save_config ("CANVAS_URL" , tf_canvas_url .value .strip ())
1089- _snack ("Canvas URL saved successfully!" )
1090- except Exception as e :
1091- _snack (f"Error: { e } " , ok = False )
1092-
1093- def _save_panopto (_ ):
1094- try :
1095- _save_config ("PANOPTO_HOST" , tf_panopto .value .strip ())
1096- _snack ("Panopto host saved successfully!" )
1097- except Exception as e :
1098- _snack (f"Error: { e } " , ok = False )
1099-
11001085 conn_card = _card (ft .Column (controls = [
11011086 ft .Text ("Connection" , size = 13 ,
11021087 weight = ft .FontWeight .BOLD , color = ft .Colors .WHITE ),
11031088 ft .Text ("Saved to config.json in the project directory." ,
11041089 size = 11 , color = ft .Colors .with_opacity (0.5 , ft .Colors .WHITE )),
11051090 ft .Container (height = 4 ),
1106- _key_row ("Canvas URL" , tf_canvas_url , _save_canvas_url ),
1107- _key_row ("Panopto Host" , tf_panopto , _save_panopto ),
1091+ _field_row ("Canvas URL" , tf_canvas_url ),
1092+ _field_row ("Panopto Host" , tf_panopto ),
11081093 ], spacing = 10 ))
11091094
11101095 # ── API Keys ──────────────────────────────────────────────────────────────
@@ -1146,7 +1131,6 @@ def _do_refresh():
11461131 refresh_status .value = "Refreshing…"
11471132 refresh_status .color = ft .Colors .with_opacity (0.6 , ft .Colors .WHITE )
11481133 page .update ()
1149- # Load courses while this settings page is still attached
11501134 err = _load_courses_from_canvas ()
11511135 n = len (COURSES )
11521136 if n :
@@ -1159,56 +1143,19 @@ def _do_refresh():
11591143 refresh_status .value = "No courses found after filtering."
11601144 refresh_status .color = C_WARN
11611145 page .update ()
1162- # Rebuild pages AFTER updating status (rebuild replaces the settings page)
11631146 if on_courses_changed :
11641147 on_courses_changed ()
11651148
1166- def _save_canvas (_ ):
1167- try :
1168- canvas_file .write_text (tf_canvas .value .strip ())
1169- _snack ("Canvas token saved successfully!" )
1170- except Exception as e :
1171- _snack (f"Error: { e } " , ok = False )
1172-
1173- def _save_openai (_ ):
1174- try :
1175- openai_file .write_text (tf_openai .value .strip ())
1176- _snack ("OpenAI key saved successfully!" )
1177- except Exception as e :
1178- _snack (f"Error: { e } " , ok = False )
1179-
1180- def _save_anthropic (_ ):
1181- try :
1182- anthropic_file .write_text (tf_anthropic .value .strip ())
1183- _snack ("Anthropic key saved successfully!" )
1184- except Exception as e :
1185- _snack (f"Error: { e } " , ok = False )
1186-
1187- def _save_gemini (_ ):
1188- try :
1189- gemini_file .write_text (tf_gemini .value .strip ())
1190- _snack ("Gemini key saved successfully!" )
1191- except Exception as e :
1192- _snack (f"Error: { e } " , ok = False )
1193-
11941149 keys_card = _card (ft .Column (controls = [
11951150 ft .Text ("API Keys & Credentials" , size = 13 ,
11961151 weight = ft .FontWeight .BOLD , color = ft .Colors .WHITE ),
11971152 ft .Text ("Keys are stored in plaintext files in the project directory." ,
11981153 size = 11 , color = ft .Colors .with_opacity (0.5 , ft .Colors .WHITE )),
11991154 ft .Container (height = 4 ),
1200- _key_row ("Canvas Token" , tf_canvas , _save_canvas ),
1201- ft .Row (controls = [
1202- ft .FilledButton (
1203- "Refresh Courses" , icon = ft .Icons .REFRESH ,
1204- on_click = lambda _ : _do_refresh (),
1205- style = ft .ButtonStyle (bgcolor = C_SECONDARY , color = ft .Colors .BLACK ),
1206- ),
1207- refresh_status ,
1208- ], spacing = 10 ),
1209- _key_row ("OpenAI API Key" , tf_openai , _save_openai ),
1210- _key_row ("Anthropic API Key" ,tf_anthropic , _save_anthropic ),
1211- _key_row ("Gemini API Key" , tf_gemini , _save_gemini ),
1155+ _field_row ("Canvas Token" , tf_canvas ),
1156+ _field_row ("OpenAI API Key" , tf_openai ),
1157+ _field_row ("Anthropic API Key" , tf_anthropic ),
1158+ _field_row ("Gemini API Key" , tf_gemini ),
12121159 ft .Row (controls = [
12131160 ft .Text ("Project dir" , size = 11 ,
12141161 color = ft .Colors .with_opacity (0.5 , ft .Colors .WHITE )),
@@ -1302,6 +1249,9 @@ def _save_gemini(_):
13021249 ("generate" , "QUALITY_TARGET" , "Self-score target" , "8.0" , None ),
13031250 ]
13041251
1252+ # Collect (ctrl, script, name, default) for bulk save
1253+ _const_ctrls : list [tuple ] = []
1254+
13051255 def _const_row (script : str , name : str , desc : str , default : str ,
13061256 options : list [tuple [str , str ]] | None = None ) -> ft .Row :
13071257 cur = _read_constant (script , name )
@@ -1315,41 +1265,22 @@ def _const_row(script: str, name: str, desc: str, default: str,
13151265 bgcolor = C_OUTPUT_BG , border_color = accent , text_size = 12 ,
13161266 content_padding = ft .padding .symmetric (horizontal = 8 , vertical = 4 ),
13171267 )
1318-
1319- def _save (_ , c = ctrl , s = script , n = name ):
1320- if _write_constant (s , n , c .value or default ):
1321- _snack (f"{ n } saved." )
1322- else :
1323- _snack (f"Failed to save { n } ." , ok = False )
1324-
1325- def _reset (_ , c = ctrl , s = script , n = name , d = default ):
1326- c .value = d
1327- c .update ()
1328- if _write_constant (s , n , d ):
1329- _snack (f"{ n } reset to default." )
1330- else :
1331- _snack (f"Failed to reset { n } ." , ok = False )
1332-
13331268 else :
13341269 ctrl = ft .TextField (
13351270 value = cur , expand = True , dense = True ,
13361271 bgcolor = C_OUTPUT_BG , border_color = accent ,
13371272 text_size = 12 , content_padding = ft .padding .symmetric (horizontal = 8 , vertical = 6 ),
13381273 )
13391274
1340- def _save (_ , c = ctrl , s = script , n = name ):
1341- if _write_constant (s , n , c .value .strip ()):
1342- _snack (f"{ n } saved." )
1343- else :
1344- _snack (f"Failed to save { n } ." , ok = False )
1275+ _const_ctrls .append ((ctrl , script , name , default ))
13451276
1346- def _reset (_ , c = ctrl , s = script , n = name , d = default ):
1347- c .value = d
1348- c .update ()
1349- if _write_constant (s , n , d ):
1350- _snack (f"{ n } reset to default ( { d } ) ." )
1351- else :
1352- _snack (f"Failed to reset { n } ." , ok = False )
1277+ def _reset (_ , c = ctrl , s = script , n = name , d = default ):
1278+ c .value = d
1279+ c .update ()
1280+ if _write_constant (s , n , d ):
1281+ _snack (f"{ n } reset to default." )
1282+ else :
1283+ _snack (f"Failed to reset { n } ." , ok = False )
13531284
13541285 return ft .Row (controls = [
13551286 ft .Container (
@@ -1362,10 +1293,6 @@ def _reset(_, c=ctrl, s=script, n=name, d=default):
13621293 ft .Text (name , size = 11 , color = ft .Colors .with_opacity (0.5 , ft .Colors .WHITE ),
13631294 width = 160 , italic = True ),
13641295 ctrl ,
1365- ft .FilledButton (
1366- "Save" , icon = ft .Icons .SAVE_OUTLINED , on_click = _save ,
1367- style = ft .ButtonStyle (bgcolor = C_PRIMARY , color = ft .Colors .BLACK ),
1368- ),
13691296 ft .OutlinedButton (
13701297 "Default" , on_click = _reset ,
13711298 style = ft .ButtonStyle (side = ft .BorderSide (1 , C_SECONDARY ),
@@ -1385,13 +1312,71 @@ def _reset(_, c=ctrl, s=script, n=name, d=default):
13851312 * const_rows ,
13861313 ], spacing = 10 ))
13871314
1315+ # ── Single Save All button ────────────────────────────────────────────────
1316+
1317+ def _save_all (_ ):
1318+ errors : list [str ] = []
1319+ try :
1320+ _save_config_all ({
1321+ "CANVAS_URL" : tf_canvas_url .value .strip (),
1322+ "PANOPTO_HOST" : tf_panopto .value .strip (),
1323+ })
1324+ except Exception as e :
1325+ errors .append (f"Connection: { e } " )
1326+ for path , tf in [
1327+ (canvas_file , tf_canvas ),
1328+ (openai_file , tf_openai ),
1329+ (anthropic_file , tf_anthropic ),
1330+ (gemini_file , tf_gemini ),
1331+ ]:
1332+ try :
1333+ if tf .value .strip ():
1334+ path .write_text (tf .value .strip ())
1335+ except Exception as e :
1336+ errors .append (str (e ))
1337+ for ctrl , script , name , default in _const_ctrls :
1338+ val = (ctrl .value or default ) if isinstance (ctrl , ft .Dropdown ) \
1339+ else ctrl .value .strip ()
1340+ if not _write_constant (script , name , val ):
1341+ errors .append (f"Failed to write { name } " )
1342+ if errors :
1343+ _snack ("Errors: " + "; " .join (errors ), ok = False )
1344+ else :
1345+ _snack ("All settings saved successfully!" )
1346+
1347+ save_btn = ft .FilledButton (
1348+ "Save All Settings" ,
1349+ icon = ft .Icons .SAVE_OUTLINED ,
1350+ on_click = _save_all ,
1351+ style = ft .ButtonStyle (
1352+ bgcolor = C_PRIMARY , color = ft .Colors .BLACK ,
1353+ padding = ft .padding .symmetric (horizontal = 28 , vertical = 14 ),
1354+ ),
1355+ )
1356+ refresh_btn_settings = ft .OutlinedButton (
1357+ "Refresh Courses" ,
1358+ icon = ft .Icons .REFRESH ,
1359+ on_click = lambda _ : _do_refresh (),
1360+ style = ft .ButtonStyle (
1361+ side = ft .BorderSide (1 , C_SECONDARY ), color = C_SECONDARY ,
1362+ padding = ft .padding .symmetric (horizontal = 20 , vertical = 14 ),
1363+ ),
1364+ )
1365+
13881366 return ft .Column (
13891367 controls = [
13901368 ft .Column (controls = [
13911369 _section_title ("Settings" , ft .Icons .SETTINGS_OUTLINED ),
13921370 conn_card ,
13931371 keys_card ,
13941372 const_card ,
1373+ ft .Container (height = 8 ),
1374+ ft .Row (controls = [
1375+ save_btn ,
1376+ refresh_btn_settings ,
1377+ refresh_status ,
1378+ ], spacing = 12 ),
1379+ ft .Container (height = 16 ),
13951380 ], spacing = 12 , scroll = ft .ScrollMode .AUTO , expand = True ),
13961381 ],
13971382 expand = True ,
0 commit comments