Skip to content

Commit 5bbb546

Browse files
Copilottautcony
andcommitted
Complete UI implementation with file loading, chapter display, and export functionality
Co-authored-by: tautcony <8295052+tautcony@users.noreply.github.com>
1 parent 5e3b8e5 commit 5bbb546

File tree

4 files changed

+435
-45
lines changed

4 files changed

+435
-45
lines changed

ChapterTool.Avalonia/ViewModels/MainWindowViewModel.cs

Lines changed: 308 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
using System;
12
using System.Collections.ObjectModel;
3+
using System.IO;
4+
using System.Linq;
25
using System.Threading.Tasks;
6+
using System.Xml;
7+
using Avalonia.Controls;
8+
using Avalonia.Platform.Storage;
39
using CommunityToolkit.Mvvm.ComponentModel;
410
using CommunityToolkit.Mvvm.Input;
511
using ChapterTool.Util;
12+
using ChapterTool.Util.ChapterData;
613

714
namespace ChapterTool.Avalonia.ViewModels;
815

@@ -14,28 +21,315 @@ public partial class MainWindowViewModel : ViewModelBase
1421
[ObservableProperty]
1522
private string _statusMessage = "Ready";
1623

24+
[ObservableProperty]
25+
private string _windowTitle = "ChapterTool - Modern Edition";
26+
27+
[ObservableProperty]
28+
private bool _autoGenName = false;
29+
30+
[ObservableProperty]
31+
private int _selectedExportFormat = 0;
32+
33+
[ObservableProperty]
34+
private string _expressionText = "x";
35+
1736
public ObservableCollection<ChapterViewModel> Chapters { get; } = new();
37+
public ObservableCollection<string> ExportFormats { get; } = new()
38+
{
39+
"OGM Text (.txt)",
40+
"Matroska XML (.xml)",
41+
"QPFile (.qpf)",
42+
"JSON (.json)",
43+
"CUE Sheet (.cue)"
44+
};
45+
46+
private ChapterInfo? _currentChapterInfo;
47+
private Window? _mainWindow;
48+
49+
public void SetMainWindow(Window window)
50+
{
51+
_mainWindow = window;
52+
}
1853

