@@ -13,23 +13,36 @@ setupUI <- function(id) {
1313 column(4 ,
1414 box(
1515 title = " Data Import" , width = NULL , solidHeader = TRUE ,
16- tags $ div(style = " margin-bottom: 10px;" ,
16+ tags $ div(
17+ style = " display:flex;gap:6px;margin-bottom:10px;" ,
1718 actionButton(
1819 ns(" fcs_folder_btn" ),
19- label = tagList(icon(" folder-open" ), " Browse FCS Folder" ),
20- class = " btn btn-default btn-block" ,
20+ label = tagList(icon(" folder-open" ), " Browse Folder" ),
21+ class = " btn btn-default" ,
22+ style = " flex:1;" ,
2123 onclick = sprintf(
2224 " streamflowPickFolder('%s', 'Select folder containing FCS files')" ,
2325 ns(" fcs_folder_picked" )
2426 )
27+ ),
28+ actionButton(
29+ ns(" fcs_files_btn" ),
30+ label = tagList(icon(" file-medical" ), " Select Files" ),
31+ class = " btn btn-default" ,
32+ style = " flex:1;" ,
33+ onclick = sprintf(
34+ " streamflowPickFiles('%s', 'Select FCS files', [{name:'FCS Files',extensions:['fcs','FCS']}])" ,
35+ ns(" fcs_files_picked" )
36+ )
2537 )
2638 ),
2739 uiOutput(ns(" selected_folder_label" )),
2840 tags $ hr(),
2941 checkboxInput(ns(" recursive" ), " Include sub-folders" , value = FALSE ),
3042 checkboxInput(ns(" do_clean" ), " Run flowAI quality check (cyto_clean)" , value = FALSE ),
3143 checkboxInput(ns(" do_barcode" )," Barcode samples (cyto_barcode)" , value = FALSE ),
32- checkboxInput(ns(" parse_names" )," Auto-parse names into experiment details" , value = FALSE ),
44+ # cyto_names_parse() is interactive (prompts for delimiter) and hangs
45+ # in headless Electron — names are auto-parsed safely on load instead.
3346 actionButton(
3447 ns(" load_fcs_btn" ),
3548 label = tagList(icon(" upload" ), " Load FCS Files" ),
@@ -107,15 +120,16 @@ setupServer <- function(input, output, session, shared) {
107120 ns <- session $ ns
108121
109122 local <- reactiveValues(
110- folder_path = NULL ,
111- flowset = NULL ,
112- annotation = data.frame (
123+ folder_path = NULL ,
124+ picked_files = NULL , # set when user picks files directly (overrides folder scan)
125+ flowset = NULL ,
126+ annotation = data.frame (
113127 SampleName = character (), Group = character (),
114128 Treatment = character (), Replicate = character (),
115129 TimePoint = character (), stringsAsFactors = FALSE
116130 ),
117- marker_map = data.frame (Channel = character (), Marker = character (),
118- stringsAsFactors = FALSE )
131+ marker_map = data.frame (Channel = character (), Marker = character (),
132+ stringsAsFactors = FALSE )
119133 )
120134
121135 # ── Folder selection (native Electron dialog) ─────────────────────────────
@@ -134,6 +148,36 @@ setupServer <- function(input, output, session, shared) {
134148 }
135149 local $ folder_path <- p
136150 shared $ fcs_folder <- p
151+ showNotification(
152+ paste0(" Folder selected: " , basename(p )),
153+ type = " message" , duration = 3
154+ )
155+ })
156+
157+ # ── Direct FCS file selection ─────────────────────────────────────────────
158+ observeEvent(input $ fcs_files_picked , {
159+ sel <- input $ fcs_files_picked
160+ if (is.list(sel ) && identical(sel $ error , " no_electron" )) {
161+ showNotification(" File selection requires the StreamFLOW desktop app." ,
162+ type = " warning" , duration = 5 )
163+ return ()
164+ }
165+ paths <- sel $ paths
166+ if (is.null(paths ) || length(paths ) == 0 ) return ()
167+ valid <- paths [file.exists(paths )]
168+ if (length(valid ) == 0 ) {
169+ showNotification(" None of the selected files could be found." ,
170+ type = " error" , duration = 5 )
171+ return ()
172+ }
173+ parent_dir <- unique(dirname(valid ))
174+ local $ folder_path <- if (length(parent_dir ) == 1 ) parent_dir else dirname(valid [1 ])
175+ local $ picked_files <- valid
176+ shared $ fcs_folder <- local $ folder_path
177+ showNotification(
178+ sprintf(" %d FCS file(s) selected directly." , length(valid )),
179+ type = " message" , duration = 3
180+ )
137181 })
138182
139183 output $ selected_folder_label <- renderUI({
@@ -150,52 +194,88 @@ setupServer <- function(input, output, session, shared) {
150194 req(local $ folder_path )
151195 folder <- local $ folder_path
152196 shared $ status <- " busy"
197+ shinyjs :: disable(" load_fcs_btn" )
153198
154- withProgress(message = " Loading FCS files…" , value = 0 , {
199+ fs <- withProgress(message = " Loading FCS files…" , value = 0 , {
155200
156- incProgress(0.05 , detail = " Scanning for .fcs files…" )
157- fcs_files <- list.files(folder , pattern = " \\ .fcs$" ,
158- recursive = isTRUE(input $ recursive ),
159- full.names = TRUE , ignore.case = TRUE )
201+ incProgress(0.05 , detail = " Locating FCS files…" )
202+ fcs_files <- if (! is.null(local $ picked_files )) {
203+ local $ picked_files
204+ } else {
205+ list.files(folder , pattern = " \\ .fcs$" ,
206+ recursive = isTRUE(input $ recursive ),
207+ full.names = TRUE , ignore.case = TRUE )
208+ }
160209
161210 if (length(fcs_files ) == 0 ) {
162- showNotification(" No FCS files found in the selected folder." ,
163- type = " warning" , duration = 5 )
164- shared $ status <- " idle"
165- return ()
211+ showNotification(
212+ paste0(" No .fcs files found in: " , basename(folder ),
213+ if (isTRUE(input $ recursive )) " " else " — try enabling 'Include sub-folders'" ),
214+ type = " warning" , duration = 7
215+ )
216+ return (NULL )
166217 }
167218
168- # ── cyto_load: load FCS into ncdfFlowSet ───────────────────────────────
169- incProgress(0.15 , detail = sprintf(" cyto_load(): reading %d files…" , length(fcs_files )))
170- fs <- tryCatch(
171- safe_cyto(
172- CytoExploreR :: cyto_load(fcs_files ),
173- " cyto_load failed"
174- ),
219+ showNotification(
220+ sprintf(" Found %d FCS file(s). Reading (this may take a minute for large files)…" , length(fcs_files )),
221+ type = " message" , duration = 5
222+ )
223+
224+ # ── Validate each file header before bulk read ─────────────────────────
225+ # cyto_load() loads saved workspaces, NOT raw FCS — do not use it here.
226+ incProgress(0.10 , detail = sprintf(" Validating %d FCS headers…" , length(fcs_files )))
227+ valid_files <- Filter(function (f ) {
228+ tryCatch({ flowCore :: read.FCSheader(f ); TRUE }, error = function (e ) {
229+ message(sprintf(" [StreamFLOW] Skipping unreadable FCS: %s — %s" ,
230+ basename(f ), conditionMessage(e )))
231+ FALSE
232+ })
233+ }, fcs_files )
234+
235+ if (length(valid_files ) == 0 ) {
236+ showNotification(
237+ " No valid FCS files found. Ensure the folder contains uncorrupted .fcs files." ,
238+ type = " error" , duration = 10
239+ )
240+ return (NULL )
241+ }
242+
243+ if (length(valid_files ) < length(fcs_files )) {
244+ showNotification(
245+ sprintf(" %d of %d file(s) were skipped (unreadable headers)." ,
246+ length(fcs_files ) - length(valid_files ), length(fcs_files )),
247+ type = " warning" , duration = 6
248+ )
249+ }
250+
251+ # ── flowCore::read.flowSet — primary FCS loader ────────────────────────
252+ incProgress(0.20 , detail = sprintf(" Reading %d file(s) with flowCore…" , length(valid_files )))
253+ loaded_fs <- tryCatch(
254+ flowCore :: read.flowSet(files = valid_files , transformation = FALSE ),
175255 error = function (e ) {
176- showNotification(paste(" cyto_load error:" , conditionMessage(e )),
177- type = " error" , duration = 8 )
256+ showNotification(
257+ paste0(" flowCore::read.flowSet() failed: " , conditionMessage(e )),
258+ type = " error" , duration = 10
259+ )
178260 NULL
179261 }
180262 )
181263
182- # Fallback to flowCore if cyto_load unavailable
183- if (is.null(fs )) {
184- incProgress(0 , detail = " Falling back to flowCore::read.flowSet()…" )
185- valid_files <- Filter(function (f ) {
186- tryCatch({ read.FCSheader(f ); TRUE }, error = function (e ) {
187- showNotification(paste(" Skipping:" , basename(f )), type = " warning" , duration = 3 )
188- FALSE
189- })
190- }, fcs_files )
191- if (length(valid_files ) == 0 ) { shared $ status <- " idle" ; return () }
192- fs <- tryCatch(flowCore :: read.flowSet(files = valid_files , transformation = FALSE ),
193- error = function (e ) {
194- showNotification(paste(" Load error:" , conditionMessage(e )),
195- type = " error" , duration = 8 ); NULL })
264+ if (is.null(loaded_fs )) {
265+ return (NULL )
196266 }
197- req(fs )
198- incProgress(0.15 )
267+
268+ incProgress(0.10 )
269+ loaded_fs
270+ })
271+
272+ if (is.null(fs )) {
273+ shared $ status <- " idle"
274+ shinyjs :: enable(" load_fcs_btn" )
275+ return ()
276+ }
277+
278+ withProgress(message = " Processing loaded files…" , value = 0.5 , {
199279
200280 # ── cyto_barcode: assign sample-ID barcodes ────────────────────────────
201281 if (isTRUE(input $ do_barcode )) {
@@ -229,16 +309,20 @@ setupServer <- function(input, output, session, shared) {
229309 error = function (e ) sampleNames(fs )
230310 )
231311
232- # ── cyto_names_parse: parse details from file names ───────────────────
233- annot_df <- if (isTRUE(input $ parse_names )) {
234- tryCatch(
235- {
236- parsed <- safe_cyto(CytoExploreR :: cyto_names_parse(fs ), " cyto_names_parse failed" )
237- if (is.data.frame(parsed )) parsed else NULL
238- },
239- error = function (e ) NULL
240- )
241- } else NULL
312+ # ── Parse names into details (non-interactive, regex-based) ─────────────
313+ # cyto_names_parse() prompts interactively for a delimiter and will hang
314+ # permanently in headless Electron — never call it here.
315+ annot_df <- tryCatch({
316+ parts <- strsplit(tools :: file_path_sans_ext(basename(sample_names )),
317+ " [_\\ -\\ s]+" )
318+ max_cols <- max(lengths(parts ))
319+ if (max_cols > = 2 ) {
320+ mat <- t(sapply(parts , function (x ) {
321+ length(x ) <- max_cols ; x [is.na(x )] <- " " ; x
322+ }))
323+ as.data.frame(mat , stringsAsFactors = FALSE )
324+ } else NULL
325+ }, error = function (e ) NULL )
242326
243327 if (is.null(annot_df ) || nrow(annot_df ) == 0 ) {
244328 annot_df <- data.frame (
@@ -254,8 +338,9 @@ setupServer <- function(input, output, session, shared) {
254338 annot_df $ SampleName <- sample_names
255339 }
256340
257- local $ flowset <- fs
258- local $ annotation <- annot_df
341+ local $ picked_files <- NULL # consumed; next load uses folder scan
342+ local $ flowset <- fs
343+ local $ annotation <- annot_df
259344 shared $ raw_flowset <- fs
260345 shared $ annotation <- annot_df
261346 shared $ n_samples <- length(fs )
@@ -282,13 +367,14 @@ setupServer <- function(input, output, session, shared) {
282367
283368 incProgress(0.1 , detail = " Done!" )
284369 showNotification(
285- sprintf(" Loaded %d FCS files via cyto_load( ). %d fluorescent channels detected." ,
370+ sprintf(" Successfully loaded %d FCS file(s ). %d fluorescent channel(s) detected." ,
286371 length(fs ), length(fluor_ch )),
287- type = " message" , duration = 4
372+ type = " message" , duration = 5
288373 )
289374 })
290375
291376 shared $ status <- " idle"
377+ shinyjs :: enable(" load_fcs_btn" )
292378 })
293379
294380 # ── cyto_details_edit (button) ────────────────────────────────────────────
0 commit comments