44from tkinter import filedialog , messagebox , ttk
55
66from helvox .ui .tooltip import add_tooltip
7+ from helvox .utils .config_paths import portable_config_file , user_config_file
78from helvox .utils .data import validate_samples_for_dialect
89from helvox .utils .platform import app_font , recordings_dir
910from helvox .utils .recorder import Recorder
@@ -170,7 +171,7 @@ def setup_ui(self) -> None:
170171 )
171172 info_label .grid (row = 1 , column = 0 , sticky = "w" , pady = (5 , 0 ))
172173
173- # Output / recordings folder (default: helvox next to app)
174+ # Output / recordings folder — only shown/editable in non-portable mode
174175 rec_frame = ttk .LabelFrame (tab_data , text = "Output folder (recordings)" , padding = "15" )
175176 rec_frame .grid (row = 1 , column = 0 , sticky = "ew" , padx = (10 , 10 ), pady = (10 , 0 ))
176177 rec_frame .columnconfigure (0 , weight = 1 )
@@ -189,28 +190,35 @@ def setup_ui(self) -> None:
189190 )
190191 self .browse_out_btn .grid (row = 0 , column = 1 , pady = (0 , 0 ), sticky = "e" )
191192
192- ttk .Label (
193+ self . folder_info_label = ttk .Label (
193194 rec_frame ,
194- text = "Default: next to the exe, in a helvox folder. " ,
195+ text = "" ,
195196 style = "Info.TLabel" ,
196- ).grid (row = 1 , column = 0 , sticky = "w" , pady = (5 , 0 ))
197+ wraplength = 560 ,
198+ )
199+ self .folder_info_label .grid (row = 1 , column = 0 , sticky = "w" , pady = (5 , 0 ))
197200
198- # Settings file: portable vs user profile
201+ # Portable mode
199202 settings_store_frame = ttk .LabelFrame (
200- tab_data , text = "Settings file (config.json) " , padding = "15"
203+ tab_data , text = "Portable mode " , padding = "15"
201204 )
202205 settings_store_frame .grid (row = 2 , column = 0 , sticky = "ew" , padx = (10 , 10 ), pady = (10 , 0 ))
203206 settings_store_frame .columnconfigure (0 , weight = 1 )
204207
205208 self .config_portable_var = tk .BooleanVar (value = self .recorder .config_portable )
206209 portable_check = ttk .Checkbutton (
207210 settings_store_frame ,
208- text = "Save config.json next to the app ( in the helvox folder) " ,
211+ text = "Portable — store config and recordings in helvox/ next to the app " ,
209212 variable = self .config_portable_var ,
210213 command = self .on_config_portable_toggle ,
211214 )
212215 portable_check .grid (row = 0 , column = 0 , sticky = "w" )
213216
217+ self .config_path_label = ttk .Label (
218+ settings_store_frame , text = "" , style = "Info.TLabel" , wraplength = 560
219+ )
220+ self .config_path_label .grid (row = 1 , column = 0 , sticky = "w" , pady = (4 , 0 ))
221+
214222 options_frame = ttk .LabelFrame (tab_data , text = "Options" , padding = "15" )
215223 options_frame .grid (row = 3 , column = 0 , sticky = "ew" , padx = (10 , 10 ), pady = (10 , 0 ))
216224 options_frame .columnconfigure (0 , weight = 1 )
@@ -326,12 +334,31 @@ def setup_ui(self) -> None:
326334 self .on_config_portable_toggle ()
327335
328336 def on_config_portable_toggle (self ) -> None :
329- # Keep path fields editable in both modes.
330- # NOTE: toggling the config location must NOT affect the output folder.
337+ is_portable = self .config_portable_var .get ()
338+
339+ if is_portable :
340+ # Lock output folder to helvox/ next to the app.
341+ base = recordings_dir ()
342+ self .folder_var .set (str (base ))
343+ self .folder_input .configure (state = "disabled" )
344+ self .browse_out_btn .configure (state = "disabled" )
345+ self .folder_info_label .configure (
346+ text = f"Locked to: { base } (recordings go in <speaker>/ inside)"
347+ )
348+ self .config_path_label .configure (
349+ text = f"Config: { portable_config_file ()} \n "
350+ f"Recordings: { base / '<speaker_id>' } "
351+ )
352+ else :
353+ self .folder_input .configure (state = "normal" )
354+ self .browse_out_btn .configure (state = "normal" )
355+ self .folder_info_label .configure (text = "" )
356+ self .config_path_label .configure (
357+ text = f"Config will be saved to: { user_config_file ()} "
358+ )
359+
331360 self .file_input .configure (state = "normal" )
332- self .folder_input .configure (state = "normal" )
333361 self .browse_file_btn .configure (state = "normal" )
334- self .browse_out_btn .configure (state = "normal" )
335362
336363 def select_output_folder (self ) -> None :
337364 raw = self .folder_var .get ().strip () or str (recordings_dir ())
@@ -449,12 +476,18 @@ def on_ok(self) -> None:
449476 if not self .validate_inputs ():
450477 return
451478
452- out_folder = self .folder_var .get ().strip () or str (recordings_dir ())
479+ is_portable = self .config_portable_var .get ()
480+ # In portable mode the output folder is always recordings_dir() — locked.
481+ out_folder = (
482+ str (recordings_dir ())
483+ if is_portable
484+ else self .folder_var .get ().strip () or str (recordings_dir ())
485+ )
453486 self .result = {
454487 "speaker_id" : self .speaker_var .get ().strip (),
455488 "speaker_dialect" : self .dialect_var .get (),
456489 "output_folder" : out_folder ,
457- "config_portable" : self . config_portable_var . get () ,
490+ "config_portable" : is_portable ,
458491 "device" : self .device_var .get (),
459492 "enable_skip" : self .enable_skip_var .get (),
460493 "input_file" : self .file_var .get (),
0 commit comments