1954
[RelayCommand]
2055
private async Task LoadFile()
2156
{
22-
// TODO: Implement file loading with file dialog
23-
StatusMessage = "Loading file...";
24-
25-
// Example of using Core library
26-
Logger.Log("File loading initiated from Avalonia UI");
57+
if (_mainWindow == null) return;
58+
59+
try
60+
{
61+
var filePickerOptions = new FilePickerOpenOptions
62+
{
63+
Title = "Open Chapter File",
64+
AllowMultiple = false,
65+
FileTypeFilter = new[]
66+
{
67+
new FilePickerFileType("All Supported Files")
68+
{
69+
Patterns = new[] { "*.mpls", "*.xml", "*.txt", "*.ifo", "*.mkv", "*.mka",
70+
"*.tak", "*.flac", "*.cue", "*.xpl", "*.mp4", "*.m4a",
71+
"*.m4v", "*.vtt" }
72+
},
73+
new FilePickerFileType("Blu-ray Playlist") { Patterns = new[] { "*.mpls" } },
74+
new FilePickerFileType("XML Chapter") { Patterns = new[] { "*.xml" } },
75+
new FilePickerFileType("OGM Text") { Patterns = new[] { "*.txt" } },
76+
new FilePickerFileType("DVD IFO") { Patterns = new[] { "*.ifo" } },
77+
new FilePickerFileType("Matroska") { Patterns = new[] { "*.mkv", "*.mka" } },
78+
new FilePickerFileType("Audio with CUE") { Patterns = new[] { "*.tak", "*.flac", "*.cue" } },
79+
new FilePickerFileType("HD DVD XPL") { Patterns = new[] { "*.xpl" } },
80+
new FilePickerFileType("MP4 Files") { Patterns = new[] { "*.mp4", "*.m4a", "*.m4v" } },
81+
new FilePickerFileType("WebVTT") { Patterns = new[] { "*.vtt" } },
82+
new FilePickerFileType("All Files") { Patterns = new[] { "*" } }
83+
}
84+
};
85+
86+
var files = await _mainWindow.StorageProvider.OpenFilePickerAsync(filePickerOptions);
87+
if (files == null || files.Count == 0) return;
88+
89+
var file = files[0];
90+
FilePath = file.Path.LocalPath;
91+
92+
StatusMessage = "Loading file...";
93+
Logger.Log($"Loading file: {FilePath}");
94+
95+
await LoadChapterFile(FilePath);
96+
97+
StatusMessage = $"Loaded: {Path.GetFileName(FilePath)} - {Chapters.Count} chapters";
98+
WindowTitle = $"ChapterTool - {Path.GetFileName(FilePath)}";
99+
}
100+
catch (Exception ex)
101+
{
102+
StatusMessage = $"Error: {ex.Message}";
103+
Logger.Log($"Error loading file: {ex.Message}");
104+
}
105+
}
106+
107+
private async Task LoadChapterFile(string filePath)
108+
{
109+
await Task.Run(() =>
110+
{
111+
try
112+
{
113+
var extension = Path.GetExtension(filePath)?.ToLowerInvariant().TrimStart('.');
114+
ChapterInfo? chapterInfo = null;
115+
116+
switch (extension)
117+
{
118+
case "mpls":
119+
var mplsData = new MplsData(filePath);
120+
var mplsChapters = mplsData.GetChapters();
121+
chapterInfo = mplsChapters.FirstOrDefault();
122+
break;
123+
124+
case "xml":
125+
var xmlDoc = new XmlDocument();
126+
xmlDoc.Load(filePath);
127+
chapterInfo = XmlData.ParseXml(xmlDoc).FirstOrDefault();
128+
break;
129+
130+
case "txt":
131+
var txtContent = File.ReadAllBytes(filePath).GetUTFString();
132+
chapterInfo = OgmData.GetChapterInfo(txtContent ?? string.Empty);
133+
break;
134+
135+
case "ifo":
136+
chapterInfo = IfoData.GetStreams(filePath).FirstOrDefault();
137+
break;
138+
139+
case "mkv":
140+
case "mka":
141+
var mkvData = new MatroskaData();
142+
var mkvXml = mkvData.GetXml(filePath);
143+
chapterInfo = XmlData.ParseXml(mkvXml).FirstOrDefault();
144+
break;
145+
146+
case "cue":
147+
case "tak":
148+
case "flac":
149+
var cueSheet = new CueData(filePath, Logger.Log);
150+
chapterInfo = cueSheet.Chapter;
151+
break;
152+
153+
case "xpl":
154+
chapterInfo = XplData.GetStreams(filePath).FirstOrDefault();
155+
break;
156+
157+
case "mp4":
158+
case "m4a":
159+
case "m4v":
160+
var mp4Data = new Mp4Data(filePath);
161+
chapterInfo = mp4Data.Chapter;
162+
break;
163+
164+
case "vtt":
165+
var vttContent = File.ReadAllBytes(filePath).GetUTFString();
166+
chapterInfo = VTTData.GetChapterInfo(vttContent ?? string.Empty);
167+
break;
168+
169+
default:
170+
throw new Exception($"Unsupported file format: {extension}");
171+
}
172+
173+
if (chapterInfo != null && chapterInfo.Chapters.Count > 0)
174+
{
175+
_currentChapterInfo = chapterInfo;
176+
UpdateChapterDisplay();
177+
}
178+
else
179+
{
180+
throw new Exception("No chapters found in file");
181+
}
182+
}
183+
catch (Exception ex)
184+
{
185+
Logger.Log($"Error parsing file: {ex.Message}");
186+
throw;
187+
}
188+
});
189+
}
190+
191+
private void UpdateChapterDisplay()
192+
{
193+
Chapters.Clear();
27194

28-
await Task.Delay(100); // Placeholder for async operation
29-
StatusMessage = "File loaded successfully";
195+
if (_currentChapterInfo == null) return;
196+
197+
var nameGenerator = ChapterName.GetChapterName();
198+
int index = 1;
199+
200+
foreach (var chapter in _currentChapterInfo.Chapters.Where(c => c.Time != TimeSpan.MinValue))
201+
{
202+
Chapters.Add(new ChapterViewModel
203+
{
204+
Number = chapter.Number > 0 ? chapter.Number : index,
205+
Time = chapter.Time,
206+
TimeString = chapter.Time2String(_currentChapterInfo),
207+
Name = AutoGenName ? nameGenerator() : chapter.Name,
208+
FramesInfo = chapter.FramesInfo
209+
});
210+
index++;
211+
}
30212
}
31213

