using Google.Apis.Sheets.v4; using Google.Apis.Sheets.v4.Data; using Color = Google.Apis.Sheets.v4.Data.Color; namespace GoogleSheetsScheduleImport; /// /// Fetches raw grid data via Sheets API (public API key). /// public sealed class GoogleSheetGridReader { private readonly SheetsService _service; public GoogleSheetGridReader(string apiKey) { if (string.IsNullOrWhiteSpace(apiKey)) throw new ArgumentException("API key is required for Google Sheets access.", nameof(apiKey)); _service = new SheetsService(new Google.Apis.Services.BaseClientService.Initializer { ApiKey = apiKey, ApplicationName = "TSA GoogleSheetsScheduleImport" }); } public Spreadsheet FetchSpreadsheet(string spreadsheetId) { var req = _service.Spreadsheets.Get(spreadsheetId); req.IncludeGridData = true; return req.Execute(); } public static GridSheetModel BuildModel(Sheet sheet) { var title = sheet.Properties?.Title ?? "(untitled)"; var grid = sheet.Data?.FirstOrDefault(); if (grid?.RowData == null || grid.RowData.Count == 0) { return new GridSheetModel { SheetTitle = title, Values = Array.Empty(), BackgroundKeys = Array.Empty() }; } var rowCount = grid.RowData.Count; var colCount = grid.RowData.Max(r => r.Values?.Count ?? 0); var values = new string?[rowCount][]; var bg = new string?[rowCount][]; for (var r = 0; r < rowCount; r++) { values[r] = new string?[colCount]; bg[r] = new string?[colCount]; var row = grid.RowData[r]; for (var c = 0; c < colCount; c++) { string? text = null; string? hex = null; if (row.Values != null && c < row.Values.Count) { var cell = row.Values[c]; text = string.IsNullOrWhiteSpace(cell.FormattedValue) ? cell.EffectiveValue?.StringValue : cell.FormattedValue; if (!string.IsNullOrEmpty(text)) text = TextNormalization.ForSheetCell(text); var color = cell.UserEnteredFormat?.BackgroundColor ?? cell.EffectiveFormat?.BackgroundColor; hex = ColorToHexKey(color); } values[r][c] = string.IsNullOrWhiteSpace(text) ? null : text; bg[r][c] = hex; } } ApplyMerges(sheet.Merges, values, bg); NormalizeAllValueCells(values); return new GridSheetModel { SheetTitle = title, Values = values, BackgroundKeys = bg }; } private static void ApplyMerges(IList? merges, string?[][] values, string?[][] bg) { if (merges == null || merges.Count == 0) return; foreach (var range in merges) { var r0 = range.StartRowIndex ?? 0; var r1 = range.EndRowIndex ?? r0; var c0 = range.StartColumnIndex ?? 0; var c1 = range.EndColumnIndex ?? c0; if (r1 <= r0 || c1 <= c0) continue; var anchorText = values[r0][c0]; var anchorBg = bg[r0][c0]; for (var r = r0; r < r1; r++) { for (var c = c0; c < c1; c++) { if (values[r][c] == null && anchorText != null) values[r][c] = anchorText; if (bg[r][c] == null && anchorBg != null) bg[r][c] = anchorBg; } } } } /// Re-run after merges so copied anchor text is also single-line. private static void NormalizeAllValueCells(string?[][] values) { for (var r = 0; r < values.Length; r++) for (var c = 0; c < values[r].Length; c++) { if (values[r][c] is { } v && !string.IsNullOrWhiteSpace(v)) values[r][c] = TextNormalization.ForEmitLine(v); } } private static string? ColorToHexKey(Color? color) { if (color == null) return null; var r = color.Red ?? 1f; var g = color.Green ?? 1f; var b = color.Blue ?? 1f; // Treat near-white as no color key for grouping if (r >= 0.99f && g >= 0.99f && b >= 0.99f) return null; static byte F(float x) => (byte)(Math.Clamp(x, 0f, 1f) * 255f); return $"#{F(r):X2}{F(g):X2}{F(b):X2}"; } }