diff --git a/Elwig/Helpers/Export/Ods.cs b/Elwig/Helpers/Export/Ods.cs
new file mode 100644
index 0000000..1a92c14
--- /dev/null
+++ b/Elwig/Helpers/Export/Ods.cs
@@ -0,0 +1,187 @@
+using Elwig.Models.Dtos;
+using System;
+using System.Collections.Generic;
+using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace Elwig.Helpers.Export {
+ public class OdsFile : IDisposable, IAsyncDisposable {
+
+ protected readonly string FileName;
+ protected readonly ZipArchive ZipArchive;
+ protected StreamWriter? Content;
+
+ public OdsFile(string filename) {
+ FileName = filename;
+ File.Delete(filename);
+ ZipArchive = ZipFile.Open(FileName, ZipArchiveMode.Create); ;
+ Content = null;
+ }
+
+ public void Dispose() {
+ DisposeAsync().GetAwaiter().GetResult();
+ }
+
+ public async ValueTask DisposeAsync() {
+ await AddTrailer();
+ Content?.Close();
+ Content?.DisposeAsync();
+ ZipArchive?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ private async Task AddHeader() {
+ var mimetype = ZipArchive.CreateEntry("mimetype", CompressionLevel.NoCompression);
+ using (var stream = mimetype.Open()) {
+ using var writer = new StreamWriter(stream, Utils.UTF8);
+ await writer.WriteAsync("application/vnd.oasis.opendocument.spreadsheet");
+ }
+
+ var manifest = ZipArchive.CreateEntry("META-INF/manifest.xml");
+ using (var stream = manifest.Open()) {
+ using var writer = new StreamWriter(stream, Utils.UTF8);
+ await writer.WriteAsync("""
+
+
+
+
+
+
+
+
+ """);
+ }
+
+ var styles = ZipArchive.CreateEntry("styles.xml");
+ using (var stream = styles.Open()) {
+ using var writer = new StreamWriter(stream, Utils.UTF8);
+ await writer.WriteAsync("""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+ }
+
+ var meta = ZipArchive.CreateEntry("meta.xml");
+ using (var stream = meta.Open()) {
+ using var writer = new StreamWriter(stream, Utils.UTF8);
+ var now = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
+ await writer.WriteAsync($"""
+
+
+
+ Elwig {App.Version}
+ Elwig
+ Elwig
+ {now}
+ {now}
+
+
+
+ """);
+ }
+
+ var content = ZipArchive.CreateEntry("content.xml");
+ Content = new StreamWriter(content.Open(), Utils.UTF8);
+ await Content.WriteAsync("""
+
+
+
+
+
+
+ """);
+ }
+
+ private async Task AddTrailer() {
+ if (Content == null) await AddHeader();
+ if (Content == null) return;
+ await Content.WriteAsync(" \r\n \r\n\r\n");
+ }
+
+ public async Task AddTable(DataTable table) {
+ if (Content == null) await AddHeader();
+ if (Content == null) return;
+ var totalSpan = table.ColumnSpans.Sum(s => s.Item2);
+
+ await Content.WriteAsync(
+ $" \r\n" +
+ $" \r\n" +
+ $" \r\n" +
+ FormatCell(table.FullName, colSpan: totalSpan, style: "header") +
+ $" \r\n" +
+ $" \r\n" +
+ $" \r\n" +
+ $" \r\n" +
+ $" \r\n");
+ foreach (var (name, span) in table.ColumnSpans) {
+ await Content.WriteAsync(FormatCell(name, colSpan: span, style: "th"));
+ }
+ await Content.WriteAsync(" \r\n");
+
+ foreach (var row in table.GetData()) {
+ await FormatRow(row);
+ }
+
+ await Content.WriteAsync(" \r\n");
+ }
+
+ protected async Task FormatRow(IEnumerable