a5c227ef1c
When a recording finishes, ask for a meeting name and rename the session folder from the auto stamp `<yyyy-MM-dd'T'HH-mm-ss>_<app>` to the readable `<date>_<name>_<app>` (dropping HH-MM-SS), so sessions/ is easy to scan. Skipping or leaving it blank keeps the timestamped name. The rename runs after the recorder and visual capture finish (files closed) and before finish() captures the folder for backend processing, so the renamed folder is what flows downstream; finish() re-derives the track URLs from the possibly-moved folder. The quit path never prompts, and a quit with the prompt open ends its modal so termination isn't blocked. Naming/parsing logic lives in a pure, unit-tested SessionNaming; recapTitle moves there and now understands both folder forms.
72 lines
4.0 KiB
Swift
72 lines
4.0 KiB
Swift
import Foundation
|
|
|
|
/// Pure helpers for session-folder names. A session folder is created at start
|
|
/// with an auto name `<yyyy-MM-dd'T'HH-mm-ss>_<app>`; when the user names the
|
|
/// recording on stop it's renamed to `<yyyy-MM-dd>_<name>_<app>` (no HH-MM-SS),
|
|
/// which is far easier to scan in `sessions/`. The app label always stays the
|
|
/// LAST `_`-separated segment so `SessionController.appLabel(from:)` keeps working
|
|
/// even when the meeting name itself contains spaces or underscores.
|
|
enum SessionNaming {
|
|
/// Filesystem- and parse-safe meeting name: trims, turns path separators into
|
|
/// dashes, drops control characters, collapses whitespace runs, removes leading
|
|
/// dots (no hidden/`.`/`..` folders), and caps the length. Returns "" if nothing
|
|
/// usable is left, which callers treat as "skip the rename".
|
|
static func sanitize(_ raw: String) -> String {
|
|
var s = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
// Path-hostile separators (`/` and the classic Mac `:`, plus `\`) → dash.
|
|
s = s.components(separatedBy: CharacterSet(charactersIn: "/:\\")).joined(separator: "-")
|
|
// Strip control characters outright.
|
|
s = s.components(separatedBy: .controlCharacters).joined()
|
|
// Collapse internal whitespace runs to single spaces.
|
|
s = s.split(whereSeparator: { $0 == " " || $0 == "\t" }).joined(separator: " ")
|
|
while s.hasPrefix(".") { s.removeFirst() }
|
|
s = s.trimmingCharacters(in: .whitespaces)
|
|
if s.count > 60 { s = String(s.prefix(60)).trimmingCharacters(in: .whitespaces) }
|
|
return s
|
|
}
|
|
|
|
/// The date prefix of a session leaf name, e.g. `2026-06-17T09-59-48_signal`
|
|
/// → `2026-06-17`. Already-renamed leaves (`2026-06-17_name_signal`) return the
|
|
/// same date, so this is safe to call on either form.
|
|
static func datePrefix(ofSessionNamed leaf: String) -> String {
|
|
let head = leaf.split(separator: "_").first.map(String.init) ?? leaf
|
|
return head.split(separator: "T").first.map(String.init) ?? head
|
|
}
|
|
|
|
/// Compose the renamed leaf `<date>_<name>_<app>`. A positive `counter`
|
|
/// disambiguates a collision by suffixing the NAME segment (`<name>-2`) so the
|
|
/// trailing `_<app>` stays parseable. Returns nil when the name sanitizes to
|
|
/// empty (the caller keeps the auto timestamped name).
|
|
static func renamedLeaf(date: String, app: String, meetingName: String, counter: Int = 0) -> String? {
|
|
let clean = sanitize(meetingName)
|
|
guard !clean.isEmpty else { return nil }
|
|
let suffix = counter > 0 ? "-\(counter + 1)" : ""
|
|
return "\(date)_\(clean)\(suffix)_\(app)"
|
|
}
|
|
|
|
/// Friendly recap title from a session id, understanding both folder forms:
|
|
/// `2026-06-06T11-43-02_meet` → "Google Meet call — 2026-06-06 11:43"
|
|
/// `2026-06-06_Weekly sync_meet` → "Weekly sync — Google Meet (2026-06-06)"
|
|
static func recapTitle(app: String, sessionId: String) -> String {
|
|
let appName = CallDetector.DetectedApp(rawValue: app)?.display ?? app.capitalized
|
|
var parts = sessionId.split(separator: "_").map(String.init)
|
|
if parts.count > 1 { parts.removeLast() } // drop the trailing "_<app>"
|
|
let head = parts.first ?? sessionId
|
|
let tBits = head.split(separator: "T").map(String.init)
|
|
let date = tBits.first ?? head
|
|
let time: String = {
|
|
guard tBits.count > 1 else { return "" }
|
|
let b = tBits[1].split(separator: "-")
|
|
return b.count >= 2 ? "\(b[0]):\(b[1])" : ""
|
|
}()
|
|
let when = [date, time].filter { !$0.isEmpty }.joined(separator: " ")
|
|
// Rejoin with "_" — the faithful inverse of split("_") — so a name that
|
|
// itself contained underscores survives the round-trip through the folder name.
|
|
let name = parts.count > 1 ? parts[1...].joined(separator: "_") : ""
|
|
if name.isEmpty {
|
|
return "\(appName) call — \(when)".trimmingCharacters(in: .whitespaces)
|
|
}
|
|
return "\(name) — \(appName) (\(when))".trimmingCharacters(in: .whitespaces)
|
|
}
|
|
}
|