Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2aa7ebd
add impl for retrieving missed events from select services
WolfwithSword Sep 29, 2026
27044a1
initial patreon setup #145
WolfwithSword Oct 2, 2026
c2edd27
patreon fix handle update events
WolfwithSword Oct 2, 2026
aa18a4f
holy actions framework #338
WolfwithSword Oct 3, 2026
fcaa4d1
actions: if else, globals, secrets #338
WolfwithSword Oct 5, 2026
4a2830b
add trigger nodes and events #338
WolfwithSword Oct 6, 2026
9797efb
access globals from widgets #340 and impl POST and GET for them
WolfwithSword Oct 7, 2026
a59bdea
add optional actions to prompt completions #338
WolfwithSword Oct 7, 2026
6df280d
minor cleanup
WolfwithSword Oct 8, 2026
688fbea
add completion mark to goals
WolfwithSword Oct 8, 2026
d3fff98
allow multi run mode for multi inputs as toggle
WolfwithSword Oct 8, 2026
d2e475c
fix test
WolfwithSword Oct 8, 2026
ac2123f
minor cleanup
WolfwithSword Oct 9, 2026
ca7bddc
fix labels on http action step enums
WolfwithSword Oct 10, 2026
68a9863
add new known goaffpro store
WolfwithSword Oct 25, 2026
5cf860f
add warnings for blocked makeship issues
WolfwithSword Oct 25, 2026
dc91eb8
add makeship feature flag for warning
WolfwithSword Oct 25, 2026
1b1080d
send multi events for each goal completed
WolfwithSword Oct 25, 2026
5f29075
Fix fourthwall devtunnel startup again
WolfwithSword Oct 26, 2026
ad9d63b
use utils parser for cultured decimal numbers #345
WolfwithSword Oct 27, 2026
86c4974
update some dependencies
WolfwithSword Oct 27, 2026
9c7d7ea
fix warning
WolfwithSword Oct 29, 2026
a4b6b90
fix actions editor open location
WolfwithSword Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ jobs:
- name: Upload build artifact (Nextcloud)
id: nc
if: ${{ env.PRIMARY == 'nextcloud' || steps.gh.outcome == 'failure' }}
uses: trympet/nextcloud-artifacts-action@v2
uses: trympet/nextcloud-artifacts-action@v3
continue-on-error: false
with:
name: SubathonManager_${{ matrix.runtime }}_${{ steps.avars.outputs.suffix }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,5 @@ external/streamdeck/**/images/
# Windows installer build output
installer/windows/LICENSE.txt
installer-out/

presets/test/
127 changes: 127 additions & 0 deletions SubathonManager.Core/CsvUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Globalization;
using System.Text;

namespace SubathonManager.Core;

public static class CsvUtils {
public const string TimestampFormat = "yyyy-MM-dd HH:mm:ss";

public static string ExportFolder => Path.Combine(Config.DataFolder, "exports");

public static string Escape(object? value) {
string? text = value switch {
null => null,
string s => s,
DateTime dt => dt.ToString(TimestampFormat, CultureInfo.InvariantCulture),
DateTimeOffset dto => dto.ToString(TimestampFormat, CultureInfo.InvariantCulture),
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString()
};

if (string.IsNullOrWhiteSpace(text)) return "";
if (text.Contains(',') || text.Contains('"') || text.Contains('\n') || text.Contains('\r'))
return $"\"{text.Replace("\"", "\"\"")}\"";
return text;
}

public static string FormatRow(IEnumerable<object?> cells) {
return string.Join(',', cells.Select(Escape));
}

public static string Write<T>(IEnumerable<IEnumerable<object?>> headerRows, IEnumerable<T> rows,
Func<T, IEnumerable<object?>> toCells) {
var sb = new StringBuilder();
foreach (IEnumerable<object?> header in headerRows) sb.AppendLine(FormatRow(header));
foreach (T row in rows) sb.AppendLine(FormatRow(toCells(row)));
return sb.ToString();
}

public static async Task<string> ExportAsync<T>(string baseName, IEnumerable<IEnumerable<object?>> headerRows,
IEnumerable<T> rows, Func<T, IEnumerable<object?>> toCells) {
Directory.CreateDirectory(ExportFolder);

string name = SafeFileName.Sanitize(baseName, fallback: "export");
string stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
string path = Path.Combine(ExportFolder, $"{name}-{stamp}.csv");
await File.WriteAllTextAsync(path, Write(headerRows, rows, toCells), Encoding.UTF8);
return path;
}

public static async Task<List<string[]>?> ReadFileAsync(string path) {
try {
string text = await File.ReadAllTextAsync(path, Encoding.UTF8);
return ReadRecords(text)
.Select(r => r.Fields)
.Where(f => !f.All(string.IsNullOrWhiteSpace))
.ToList();
}
catch {
return null;
}
}

public static List<(int Line, string[] Fields)> ReadRecords(string csv) {
var records = new List<(int, string[])>();
var fields = new List<string>();
var field = new StringBuilder();
var inQuotes = false;
var line = 1;
var recordStart = 1;
var any = false;

if (csv.Length > 0 && csv[0] == (char)0xFEFF) csv = csv[1..];

for (var i = 0; i < csv.Length; i++) {
char c = csv[i];
if (inQuotes) {
if (c == '"' && i + 1 < csv.Length && csv[i + 1] == '"') {
field.Append('"');
i++;
}
else if (c == '"') {
inQuotes = false;
}
else {
if (c == '\n') line++;
if (c != '\r') field.Append(c);
}

continue;
}

switch (c) {
case '"':
inQuotes = true;
any = true;
break;
case ',':
fields.Add(field.ToString());
field.Clear();
any = true;
break;
case '\r':
break;
case '\n':
fields.Add(field.ToString());
field.Clear();
if (any || fields.Count > 1 || fields[0].Length > 0) records.Add((recordStart, fields.ToArray()));
fields.Clear();
any = false;
line++;
recordStart = line;
break;
default:
field.Append(c);
any = true;
break;
}
}

if (any || field.Length > 0) {
fields.Add(field.ToString());
records.Add((recordStart, fields.ToArray()));
}

return records;
}
}
Loading
Loading