32214
[RelayCommand]
33215
private async Task ExportChapters()
34216
{
35-
// TODO: Implement chapter export functionality
36-
StatusMessage = "Exporting chapters...";
37-
await Task.Delay(100); // Placeholder for async operation
38-
StatusMessage = "Export completed";
217+
if (_mainWindow == null || _currentChapterInfo == null)
218+
{
219+
StatusMessage = "No chapters loaded";
220+
return;
221+
}
222+
223+
try
224+
{
225+
var suggestedName = Path.GetFileNameWithoutExtension(FilePath);
226+
var extension = SelectedExportFormat switch
227+
{
228+
0 => ".txt",
229+
1 => ".xml",
230+
2 => ".qpf",
231+
3 => ".json",
232+
4 => ".cue",
233+
_ => ".txt"
234+
};
235+
236+
var filePickerOptions = new FilePickerSaveOptions
237+
{
238+
Title = "Save Chapter File",
239+
SuggestedFileName = $"{suggestedName}_chapters{extension}",
240+
DefaultExtension = extension.TrimStart('.'),
241+
FileTypeChoices = new[]
242+
{
243+
new FilePickerFileType(ExportFormats[SelectedExportFormat])
244+
{
245+
Patterns = new[] { $"*{extension}" }
246+
}
247+
}
248+
};
249+
250+
var file = await _mainWindow.StorageProvider.SaveFilePickerAsync(filePickerOptions);
251+
if (file == null) return;
252+
253+
var savePath = file.Path.LocalPath;
254+
StatusMessage = "Exporting chapters...";
255+
Logger.Log($"Exporting to: {savePath}");
256+
257+
await Task.Run(() =>
258+
{
259+
switch (SelectedExportFormat)
260+
{
261+
case 0: // OGM Text
262+
var text = _currentChapterInfo.GetText(AutoGenName);
263+
File.WriteAllText(savePath, text, new System.Text.UTF8Encoding(true));
264+
break;
265+
case 1: // XML
266+
_currentChapterInfo.SaveXml(savePath, "und", AutoGenName);
267+
break;
268+
case 2: // QPFile
269+
var qpfile = _currentChapterInfo.GetQpfile();
270+
File.WriteAllLines(savePath, qpfile);
271+
break;
272+
case 3: // JSON
273+
var json = _currentChapterInfo.GetJson(AutoGenName);
274+
File.WriteAllText(savePath, json.ToString());
275+
break;
276+
case 4: // CUE
277+
var cue = _currentChapterInfo.GetCue(Path.GetFileName(FilePath), AutoGenName);
278+
File.WriteAllText(savePath, cue.ToString(), new System.Text.UTF8Encoding(false));
279+
break;
280+
}
281+
});
282+
283+
StatusMessage = $"Exported to: {Path.GetFileName(savePath)}";
284+
Logger.Log($"Export completed successfully");
285+
}
286+
catch (Exception ex)
287+
{
288+
StatusMessage = $"Export failed: {ex.Message}";
289+
Logger.Log($"Export error: {ex.Message}");
290+
}
291+
}
292+
293+
[RelayCommand]
294+
private void ApplyExpression()
295+
{
296+
if (_currentChapterInfo == null || string.IsNullOrWhiteSpace(ExpressionText))
297+
{
298+
StatusMessage = "No expression to apply";
299+
return;
300+
}
301+
302+
try
303+
{
304+
_currentChapterInfo.Expr = new Expression(ExpressionText);
305+
UpdateChapterDisplay();
306+
StatusMessage = $"Expression applied: {ExpressionText}";
307+
Logger.Log($"Applied expression: {ExpressionText}");
308+
}
309+
catch (Exception ex)
310+
{
311+
StatusMessage = $"Expression error: {ex.Message}";
312+
Logger.Log($"Expression error: {ex.Message}");
313+
}
314+
}
315+
316+
[RelayCommand]
317+
private void ShowLog()
318+
{
319+
// Log viewer - show current log content
320+
var logText = Logger.LogText;
321+
StatusMessage = $"Log has {logText.Split('\n').Length} lines";
322+
}
323+
324+
[RelayCommand]
325+
private void ShowAbout()
326+
{
327+
StatusMessage = "ChapterTool - Modern Edition | .NET 8 + Avalonia UI";
328+
}
329+
330+
partial void OnAutoGenNameChanged(bool value)
331+
{
332+
UpdateChapterDisplay();
39333
}
40334
}
41335

@@ -47,6 +341,9 @@ public partial class ChapterViewModel : ObservableObject
47341
[ObservableProperty]
48342
private int _number;
49343

344+
[ObservableProperty]
345+
private TimeSpan _time;
346+
50347
[ObservableProperty]
51348
private string _timeString = "00:00:00.000";
52349

0 commit comments

Comments
 (0)