Skip to content

Commit d0d7173

Browse files
update nights in calicut
1 parent 7013225 commit d0d7173

4 files changed

Lines changed: 179 additions & 57 deletions

File tree

‎electron/main.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,29 @@ ipcMain.handle('dialog:selectFile', async (_evt, options) => {
538538
}
539539
});
540540

541+
ipcMain.handle('dialog:selectFiles', async (_evt, options) => {
542+
const parent = dialogParent();
543+
if (!parent) return { canceled: true, paths: null };
544+
const opts = options || {};
545+
try {
546+
const result = await dialog.showOpenDialog(parent, {
547+
title: opts.title || 'Select files',
548+
defaultPath: opts.defaultPath || undefined,
549+
filters: Array.isArray(opts.filters) ? opts.filters : undefined,
550+
properties: ['openFile', 'multiSelections']
551+
});
552+
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
553+
log.info('selectFiles: canceled');
554+
return { canceled: true, paths: null };
555+
}
556+
log.info(`selectFiles: ${result.filePaths.length} file(s) selected`);
557+
return { canceled: false, paths: result.filePaths };
558+
} catch (err) {
559+
log.error(`selectFiles failed: ${err.message}`);
560+
return { canceled: true, paths: null };
561+
}
562+
});
563+
541564
ipcMain.handle('dialog:saveFile', async (_evt, options) => {
542565
const parent = dialogParent();
543566
if (!parent) return { canceled: true, path: null };

‎electron/preload.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
1616
// Each resolves to { canceled: boolean, path: string | null }.
1717
selectFolder: (options) => ipcRenderer.invoke('dialog:selectFolder', options),
1818
selectFile: (options) => ipcRenderer.invoke('dialog:selectFile', options),
19+
selectFiles: (options) => ipcRenderer.invoke('dialog:selectFiles', options),
1920
saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
2021

2122
// Menu event listeners (Shiny listens for these via JS)

‎shiny/modules/mod_setup.R‎

Lines changed: 143 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -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) ────────────────────────────────────────────

‎shiny/ui.R‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,18 @@ full_ui <- tagList(
582582
});
583583
};
584584
585+
window.streamflowPickFiles = function(inputId, title, filters) {
586+
if (!window.electronAPI || !window.electronAPI.selectFiles) {
587+
Shiny.setInputValue(inputId, {paths: null, error: 'no_electron'}, {priority: 'event'});
588+
return;
589+
}
590+
window.electronAPI.selectFiles({ title: title, filters: filters }).then(function(result) {
591+
if (!result.canceled && result.paths && result.paths.length > 0) {
592+
Shiny.setInputValue(inputId, {paths: result.paths, ts: Date.now()}, {priority: 'event'});
593+
}
594+
});
595+
};
596+
585597
window.streamflowSaveFile = function(inputId, title, filters, defaultPath) {
586598
if (!window.electronAPI || !window.electronAPI.saveFile) {
587599
Shiny.setInputValue(inputId, {path: null, error: 'no_electron'}, {priority: 'event'});

0 commit comments

Comments
 (0)