diff --git a/AudioCuesheetEditor.End2EndTests/AudioCuesheetEditor.End2EndTests.csproj b/AudioCuesheetEditor.End2EndTests/AudioCuesheetEditor.End2EndTests.csproj index 1611da6b..5fbad912 100644 --- a/AudioCuesheetEditor.End2EndTests/AudioCuesheetEditor.End2EndTests.csproj +++ b/AudioCuesheetEditor.End2EndTests/AudioCuesheetEditor.End2EndTests.csproj @@ -16,11 +16,11 @@ - - + + - - + + @@ -41,11 +41,23 @@ PreserveNewest PreserveNewest + + PreserveNewest + true PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/AudioCuesheetEditor.End2EndTests/Pages/IndexTest.cs b/AudioCuesheetEditor.End2EndTests/Pages/IndexTest.cs index dd2b7265..8dbe4c4f 100644 --- a/AudioCuesheetEditor.End2EndTests/Pages/IndexTest.cs +++ b/AudioCuesheetEditor.End2EndTests/Pages/IndexTest.cs @@ -68,6 +68,7 @@ public async Task RecordAsync() await Page.GetByRole(AriaRole.Textbox, new() { Name = "Title", Exact = true }).FillAsync("Test Track 2 Title"); await Page.GetByRole(AriaRole.Textbox, new() { Name = "Title", Exact = true }).PressAsync("Tab"); await Page.GetByRole(AriaRole.Button, new() { Name = "Add track" }).ClickAsync(); + await Page.Locator(".mud-overlay").ClickAsync(); await Page.GetByRole(AriaRole.Button, new() { Name = "Stop recording" }).ClickAsync(); await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Test Track 1 Artist Clear" })).ToBeVisibleAsync(); await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Test Track 1 Title Clear" })).ToBeVisibleAsync(); @@ -153,5 +154,139 @@ public async Task RenameAudiofileTestAsync() await Page.GetByRole(AriaRole.Button, new() { Name = "Ok" }).ClickAsync(); await Expect(Page.Locator("#app")).ToMatchAriaSnapshotAsync("- textbox \"Cuesheet artist\"\n- group \"Cuesheet artist\"\n- text: Cuesheet artist\n- textbox \"Cuesheet title\"\n- group \"Cuesheet title\"\n- text: Cuesheet title\n- group:\n - button \"Choose File\"\n - textbox \"Audiofile\": /Kalimba test \\d+\\.mp3/\n - group \"Audiofile\"\n - text: Audiofile\n - button \"Search\"\n - button\n - button\n- group:\n - button \"Choose File\"\n - textbox \"CD Textfile\": No file selected\n - group \"CD Textfile\"\n - text: CD Textfile\n - button \"Search\"\n - button [disabled]\n - button\n- textbox \"Cataloguenumber\"\n- group \"Cataloguenumber\"\n- text: Cataloguenumber"); } + + [TestMethod] + public async Task ImportUndoRedoTestAsync() + { + await Page.GotoAsync("http://localhost:5132/"); + await Page.GetByText("Import view").ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync("Textimport with Cuesheetdata.txt"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Scheme common data" }).FillAsync("Artist - Title - "); + await Page.Locator("div").Filter(new() { HasTextRegex = new Regex("^Scheme common data$") }).GetByRole(AriaRole.Button).Nth(1).ClickAsync(); + await Page.GetByRole(AriaRole.Paragraph).Filter(new() { HasText = "Cataloguenumber" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Toolbar)).ToMatchAriaSnapshotAsync("- button \"undo\" [disabled]"); + await Expect(Page.GetByRole(AriaRole.Toolbar)).ToMatchAriaSnapshotAsync("- button \"redo\" [disabled]"); + await Page.GetByRole(AriaRole.Button, new() { Name = "Complete" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Button, new() { Name = "undo" })).ToMatchAriaSnapshotAsync("- button \"undo\""); + await Expect(Page.GetByRole(AriaRole.Toolbar)).ToMatchAriaSnapshotAsync("- button \"redo\" [disabled]"); + await Page.GetByRole(AriaRole.Button, new() { Name = "undo" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Toolbar)).ToMatchAriaSnapshotAsync("- button \"undo\" [disabled]"); + await Expect(Page.GetByRole(AriaRole.Button, new() { Name = "redo" })).ToMatchAriaSnapshotAsync("- button \"redo\""); + await Page.GetByRole(AriaRole.Button, new() { Name = "redo" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Textbox, new() { Name = "Cuesheet artist" })).ToHaveValueAsync("DJFreezeT"); + await Expect(Page.GetByRole(AriaRole.Textbox, new() { Name = "Cuesheet title" })).ToHaveValueAsync("Rabbit Hole Mix"); + await Expect(Page.GetByRole(AriaRole.Textbox, new() { Name = "Cataloguenumber" })).ToHaveValueAsync("01 23456 78912 3"); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Einmusik Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Missing Path (Original Mix)" })).ToBeVisibleAsync(); + } + + [TestMethod] + public async Task ImportTestBug54Async() + { + await Page.GotoAsync("http://localhost:5132/"); + await Page.GetByText("Import view").ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync("Textimport-Bug-#54.txt"); + await Page.GetByText("Textfile (common data in").ClickAsync(); + await Page.GetByText("Textfile (just track data)").ClickAsync(); + await Expect(Page.GetByLabel("Import view")).ToMatchAriaSnapshotAsync("- table:\n - rowgroup:\n - row \"# Sort Column options Artist Sort Column options Title Sort Column options Begin Sort Column options End Sort Column options Length Sort Column options\":\n - columnheader:\n - checkbox\n - columnheader \"# Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Artist Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Title Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Begin Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"End Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Length Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - rowgroup:\n - row /Increment Decrement Adriatique Clear X\\. Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"1\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Adriatique Clear\":\n - textbox: Adriatique\n - button \"Clear\"\n - button\n - cell \"X. Clear\":\n - textbox: X.\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Third Harmony Clear Fears And Dreams \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"2\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Third Harmony Clear\":\n - textbox: Third Harmony\n - button \"Clear\"\n - button\n - cell \"Fears And Dreams (Original Mix) Clear\":\n - textbox: Fears And Dreams (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Dele Sosimi Afrobeat Orchestra Clear Too Much Information \\(Laolu Remix; Edit\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"3\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Dele Sosimi Afrobeat Orchestra Clear\":\n - textbox: Dele Sosimi Afrobeat Orchestra\n - button \"Clear\"\n - button\n - cell \"Too Much Information (Laolu Remix; Edit) Clear\":\n - textbox: Too Much Information (Laolu Remix; Edit)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Edem, Govan Clear Ankh \\(Onetwo MX Remix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"4\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Edem, Govan Clear\":\n - textbox: Edem, Govan\n - button \"Clear\"\n - button\n - cell \"Ankh (Onetwo MX Remix) Clear\":\n - textbox: Ankh (Onetwo MX Remix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Jody Wisternoff Clear For All Time \\(feat\\. Hendrik Burkhard\\) \\(Extended Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"5\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Jody Wisternoff Clear\":\n - textbox: Jody Wisternoff\n - button \"Clear\"\n - button\n - cell \"For All Time (feat. Hendrik Burkhard) (Extended Mix) Clear\":\n - textbox: For All Time (feat. Hendrik Burkhard) (Extended Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Einmusik Clear Bead \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"6\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Einmusik Clear\":\n - textbox: Einmusik\n - button \"Clear\"\n - button\n - cell \"Bead (Original Mix) Clear\":\n - textbox: Bead (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Sebastien Leger Clear La Danse du Scorpion Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"7\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Sebastien Leger Clear\":\n - textbox: Sebastien Leger\n - button \"Clear\"\n - button\n - cell \"La Danse du Scorpion Clear\":\n - textbox: La Danse du Scorpion\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Paul Thomas & Solid Stone Clear La Bombo \\(Solid Stone Remix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"8\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Paul Thomas & Solid Stone Clear\":\n - textbox: Paul Thomas & Solid Stone\n - button \"Clear\"\n - button\n - cell \"La Bombo (Solid Stone Remix) Clear\":\n - textbox: La Bombo (Solid Stone Remix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement GusGus Clear Crossfade \\(Maceo Plex Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"9\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"GusGus Clear\":\n - textbox: GusGus\n - button \"Clear\"\n - button\n - cell \"Crossfade (Maceo Plex Mix) Clear\":\n - textbox: Crossfade (Maceo Plex Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Klangkarussell Clear Time \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Klangkarussell Clear\":\n - textbox: Klangkarussell\n - button \"Clear\"\n - button\n - cell \"Time (Original Mix) Clear\":\n - textbox: Time (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Anysense & Un:said Clear Missing Path \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Anysense & Un:said Clear\":\n - textbox: Anysense & Un:said\n - button \"Clear\"\n - button\n - cell \"Missing Path (Original Mix) Clear\":\n - textbox: Missing Path (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Space Food Clear Bombay Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Space Food Clear\":\n - textbox: Space Food\n - button \"Clear\"\n - button\n - cell \"Bombay Clear\":\n - textbox: Bombay\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement SHDW & Obscure Shape Clear Wächter der Nacht \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"SHDW & Obscure Shape Clear\":\n - textbox: SHDW & Obscure Shape\n - button \"Clear\"\n - button\n - cell \"Wächter der Nacht (Original Mix) Clear\":\n - textbox: Wächter der Nacht (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement HOSH Clear Karma Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"HOSH Clear\":\n - textbox: HOSH\n - button \"Clear\"\n - button\n - cell \"Karma Clear\":\n - textbox: Karma\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Alexey Union Clear Olympia \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Alexey Union Clear\":\n - textbox: Alexey Union\n - button \"Clear\"\n - button\n - cell \"Olympia (Original Mix) Clear\":\n - textbox: Olympia (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Paul Taylor Clear Afterglow Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Paul Taylor Clear\":\n - textbox: Paul Taylor\n - button \"Clear\"\n - button\n - cell \"Afterglow Clear\":\n - textbox: Afterglow\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Philter Clear Stranger Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Philter Clear\":\n - textbox: Philter\n - button \"Clear\"\n - button\n - cell \"Stranger Clear\":\n - textbox: Stranger\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Skizologic Clear Hypersphere \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Skizologic Clear\":\n - textbox: Skizologic\n - button \"Clear\"\n - button\n - cell \"Hypersphere (Original Mix) Clear\":\n - textbox: Hypersphere (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Thomas Schumacher, Caitlin Clear All of You \\(Remix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Thomas Schumacher, Caitlin Clear\":\n - textbox: Thomas Schumacher, Caitlin\n - button \"Clear\"\n - button\n - cell \"All of You (Remix) Clear\":\n - textbox: All of You (Remix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement A\\. Skomoroh Clear White Horse Conquest \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"A. Skomoroh Clear\":\n - textbox: A. Skomoroh\n - button \"Clear\"\n - button\n - cell \"White Horse Conquest (Original Mix) Clear\":\n - textbox: White Horse Conquest (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Patrik Berg Clear Bright \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Patrik Berg Clear\":\n - textbox: Patrik Berg\n - button \"Clear\"\n - button\n - cell \"Bright (Original Mix) Clear\":\n - textbox: Bright (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Hidden Empire Clear Bengal Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Hidden Empire Clear\":\n - textbox: Hidden Empire\n - button \"Clear\"\n - button\n - cell \"Bengal Clear\":\n - textbox: Bengal\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Mario Ochoa Clear Levitate Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Mario Ochoa Clear\":\n - textbox: Mario Ochoa\n - button \"Clear\"\n - button\n - cell \"Levitate Clear\":\n - textbox: Levitate\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Raul Facio Clear Eyes Wide Shut \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Raul Facio Clear\":\n - textbox: Raul Facio\n - button \"Clear\"\n - button\n - cell \"Eyes Wide Shut (Original Mix) Clear\":\n - textbox: Eyes Wide Shut (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Soolver Clear Regular \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Soolver Clear\":\n - textbox: Soolver\n - button \"Clear\"\n - button\n - cell \"Regular (Original Mix) Clear\":\n - textbox: Regular (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Weska Clear EQ64 \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Weska Clear\":\n - textbox: Weska\n - button \"Clear\"\n - button\n - cell \"EQ64 (Original Mix) Clear\":\n - textbox: EQ64 (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Tempo Giusto Clear The Fall \\(Extended Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Tempo Giusto Clear\":\n - textbox: Tempo Giusto\n - button \"Clear\"\n - button\n - cell \"The Fall (Extended Mix) Clear\":\n - textbox: The Fall (Extended Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Vlind & Asteroid & Gary Leroy Clear Trinity \\(Extended Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Vlind & Asteroid & Gary Leroy Clear\":\n - textbox: Vlind & Asteroid & Gary Leroy\n - button \"Clear\"\n - button\n - cell \"Trinity (Extended Mix) Clear\":\n - textbox: Trinity (Extended Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Astral Legacy Clear Vaveyla \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Astral Legacy Clear\":\n - textbox: Astral Legacy\n - button \"Clear\"\n - button\n - cell \"Vaveyla (Original Mix) Clear\":\n - textbox: Vaveyla (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Gerrox Clear Chakra \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Gerrox Clear\":\n - textbox: Gerrox\n - button \"Clear\"\n - button\n - cell \"Chakra (Original Mix) Clear\":\n - textbox: Chakra (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Charlotte De Witte Clear Pattern Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Charlotte De Witte Clear\":\n - textbox: Charlotte De Witte\n - button \"Clear\"\n - button\n - cell \"Pattern Clear\":\n - textbox: Pattern\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Space Food Clear Amabey Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Space Food Clear\":\n - textbox: Space Food\n - button \"Clear\"\n - button\n - cell \"Amabey Clear\":\n - textbox: Amabey\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement ARTBAT Clear Papilion \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"ARTBAT Clear\":\n - textbox: ARTBAT\n - button \"Clear\"\n - button\n - cell \"Papilion (Original Mix) Clear\":\n - textbox: Papilion (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement PETER PAHN Clear Enjoy Infinity \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"PETER PAHN Clear\":\n - textbox: PETER PAHN\n - button \"Clear\"\n - button\n - cell \"Enjoy Infinity (Original Mix) Clear\":\n - textbox: Enjoy Infinity (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Solitek Clear Instinct \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Solitek Clear\":\n - textbox: Solitek\n - button \"Clear\"\n - button\n - cell \"Instinct (Original Mix) Clear\":\n - textbox: Instinct (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Veerus Clear Heavy Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Veerus Clear\":\n - textbox: Veerus\n - button \"Clear\"\n - button\n - cell \"Heavy Clear\":\n - textbox: Heavy\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Secret Cinema & Reinier Zonneveld Clear Pain Thing \\(Original Mix\\) Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Secret Cinema & Reinier Zonneveld Clear\":\n - textbox: Secret Cinema & Reinier Zonneveld\n - button \"Clear\"\n - button\n - cell \"Pain Thing (Original Mix) Clear\":\n - textbox: Pain Thing (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Amelie Lens Clear Hypnotized Clear \\d+:\\d+:\\d+ \\d+:\\d+:\\d+ \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Amelie Lens Clear\":\n - textbox: Amelie Lens\n - button \"Clear\"\n - button\n - cell \"Hypnotized Clear\":\n - textbox: Hypnotized\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - row /Increment Decrement Nikolay Kirov Clear Chasing the Sun \\(Original Mix\\) Clear \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: /\\d+/\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Nikolay Kirov Clear\":\n - textbox: Nikolay Kirov\n - button \"Clear\"\n - button\n - cell \"Chasing the Sun (Original Mix) Clear\":\n - textbox: Chasing the Sun (Original Mix)\n - button \"Clear\"\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell:\n - textbox\n - cell:\n - textbox\n - rowgroup:\n - row"); + } + + [TestMethod] + public async Task ImportTestSampleInputfile2Async() + { + await Page.GotoAsync("http://localhost:5132/"); + await Page.GetByText("Import view").ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync("Sample_Inputfile2.txt"); + await Page.Locator("div").Filter(new() { HasTextRegex = new Regex("^Scheme common data$") }).GetByLabel("Clear").ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Textbox, new() { Name = "Cuesheet artist" })).ToBeEmptyAsync(); + await Expect(Page.GetByRole(AriaRole.Textbox, new() { Name = "Cuesheet title" })).ToBeEmptyAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Sample Title 1 Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":09:23" }).Nth(1)).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "01:15:" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Sample Artist 8 Clear" })).ToBeVisibleAsync(); + } + + [TestMethod] + public async Task ImportTestTraktorAsync() + { + await Page.GotoAsync("http://localhost:5132/"); + await Page.GetByText("Import view").ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync(new[] { "Traktor Export.html" }); + await Page.GetByText("Textfile (common data in").ClickAsync(); + await Page.GetByText("Traktor history").ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Arba Han Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":48:53" }).Nth(1)).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Acid Rain Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":48:40" }).Nth(1)).ToBeVisibleAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Complete" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Space Yoda (Snyl Remix) Clear" })).ToBeVisibleAsync(); + } + + [TestMethod] + public async Task EditTrackModalUndoRedoTestAsync() + { + await Page.GotoAsync("http://localhost:5132/"); + await Page.GetByRole(AriaRole.Group).Filter(new() { HasText = "Edit selected tracks Copy" }).GetByRole(AriaRole.Button).First.ClickAsync(); + await Page.GetByRole(AriaRole.Row, new() { Name = "Increment Decrement 00:00:" }).GetByLabel("", new() { Exact = true }).CheckAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Edit selected tracks" }).ClickAsync(); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Artist", Exact = true }).FillAsync("Test Track Artist 1"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Artist", Exact = true }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Title", Exact = true }).FillAsync("Test Track Title 1"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "Title", Exact = true }).PressAsync("Tab"); + await Page.GetByRole(AriaRole.Textbox, new() { Name = "End" }).FillAsync("00:02:23"); + await Page.GetByRole(AriaRole.Button, new() { Name = "channel audio (4CH)" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Serial copy management system" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "Save changes" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Test Track Artist 1 Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Test Track Title 1 Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":02:23" }).First).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":02:23" }).Nth(1)).ToBeVisibleAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "undo" }).ClickAsync(); + await Expect(Page.Locator("td:nth-child(3)")).ToBeVisibleAsync(); + await Expect(Page.Locator("td:nth-child(4)")).ToBeVisibleAsync(); + await Expect(Page.Locator("td:nth-child(6)")).ToBeVisibleAsync(); + await Expect(Page.Locator("td:nth-child(7)")).ToBeVisibleAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "redo" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Test Track Artist 1 Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = "Test Track Title 1 Clear" })).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":02:23" }).First).ToBeVisibleAsync(); + await Expect(Page.GetByRole(AriaRole.Cell, new() { Name = ":02:23" }).Nth(1)).ToBeVisibleAsync(); + } + + [TestMethod] + public async Task UndoRedoTrackTableTestAsync() + { + await Page.GotoAsync("http://localhost:5132/"); + await Page.GetByRole(AriaRole.Group).Filter(new() { HasText = "Edit selected tracks Copy" }).GetByRole(AriaRole.Button).First.ClickAsync(); + await Page.Locator("td:nth-child(3)").ClickAsync(); + await Page.GetByRole(AriaRole.Row, new() { Name = "Increment Decrement 00:00:" }).GetByRole(AriaRole.Textbox).First.FillAsync("Test Artist 1"); + await Page.Locator(".mud-overlay").ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Button, new() { Name = "undo" })).ToBeEnabledAsync(); + await Page.Locator("td:nth-child(4)").ClickAsync(); + await Page.GetByRole(AriaRole.Row, new() { Name = "Increment Decrement Test" }).GetByRole(AriaRole.Textbox).Nth(1).FillAsync("Test Title 1"); + await Page.Locator(".mud-overlay").ClickAsync(); + await Page.GetByRole(AriaRole.Heading, new() { Name = "Playback" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "undo" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "undo" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Table)).ToMatchAriaSnapshotAsync("- cell:\n - textbox\n - button"); + await Expect(Page.GetByRole(AriaRole.Table)).ToMatchAriaSnapshotAsync("- cell:\n - textbox\n - button"); + await Page.GetByRole(AriaRole.Button, new() { Name = "redo" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "redo" }).ClickAsync(); + await Expect(Page.GetByRole(AriaRole.Table)).ToMatchAriaSnapshotAsync("- cell \"Test Artist 1 Clear\":\n - textbox: Test Artist 1\n - button \"Clear\"\n - button"); + await Expect(Page.GetByRole(AriaRole.Table)).ToMatchAriaSnapshotAsync("- cell \"Test Title 1 Clear\":\n - textbox: Test Title 1\n - button \"Clear\"\n - button"); + await Page.GetByRole(AriaRole.Cell, new() { Name = "Test Title 1 Clear" }).GetByLabel("Clear").ClickAsync(); + await Page.Locator("td:nth-child(3)").ClickAsync(); + await Page.Locator(".mud-overlay").ClickAsync(); + await Page.GetByRole(AriaRole.Cell, new() { Name = "Test Artist 1 Clear" }).GetByRole(AriaRole.Textbox).First.FillAsync("Mozart"); + await Page.Locator(".mud-overlay").ClickAsync(); + await Page.Locator("td:nth-child(4)").ClickAsync(); + await Page.GetByRole(AriaRole.Row, new() { Name = "Increment Decrement Mozart" }).GetByRole(AriaRole.Textbox).Nth(1).FillAsync("Eine kleine Nachtmusik"); + await Page.GetByText("Eine kleine Nachtmusik", new() { Exact = true }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "undo" }).ClickAsync(); + await Page.GetByRole(AriaRole.Button, new() { Name = "undo" }).ClickAsync(); + await Expect(Page.Locator("#app")).ToMatchAriaSnapshotAsync("- group:\n - button\n - button \"Edit selected tracks\" [disabled]\n - button \"Copy selected tracks\" [disabled]\n - button \"Delete selected tracks\" [disabled]\n - button \"Delete all tracks\"\n- group:\n - button [disabled]\n - button [disabled]\n- button\n- table:\n - rowgroup:\n - row \"# Sort Column options Artist Sort Column options Title Sort Column options Begin Sort Column options End Sort Column options Length Sort Column options Status\":\n - columnheader:\n - checkbox\n - columnheader \"# Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Artist Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Title Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Begin Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"End Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Length Sort Column options\":\n - button \"Sort\"\n - button \"Column options\"\n - columnheader \"Status\"\n - rowgroup:\n - row /Increment Decrement Test Artist 1 Clear \\d+:\\d+:\\d+/:\n - cell:\n - checkbox\n - cell \"Increment Decrement\":\n - spinbutton: \"1\"\n - button \"Increment\"\n - button \"Decrement\"\n - cell \"Test Artist 1 Clear\":\n - textbox: Test Artist 1\n - button \"Clear\"\n - button\n - cell:\n - textbox\n - button\n - cell /\\d+:\\d+:\\d+/:\n - textbox: /\\d+:\\d+:\\d+/\n - cell:\n - textbox\n - cell:\n - textbox\n - cell\n - rowgroup:\n - row"); + } } } diff --git a/AudioCuesheetEditor.Tests/Sample_Inputfile2.txt b/AudioCuesheetEditor.End2EndTests/Sample_Inputfile2.txt similarity index 100% rename from AudioCuesheetEditor.Tests/Sample_Inputfile2.txt rename to AudioCuesheetEditor.End2EndTests/Sample_Inputfile2.txt diff --git a/AudioCuesheetEditor.End2EndTests/Textimport with Cuesheetdata.txt b/AudioCuesheetEditor.End2EndTests/Textimport with Cuesheetdata.txt new file mode 100644 index 00000000..f476fd54 --- /dev/null +++ b/AudioCuesheetEditor.End2EndTests/Textimport with Cuesheetdata.txt @@ -0,0 +1,40 @@ +DJFreezeT - Rabbit Hole Mix - 0123456789123 +Adriatique - X. 00:05:24.250 +Third Harmony - Fears And Dreams (Original Mix) 00:10:39 +Dele Sosimi Afrobeat Orchestra - Too Much Information (Laolu Remix; Edit) 00:17:06 +Edem, Govan - Ankh (Onetwo MX Remix) 00:23:21 +Jody Wisternoff - For All Time (feat. Hendrik Burkhard) (Extended Mix) 00:29:02 +Einmusik - Bead (Original Mix) 00:34:27 +Sebastien Leger - La Danse du Scorpion 00:40:59 +Paul Thomas & Solid Stone - La Bombo (Solid Stone Remix) 00:46:19 +GusGus - Crossfade (Maceo Plex Mix) 00:52:20 +Klangkarussell - Time (Original Mix) 00:56:19 +Anysense & Un:said - Missing Path (Original Mix) 01:01:41 +Space Food - Bombay 01:06:33 +SHDW & Obscure Shape - Wächter der Nacht (Original Mix) 01:11:04 +HOSH - Karma 01:15:28 +Alexey Union - Olympia (Original Mix) 01:21:08 +Paul Taylor - Afterglow 01:25:38 +Philter - Stranger 01:31:52 +Skizologic - Hypersphere (Original Mix) 01:36:40 +Thomas Schumacher, Caitlin - All of You (Remix) 01:42:16 +A. Skomoroh - White Horse Conquest (Original Mix) 01:47:04 +Patrik Berg - Bright (Original Mix) 01:52:37 +Hidden Empire - Bengal 01:58:05 +Mario Ochoa - Levitate 02:03:00 +Raul Facio - Eyes Wide Shut (Original Mix) 02:08:21 +Soolver - Regular (Original Mix) 02:14:31 +Weska - EQ64 (Original Mix) 02:18:35 +Tempo Giusto - The Fall (Extended Mix) 02:24:12 +Vlind & Asteroid & Gary Leroy - Trinity (Extended Mix) 02:29:38 +Astral Legacy - Vaveyla (Original Mix) 02:32:52 +Gerrox - Chakra (Original Mix) 02:37:00 +Charlotte De Witte - Pattern 02:41:55 +Space Food - Amabey 02:46:55 +ARTBAT - Papilion (Original Mix) 02:51:13 +PETER PAHN - Enjoy Infinity (Original Mix) 02:56:08 +Solitek - Instinct (Original Mix) 03:00:57 +Veerus - Heavy 03:05:19 +Secret Cinema & Reinier Zonneveld - Pain Thing (Original Mix) 03:09:38 +Amelie Lens - Hypnotized 03:13:13 +Nikolay Kirov - Chasing the Sun (Original Mix) diff --git a/AudioCuesheetEditor.End2EndTests/Textimport-Bug-#54.txt b/AudioCuesheetEditor.End2EndTests/Textimport-Bug-#54.txt new file mode 100644 index 00000000..9cda0a9e --- /dev/null +++ b/AudioCuesheetEditor.End2EndTests/Textimport-Bug-#54.txt @@ -0,0 +1,40 @@ + +Adriatique - X. 00:05:24 +Third Harmony - Fears And Dreams (Original Mix) 00:10:39 +Dele Sosimi Afrobeat Orchestra - Too Much Information (Laolu Remix; Edit) 00:17:06 +Edem, Govan - Ankh (Onetwo MX Remix) 00:23:21 +Jody Wisternoff - For All Time (feat. Hendrik Burkhard) (Extended Mix) 00:29:02 +Einmusik - Bead (Original Mix) 00:34:27 +Sebastien Leger - La Danse du Scorpion 00:40:59 +Paul Thomas & Solid Stone - La Bombo (Solid Stone Remix) 00:46:19 +GusGus - Crossfade (Maceo Plex Mix) 00:52:20 +Klangkarussell - Time (Original Mix) 00:56:19 +Anysense & Un:said - Missing Path (Original Mix) 01:01:41 +Space Food - Bombay 01:06:33 +SHDW & Obscure Shape - Wächter der Nacht (Original Mix) 01:11:04 +HOSH - Karma 01:15:28 +Alexey Union - Olympia (Original Mix) 01:21:08 +Paul Taylor - Afterglow 01:25:38 +Philter - Stranger 01:31:52 +Skizologic - Hypersphere (Original Mix) 01:36:40 +Thomas Schumacher, Caitlin - All of You (Remix) 01:42:16 +A. Skomoroh - White Horse Conquest (Original Mix) 01:47:04 +Patrik Berg - Bright (Original Mix) 01:52:37 +Hidden Empire - Bengal 01:58:05 +Mario Ochoa - Levitate 02:03:00 +Raul Facio - Eyes Wide Shut (Original Mix) 02:08:21 +Soolver - Regular (Original Mix) 02:14:31 +Weska - EQ64 (Original Mix) 02:18:35 +Tempo Giusto - The Fall (Extended Mix) 02:24:12 +Vlind & Asteroid & Gary Leroy - Trinity (Extended Mix) 02:29:38 +Astral Legacy - Vaveyla (Original Mix) 02:32:52 +Gerrox - Chakra (Original Mix) 02:37:00 +Charlotte De Witte - Pattern 02:41:55 +Space Food - Amabey 02:46:55 +ARTBAT - Papilion (Original Mix) 02:51:13 +PETER PAHN - Enjoy Infinity (Original Mix) 02:56:08 +Solitek - Instinct (Original Mix) 03:00:57 +Veerus - Heavy 03:05:19 +Secret Cinema & Reinier Zonneveld - Pain Thing (Original Mix) 03:09:38 +Amelie Lens - Hypnotized 03:13:13 +Nikolay Kirov - Chasing the Sun (Original Mix) diff --git a/AudioCuesheetEditor.End2EndTests/Traktor Export.html b/AudioCuesheetEditor.End2EndTests/Traktor Export.html new file mode 100644 index 00000000..bdc497c7 --- /dev/null +++ b/AudioCuesheetEditor.End2EndTests/Traktor Export.html @@ -0,0 +1,306 @@ + + + + + + + + Track List HISTORY + + + + +

Track List: HISTORY

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Num.ArtistTitleStart Time
1NachapGlass2025/1/29 18:52:10
2Progressive + Melodic DECK2025/1/29 19:04:06
3Nomer 21Depersonalization2025/1/29 19:05:41
4SevenEver, Nopopstar, 2JOHN'S & Eugene JayShowing Off2025/1/29 19:11:24
5Carlo WhaleUnconscious2025/1/29 19:16:12
6Arba HanTimelaps2025/1/29 19:21:47
7SavillEnergy Surrounds2025/1/29 19:25:23
8TeklixThe Tribal Code2025/1/29 19:32:40
9NeuralisI'm Looking for Answers2025/1/29 19:41:03
10Nopopstar & ArsiaDirty Moves2025/1/29 19:46:44
11SevenEver, Nopopstar, 2JOHN'S & Eugene JayLost (Maze 28 Remix)2025/1/29 19:52:34
12SevenEver, Nopopstar, 2JOHN'S & Eugene JayLost (Redspace Remix)2025/1/29 19:57:27
13Gadoz5d2025/1/29 20:00:32
14DJ DanzikOut of Space2025/1/29 20:08:44
15Enis ÇobanInternet2025/1/29 20:11:32
16Cold Mind & Alex YikkerRage2025/1/29 20:17:24
17Maze 28Sol (JAHAYA Remix)2025/1/29 20:23:43
18Alex GraftonHi Baby2025/1/29 20:25:01
19Che&Mos & Halo FarDaddy2025/1/29 20:34:23
20K KARDENAcid Rain2025/1/29 20:37:32
21Dobrov & Gar1ssonAnalogic (Redspace Remix)2025/1/29 20:40:50
22GazfluzVargan2025/1/29 20:48:50
23SHKAPOVControl2025/1/29 20:52:49
24QazaQOn the Line2025/1/29 20:58:57
25Alex SchaufelElizabeth (Larsun Hesh Remix)2025/1/29 21:01:19
26OiroJust Business2025/1/29 21:06:28
27MolexMind Split (Redspace Remix)2025/1/29 21:11:32
28SOLIGive Me Your Mind2025/1/29 21:19:58
29MANDUJacky2025/1/29 21:25:58
30SOLISpacetoon2025/1/29 21:31:15
31Sasha FashionMoqton2025/1/29 21:33:41
32NAASAPoison2025/1/29 21:42:28
33Nopopstar, 2JOHN'S & Eugene JayNightlong2025/1/29 21:48:55
34Skillz jayChoir2025/1/29 21:51:24
35KovaxController2025/1/29 21:57:59
36MumboiJust a Beat2025/1/29 22:00:41
37EcleptSprut2025/1/29 22:07:28
38RudenskyDark Escort2025/1/29 22:12:45
39Alexey Union, Kinky Sound & KOCHETOVConnected2025/1/29 22:17:12
40ANMA (MD)Space Yoda (Snyl Remix)2025/1/29 22:24:40
41InacheAndale (MONTA (TN) Remix)2025/1/29 22:30:03
+ + + diff --git a/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj b/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj index 4ebd0bab..59451bb3 100644 --- a/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj +++ b/AudioCuesheetEditor.Tests/AudioCuesheetEditor.Tests.csproj @@ -7,10 +7,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,13 +36,4 @@ - - - PreserveNewest - - - PreserveNewest - - - diff --git a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs index 7b6f0454..1849ce87 100644 --- a/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs +++ b/AudioCuesheetEditor.Tests/Model/AudioCuesheet/CuesheetTests.cs @@ -71,90 +71,6 @@ public void EmptyCuesheetTracksValidationTest() Assert.AreEqual(ValidationStatus.Success, validationErrorTracks.Status); } - [TestMethod()] - public void ImportTest() - { - // Arrange - var fileContent = new List - { - "CuesheetArtist - CuesheetTitle c:\\tmp\\Testfile.mp3", - "Sample Artist 1 - Sample Title 1 00:05:00", - "Sample Artist 2 - Sample Title 2 00:09:23", - "Sample Artist 3 - Sample Title 3 00:15:54", - "Sample Artist 4 - Sample Title 4 00:20:13", - "Sample Artist 5 - Sample Title 5 00:24:54", - "Sample Artist 6 - Sample Title 6 00:31:54", - "Sample Artist 7 - Sample Title 7 00:45:54", - "Sample Artist 8 - Sample Title 8 01:15:54" - }; - - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var localStorageOptionsProviderMock = new Mock(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions(); - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); - var fileInputManagerMock = new Mock(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - var testHelper = new TestHelper(); - // Act - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - - // Assert - Assert.IsNull(sessionStateContainer.ImportCuesheet?.CDTextfile); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(0).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(1).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(2).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(3).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(4).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(5).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(6).Validate().Status); - Assert.AreEqual(ValidationStatus.Success, sessionStateContainer.ImportCuesheet?.Tracks.ElementAt(7).Validate().Status); - } - - [TestMethod()] - public void ImportTestCalculateEndCorrectly() - { - // Arrange - var textImportMemoryStream = new MemoryStream(Resources.Textimport_Bug_54); - using var reader = new StreamReader(textImportMemoryStream); - List lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); - var testHelper = new TestHelper(); - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var localStorageOptionsProviderMock = new Mock(); - var options = new ApplicationOptions(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - options.ImportScheme = textImportScheme; - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); - var fileInputManagerMock = new Mock(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - var timeSpanFormat = new TimeSpanFormat(); - // Act - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - // Assert - Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); - Assert.IsNotNull(sessionStateContainer.ImportCuesheet); - Assert.AreEqual(39, sessionStateContainer.ImportCuesheet.Tracks.Count); - Assert.AreEqual(new TimeSpan(0, 5, 24), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(0).End); - Assert.AreEqual(new TimeSpan(3, 13, 13), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(38).Begin); - } - [TestMethod()] public void RecordTest() { @@ -426,68 +342,6 @@ public void TrackLengthChangedWithIsLinkedToPreivousTest() Assert.AreEqual(track1.End, track2.Begin); Assert.AreEqual(editedTrack.End, track2.Begin); } - [TestMethod()] - public void ImportSamplesTest() - { - // Arrange - var fileContent = File.ReadAllLines("Sample_Inputfile.txt"); - var testHelper = new TestHelper(); - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var localStorageOptionsProviderMock = new Mock(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions(); - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); - var fileInputManagerMock = new Mock(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - // Act - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - // Assert - Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); - Assert.IsNotNull(sessionStateContainer.ImportCuesheet); - Assert.AreEqual("CuesheetArtist", sessionStateContainer.ImportCuesheet.Artist); - Assert.AreEqual("CuesheetTitle", sessionStateContainer.ImportCuesheet.Title); - Assert.AreEqual(8, sessionStateContainer.ImportCuesheet.Tracks.Count); - Assert.AreEqual(new TimeSpan(1, 15, 54), sessionStateContainer.ImportCuesheet.Tracks.Last().End); - } - - [TestMethod()] - public void ImportSamples2Test() - { - // Arrange - var fileContent = File.ReadAllLines("Sample_Inputfile2.txt"); - var testHelper = new TestHelper(); - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var localStorageOptionsProviderMock = new Mock(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions - { - ImportScheme = textImportScheme - }; - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); - var fileInputManagerMock = new Mock(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - // Act - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - // Assert - Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); - Assert.IsNotNull(sessionStateContainer.ImportCuesheet); - Assert.IsNull(sessionStateContainer.ImportCuesheet.Artist); - Assert.IsNull(sessionStateContainer.ImportCuesheet.Title); - Assert.AreEqual(8, sessionStateContainer.ImportCuesheet.Tracks.Count); - Assert.AreEqual(new TimeSpan(1, 15, 54), sessionStateContainer.ImportCuesheet.Tracks.Last().End); - } [TestMethod()] public void ValidateTest() diff --git a/AudioCuesheetEditor.Tests/Model/IO/Import/ImportprofileTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Import/ImportprofileTests.cs new file mode 100644 index 00000000..b25e9756 --- /dev/null +++ b/AudioCuesheetEditor.Tests/Model/IO/Import/ImportprofileTests.cs @@ -0,0 +1,47 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//. + +using AudioCuesheetEditor.Model.Entity; +using AudioCuesheetEditor.Model.IO.Import; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Linq; + +namespace AudioCuesheetEditor.Tests.Model.IO.Import +{ + [TestClass()] + public class ImportprofileTests + { + [TestMethod()] + public void Validate_WithoutPlaceholder_ReturnsInvalid() + { + // Arrange + var importprofile = new Importprofile() + { + SchemeCuesheet = "Test 1", + SchemeTracks = "Here comes next track" + }; + // Act + var result = importprofile.Validate(); + // Assert + Assert.AreEqual(ValidationStatus.Error, result.Status); + Assert.AreEqual(2, result.ValidationMessages.Count); + Assert.AreEqual("{0} contains no placeholder!", result.ValidationMessages.First().Message); + Assert.AreEqual(nameof(Importprofile.SchemeCuesheet), result.ValidationMessages.First().Parameter?.First().ToString()); + Assert.AreEqual("{0} contains no placeholder!", result.ValidationMessages.Last().Message); + Assert.AreEqual(nameof(Importprofile.SchemeTracks), result.ValidationMessages.Last().Parameter?.First().ToString()); + } + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs b/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs deleted file mode 100644 index 17be72f0..00000000 --- a/AudioCuesheetEditor.Tests/Model/IO/Import/TextImportSchemeTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -//This file is part of AudioCuesheetEditor. - -//AudioCuesheetEditor is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. - -//AudioCuesheetEditor is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. - -//You should have received a copy of the GNU General Public License -//along with Foobar. If not, see -//. -using AudioCuesheetEditor.Model.Entity; -using AudioCuesheetEditor.Model.IO.Import; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Linq; - -namespace AudioCuesheetEditor.Tests.Model.IO.Import -{ - [TestClass()] - public class TextImportSchemeTests - { - [TestMethod] - public void Validate_SchemeCuesheet_WithValidPlaceholders_ShouldReturnSuccess() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeCuesheet = "(?'Artist'.+) - (?'Title'.+)\\t+(?'Audiofile'.+)" - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); - - // Assert - Assert.AreEqual(ValidationStatus.Success, result.Status); - Assert.AreEqual(0, result.ValidationMessages.Count); - } - - [TestMethod] - public void Validate_SchemeCuesheet_WithSimplePlaceholders_ShouldReturnSuccess() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeCuesheet = "Artist - Title" - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); - - // Assert - Assert.AreEqual(ValidationStatus.Success, result.Status); - Assert.AreEqual(0, result.ValidationMessages.Count); - } - - [TestMethod] - public void Validate_SchemeCuesheet_WithoutPlaceholders_ShouldReturnError() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeCuesheet = "InvalidPattern" - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); - - // Assert - Assert.AreEqual(ValidationStatus.Error, result.Status); - Assert.AreEqual(1, result.ValidationMessages.Count); - var message = result.ValidationMessages.Single(); - Assert.AreEqual("{0} contains no placeholder!", message.Message); - Assert.AreEqual(nameof(TextImportScheme.SchemeCuesheet), message.Parameter?.FirstOrDefault()); - } - - [TestMethod] - public void Validate_SchemeTracks_WithValidPlaceholders_ShouldReturnSuccess() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeTracks = "(?'Artist'.+) - (?'Title'.+)(?:...\\t)(?'End'.+)" - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); - - // Assert - Assert.AreEqual(ValidationStatus.Success, result.Status); - Assert.AreEqual(0, result.ValidationMessages.Count); - } - - [TestMethod] - public void Validate_SchemeTracks_WithSimplePlaceholders_ShouldReturnSuccess() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeTracks = "Artist - Title\tEnd" - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); - - // Assert - Assert.AreEqual(ValidationStatus.Success, result.Status); - Assert.AreEqual(0, result.ValidationMessages.Count); - } - - [TestMethod] - public void Validate_SchemeTracks_WithoutPlaceholders_ShouldReturnError() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeTracks = "InvalidPattern" - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); - - // Assert - Assert.AreEqual(ValidationStatus.Error, result.Status); - Assert.AreEqual(1, result.ValidationMessages.Count); - var message = result.ValidationMessages.Single(); - Assert.AreEqual("{0} contains no placeholder!", message.Message); - Assert.AreEqual(nameof(TextImportScheme.SchemeTracks), message.Parameter?.FirstOrDefault()); - } - - [TestMethod] - public void Validate_SchemeCuesheetEmpty_ShouldReturnSuccess() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeCuesheet = string.Empty - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeCuesheet)); - - // Assert - Assert.AreEqual(ValidationStatus.Success, result.Status); - Assert.AreEqual(0, result.ValidationMessages.Count); - } - - [TestMethod] - public void Validate_SchemeTrackstEmpty_ShouldReturnSuccess() - { - // Arrange - var scheme = new TextImportScheme - { - SchemeTracks = string.Empty - }; - - // Act - var result = scheme.Validate(nameof(TextImportScheme.SchemeTracks)); - - // Assert - Assert.AreEqual(ValidationStatus.Success, result.Status); - Assert.AreEqual(0, result.ValidationMessages.Count); - } - } -} diff --git a/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs b/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs index 850c6c56..847e2528 100644 --- a/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs +++ b/AudioCuesheetEditor.Tests/Model/IO/ProjectfileTests.cs @@ -131,7 +131,7 @@ public void GenerateFile_WithSections_GeneratesOneFile() public void ImportFile_ValidProjectfile_ShouldImportFile() { // Arrange - var fileContent = Encoding.UTF8.GetBytes("{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Cataloguenumber\":\"A123\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"}}"); + var fileContent = "{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Cataloguenumber\":\"A123\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"}}"; // Act var cuesheet = Projectfile.ImportFile(fileContent); // Assert @@ -156,7 +156,7 @@ public void ImportFile_ValidProjectfile_ShouldImportFile() public void ImportFile_ValidProjectfileWithSections_ShouldImportFile() { //Arrange - var fileContent = Encoding.UTF8.GetBytes("{\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"},\"Cataloguenumber\":\"A123\",\"Sections\":[{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"00:30:00\"},{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"01:00:00\"}]}"); + var fileContent = "{\"Tracks\":[{\"Position\":1,\"Artist\":\"Artist 1\",\"Title\":\"Title 1\",\"Begin\":\"00:00:00\",\"End\":\"00:01:01\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":2,\"Artist\":\"Artist 2\",\"Title\":\"Title 2\",\"Begin\":\"00:01:01\",\"End\":\"00:03:03\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":3,\"Artist\":\"Artist 3\",\"Title\":\"Title 3\",\"Begin\":\"00:03:03\",\"End\":\"00:06:06\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":4,\"Artist\":\"Artist 4\",\"Title\":\"Title 4\",\"Begin\":\"00:06:06\",\"End\":\"00:10:10\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":5,\"Artist\":\"Artist 5\",\"Title\":\"Title 5\",\"Begin\":\"00:10:10\",\"End\":\"00:15:15\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":6,\"Artist\":\"Artist 6\",\"Title\":\"Title 6\",\"Begin\":\"00:15:15\",\"End\":\"00:21:21\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":7,\"Artist\":\"Artist 7\",\"Title\":\"Title 7\",\"Begin\":\"00:21:21\",\"End\":\"00:28:28\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":8,\"Artist\":\"Artist 8\",\"Title\":\"Title 8\",\"Begin\":\"00:28:28\",\"End\":\"00:36:36\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":9,\"Artist\":\"Artist 9\",\"Title\":\"Title 9\",\"Begin\":\"00:36:36\",\"End\":\"00:45:45\",\"Flags\":[\"4CH\"],\"IsLinkedToPreviousTrack\":true},{\"Position\":10,\"Artist\":\"Artist 10\",\"Title\":\"Title 10\",\"Begin\":\"00:45:45\",\"End\":\"00:55:55\",\"Flags\":[\"4CH\",\"DCP\"],\"IsLinkedToPreviousTrack\":true}],\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Audiofile\":{\"Name\":\"AudioFile.mp3\"},\"CDTextfile\":{\"Name\":\"CDTextfile.cdt\"},\"Cataloguenumber\":\"A123\",\"Sections\":[{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"00:30:00\"},{\"Artist\":\"CuesheetArtist\",\"Title\":\"CuesheetTitle\",\"Begin\":\"01:00:00\"}]}"; // Act var cuesheet = Projectfile.ImportFile(fileContent); // Assert diff --git a/AudioCuesheetEditor.Tests/Properties/Resources.Designer.cs b/AudioCuesheetEditor.Tests/Properties/Resources.Designer.cs index 20d2a794..c8637f77 100644 --- a/AudioCuesheetEditor.Tests/Properties/Resources.Designer.cs +++ b/AudioCuesheetEditor.Tests/Properties/Resources.Designer.cs @@ -90,6 +90,16 @@ internal static byte[] Playlist_Bug_57 { } } + /// + /// Sucht eine lokalisierte Ressource vom Typ System.Byte[]. + /// + internal static byte[] Sample_Inputfile { + get { + object obj = ResourceManager.GetObject("Sample_Inputfile", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Sucht eine lokalisierte Ressource vom Typ System.Byte[]. /// @@ -125,7 +135,17 @@ internal static byte[] Textimport_Bug_54 { /// internal static byte[] Textimport_with_Cuesheetdata { get { - object obj = ResourceManager.GetObject("Textimport_with_Cuesheetdata", resourceCulture); + object obj = ResourceManager.GetObject("Textimport with Cuesheetdata", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Sucht eine lokalisierte Ressource vom Typ System.Byte[]. + /// + internal static byte[] Traktor_Export { + get { + object obj = ResourceManager.GetObject("Traktor Export", resourceCulture); return ((byte[])(obj)); } } diff --git a/AudioCuesheetEditor.Tests/Properties/Resources.resx b/AudioCuesheetEditor.Tests/Properties/Resources.resx index 243026f0..0370cfee 100644 --- a/AudioCuesheetEditor.Tests/Properties/Resources.resx +++ b/AudioCuesheetEditor.Tests/Properties/Resources.resx @@ -136,7 +136,13 @@ ..\Resources\Textimport_Bug_#233.txt;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + ..\Resources\Traktor Export.html;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\Textimport with Cuesheetdata.txt;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\Sample_Inputfile.txt;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Sample_Inputfile.txt b/AudioCuesheetEditor.Tests/Resources/Sample_Inputfile.txt similarity index 100% rename from AudioCuesheetEditor.Tests/Sample_Inputfile.txt rename to AudioCuesheetEditor.Tests/Resources/Sample_Inputfile.txt diff --git a/AudioCuesheetEditor.Tests/Resources/Textimport with Cuesheetdata.txt b/AudioCuesheetEditor.Tests/Resources/Textimport with Cuesheetdata.txt index 754820c5..f476fd54 100644 --- a/AudioCuesheetEditor.Tests/Resources/Textimport with Cuesheetdata.txt +++ b/AudioCuesheetEditor.Tests/Resources/Textimport with Cuesheetdata.txt @@ -37,4 +37,4 @@ Solitek - Instinct (Original Mix) 03:00:57 Veerus - Heavy 03:05:19 Secret Cinema & Reinier Zonneveld - Pain Thing (Original Mix) 03:09:38 Amelie Lens - Hypnotized 03:13:13 -Nikolay Kirov - Chasing the Sun (Original Mix) +Nikolay Kirov - Chasing the Sun (Original Mix) diff --git a/AudioCuesheetEditor.Tests/Resources/Traktor Export.html b/AudioCuesheetEditor.Tests/Resources/Traktor Export.html new file mode 100644 index 00000000..bdc497c7 --- /dev/null +++ b/AudioCuesheetEditor.Tests/Resources/Traktor Export.html @@ -0,0 +1,306 @@ + + + + + + + + Track List HISTORY + + + + +

Track List: HISTORY

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Num.ArtistTitleStart Time
1NachapGlass2025/1/29 18:52:10
2Progressive + Melodic DECK2025/1/29 19:04:06
3Nomer 21Depersonalization2025/1/29 19:05:41
4SevenEver, Nopopstar, 2JOHN'S & Eugene JayShowing Off2025/1/29 19:11:24
5Carlo WhaleUnconscious2025/1/29 19:16:12
6Arba HanTimelaps2025/1/29 19:21:47
7SavillEnergy Surrounds2025/1/29 19:25:23
8TeklixThe Tribal Code2025/1/29 19:32:40
9NeuralisI'm Looking for Answers2025/1/29 19:41:03
10Nopopstar & ArsiaDirty Moves2025/1/29 19:46:44
11SevenEver, Nopopstar, 2JOHN'S & Eugene JayLost (Maze 28 Remix)2025/1/29 19:52:34
12SevenEver, Nopopstar, 2JOHN'S & Eugene JayLost (Redspace Remix)2025/1/29 19:57:27
13Gadoz5d2025/1/29 20:00:32
14DJ DanzikOut of Space2025/1/29 20:08:44
15Enis ÇobanInternet2025/1/29 20:11:32
16Cold Mind & Alex YikkerRage2025/1/29 20:17:24
17Maze 28Sol (JAHAYA Remix)2025/1/29 20:23:43
18Alex GraftonHi Baby2025/1/29 20:25:01
19Che&Mos & Halo FarDaddy2025/1/29 20:34:23
20K KARDENAcid Rain2025/1/29 20:37:32
21Dobrov & Gar1ssonAnalogic (Redspace Remix)2025/1/29 20:40:50
22GazfluzVargan2025/1/29 20:48:50
23SHKAPOVControl2025/1/29 20:52:49
24QazaQOn the Line2025/1/29 20:58:57
25Alex SchaufelElizabeth (Larsun Hesh Remix)2025/1/29 21:01:19
26OiroJust Business2025/1/29 21:06:28
27MolexMind Split (Redspace Remix)2025/1/29 21:11:32
28SOLIGive Me Your Mind2025/1/29 21:19:58
29MANDUJacky2025/1/29 21:25:58
30SOLISpacetoon2025/1/29 21:31:15
31Sasha FashionMoqton2025/1/29 21:33:41
32NAASAPoison2025/1/29 21:42:28
33Nopopstar, 2JOHN'S & Eugene JayNightlong2025/1/29 21:48:55
34Skillz jayChoir2025/1/29 21:51:24
35KovaxController2025/1/29 21:57:59
36MumboiJust a Beat2025/1/29 22:00:41
37EcleptSprut2025/1/29 22:07:28
38RudenskyDark Escort2025/1/29 22:12:45
39Alexey Union, Kinky Sound & KOCHETOVConnected2025/1/29 22:17:12
40ANMA (MD)Space Yoda (Snyl Remix)2025/1/29 22:24:40
41InacheAndale (MONTA (TN) Remix)2025/1/29 22:30:03
+ + + diff --git a/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs b/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs index 2b373da0..03f2ae32 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/CuesheetImportServiceTests.cs @@ -18,7 +18,6 @@ using AudioCuesheetEditor.Tests.Properties; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using System.Collections.Generic; using System.IO; using System.Linq; @@ -31,46 +30,43 @@ public class CuesheetImportServiceTests public void Analyse_WithSampleCuesheet_CreatesValidCuesheet() { // Arrange - var fileContent = new List - { - "PERFORMER \"Sample CD Artist\"", - "TITLE \"Sample CD Title\"", - "FILE \"AC DC - TNT.mp3\" MP3", - "CDTEXTFILE \"Testfile.cdt\"", - "CATALOG 0123456789012", - "TRACK 01 AUDIO", - " PERFORMER \"Sample Artist 1\"", - " TITLE \"Sample Title 1\"", - " INDEX 01 00:00:00", - "TRACK 02 AUDIO", - " PERFORMER \"Sample Artist 2\"", - " TITLE \"Sample Title 2\"", - " INDEX 01 05:00:00", - "TRACK 03 AUDIO", - " PERFORMER \"Sample Artist 3\"", - " TITLE \"Sample Title 3\"", - " INDEX 01 09:23:00", - "TRACK 04 AUDIO", - " PERFORMER \"Sample Artist 4\"", - " TITLE \"Sample Title 4\"", - " INDEX 01 15:54:00", - "TRACK 05 AUDIO", - " PERFORMER \"Sample Artist 5\"", - " TITLE \"Sample Title 5\"", - " INDEX 01 20:13:00", - "TRACK 06 AUDIO", - " PERFORMER \"Sample Artist 6\"", - " TITLE \"Sample Title 6\"", - " INDEX 01 24:54:00", - "TRACK 07 AUDIO", - " PERFORMER \"Sample Artist 7\"", - " TITLE \"Sample Title 7\"", - " INDEX 01 31:54:00", - "TRACK 08 AUDIO", - " PERFORMER \"Sample Artist 8\"", - " TITLE \"Sample Title 8\"", - " INDEX 01 45:51:00" - }; + var fileContent = @"PERFORMER ""Sample CD Artist"" +TITLE ""Sample CD Title"" +FILE ""AC DC - TNT.mp3"" MP3 +CDTEXTFILE ""Testfile.cdt"" +CATALOG 0123456789012 +TRACK 01 AUDIO + PERFORMER ""Sample Artist 1"" + TITLE ""Sample Title 1"" + INDEX 01 00:00:00 +TRACK 02 AUDIO + PERFORMER ""Sample Artist 2"" + TITLE ""Sample Title 2"" + INDEX 01 05:00:00 +TRACK 03 AUDIO + PERFORMER ""Sample Artist 3"" + TITLE ""Sample Title 3"" + INDEX 01 09:23:00 +TRACK 04 AUDIO + PERFORMER ""Sample Artist 4"" + TITLE ""Sample Title 4"" + INDEX 01 15:54:00 +TRACK 05 AUDIO + PERFORMER ""Sample Artist 5"" + TITLE ""Sample Title 5"" + INDEX 01 20:13:00 +TRACK 06 AUDIO + PERFORMER ""Sample Artist 6"" + TITLE ""Sample Title 6"" + INDEX 01 24:54:00 +TRACK 07 AUDIO + PERFORMER ""Sample Artist 7"" + TITLE ""Sample Title 7"" + INDEX 01 31:54:00 +TRACK 08 AUDIO + PERFORMER ""Sample Artist 8"" + TITLE ""Sample Title 8"" + INDEX 01 45:51:00"; // Act var importFile = CuesheetImportService.Analyse(fileContent); // Assert @@ -78,15 +74,17 @@ public void Analyse_WithSampleCuesheet_CreatesValidCuesheet() Assert.IsNull(importFile.AnalyseException); Assert.IsNotNull(importFile.AnalysedCuesheet); Assert.AreEqual(8, importFile.AnalysedCuesheet.Tracks.Count); - Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "PERFORMER \"Sample CD Artist\""), importFile.FileContentRecognized?.ElementAt(0)); - Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "TITLE \"Sample CD Title\""), importFile.FileContentRecognized?.ElementAt(1)); - Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "FILE \"AC DC - TNT.mp3\" MP3"), importFile.FileContentRecognized?.ElementAt(2)); - Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "CDTEXTFILE \"Testfile.cdt\""), importFile.FileContentRecognized?.ElementAt(3)); - Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "CATALOG 0123456789012"), importFile.FileContentRecognized?.ElementAt(4)); - Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "TRACK 01 AUDIO"), importFile.FileContentRecognized?.ElementAt(5)); - Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "PERFORMER \"Sample Artist 1\"")), importFile.FileContentRecognized?.ElementAt(6)); - Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "TITLE \"Sample Title 1\"")), importFile.FileContentRecognized?.ElementAt(7)); - Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "INDEX 01 00:00:00")), importFile.FileContentRecognized?.ElementAt(8)); + Assert.IsNotNull(importFile.FileContentRecognized); + var lines = importFile.FileContentRecognized.Split(Environment.NewLine); + Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "PERFORMER \"Sample CD Artist\""), lines.ElementAt(0)); + Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "TITLE \"Sample CD Title\""), lines.ElementAt(1)); + Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "FILE \"AC DC - TNT.mp3\" MP3"), lines.ElementAt(2)); + Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "CDTEXTFILE \"Testfile.cdt\""), lines.ElementAt(3)); + Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "CATALOG 0123456789012"), lines.ElementAt(4)); + Assert.AreEqual(string.Format(CuesheetConstants.RecognizedMarkHTML, "TRACK 01 AUDIO"), lines.ElementAt(5)); + Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "PERFORMER \"Sample Artist 1\"")), lines.ElementAt(6)); + Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "TITLE \"Sample Title 1\"")), lines.ElementAt(7)); + Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "INDEX 01 00:00:00")), lines.ElementAt(8)); } [TestMethod()] @@ -95,12 +93,7 @@ public void Analyse_WithCuesheetBug30_CreatesValidCuesheet() //Arrange var textImportMemoryStream = new MemoryStream(Resources.Playlist_Bug_30); using var reader = new StreamReader(textImportMemoryStream); - List lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); + var fileContent = reader.ReadToEnd(); //Act var importFile = CuesheetImportService.Analyse(fileContent); //Assert @@ -114,12 +107,7 @@ public void Analyse_WithCuesheetBug57_CreatesValidCuesheet() //Arrange var textImportMemoryStream = new MemoryStream(Resources.Playlist_Bug_57); using var reader = new StreamReader(textImportMemoryStream); - List lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); + var fileContent = reader.ReadToEnd(); //Act var importFile = CuesheetImportService.Analyse(fileContent); //Assert @@ -135,12 +123,7 @@ public void Analyse_WithCuesheetBug36_CreatesValidCuesheet() //Arrange var textImportMemoryStream = new MemoryStream(Resources.Playlist__36_Frames); using var reader = new StreamReader(textImportMemoryStream); - List lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); + var fileContent = reader.ReadToEnd(); //Act var importFile = CuesheetImportService.Analyse(fileContent); //Assert @@ -154,57 +137,56 @@ public void Analyse_WithCuesheetBug36_CreatesValidCuesheet() public void Analyse_WithCDTextFileCatalogueNumberAndPreAndPostGap_CreatesValidCuesheet() { // Arrange - var fileContent = new List - { - "PERFORMER \"Sample CD Artist\"", - "TITLE \"Sample CD Title\"", - "FILE \"AC DC - TNT.mp3\" MP3", - "CDTEXTFILE \"Testfile.cdt\"", - "CATALOG 0123456789012", - "TRACK 01 AUDIO", - " PERFORMER \"Sample Artist 1\"", - " TITLE \"Sample Title 1\"", - " FLAGS 4CH DCP PRE SCMS", - " INDEX 01 00:00:00", - "TRACK 02 AUDIO", - " PERFORMER \"Sample Artist 2\"", - " TITLE \"Sample Title 2\"", - " FLAGS DCP PRE", - " INDEX 01 05:00:00", - "TRACK 03 AUDIO", - " PERFORMER \"Sample Artist 3\"", - " TITLE \"Sample Title 3\"", - " INDEX 01 09:23:00", - "TRACK 04 AUDIO", - " PERFORMER \"Sample Artist 4\"", - " TITLE \"Sample Title 4\"", - " INDEX 01 15:54:00", - "TRACK 05 AUDIO", - " PERFORMER \"Sample Artist 5\"", - " TITLE \"Sample Title 5\"", - " INDEX 01 20:13:00", - " POSTGAP 00:02:00", - "TRACK 06 AUDIO", - " PERFORMER \"Sample Artist 6\"", - " TITLE \"Sample Title 6\"", - " INDEX 01 24:54:00", - "TRACK 07 AUDIO", - " PERFORMER \"Sample Artist 7\"", - " TITLE \"Sample Title 7\"", - " PREGAP 00:04:00", - " INDEX 01 31:54:00", - "TRACK 08 AUDIO", - " PERFORMER \"Sample Artist 8\"", - " TITLE \"Sample Title 8\"", - " INDEX 01 45:51:00" - }; + var fileContent = @"PERFORMER ""Sample CD Artist"" +TITLE ""Sample CD Title"" +FILE ""AC DC - TNT.mp3"" MP3 +CDTEXTFILE ""Testfile.cdt"" +CATALOG 0123456789012 +TRACK 01 AUDIO + PERFORMER ""Sample Artist 1"" + TITLE ""Sample Title 1"" + FLAGS 4CH DCP PRE SCMS + INDEX 01 00:00:00 +TRACK 02 AUDIO + PERFORMER ""Sample Artist 2"" + TITLE ""Sample Title 2"" + FLAGS DCP PRE + INDEX 01 05:00:00 +TRACK 03 AUDIO + PERFORMER ""Sample Artist 3"" + TITLE ""Sample Title 3"" + INDEX 01 09:23:00 +TRACK 04 AUDIO + PERFORMER ""Sample Artist 4"" + TITLE ""Sample Title 4"" + INDEX 01 15:54:00 +TRACK 05 AUDIO + PERFORMER ""Sample Artist 5"" + TITLE ""Sample Title 5"" + INDEX 01 20:13:00 + POSTGAP 00:02:00 +TRACK 06 AUDIO + PERFORMER ""Sample Artist 6"" + TITLE ""Sample Title 6"" + INDEX 01 24:54:00 +TRACK 07 AUDIO + PERFORMER ""Sample Artist 7"" + TITLE ""Sample Title 7"" + PREGAP 00:04:00 + INDEX 01 31:54:00 +TRACK 08 AUDIO + PERFORMER ""Sample Artist 8"" + TITLE ""Sample Title 8"" + INDEX 01 45:51:00"; // Act var importFile = CuesheetImportService.Analyse(fileContent); // Assert Assert.IsNull(importFile.AnalyseException); Assert.IsNotNull(importFile.AnalysedCuesheet); - Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "FLAGS 4CH DCP PRE SCMS")), importFile.FileContentRecognized?.ElementAt(8)); - Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "PREGAP 00:04:00")), importFile.FileContentRecognized?.ElementAt(35)); + Assert.IsNotNull(importFile.FileContentRecognized); + var lines = importFile.FileContentRecognized.Split(Environment.NewLine); + Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "FLAGS 4CH DCP PRE SCMS")), lines.ElementAt(8)); + Assert.AreEqual(string.Format(" {0}", string.Format(CuesheetConstants.RecognizedMarkHTML, "PREGAP 00:04:00")), lines.ElementAt(35)); Assert.AreEqual(8, importFile.AnalysedCuesheet.Tracks.Count); Assert.IsNotNull(importFile.AnalysedCuesheet.CDTextfile); Assert.AreEqual(4, importFile.AnalysedCuesheet.Tracks.ElementAt(0).Flags.Count()); diff --git a/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs b/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs index 2b293be9..f78b881c 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/FileInputManagerTests.cs @@ -13,6 +13,7 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. +using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Services.IO; using Microsoft.AspNetCore.Components.Forms; using Microsoft.Extensions.Logging; @@ -37,7 +38,7 @@ public void CheckFileMimeType_ReturnsTrue_WhenContentTypeDoesNotMatchButExtensio var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); // Act - var result = manager.CheckFileMimeType(file, "audio/mpeg", ".mp3"); + var result = manager.CheckFileMimeType(file, "audio/mpeg", [".mp3"]); // Assert Assert.IsTrue(result); @@ -54,7 +55,7 @@ public void CheckFileMimeType_ReturnsTrue_WhenContentTypeDoesMatchButNotExtensio var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); // Act - var result = manager.CheckFileMimeType(file, "audio/mpeg", ".mp3"); + var result = manager.CheckFileMimeType(file, "audio/mpeg", [".mp3", ".txt"]); // Assert Assert.IsTrue(result); @@ -71,7 +72,7 @@ public void CheckFileMimeType_ReturnsFalse_WhenExtensionDoesNotMatchAndContentTy var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); // Act - var result = manager.CheckFileMimeType(file, "audio/flac", ".mp3"); + var result = manager.CheckFileMimeType(file, "audio/flac", [".mp3"]); // Assert Assert.IsFalse(result); @@ -88,12 +89,150 @@ public void CheckFileMimeType_ReturnsTrue_WhenContentTypeAndExtensionMatch() var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); // Act - var result = manager.CheckFileMimeType(file, "audio/wave", ".wav"); + var result = manager.CheckFileMimeType(file, "audio/wave", [".wav"]); // Assert Assert.IsTrue(result); } + [TestMethod()] + public void CheckFileMimeType_ReturnsTrue_WhenContentMainTypeMatch() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("history.txt", "text/plain"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.CheckFileMimeType(file, "text/*", [".txt", ".text"]); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod()] + public void IsValidAudiofile_ReturnsTrue_WithValidAudiocodec() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.wav", "audio/wav"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.IsValidAudiofile(file); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod()] + public void IsValidAudiofile_ReturnsFalse_WithInvalidAudiocodecAndExtension() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.mock", "just a fantasy"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.IsValidAudiofile(file); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod()] + public void GetAudioCodec_ReturnsAudiocodec_WhenContentTypeMatches() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.wbem", "audio/webm"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.GetAudioCodec(file); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(Audiofile.AudioCodecWEBM, result); + } + + [TestMethod()] + public void GetAudioCodec_ReturnsAudiocodec_WhenContentTypeAndFileExtensionMatches() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.webm", "audio/webm"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.GetAudioCodec(file); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(Audiofile.AudioCodecWEBM, result); + } + + [TestMethod()] + public void GetAudioCodec_ReturnsNull_WhenContentTypeAndFileExtensionNotMatch() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.acx", "fantasy stuff"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.GetAudioCodec(file); + + // Assert + Assert.IsNull(result); + } + + [TestMethod()] + public void IsValidForImportView_ReturnsTrue_WhenFileIsHtml() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.html", "text/html"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.IsValidForImportView(file); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod()] + public void IsValidForImportView_ReturnsFalse_WhenFileIsBinary() + { + // Arrange + var jsRuntimeMock = new Mock(); + var httpClientMock = new Mock(); + var loggerMock = new Mock>(); + var file = CreateBrowserFile("test.dat", "application/octet-stream"); + var manager = new FileInputManager(jsRuntimeMock.Object, httpClientMock.Object, loggerMock.Object); + + // Act + var result = manager.IsValidForImportView(file); + + // Assert + Assert.IsFalse(result); + } + private static IBrowserFile CreateBrowserFile(string name, string contentType) { var fileMock = new Mock(); diff --git a/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs b/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs index 08b24803..36671194 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/ImportManagerTests.cs @@ -14,18 +14,24 @@ //along with Foobar. If not, see //. -using AudioCuesheetEditor.Data.Options; +using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.AudioCuesheet.Import; +using AudioCuesheetEditor.Model.IO; using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.IO; using AudioCuesheetEditor.Services.UI; using AudioCuesheetEditor.Tests.Utility; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace AudioCuesheetEditor.Tests.Services.IO { @@ -33,78 +39,181 @@ namespace AudioCuesheetEditor.Tests.Services.IO public class ImportManagerTests { [TestMethod()] - public void ImportTextAsync_TextfileWithStartDateTime_CreatesValidCuesheet() + public async Task AnalyseImportfile_WithTextfile_SetsImportCuesheet() { // Arrange - var fileContent = new List + var fileContent = "This is just a test"; + var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); + var sessionStateContainer = new SessionStateContainer(traceChangeManager); + var fileInputManagerMock = new Mock(); + var textImportServiceMock = new Mock(); + var importCuesheet = new ImportCuesheet() + { + Artist = "Test Cuesheet Artist", + Title = "Test Cuesheet Title", + Audiofile = "Test Cuesheet Audiofile", + Cataloguenumber = "Test Cuesheet Cataloguenumber", + CDTextfile = "Test Cuesheet CDTextfile" + }; + importCuesheet.Tracks.Add(new() + { + Artist = "Test Track Artist 1", + Title = "Test Track Title 1", + Begin = new TimeSpan(0, 3, 20), + End = new TimeSpan(0, 7, 43), + Flags = [Flag.DCP], + Position = 1, + PreGap = new TimeSpan(0, 0, 2), + PostGap = new TimeSpan(0, 0, 4) + }); + var importfile = new Importfile() + { + FileContent = fileContent, + FileType = ImportFileType.Textfile, + AnalysedCuesheet = importCuesheet + }; + textImportServiceMock.Setup(x => x.AnalyseAsync(fileContent)).ReturnsAsync(importfile); + var loggerMock = new Mock>(); + var importManager = new ImportManager(sessionStateContainer, traceChangeManager, fileInputManagerMock.Object, textImportServiceMock.Object, loggerMock.Object); + var testHelper = new TestHelper(); + sessionStateContainer.Importfile = new Importfile() { - "Innellea~The Golden Fort~02.08.2024 20:10:48", - "Nora En Pure~Diving with Whales (Daniel Portman Remix)~02.08.2024 20:15:21", - "WhoMadeWho & Adriatique~Miracle (RÜFÜS DU SOL Remix)~02.08.2024 20:20:42", - "Ella Wild~Poison D'araignee (Original Mix)~02.08.2024 20:28:03", - "Stil & Bense~On The Edge (Original Mix)~02.08.2024 20:32:42", - "Nebula~Clairvoyant Dreams~02.08.2024 20:39:01", - "Valentina Black~I'm a Tree (Extended Mix)~02.08.2024 20:47:08", - "Nebula~Clairvoyant Dreams~02.08.2024 20:53:20", - "Kiko & Dave Davis feat. Phoebe~Living in Space (Dub Mix)~02.08.2024 20:58:11", - "Lilly Palmer~Before Acid~02.08.2024 21:03:53", - "Sofi Tukker~Drinkee (Vintage Culture & John Summit Extended Mix)~02.08.2024 21:09:52", - "CID & Truth x Lies~Caroline (Extended Mix)~02.08.2024 21:14:09", - "Moby~Why Does My Heart Feel So Bad? (Oxia Remix)~02.08.2024 21:17:15", - "Ammo Avenue~Little Gurl (Extended Mix)~02.08.2024 21:22:46", - "James Hurr & Smokin Jo & Stealth~Beggin' For Change~02.08.2024 21:28:37", - "Kristine Blond~Love Shy (Sam Divine & CASSIMM Extended Remix)~02.08.2024 21:30:47", - "Vanilla Ace~Work On You (Original Mix)~02.08.2024 21:36:28", - "Truth X Lies~Like This~02.08.2024 21:42:05", - "Terri-Anne~Round Round~02.08.2024 21:44:07", - "Joanna Magik~Maneater~02.08.2024 21:46:32", - "Jen Payne & Kevin McKay~Feed Your Soul~02.08.2024 21:48:45", - "Kevin McKay & Eppers & Notelle~On My Own~02.08.2024 21:51:37", - "Nader Razdar & Kevin McKay~Get Ur Freak On (Kevin McKay Extended Mix)~02.08.2024 21:53:49", - "Philip Z~Yala (Extended Mix)~02.08.2024 21:59:40", - "Kyle Kinch & Kevin McKay~Hella~02.08.2024 22:05:53", - "Roze Wild~B-O-D-Y~02.08.2024 22:08:26", - "Jey Kurmis~Snoop~02.08.2024 22:11:09", - "Bootie Brown & Tame Impala & Gorillaz~New Gold (Dom Dolla Remix Extended)~02.08.2024 22:16:23", - "Eli Brown & Love Regenerator~Don't You Want Me (Original Mix)~02.08.2024 22:21:23", - "Local Singles~Voices~02.08.2024 22:25:59" + FileContent = "This is just a test", + FileType = ImportFileType.Textfile }; + // Act + await importManager.AnalyseImportfile(); + // Assert + Assert.AreEqual(importfile, sessionStateContainer.Importfile); + Assert.IsNotNull(sessionStateContainer.ImportCuesheet); + Assert.AreEqual(importCuesheet.Artist, sessionStateContainer.ImportCuesheet.Artist); + Assert.AreEqual(importCuesheet.Title, sessionStateContainer.ImportCuesheet.Title); + Assert.IsNotNull(sessionStateContainer.ImportCuesheet.Audiofile); + Assert.AreEqual(importCuesheet.Audiofile, sessionStateContainer.ImportCuesheet.Audiofile.Name); + Assert.AreEqual(importCuesheet.Cataloguenumber, sessionStateContainer.ImportCuesheet.Cataloguenumber); + Assert.IsNotNull(sessionStateContainer.ImportCuesheet.CDTextfile); + Assert.AreEqual(importCuesheet.CDTextfile, sessionStateContainer.ImportCuesheet.CDTextfile.Name); + Assert.AreEqual(importCuesheet.Tracks.First().Artist, sessionStateContainer.ImportCuesheet.Tracks.First().Artist); + Assert.AreEqual(importCuesheet.Tracks.First().Title, sessionStateContainer.ImportCuesheet.Tracks.First().Title); + Assert.AreEqual(importCuesheet.Tracks.First().Begin, sessionStateContainer.ImportCuesheet.Tracks.First().Begin); + Assert.AreEqual(importCuesheet.Tracks.First().End, sessionStateContainer.ImportCuesheet.Tracks.First().End); + CollectionAssert.AreEquivalent(importCuesheet.Tracks.First().Flags.ToList(), sessionStateContainer.ImportCuesheet.Tracks.First().Flags.ToList()); + Assert.AreEqual(importCuesheet.Tracks.First().Position, sessionStateContainer.ImportCuesheet.Tracks.First().Position); + Assert.AreEqual(importCuesheet.Tracks.First().PreGap, sessionStateContainer.ImportCuesheet.Tracks.First().PreGap); + Assert.AreEqual(importCuesheet.Tracks.First().PostGap, sessionStateContainer.ImportCuesheet.Tracks.First().PostGap); + } + [TestMethod()] + public async Task AnalyseImportfile_WithoutAnalysedCuesheet_DoesNothing() + { + // Arrange + var fileContent = "This is just a test"; var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var localStorageOptionsProviderMock = new Mock(); - var textImportScheme = new TextImportScheme() + var fileInputManagerMock = new Mock(); + var textImportServiceMock = new Mock(); + var importfile = new Importfile() { - SchemeCuesheet = null, - SchemeTracks = @"(?'Artist'[a-zA-Z0-9_ .();äöü&:,'*-?:]{1,})~(?'Title'[a-zA-Z0-9_ .();äöü&'*-?:Ü]{1,})~(?'StartDateTime'.{1,})" + FileContent = fileContent, + FileType = ImportFileType.Textfile, }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions(); - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); - var fileInputManagerMock = new Mock(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); + textImportServiceMock.Setup(x => x.AnalyseAsync(fileContent)).ReturnsAsync(importfile); + var loggerMock = new Mock>(); + var importManager = new ImportManager(sessionStateContainer, traceChangeManager, fileInputManagerMock.Object, textImportServiceMock.Object, loggerMock.Object); var testHelper = new TestHelper(); + sessionStateContainer.Importfile = importfile; // Act - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); + await importManager.AnalyseImportfile(); // Assert - Assert.IsNull(sessionStateContainer.Importfile?.AnalyseException); - Assert.IsNotNull(sessionStateContainer.ImportCuesheet); - Assert.AreEqual(30, sessionStateContainer.ImportCuesheet.Tracks.Count); - Assert.AreEqual("Innellea", sessionStateContainer.ImportCuesheet.Tracks.ElementAt(0).Artist); - Assert.AreEqual("The Golden Fort", sessionStateContainer.ImportCuesheet.Tracks.ElementAt(0).Title); - Assert.AreEqual(TimeSpan.Zero, sessionStateContainer.ImportCuesheet.Tracks.ElementAt(0).Begin); - Assert.AreEqual(new TimeSpan(0, 4, 33), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(0).End); - Assert.AreEqual(new TimeSpan(0, 4, 33), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(0).Length); - Assert.AreEqual("Nora En Pure", sessionStateContainer.ImportCuesheet.Tracks.ElementAt(1).Artist); - Assert.AreEqual("Diving with Whales (Daniel Portman Remix)", sessionStateContainer.ImportCuesheet.Tracks.ElementAt(1).Title); - Assert.AreEqual(new TimeSpan(0, 4, 33), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(1).Begin); - Assert.AreEqual(new TimeSpan(0, 9, 54), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(1).End); - Assert.AreEqual(new TimeSpan(0, 5, 21), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(1).Length); - Assert.AreEqual("Local Singles", sessionStateContainer.ImportCuesheet.Tracks.ElementAt(29).Artist); - Assert.AreEqual("Voices", sessionStateContainer.ImportCuesheet.Tracks.ElementAt(29).Title); - Assert.AreEqual(new TimeSpan(2, 15, 11), sessionStateContainer.ImportCuesheet.Tracks.ElementAt(29).Begin); - Assert.IsNull(sessionStateContainer.ImportCuesheet.Tracks.ElementAt(29).End); - Assert.IsNull(sessionStateContainer.ImportCuesheet.Tracks.ElementAt(29).Length); + Assert.AreEqual(importfile, sessionStateContainer.Importfile); + Assert.IsNull(sessionStateContainer.ImportCuesheet); + } + + [TestMethod] + public async Task ImportFilesAsync_ProjectFile_ImportsCorrectly() + { + // Arrange + var fileContent = "This is the content"; + var file = CreateBrowserFileMock("test.projectfile", fileContent); + var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); + var sessionStateContainer = new SessionStateContainer(traceChangeManager); + var fileInputManagerMock = new Mock(); + var textImportServiceMock = new Mock(); + fileInputManagerMock.Setup(f => f.CheckFileMimeType(file, FileMimeTypes.Projectfile, It.IsAny>())).Returns(true); + fileInputManagerMock.Setup(f => f.CheckFileMimeType(file, FileMimeTypes.Cuesheet, It.IsAny>())).Returns(false); + fileInputManagerMock.Setup(f => f.IsValidForImportView(file)).Returns(false); + + var loggerMock = new Mock>(); + var importManager = new ImportManager(sessionStateContainer, traceChangeManager, fileInputManagerMock.Object, textImportServiceMock.Object, loggerMock.Object); + // Act + await importManager.ImportFilesAsync([file]); + + // Assert + Assert.IsNotNull(sessionStateContainer.Importfile); + Assert.AreEqual(fileContent, sessionStateContainer.Importfile.FileContent); + Assert.AreEqual(fileContent, sessionStateContainer.Importfile.FileContentRecognized); + Assert.AreEqual(ImportFileType.ProjectFile, sessionStateContainer.Importfile.FileType); + } + + [TestMethod] + public async Task ImportFilesAsync_CuesheetFile_ImportsCorrectly() + { + // Arrange + var fileContent = "Cuesheet file content"; + var file = CreateBrowserFileMock("test.cue", fileContent); + var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); + var sessionStateContainer = new SessionStateContainer(traceChangeManager); + var fileInputManagerMock = new Mock(); + var textImportServiceMock = new Mock(); + fileInputManagerMock.Setup(f => f.CheckFileMimeType(file, FileMimeTypes.Projectfile, It.IsAny>())).Returns(false); + fileInputManagerMock.Setup(f => f.CheckFileMimeType(file, FileMimeTypes.Cuesheet, It.IsAny>())).Returns(true); + fileInputManagerMock.Setup(f => f.IsValidForImportView(file)).Returns(false); + + var loggerMock = new Mock>(); + var importManager = new ImportManager(sessionStateContainer, traceChangeManager, fileInputManagerMock.Object, textImportServiceMock.Object, loggerMock.Object); + // Act + await importManager.ImportFilesAsync([file]); + + // Assert + Assert.IsNotNull(sessionStateContainer.Importfile); + Assert.AreEqual(fileContent, sessionStateContainer.Importfile.FileContent); + Assert.AreEqual(fileContent, sessionStateContainer.Importfile.FileContentRecognized); + Assert.AreEqual(ImportFileType.Cuesheet, sessionStateContainer.Importfile.FileType); + } + + [TestMethod] + public async Task ImportFilesAsync_TextFile_ImportsCorrectly() + { + // Arrange + var fileContent = "TextFileContent"; + var file = CreateBrowserFileMock("test.txt", fileContent); + var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger()); + var sessionStateContainer = new SessionStateContainer(traceChangeManager); + var fileInputManagerMock = new Mock(); + var textImportServiceMock = new Mock(); + fileInputManagerMock.Setup(f => f.CheckFileMimeType(file, FileMimeTypes.Projectfile, It.IsAny>())).Returns(false); + fileInputManagerMock.Setup(f => f.CheckFileMimeType(file, FileMimeTypes.Cuesheet, It.IsAny>())).Returns(false); + fileInputManagerMock.Setup(f => f.IsValidForImportView(file)).Returns(true); + + var loggerMock = new Mock>(); + var importManager = new ImportManager(sessionStateContainer, traceChangeManager, fileInputManagerMock.Object, textImportServiceMock.Object, loggerMock.Object); + // Act + await importManager.ImportFilesAsync([file]); + + // Assert + Assert.IsNotNull(sessionStateContainer.Importfile); + Assert.AreEqual(fileContent, sessionStateContainer.Importfile.FileContent); + Assert.AreEqual(fileContent, sessionStateContainer.Importfile.FileContentRecognized); + Assert.AreEqual(ImportFileType.Textfile, sessionStateContainer.Importfile.FileType); + } + + private static IBrowserFile CreateBrowserFileMock(string name, string content = "TestContent") + { + var fileMock = new Mock(); + fileMock.Setup(f => f.Name).Returns(name); + fileMock.Setup(f => f.OpenReadStream(It.IsAny(), It.IsAny())) + .Returns(new MemoryStream(Encoding.UTF8.GetBytes(content))); + return fileMock.Object; } } } \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs b/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs index e95cfd6c..a59f9a35 100644 --- a/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs +++ b/AudioCuesheetEditor.Tests/Services/IO/TextImportServiceTests.cs @@ -13,45 +13,49 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //. +using AudioCuesheetEditor.Data.Options; using AudioCuesheetEditor.Model.AudioCuesheet; +using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO.Import; +using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.IO; using AudioCuesheetEditor.Tests.Properties; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; using System; -using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace AudioCuesheetEditor.Tests.Services.IO { [TestClass()] public class TextImportServiceTests { + [TestMethod()] - public void Analyse_SampleCuesheet_CreatesValidCuesheet() + public async Task AnalyseAsync_SampleCuesheet_CreatesValidCuesheetAsync() { // Arrange - var fileContent = new List + var fileContent = @"CuesheetArtist - CuesheetTitle c:\tmp\Testfile.mp3 +Sample Artist 1 - Sample Title 1 00:05:00 +Sample Artist 2 - Sample Title 2 00:09:23 +Sample Artist 3 - Sample Title 3 00:15:54 +Sample Artist 4 - Sample Title 4 00:20:13 +Sample Artist 5 - Sample Title 5 00:24:54 +Sample Artist 6 - Sample Title 6 00:31:54 +Sample Artist 7 - Sample Title 7 00:45:54 +Sample Artist 8 - Sample Title 8 01:15:54"; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - "CuesheetArtist - CuesheetTitle c:\\tmp\\Testfile.mp3", - "Sample Artist 1 - Sample Title 1 00:05:00", - "Sample Artist 2 - Sample Title 2 00:09:23", - "Sample Artist 3 - Sample Title 3 00:15:54", - "Sample Artist 4 - Sample Title 4 00:20:13", - "Sample Artist 5 - Sample Title 5 00:24:54", - "Sample Artist 6 - Sample Title 6 00:31:54", - "Sample Artist 7 - Sample Title 7 00:45:54", - "Sample Artist 8 - Sample Title 8 01:15:54" - }; - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks + SelectedImportProfile = ImportOptions.DefaultSelectedImportprofile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -65,55 +69,65 @@ public void Analyse_SampleCuesheet_CreatesValidCuesheet() } [TestMethod()] - public void Analyse_InvalidSchemeTracks_CreatesAnalyseException() + public async Task AnalyseAsync_InvalidSchemeTracks_CreatesAnalyseExceptionAsync() { // Arrange - var fileContent = new List - { - "CuesheetArtist|CuesheetTitle c:\\tmp\\TestTextFile.cdt", - "1|Sample Artist 1 - Sample Title 1 00:05:00", - "2|Sample Artist 2 - Sample Title 2 00:09:23", - "3|Sample Artist 3 - Sample Title 3 00:15:54", - "4|Sample Artist 4 - Sample Title 4 00:20:13", - "5|Sample Artist 5 - Sample Title 5 00:24:54", - "6|Sample Artist 6 - Sample Title 6 00:31:54", - "7|Sample Artist 7 - Sample Title 7 00:45:54", - "8|Sample Artist 8 - Sample Title 8 01:15:54" - }; - var textImportScheme = new TextImportScheme() + var fileContent = @"CuesheetArtist|CuesheetTitle c:\tmp\TestTextFile.cdt +1|Sample Artist 1 - Sample Title 1 00:05:00 +2|Sample Artist 2 - Sample Title 2 00:09:23 +3|Sample Artist 3 - Sample Title 3 00:15:54 +4|Sample Artist 4 - Sample Title 4 00:20:13 +5|Sample Artist 5 - Sample Title 5 00:24:54 +6|Sample Artist 6 - Sample Title 6 00:31:54 +7|Sample Artist 7 - Sample Title 7 00:45:54 +8|Sample Artist 8 - Sample Title 8 01:15:54"; + var profile = new Importprofile() { + UseRegularExpression = true, SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'.{1,})", SchemeTracks = @"(?'Track.Position'.{1,})|(?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" }; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNotNull(importfile.AnalyseException); } [TestMethod()] - public void Analyse_InputfileWithExtraSeperator_CreatesValidCuesheet() + public async Task AnalyseAsync_InputfileWithExtraSeperator_CreatesValidCuesheetAsync() { // Arrange - var fileContent = new List + var fileContent = @"CuesheetArtist|CuesheetTitle c:\tmp\TestTextFile.cdt +1|Sample Artist 1 - Sample Title 1 00:05:00 +2|Sample Artist 2 - Sample Title 2 00:09:23 +3|Sample Artist 3 - Sample Title 3 00:15:54 +4|Sample Artist 4 - Sample Title 4 00:20:13 +5|Sample Artist 5 - Sample Title 5 00:24:54 +6|Sample Artist 6 - Sample Title 6 00:31:54 +7|Sample Artist 7 - Sample Title 7 00:45:54 +8|Sample Artist 8 - Sample Title 8 01:15:54"; + var profile = new Importprofile() { - "CuesheetArtist|CuesheetTitle c:\\tmp\\TestTextFile.cdt", - "1|Sample Artist 1 - Sample Title 1 00:05:00", - "2|Sample Artist 2 - Sample Title 2 00:09:23", - "3|Sample Artist 3 - Sample Title 3 00:15:54", - "4|Sample Artist 4 - Sample Title 4 00:20:13", - "5|Sample Artist 5 - Sample Title 5 00:24:54", - "6|Sample Artist 6 - Sample Title 6 00:31:54", - "7|Sample Artist 7 - Sample Title 7 00:45:54", - "8|Sample Artist 8 - Sample Title 8 01:15:54" + UseRegularExpression = true, + SchemeCuesheet = @"(?'Artist'\A.*)[|](?'Title'\w{1,})\t{1,}(?'CDTextfile'[^\r\n]+)", + SchemeTracks = @"(?'Position'\d{1,})[|](?'Artist'.{1,}) - (?'Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'End'.{1,})" }; - var textImportScheme = new TextImportScheme() + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = @"(?'Artist'\A.*)[|](?'Title'\w{1,})\t{1,}(?'CDTextfile'.{1,})", - SchemeTracks = @"(?'Position'\d{1,})[|](?'Artist'.{1,}) - (?'Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'End'.{1,})" + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -126,30 +140,34 @@ public void Analyse_InputfileWithExtraSeperator_CreatesValidCuesheet() Assert.AreEqual("Sample Title 1", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Title); Assert.AreEqual(new TimeSpan(0, 5, 0), importfile.AnalysedCuesheet.Tracks.ElementAt(0).End); } - + [TestMethod()] - public void Analyse_InputfileWithSimplifiedScheme_CreatesValidCuesheet() + public async Task AnalyseAsync_InputfileWithSimplifiedScheme_CreatesValidCuesheetAsync() { - // Arrange - var fileContent = new List - { - "CuesheetArtist|CuesheetTitle c:\\tmp\\TestTextFile.cdt", - "1|Sample Artist 1 - Sample Title 1 00:05:00", - "2|Sample Artist 2 - Sample Title 2 00:09:23", - "3|Sample Artist 3 - Sample Title 3 00:15:54", - "4|Sample Artist 4 - Sample Title 4 00:20:13", - "5|Sample Artist 5 - Sample Title 5 00:24:54", - "6|Sample Artist 6 - Sample Title 6 00:31:54", - "7|Sample Artist 7 - Sample Title 7 00:45:54", - "8|Sample Artist 8 - Sample Title 8 01:15:54" - }; - var textImportScheme = new TextImportScheme() + var fileContent = @"CuesheetArtist|CuesheetTitle c:\tmp\TestTextFile.cdt +1|Sample Artist 1 - Sample Title 1 00:05:00 +2|Sample Artist 2 - Sample Title 2 00:09:23 +3|Sample Artist 3 - Sample Title 3 00:15:54 +4|Sample Artist 4 - Sample Title 4 00:20:13 +5|Sample Artist 5 - Sample Title 5 00:24:54 +6|Sample Artist 6 - Sample Title 6 00:31:54 +7|Sample Artist 7 - Sample Title 7 00:45:54 +8|Sample Artist 8 - Sample Title 8 01:15:54"; + var profile = new Importprofile() { + UseRegularExpression = false, SchemeCuesheet = @"Artist|Title CDTextfile", SchemeTracks = @"Position|Artist - Title End" }; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -164,71 +182,83 @@ public void Analyse_InputfileWithSimplifiedScheme_CreatesValidCuesheet() } [TestMethod()] - public void Analyse_InvalidScheme_CreatesAnalyseException() + public async Task AnalyseAsync_InvalidScheme_CreatesAnalyseExceptionAsync() { // Arrange - var fileContent = new List - { - "CuesheetArtist|CuesheetTitle c:\\tmp\\TestTextFile.cdt A83412346734", - "1|Sample Artist 1 - Sample Title 1 00:05:00", - "2|Sample Artist 2 - Sample Title 2 00:09:23", - "3|Sample Artist 3 - Sample Title 3 00:15:54", - "4|Sample Artist 4 - Sample Title 4 00:20:13", - "5|Sample Artist 5 - Sample Title 5 00:24:54", - "6|Sample Artist 6 - Sample Title 6 00:31:54", - "7|Sample Artist 7 - Sample Title 7 00:45:54", - "8|Sample Artist 8 - Sample Title 8 01:15:54", - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty - }; - var textImportScheme = new TextImportScheme() + var fileContent = @"CuesheetArtist|CuesheetTitle c:\tmp\TestTextFile.cdt A83412346734 +1|Sample Artist 1 - Sample Title 1 00:05:00 +2|Sample Artist 2 - Sample Title 2 00:09:23 +3|Sample Artist 3 - Sample Title 3 00:15:54 +4|Sample Artist 4 - Sample Title 4 00:20:13 +5|Sample Artist 5 - Sample Title 5 00:24:54 +6|Sample Artist 6 - Sample Title 6 00:31:54 +7|Sample Artist 7 - Sample Title 7 00:45:54 +8|Sample Artist 8 - Sample Title 8 01:15:54 + + + + + + + + +"; + var profile = new Importprofile() { + UseRegularExpression = true, SchemeCuesheet = @"(?'Cuesheet.Artist'\A.*)[|](?'Cuesheet.Title'\w{1,})\t{1,}(?'Cuesheet.CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cuesheet.Cataloguenumber'.{1,})", SchemeTracks = @"(?'Track.Position'.{1,})|(?'Track.Artist'.{1,}) - (?'Track.Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'Track.End'.{1,})" }; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNotNull(importfile.AnalyseException); } [TestMethod()] - public void Analyse_CuesheetWithTextfileAndCatalogueNumber_CreatesValidCuesheet() + public async Task AnalyseAsync_CuesheetWithTextfileAndCatalogueNumber_CreatesValidCuesheetAsync() { // Arrange - var fileContent = new List - { - "CuesheetArtist|CuesheetTitle c:\\tmp\\TestTextFile.cdt A83412346734", - "1|Sample Artist 1 - Sample Title 1 00:05:00", - "2|Sample Artist 2 - Sample Title 2 00:09:23", - "3|Sample Artist 3 - Sample Title 3 00:15:54", - "4|Sample Artist 4 - Sample Title 4 00:20:13", - "5|Sample Artist 5 - Sample Title 5 00:24:54", - "6|Sample Artist 6 - Sample Title 6 00:31:54", - "7|Sample Artist 7 - Sample Title 7 00:45:54", - "8|Sample Artist 8 - Sample Title 8 01:15:54", - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty, - string.Empty - }; - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = @"(?'Artist'\A.*)[|](?'Title'\w{1,})\t{1,}(?'CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cataloguenumber'.{1,})", + var fileContent = @"CuesheetArtist|CuesheetTitle c:\tmp\TestTextFile.cdt A83412346734 +1|Sample Artist 1 - Sample Title 1 00:05:00 +2|Sample Artist 2 - Sample Title 2 00:09:23 +3|Sample Artist 3 - Sample Title 3 00:15:54 +4|Sample Artist 4 - Sample Title 4 00:20:13 +5|Sample Artist 5 - Sample Title 5 00:24:54 +6|Sample Artist 6 - Sample Title 6 00:31:54 +7|Sample Artist 7 - Sample Title 7 00:45:54 +8|Sample Artist 8 - Sample Title 8 01:15:54 + + + + + + + + +"; + var profile = new Importprofile() + { + UseRegularExpression = true, + SchemeCuesheet = @"(?'Artist'\A.*)[|](?'Title'\w{1,})\t{1,}(?'CDTextfile'[a-zA-Z0-9_ .();äöü&:,\\]{1,})\t{1,}(?'Cataloguenumber'[^\r\n]+)", SchemeTracks = @"(?'Position'.{1,})[|](?'Artist'.{1,}) - (?'Title'[a-zA-Z0-9_ ]{1,})\t{1,}(?'End'.{1,})" }; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -244,28 +274,31 @@ public void Analyse_CuesheetWithTextfileAndCatalogueNumber_CreatesValidCuesheet( } [TestMethod()] - public void Analyse_CuesheeTracksOnly_CreatesValidCuesheet() + public async Task AnalyseAsync_CuesheeTracksOnly_CreatesValidCuesheetAsync() { // Arrange - var fileContent = new List + var fileContent = @"Sample Artist 1 - Sample Title 1 00:05:00 +Sample Artist 2 - Sample Title 2 00:09:23 +Sample Artist 3 - Sample Title 3 00:15:54 +Sample Artist 4 - Sample Title 4 00:20:13 +Sample Artist 5 - Sample Title 5 00:24:54 +Sample Artist 6 - Sample Title 6 00:31:54 +Sample Artist 7 - Sample Title 7 00:45:54 +Sample Artist 8 - Sample Title 8 01:15:54 +Sample Artist 9 - Sample Title 9 "; + var profile = new Importprofile() { - "Sample Artist 1 - Sample Title 1 00:05:00", - "Sample Artist 2 - Sample Title 2 00:09:23", - "Sample Artist 3 - Sample Title 3 00:15:54", - "Sample Artist 4 - Sample Title 4 00:20:13", - "Sample Artist 5 - Sample Title 5 00:24:54", - "Sample Artist 6 - Sample Title 6 00:31:54", - "Sample Artist 7 - Sample Title 7 00:45:54", - "Sample Artist 8 - Sample Title 8 01:15:54", - "Sample Artist 9 - Sample Title 9" + SchemeTracks = $"{nameof(ImportTrack.Artist)} - {nameof(ImportTrack.Title)}\t{nameof(ImportTrack.End)}" }; - var textImportScheme = new TextImportScheme() + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -276,25 +309,26 @@ public void Analyse_CuesheeTracksOnly_CreatesValidCuesheet() } [TestMethod()] - public void Analyse_CuesheetBug213_CreatesValidCuesheet() + public async Task AnalyseAsync_CuesheetBug213_CreatesValidCuesheetAsync() { // Arrange var textImportMemoryStream = new MemoryStream(Resources.Textimport_Bug_213); using var reader = new StreamReader(textImportMemoryStream); - List lines = []; - while (reader.EndOfStream == false) + var fileContent = reader.ReadToEnd(); + var profile = new Importprofile() { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); - var timeSpanFormat = new TimeSpanFormat() { Scheme = "Minutes:Seconds" }; - var textImportScheme = new TextImportScheme() + SchemeTracks = $"{nameof(ImportTrack.Artist)} - {nameof(ImportTrack.Title)}\t{nameof(ImportTrack.End)}", + TimeSpanFormat = new() { Scheme = $"{nameof(TimeSpanFormat.Minutes)}:{nameof(TimeSpanFormat.Seconds)}" } + }; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent, timeSpanFormat); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -303,27 +337,31 @@ public void Analyse_CuesheetBug213_CreatesValidCuesheet() } [TestMethod()] - public void Analyse_CuesheetWithFlags_CreatesValidCuesheet() + public async Task AnalyseAsync_CuesheetWithFlags_CreatesValidCuesheetAsync() { // Arrange - var fileContent = new List + var fileContent = @"Sample Artist 1 - Sample Title 1 00:05:00 DCP +Sample Artist 2 - Sample Title 2 00:09:23 +Sample Artist 3 - Sample Title 3 00:15:54 PRE, DCP +Sample Artist 4 - Sample Title 4 00:20:13 4CH +Sample Artist 5 - Sample Title 5 00:24:54 +Sample Artist 6 - Sample Title 6 00:31:54 PRE DCP 4CH +Sample Artist 7 - Sample Title 7 00:45:54 +Sample Artist 8 - Sample Title 8 01:15:54 PRE DCP 4CH SCMS"; + var profile = new Importprofile() { - "Sample Artist 1 - Sample Title 1 00:05:00 DCP", - "Sample Artist 2 - Sample Title 2 00:09:23", - "Sample Artist 3 - Sample Title 3 00:15:54 PRE, DCP", - "Sample Artist 4 - Sample Title 4 00:20:13 4CH", - "Sample Artist 5 - Sample Title 5 00:24:54", - "Sample Artist 6 - Sample Title 6 00:31:54 PRE DCP 4CH", - "Sample Artist 7 - Sample Title 7 00:45:54", - "Sample Artist 8 - Sample Title 8 01:15:54 PRE DCP 4CH SCMS" + UseRegularExpression = true, + SchemeTracks = "(?'Artist'[a-zA-Z0-9_ .();äöü&:,]+) - (?'Title'[a-zA-Z0-9_ .();äöü]+)[\t ]+(?'End'[0-9]{2}:[0-9]{2}:[0-9]{2})(?:[\t ]+(?'Flags'[A-Za-z0-9 ,]+))?(?=[\t ]*(?:\r?\n|$))" }; - var textImportScheme = new TextImportScheme() + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = null, - SchemeTracks = "(?'Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'Flags'[a-zA-Z 0-9,]{1,})" + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -342,27 +380,31 @@ public void Analyse_CuesheetWithFlags_CreatesValidCuesheet() } [TestMethod()] - public void Analyse_CuesheetWithPreGapAndPostGap_CreatesValidCuesheet() + public async Task AnalyseAsync_CuesheetWithPreGapAndPostGap_CreatesValidCuesheetAsync() { // Arrange - var fileContent = new List + var fileContent = @"Sample Artist 1 - Sample Title 1 00:00:02 00:05:00 00:00:00 +Sample Artist 2 - Sample Title 2 00:00:04 00:09:23 00:00:00 +Sample Artist 3 - Sample Title 3 00:00:00 00:15:54 00:00:02 +Sample Artist 4 - Sample Title 4 00:00:00 00:20:13 00:00:03 +Sample Artist 5 - Sample Title 5 00:00:00 00:24:54 00:00:04 +Sample Artist 6 - Sample Title 6 00:00:00 00:31:54 00:00:01 +Sample Artist 7 - Sample Title 7 00:00:00 00:45:54 00:00:00 +Sample Artist 8 - Sample Title 8 00:00:02 01:15:54 00:00:00"; + var profile = new Importprofile() { - "Sample Artist 1 - Sample Title 1 00:00:02 00:05:00 00:00:00", - "Sample Artist 2 - Sample Title 2 00:00:04 00:09:23 00:00:00", - "Sample Artist 3 - Sample Title 3 00:00:00 00:15:54 00:00:02", - "Sample Artist 4 - Sample Title 4 00:00:00 00:20:13 00:00:03", - "Sample Artist 5 - Sample Title 5 00:00:00 00:24:54 00:00:04", - "Sample Artist 6 - Sample Title 6 00:00:00 00:31:54 00:00:01", - "Sample Artist 7 - Sample Title 7 00:00:00 00:45:54 00:00:00", - "Sample Artist 8 - Sample Title 8 00:00:02 01:15:54 00:00:00" + UseRegularExpression = true, + SchemeTracks = "(?'Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'PreGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'PostGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})" }; - var textImportScheme = new TextImportScheme() + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = null, - SchemeTracks = "(?'Artist'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Title'[a-zA-Z0-9_ .();äöü]{1,})\t{1,}(?'PreGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'End'[0-9]{2}[:][0-9]{2}[:][0-9]{2})\t{1,}(?'PostGap'[0-9]{2}[:][0-9]{2}[:][0-9]{2})" + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -386,73 +428,76 @@ public void Analyse_CuesheetWithPreGapAndPostGap_CreatesValidCuesheet() } [TestMethod()] - public void Analyse_SchemeCuesheetOnly_CreatesFileContentRecognizedOnlyForCuesheet() + public async Task AnalyseAsync_SchemeCuesheetOnly_CreatesFileContentRecognizedOnlyForCuesheetAsync() { // Arrange - var fileContent = new List + var fileContent = @"CuesheetArtist - CuesheetTitle c:\tmp\Testfile.mp3 +Sample Artist 1 - Sample Title 1 00:05:00 +Sample Artist 2 - Sample Title 2 00:09:23 +Sample Artist 3 - Sample Title 3 00:15:54 +Sample Artist 4 - Sample Title 4 00:20:13 +Sample Artist 5 - Sample Title 5 00:24:54 +Sample Artist 6 - Sample Title 6 00:31:54 +Sample Artist 7 - Sample Title 7 00:45:54 +Sample Artist 8 - Sample Title 8 01:15:54"; + var profile = new Importprofile() { - "CuesheetArtist - CuesheetTitle c:\\tmp\\Testfile.mp3", - "Sample Artist 1 - Sample Title 1 00:05:00", - "Sample Artist 2 - Sample Title 2 00:09:23", - "Sample Artist 3 - Sample Title 3 00:15:54", - "Sample Artist 4 - Sample Title 4 00:20:13", - "Sample Artist 5 - Sample Title 5 00:24:54", - "Sample Artist 6 - Sample Title 6 00:31:54", - "Sample Artist 7 - Sample Title 7 00:45:54", - "Sample Artist 8 - Sample Title 8 01:15:54" + SchemeCuesheet = $"{nameof(ImportCuesheet.Artist)} - {nameof(ImportCuesheet.Title)}\t{nameof(ImportCuesheet.Audiofile)}" }; - var textImportScheme = new TextImportScheme() + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = null + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); Assert.IsNotNull(importfile.FileContentRecognized); + var lines = importfile.FileContentRecognized.Split(Environment.NewLine); Assert.AreEqual(string.Format("{0} - {1} {2}", string.Format(CuesheetConstants.RecognizedMarkHTML, "CuesheetArtist"), string.Format(CuesheetConstants.RecognizedMarkHTML, "CuesheetTitle"), - string.Format(CuesheetConstants.RecognizedMarkHTML, "c:\\tmp\\Testfile.mp3")), importfile.FileContentRecognized.First()); + string.Format(CuesheetConstants.RecognizedMarkHTML, "c:\\tmp\\Testfile.mp3")), lines.First()); Assert.AreEqual("CuesheetArtist", importfile.AnalysedCuesheet.Artist); Assert.AreEqual("CuesheetTitle", importfile.AnalysedCuesheet.Title); Assert.AreEqual("c:\\tmp\\Testfile.mp3", importfile.AnalysedCuesheet.Audiofile); Assert.AreEqual(0, importfile.AnalysedCuesheet.Tracks.Count); - Assert.AreEqual("Sample Artist 1 - Sample Title 1 00:05:00", importfile.FileContentRecognized.ElementAt(1)); - Assert.AreEqual("Sample Artist 2 - Sample Title 2 00:09:23", importfile.FileContentRecognized.ElementAt(2)); - Assert.AreEqual("Sample Artist 3 - Sample Title 3 00:15:54", importfile.FileContentRecognized.ElementAt(3)); - Assert.AreEqual("Sample Artist 4 - Sample Title 4 00:20:13", importfile.FileContentRecognized.ElementAt(4)); - Assert.AreEqual("Sample Artist 5 - Sample Title 5 00:24:54", importfile.FileContentRecognized.ElementAt(5)); - Assert.AreEqual("Sample Artist 6 - Sample Title 6 00:31:54", importfile.FileContentRecognized.ElementAt(6)); - Assert.AreEqual("Sample Artist 7 - Sample Title 7 00:45:54", importfile.FileContentRecognized.ElementAt(7)); - Assert.AreEqual("Sample Artist 8 - Sample Title 8 01:15:54", importfile.FileContentRecognized.ElementAt(8)); + Assert.AreEqual("Sample Artist 1 - Sample Title 1 00:05:00", lines.ElementAt(1)); + Assert.AreEqual("Sample Artist 2 - Sample Title 2 00:09:23", lines.ElementAt(2)); + Assert.AreEqual("Sample Artist 3 - Sample Title 3 00:15:54", lines.ElementAt(3)); + Assert.AreEqual("Sample Artist 4 - Sample Title 4 00:20:13", lines.ElementAt(4)); + Assert.AreEqual("Sample Artist 5 - Sample Title 5 00:24:54", lines.ElementAt(5)); + Assert.AreEqual("Sample Artist 6 - Sample Title 6 00:31:54", lines.ElementAt(6)); + Assert.AreEqual("Sample Artist 7 - Sample Title 7 00:45:54", lines.ElementAt(7)); + Assert.AreEqual("Sample Artist 8 - Sample Title 8 01:15:54", lines.ElementAt(8)); } [TestMethod()] - public void Analyse_CuesheetWithoutTracks_CreatesValidFileContentRecognized() + public async Task AnalyseAsync_CuesheetWithoutTracks_CreatesValidFileContentRecognizedAsync() { // Arrange - var fileContent = new List - { - "CuesheetArtist - CuesheetTitle c:\\tmp\\Testfile.mp3", - "Sample Artist 1 - Sample Title 1 00:05:00", - "Sample Artist 2 - Sample Title 2 00:09:23", - "Sample Artist 3 - Sample Title 3 00:15:54", - "Sample Artist 4 - Sample Title 4 00:20:13", - "Sample Artist 5 - Sample Title 5 00:24:54", - "Sample Artist 6 - Sample Title 6 00:31:54", - "Sample Artist 7 - Sample Title 7 00:45:54", - "Sample Artist 8 - Sample Title 8 01:15:54" - }; - var textImportScheme = new TextImportScheme() + var fileContent = @"CuesheetArtist - CuesheetTitle c:\tmp\Testfile.mp3 +Sample Artist 1 - Sample Title 1 00:05:00 +Sample Artist 2 - Sample Title 2 00:09:23 +Sample Artist 3 - Sample Title 3 00:15:54 +Sample Artist 4 - Sample Title 4 00:20:13 +Sample Artist 5 - Sample Title 5 00:24:54 +Sample Artist 6 - Sample Title 6 00:31:54 +Sample Artist 7 - Sample Title 7 00:45:54 +Sample Artist 8 - Sample Title 8 01:15:54"; + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = TextImportScheme.DefaultSchemeCuesheet, - SchemeTracks = TextImportScheme.DefaultSchemeTracks + SelectedImportProfile = ImportOptions.DefaultSelectedImportprofile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); @@ -461,39 +506,225 @@ public void Analyse_CuesheetWithoutTracks_CreatesValidFileContentRecognized() Assert.AreEqual("Sample Artist 1", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Artist); Assert.AreEqual("Sample Title 1", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Title); Assert.AreEqual(new TimeSpan(0, 5, 0), importfile.AnalysedCuesheet.Tracks.ElementAt(0).End); + var lines = importfile.FileContentRecognized.Split(Environment.NewLine); Assert.AreEqual(string.Format("{0} - {1} {2}", string.Format(CuesheetConstants.RecognizedMarkHTML, "Sample Artist 8"), string.Format(CuesheetConstants.RecognizedMarkHTML, "Sample Title 8"), - string.Format(CuesheetConstants.RecognizedMarkHTML, "01:15:54")), importfile.FileContentRecognized.Last()); + string.Format(CuesheetConstants.RecognizedMarkHTML, "01:15:54")), lines.Last()); } [TestMethod()] - public void Analyse_TextfileBug233_CreatesValidFileContentRecognized() + public async Task AnalyseAsync_TextfileBug233_CreatesValidFileContentRecognizedAsync() { // Arrange - var textImportMemoryStream = new MemoryStream(Resources.Textimport_Bug__233); - using var reader = new StreamReader(textImportMemoryStream); - List lines = []; - while (reader.EndOfStream == false) + var profile = new Importprofile() { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); - var textImportScheme = new TextImportScheme() + SchemeTracks = $"{nameof(ImportTrack.Artist)} - {nameof(ImportTrack.Title)}\t{nameof(ImportTrack.End)}" + }; + var textImportMemoryStream = new MemoryStream(Resources.Textimport_Bug__233); + using var reader = new StreamReader(textImportMemoryStream); + var fileContent = reader.ReadToEnd(); + var localStorageOptionsProviderMock = new Mock(); + var options = new ImportOptions { - SchemeCuesheet = null, - SchemeTracks = TextImportScheme.DefaultSchemeTracks + SelectedImportProfile = profile }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); // Act - var importfile = TextImportService.Analyse(textImportScheme, fileContent); + var importfile = await service.AnalyseAsync(fileContent); // Assert Assert.IsNull(importfile.AnalyseException); Assert.IsNotNull(importfile.AnalysedCuesheet); Assert.IsNotNull(importfile.FileContentRecognized); + var lines = importfile.FileContentRecognized.Split(Environment.NewLine); Assert.AreEqual(string.Format("{0} - {1}\t\t\t\t\t\t\t\t{2}", string.Format(CuesheetConstants.RecognizedMarkHTML, "Age Of Love"), string.Format(CuesheetConstants.RecognizedMarkHTML, "The Age Of Love (Charlotte De Witte & Enrico Sangiuliano Remix)"), - string.Format(CuesheetConstants.RecognizedMarkHTML, "04:29:28")), importfile.FileContentRecognized.ElementAt(53)); + string.Format(CuesheetConstants.RecognizedMarkHTML, "04:29:28")), lines.ElementAt(53)); + } + + [TestMethod()] + public async Task AnalyseAsync_WithRegularExpression_ReturnsCuesheetAsync() + { + // Arrange + var profile = new Importprofile() + { + UseRegularExpression = true, + SchemeTracks = "\\s*\\d+\\s*(?.*?)\\s*(?.*?)</td>\\s*<td>(?<StartDateTime>.*?)</td>\\s*</tr>" + }; + var textImportMemoryStream = new MemoryStream(Resources.Traktor_Export); + var reader = new StreamReader(textImportMemoryStream); + var fileContent = reader.ReadToEnd(); + var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ImportOptions>()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); + // Act + var importfile = await service.AnalyseAsync(fileContent); + // Assert + Assert.AreEqual(fileContent, importfile.FileContent); + Assert.IsNull(importfile.AnalyseException); + Assert.IsNotNull(importfile.AnalysedCuesheet); + Assert.IsNull(importfile.AnalysedCuesheet.Artist); + Assert.IsNull(importfile.AnalysedCuesheet.Title); + Assert.AreEqual(41, importfile.AnalysedCuesheet.Tracks.Count); + Assert.AreEqual("Nachap", importfile.AnalysedCuesheet.Tracks.First().Artist); + Assert.AreEqual("Glass", importfile.AnalysedCuesheet.Tracks.First().Title); + Assert.AreEqual(new DateTime(2025, 1, 29, 18, 52, 10), importfile.AnalysedCuesheet.Tracks.First().StartDateTime); + Assert.AreEqual("Inache", importfile.AnalysedCuesheet.Tracks.Last().Artist); + Assert.AreEqual("Andale (MONTA (TN) Remix)", importfile.AnalysedCuesheet.Tracks.Last().Title); + Assert.AreEqual(new DateTime(2025, 1, 29, 22, 30, 3), importfile.AnalysedCuesheet.Tracks.Last().StartDateTime); + Assert.IsNotNull(importfile.FileContentRecognized); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "Sasha Fashion"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "2025/1/29 21:48:55"))); + } + + [TestMethod()] + public async Task AnalyseAsync_WithoutRegularExpression_ReturnsCuesheetAsync() + { + // Arrange + var profile = new Importprofile() + { + UseRegularExpression = false, + SchemeCuesheet = "Artist - Title - Cataloguenumber", + SchemeTracks = "Artist - Title\tBegin" + }; + var textImportMemoryStream = new MemoryStream(Resources.Textimport_with_Cuesheetdata); + var reader = new StreamReader(textImportMemoryStream); + var fileContent = reader.ReadToEnd(); + var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ImportOptions>()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); + // Act + var importfile = await service.AnalyseAsync(fileContent); + // Assert + Assert.IsNull(importfile.AnalyseException); + Assert.IsNotNull(importfile.AnalysedCuesheet); + Assert.AreEqual("DJFreezeT", importfile.AnalysedCuesheet.Artist); + Assert.AreEqual("Rabbit Hole Mix", importfile.AnalysedCuesheet.Title); + Assert.AreEqual("0123456789123", importfile.AnalysedCuesheet.Cataloguenumber); + Assert.AreEqual("Adriatique", importfile.AnalysedCuesheet.Tracks.First().Artist); + Assert.AreEqual("X.", importfile.AnalysedCuesheet.Tracks.First().Title); + Assert.AreEqual(new TimeSpan(0, 0, 5, 24, 250), importfile.AnalysedCuesheet.Tracks.First().Begin); + Assert.AreEqual("Nikolay Kirov", importfile.AnalysedCuesheet.Tracks.Last().Artist); + Assert.AreEqual("Chasing the Sun (Original Mix)", importfile.AnalysedCuesheet.Tracks.Last().Title); + Assert.IsNull(importfile.AnalysedCuesheet.Tracks.Last().Begin); + Assert.IsNotNull(importfile.FileContentRecognized); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "DJFreezeT"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "Rabbit Hole Mix"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "0123456789123"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "Nikolay Kirov"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "Chasing the Sun (Original Mix)"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "SHDW & Obscure Shape"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "Wächter der Nacht (Original Mix)"))); + } + + [TestMethod()] + public async Task AnalyseAsync_TextfileWithStartDateTime_CreatesValidCuesheetAsync() + { + // Arrange + var fileContent = $@"Innellea~The Golden Fort~{new DateTime(2024, 8, 14, 20, 10, 48)} +Nora En Pure~Diving with Whales (Daniel Portman Remix)~{new DateTime(2024, 8, 14, 20, 15, 21)} +WhoMadeWho & Adriatique~Miracle (RÜFÜS DU SOL Remix)~{new DateTime(2024, 8, 14, 20, 20, 42)} +Ella Wild~Poison D'araignee (Original Mix)~{new DateTime(2024, 8, 14, 20, 28, 03)} +Stil & Bense~On The Edge (Original Mix)~{new DateTime(2024, 8, 14, 20, 32, 42)} +Nebula~Clairvoyant Dreams~{new DateTime(2024, 8, 14, 20, 39, 1)} +Valentina Black~I'm a Tree (Extended Mix)~{new DateTime(2024, 8, 14, 20, 47, 08)} +Nebula~Clairvoyant Dreams~{new DateTime(2024, 8, 14, 20, 53, 20)} +Kiko & Dave Davis feat. Phoebe~Living in Space (Dub Mix)~{new DateTime(2024, 8, 14, 20, 58, 11)} +Lilly Palmer~Before Acid~{new DateTime(2024, 8, 14, 21, 03, 53)} +Sofi Tukker~Drinkee (Vintage Culture & John Summit Extended Mix)~{new DateTime(2024, 8, 14, 21, 09, 52)} +CID & Truth x Lies~Caroline (Extended Mix)~{new DateTime(2024, 8, 14, 21, 14, 09)} +Moby~Why Does My Heart Feel So Bad? (Oxia Remix)~{new DateTime(2024, 8, 14, 21, 17, 15)} +Ammo Avenue~Little Gurl (Extended Mix)~{new DateTime(2024, 8, 14, 21, 22, 46)} +James Hurr & Smokin Jo & Stealth~Beggin' For Change~{new DateTime(2024, 8, 14, 21, 28, 37)} +Kristine Blond~Love Shy (Sam Divine & CASSIMM Extended Remix)~{new DateTime(2024, 8, 14, 21, 30, 47)} +Vanilla Ace~Work On You (Original Mix)~{new DateTime(2024, 8, 14, 21, 36, 28)} +Truth X Lies~Like This~{new DateTime(2024, 8, 14, 21, 42, 05)} +Terri-Anne~Round Round~{new DateTime(2024, 8, 14, 21, 44, 07)} +Joanna Magik~Maneater~{new DateTime(2024, 8, 14, 21, 46, 32)} +Jen Payne & Kevin McKay~Feed Your Soul~1{new DateTime(2024, 8, 14, 21, 48, 45)} +Kevin McKay & Eppers & Notelle~On My Own~{new DateTime(2024, 8, 14, 21, 51, 37)} +Nader Razdar & Kevin McKay~Get Ur Freak On (Kevin McKay Extended Mix)~{new DateTime(2024, 8, 14, 21, 53, 49)} +Philip Z~Yala (Extended Mix)~{new DateTime(2024, 8, 14, 21, 59, 40)} +Kyle Kinch & Kevin McKay~Hella~{new DateTime(2024, 8, 14, 22, 05, 53)} +Roze Wild~B-O-D-Y~{new DateTime(2024, 8, 14, 22, 08, 26)} +Jey Kurmis~Snoop~{new DateTime(2024, 8, 14, 22, 11, 09)} +Bootie Brown & Tame Impala & Gorillaz~New Gold (Dom Dolla Remix Extended)~{new DateTime(2024, 8, 14, 22, 16, 23)} +Eli Brown & Love Regenerator~Don't You Want Me (Original Mix)~{new DateTime(2024, 8, 14, 22, 21, 23)} +Local Singles~Voices~{new DateTime(2024, 8, 14, 22, 25, 59)}"; + + var profile = new Importprofile() + { + UseRegularExpression = false, + SchemeTracks = $"{nameof(ImportTrack.Artist)}~{nameof(ImportTrack.Title)}~{nameof(ImportTrack.StartDateTime)}" + }; + var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ImportOptions>()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); + // Act + var importfile = await service.AnalyseAsync(fileContent); + // Assert + Assert.IsNull(importfile.AnalyseException); + Assert.IsNotNull(importfile.AnalysedCuesheet); + Assert.AreEqual(30, importfile.AnalysedCuesheet.Tracks.Count); + Assert.AreEqual("Innellea", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Artist); + Assert.AreEqual("The Golden Fort", importfile.AnalysedCuesheet.Tracks.ElementAt(0).Title); + Assert.AreEqual(new DateTime(2024, 8, 14, 20, 10, 48), importfile.AnalysedCuesheet.Tracks.ElementAt(0).StartDateTime); + Assert.AreEqual("Nora En Pure", importfile.AnalysedCuesheet.Tracks.ElementAt(1).Artist); + Assert.AreEqual("Diving with Whales (Daniel Portman Remix)", importfile.AnalysedCuesheet.Tracks.ElementAt(1).Title); + Assert.AreEqual(new DateTime(2024, 8, 14, 20, 15, 21), importfile.AnalysedCuesheet.Tracks.ElementAt(1).StartDateTime); + Assert.AreEqual("Local Singles", importfile.AnalysedCuesheet.Tracks.ElementAt(29).Artist); + Assert.AreEqual("Voices", importfile.AnalysedCuesheet.Tracks.ElementAt(29).Title); + Assert.AreEqual(new DateTime(2024, 8, 14, 22, 25, 59), importfile.AnalysedCuesheet.Tracks.ElementAt(29).StartDateTime); + } + + [TestMethod()] + public async Task AnalyseAsync_WithCommonDataMatchingMultipleLines_SetsCommonDataOnceAsync() + { + // Arrange + var profile = new Importprofile() + { + UseRegularExpression = false, + SchemeCuesheet = "Artist - Title\tAudiofile", + SchemeTracks = "Artist - Title\tBegin" + }; + var textImportMemoryStream = new MemoryStream(Resources.Sample_Inputfile); + var reader = new StreamReader(textImportMemoryStream); + var fileContent = reader.ReadToEnd(); + var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); + var options = new ImportOptions + { + SelectedImportProfile = profile + }; + localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ImportOptions>()).ReturnsAsync(options); + var service = new TextImportService(localStorageOptionsProviderMock.Object); + // Act + var importfile = await service.AnalyseAsync(fileContent); + // Assert + Assert.IsNull(importfile.AnalyseException); + Assert.IsNotNull(importfile.AnalysedCuesheet); + Assert.AreEqual("CuesheetArtist", importfile.AnalysedCuesheet.Artist); + Assert.AreEqual("CuesheetTitle", importfile.AnalysedCuesheet.Title); + Assert.AreEqual("c:\\AudioFile.mp3", importfile.AnalysedCuesheet.Audiofile); + Assert.AreEqual(8, importfile.AnalysedCuesheet.Tracks.Count); + Assert.IsNotNull(importfile.FileContentRecognized); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "CuesheetArtist"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "CuesheetTitle"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "c:\\AudioFile.mp3"))); + Assert.IsTrue(importfile.FileContentRecognized.Contains(String.Format(CuesheetConstants.RecognizedMarkHTML, "Sample Artist 8"))); } } } \ No newline at end of file diff --git a/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs b/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs index 76c4e24b..155ed70c 100644 --- a/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs +++ b/AudioCuesheetEditor.Tests/Services/UI/TraceChangeManagerTests.cs @@ -13,21 +13,13 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //<http: //www.gnu.org/licenses />. -using AudioCuesheetEditor.Data.Options; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.Entity; -using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.Utility; -using AudioCuesheetEditor.Services.IO; using AudioCuesheetEditor.Services.UI; -using AudioCuesheetEditor.Tests.Properties; using AudioCuesheetEditor.Tests.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; using System; using System.Collections.Generic; -using System.IO; using System.Linq; namespace AudioCuesheetEditor.Tests.Services.UI @@ -164,115 +156,6 @@ public void TrackListTest() Assert.IsFalse(manager.CanRedo); } - [TestMethod()] - public void Import_ValidTextfile_IsUndoable() - { - // Arrange - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger<TraceChangeManager>()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var textImportMemoryStream = new MemoryStream(Resources.Textimport_with_Cuesheetdata); - using var reader = new StreamReader(textImportMemoryStream); - List<string?> lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); - var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})", - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions(); - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ApplicationOptions>()).ReturnsAsync(options); - var fileInputManagerMock = new Mock<IFileInputManager>(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - // Act - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - // Assert - Assert.IsFalse(traceChangeManager.CanUndo); - Assert.IsFalse(traceChangeManager.CanRedo); - Assert.IsNotNull(sessionStateContainer.ImportCuesheet); - Assert.AreEqual("DJFreezeT", sessionStateContainer.ImportCuesheet.Artist); - Assert.AreEqual("0123456789123", sessionStateContainer.ImportCuesheet.Cataloguenumber); - Assert.AreNotEqual(0, sessionStateContainer.ImportCuesheet.Tracks.Count); - } - - [TestMethod()] - public void UndoImport_ValidTextfile_ResetsToEmptyCuesheet() - { - // Arrange - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger<TraceChangeManager>()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var textImportMemoryStream = new MemoryStream(Resources.Textimport_with_Cuesheetdata); - using var reader = new StreamReader(textImportMemoryStream); - List<string?> lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); - var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})", - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions(); - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ApplicationOptions>()).ReturnsAsync(options); - var fileInputManagerMock = new Mock<IFileInputManager>(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - importManager.ImportCuesheet(); - // Act - traceChangeManager.Undo(); - // Assert - Assert.AreEqual(0, sessionStateContainer.Cuesheet.Tracks.Count); - Assert.IsTrue(string.IsNullOrEmpty(sessionStateContainer.Cuesheet.Artist)); - Assert.IsTrue(string.IsNullOrEmpty(sessionStateContainer.Cuesheet.Cataloguenumber)); - Assert.IsFalse(traceChangeManager.CanUndo); - Assert.IsTrue(traceChangeManager.CanRedo); - } - - [TestMethod()] - public void UndoAndRedoImport_ValidTextfile_ResetsTextfileValues() - { - // Arrange - var testhelper = new TestHelper(); - var traceChangeManager = new TraceChangeManager(TestHelper.CreateLogger<TraceChangeManager>()); - var sessionStateContainer = new SessionStateContainer(traceChangeManager); - var textImportMemoryStream = new MemoryStream(Resources.Textimport_with_Cuesheetdata); - using var reader = new StreamReader(textImportMemoryStream); - List<string?> lines = []; - while (reader.EndOfStream == false) - { - lines.Add(reader.ReadLine()); - } - var fileContent = lines.AsReadOnly(); - var localStorageOptionsProviderMock = new Mock<ILocalStorageOptionsProvider>(); - var textImportScheme = new TextImportScheme() - { - SchemeCuesheet = "(?'Artist'\\A.*) - (?'Title'[a-zA-Z0-9_ .();äöü&:,]{1,}) - (?'Cataloguenumber'.{1,})", - SchemeTracks = TextImportScheme.DefaultSchemeTracks - }; - var timeSpanFormat = new TimeSpanFormat(); - var options = new ApplicationOptions(); - localStorageOptionsProviderMock.Setup(x => x.GetOptionsAsync<ApplicationOptions>()).ReturnsAsync(options); - var fileInputManagerMock = new Mock<IFileInputManager>(); - var importManager = new ImportManager(sessionStateContainer, localStorageOptionsProviderMock.Object, traceChangeManager, fileInputManagerMock.Object); - importManager.ImportText(fileContent, textImportScheme, timeSpanFormat); - traceChangeManager.Undo(); - // Act - traceChangeManager.Redo(); - // Assert - Assert.AreEqual("DJFreezeT", sessionStateContainer.ImportCuesheet?.Artist); - Assert.AreEqual("0123456789123", sessionStateContainer.ImportCuesheet?.Cataloguenumber); - Assert.AreEqual(39, sessionStateContainer.ImportCuesheet?.Tracks.Count); - } - [TestMethod()] public void RemoveTracksTest() { diff --git a/AudioCuesheetEditor/AudioCuesheetEditor.csproj b/AudioCuesheetEditor/AudioCuesheetEditor.csproj index 826022e8..9e25d5a0 100644 --- a/AudioCuesheetEditor/AudioCuesheetEditor.csproj +++ b/AudioCuesheetEditor/AudioCuesheetEditor.csproj @@ -7,7 +7,7 @@ <ImplicitUsings>enable</ImplicitUsings> <PackageProjectUrl>https://github.com/NeoCoderMatrix86/AudioCuesheetEditor</PackageProjectUrl> <RazorLangVersion>3.0</RazorLangVersion> - <Version>9.2.0</Version> + <Version>10.0.0</Version> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <PublishTrimmed>true</PublishTrimmed> <RunAOTCompilation>true</RunAOTCompilation> @@ -20,17 +20,18 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="HtmlSanitizer" Version="9.0.886" /> <PackageReference Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" /> - <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" /> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.8" /> + <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.8" PrivateAssets="all" /> <PackageReference Include="BlazorDownloadFile" Version="2.4.0.2" /> <PackageReference Include="Howler.Blazor" Version="0.9.8" /> - <PackageReference Include="Markdig" Version="0.41.1" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" /> - <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.5" /> - <PackageReference Include="MudBlazor" Version="8.6.0" /> + <PackageReference Include="Markdig" Version="0.41.3" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" /> + <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="9.0.8" /> + <PackageReference Include="MudBlazor" Version="8.11.0" /> <PackageReference Include="Toolbelt.Blazor.HotKeys2" Version="6.0.1" /> - <PackageReference Include="z440.atl.core" Version="6.24.0" /> + <PackageReference Include="z440.atl.core" Version="7.3.0" /> </ItemGroup> <ItemGroup> diff --git a/AudioCuesheetEditor/Model/AudioCuesheet/CuesheetConstants.cs b/AudioCuesheetEditor/Model/AudioCuesheet/CuesheetConstants.cs index 2994ab3d..f0121747 100644 --- a/AudioCuesheetEditor/Model/AudioCuesheet/CuesheetConstants.cs +++ b/AudioCuesheetEditor/Model/AudioCuesheet/CuesheetConstants.cs @@ -31,6 +31,7 @@ public class CuesheetConstants public static readonly String Tab = "\t"; public static readonly String CuesheetCDTextfile = "CDTEXTFILE"; public static readonly String CuesheetCatalogueNumber = "CATALOG"; - public static readonly String RecognizedMarkHTML = "<Mark>{0}</Mark>"; + public static readonly String MarkHTMLStart = "<Mark>"; + public static readonly String RecognizedMarkHTML = MarkHTMLStart + "{0}" + "</Mark>"; } } diff --git a/AudioCuesheetEditor/Model/IO/FileExtensions.cs b/AudioCuesheetEditor/Model/IO/FileExtensions.cs index 439bf6df..980f574d 100644 --- a/AudioCuesheetEditor/Model/IO/FileExtensions.cs +++ b/AudioCuesheetEditor/Model/IO/FileExtensions.cs @@ -21,5 +21,6 @@ public class FileExtensions public const string Projectfile = ".ace"; public const string Cuesheet = ".cue"; public const string CDTextfile = ".cdt"; + public const string HTML = ".html"; } } diff --git a/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs b/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs index 1dc3c20d..37483831 100644 --- a/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs +++ b/AudioCuesheetEditor/Model/IO/FileMimeTypes.cs @@ -17,9 +17,9 @@ namespace AudioCuesheetEditor.Model.IO { public static class FileMimeTypes { - public const string Text = "text/plain"; + public const string TextPlain = "text/plain"; public const string Projectfile = "application/x-ace"; public const string Cuesheet = "application/x-cue"; - public const string CDTextfile = "text/*"; + public const string Text = "text/*"; } } diff --git a/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs b/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs index b99d07b0..a10f1d99 100644 --- a/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs +++ b/AudioCuesheetEditor/Model/IO/Import/IImportfile.cs @@ -21,15 +21,15 @@ namespace AudioCuesheetEditor.Model.IO.Import public interface IImportfile { /// <summary> - /// File content (each element is a file line) + /// File content /// </summary> - IEnumerable<String?>? FileContent { get; set; } + String? FileContent { get; set; } /// <summary> /// File content with marking which passages has been reconized by scheme /// </summary> - IEnumerable<String?>? FileContentRecognized { get; set; } + String? FileContentRecognized { get; set; } /// <summary> - /// Exception that has been thrown while readinng out the file + /// Exception that has been thrown while reading out the file /// </summary> Exception? AnalyseException { get; set; } /// <summary> diff --git a/AudioCuesheetEditor/Model/IO/Import/Importfile.cs b/AudioCuesheetEditor/Model/IO/Import/Importfile.cs index 033e1989..c0726e8e 100644 --- a/AudioCuesheetEditor/Model/IO/Import/Importfile.cs +++ b/AudioCuesheetEditor/Model/IO/Import/Importfile.cs @@ -20,14 +20,14 @@ namespace AudioCuesheetEditor.Model.IO.Import { public class Importfile : IImportfile { - /// <inheritdoc /> - public IEnumerable<String?>? FileContent { get; set; } - /// <inheritdoc /> - public IEnumerable<String?>? FileContentRecognized { get; set; } /// <inheritdoc /> public Exception? AnalyseException { get; set; } /// <inheritdoc /> public ImportCuesheet? AnalysedCuesheet { get; set; } public ImportFileType FileType { get; set; } + /// <inheritdoc /> + public string? FileContentRecognized { get; set; } + /// <inheritdoc /> + public string? FileContent { get; set; } } } diff --git a/AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs b/AudioCuesheetEditor/Model/IO/Import/Importprofile.cs similarity index 76% rename from AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs rename to AudioCuesheetEditor/Model/IO/Import/Importprofile.cs index e63c3fce..b1f824c4 100644 --- a/AudioCuesheetEditor/Model/IO/Import/TextImportScheme.cs +++ b/AudioCuesheetEditor/Model/IO/Import/Importprofile.cs @@ -16,55 +16,26 @@ using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.Entity; +using AudioCuesheetEditor.Model.Utility; namespace AudioCuesheetEditor.Model.IO.Import { - public class TextImportScheme : Validateable + public class Importprofile : Validateable { public static readonly IEnumerable<String> AvailableSchemeCuesheet; public static readonly IEnumerable<String> AvailableSchemesTrack; - static TextImportScheme() + static Importprofile() { AvailableSchemeCuesheet = [nameof(Cuesheet.Artist), nameof(Cuesheet.Title), nameof(Cuesheet.Audiofile), nameof(Cuesheet.CDTextfile), nameof(Cuesheet.Cataloguenumber)]; AvailableSchemesTrack = [nameof(Track.Artist), nameof(Track.Title), nameof(Track.Begin), nameof(Track.End), nameof(Track.Length), nameof(Track.Position), nameof(Track.Flags), nameof(Track.PreGap), nameof(Track.PostGap), nameof(ImportTrack.StartDateTime)]; } - - public static readonly String DefaultSchemeCuesheet = @"(?'Artist'\w*) - (?'Title'\w*)\t{1,}(?'Audiofile'.*)"; - public static readonly String DefaultSchemeTracks = @"(?'Artist'.+?) - (?'Title'.+?)\s*\t+(?'End'.+)"; - - public static readonly TextImportScheme DefaultTextImportScheme = new() - { - SchemeCuesheet = DefaultSchemeCuesheet, - SchemeTracks = DefaultSchemeTracks - }; - - private string? schemeTracks; - private string? schemeCuesheet; - - public event EventHandler<String>? SchemeChanged; - - public String? SchemeTracks - { - get { return schemeTracks; } - set - { - schemeTracks = value; - SchemeChanged?.Invoke(this, nameof(SchemeTracks)); - OnValidateablePropertyChanged(); - } - } - - public String? SchemeCuesheet - { - get { return schemeCuesheet; } - set - { - schemeCuesheet = value; - SchemeChanged?.Invoke(this, nameof(SchemeCuesheet)); - OnValidateablePropertyChanged(); - } - } + public Guid Id { get; init; } = Guid.NewGuid(); + public String? Name { get; set; } + public Boolean UseRegularExpression { get; set; } + public String? SchemeCuesheet { get; set; } + public String? SchemeTracks { get; set; } + public TimeSpanFormat? TimeSpanFormat { get; set; } public override ValidationResult Validate(string property) { ValidationStatus validationStatus = ValidationStatus.NoValidation; diff --git a/AudioCuesheetEditor/Model/IO/Projectfile.cs b/AudioCuesheetEditor/Model/IO/Projectfile.cs index 075cd7ec..c54dace0 100644 --- a/AudioCuesheetEditor/Model/IO/Projectfile.cs +++ b/AudioCuesheetEditor/Model/IO/Projectfile.cs @@ -37,10 +37,9 @@ public class Projectfile(Cuesheet cuesheet) } }; - public static Cuesheet? ImportFile(byte[] fileContent) + public static Cuesheet? ImportFile(string fileContent) { - var json = Encoding.UTF8.GetString(fileContent); - return JsonSerializer.Deserialize<Cuesheet>(json, Options); + return JsonSerializer.Deserialize<Cuesheet>(fileContent, Options); } public Cuesheet Cuesheet { get; private set; } = cuesheet; diff --git a/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs b/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs index 97af692a..6773e70c 100644 --- a/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs +++ b/AudioCuesheetEditor/Model/Options/ApplicationOptions.cs @@ -13,10 +13,6 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //<http: //www.gnu.org/licenses />. -using AudioCuesheetEditor.Model.Entity; -using AudioCuesheetEditor.Model.IO; -using AudioCuesheetEditor.Model.IO.Export; -using AudioCuesheetEditor.Model.IO.Import; using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.UI; using System.Globalization; @@ -24,33 +20,9 @@ namespace AudioCuesheetEditor.Model.Options { - public enum ViewMode - { - DetailView = 0, - RecordView = 1, - ImportView = 2 - } - public class ApplicationOptions : Validateable, IOptions + public class ApplicationOptions : IOptions { public const LogLevel DefaultLogLevel = LogLevel.Information; - private string? projectFilename = Projectfile.DefaultFilename; - private string? cuesheetFilename = Exportfile.DefaultCuesheetFilename; - public String? CuesheetFilename - { - get => cuesheetFilename; - set - { - if (String.IsNullOrEmpty(value) == false) - { - var extension = Path.GetExtension(value); - if (extension?.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) - { - value = $"{value}{FileExtensions.Cuesheet}"; - } - } - cuesheetFilename = value; - } - } public String? CultureName { get; set; } = LocalizationService.DefaultCulture; [JsonIgnore] public CultureInfo Culture @@ -67,102 +39,10 @@ public CultureInfo Culture } } } - [JsonIgnore] - public ViewMode ActiveTab { get; set; } - public String? ActiveTabName - { - get => Enum.GetName(ActiveTab); - set - { - if (value != null) - { - ActiveTab = Enum.Parse<ViewMode>(value); - } - else - { - throw new ArgumentNullException(nameof(value)); - } - } - } - public String? ProjectFilename - { - get => projectFilename; - set - { - if (String.IsNullOrEmpty(value) == false) - { - var extension = Path.GetExtension(value); - if (extension?.Equals(FileExtensions.Projectfile, StringComparison.OrdinalIgnoreCase) == false) - { - value = $"{value}{FileExtensions.Projectfile}"; - } - } - projectFilename = value; - } - } public TimeSpanFormat? TimeSpanFormat { get; set; } - public Boolean LinkTracks { get; set; } = true; - public TextImportScheme ImportScheme { get; set; } = TextImportScheme.DefaultTextImportScheme; - public TimeSpanFormat ImportTimeSpanFormat { get; set; } = new(); - public uint RecordCountdownTimer { get; set; } = 5; + public Boolean DefaultIsLinkedToPreviousTrack { get; set; } = true; public Boolean FixedTracksTableHeader { get; set; } = false; public String? DisplayTimeSpanFormat { get; set; } public LogLevel MinimumLogLevel { get; set; } = DefaultLogLevel; - - public override ValidationResult Validate(string property) - { - ValidationStatus validationStatus = ValidationStatus.NoValidation; - List<ValidationMessage>? validationMessages = null; - switch (property) - { - case nameof(CuesheetFilename): - validationStatus = ValidationStatus.Success; - if (string.IsNullOrEmpty(CuesheetFilename)) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} has no value!", nameof(CuesheetFilename))); - } - else - { - var extension = Path.GetExtension(CuesheetFilename); - if (extension.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must end with '{1}'!", nameof(CuesheetFilename), FileExtensions.Cuesheet)); - } - var filenameWithoutExtension = Path.GetFileNameWithoutExtension(CuesheetFilename); - if (string.IsNullOrEmpty(filenameWithoutExtension)) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must have a filename!", nameof(CuesheetFilename))); - } - } - break; - case nameof(ProjectFilename): - validationStatus = ValidationStatus.Success; - if (String.IsNullOrEmpty(ProjectFilename)) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} has no value!", nameof(ProjectFilename))); - } - else - { - var extension = Path.GetExtension(ProjectFilename); - if (extension.Equals(FileExtensions.Projectfile, StringComparison.OrdinalIgnoreCase) == false) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must end with '{1}'!", nameof(ProjectFilename), FileExtensions.Projectfile)); - } - var filename = Path.GetFileNameWithoutExtension(ProjectFilename); - if (String.IsNullOrEmpty(filename)) - { - validationMessages ??= []; - validationMessages.Add(new ValidationMessage("{0} must have a filename!", nameof(ProjectFilename))); - } - } - break; - } - return ValidationResult.Create(validationStatus, validationMessages); - } } } diff --git a/AudioCuesheetEditor/Model/Options/DownloadOptions.cs b/AudioCuesheetEditor/Model/Options/DownloadOptions.cs new file mode 100644 index 00000000..b74e136c --- /dev/null +++ b/AudioCuesheetEditor/Model/Options/DownloadOptions.cs @@ -0,0 +1,114 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//<http: //www.gnu.org/licenses />. +using AudioCuesheetEditor.Model.Entity; +using AudioCuesheetEditor.Model.IO; +using AudioCuesheetEditor.Model.IO.Export; + +namespace AudioCuesheetEditor.Model.Options +{ + public class DownloadOptions : Validateable, IOptions + { + private string? projectFilename = Projectfile.DefaultFilename; + private string? cuesheetFilename = Exportfile.DefaultCuesheetFilename; + public String? CuesheetFilename + { + get => cuesheetFilename; + set + { + if (String.IsNullOrEmpty(value) == false) + { + var extension = Path.GetExtension(value); + if (extension?.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) + { + value = $"{value}{FileExtensions.Cuesheet}"; + } + } + cuesheetFilename = value; + } + } + public String? ProjectFilename + { + get => projectFilename; + set + { + if (String.IsNullOrEmpty(value) == false) + { + var extension = Path.GetExtension(value); + if (extension?.Equals(FileExtensions.Projectfile, StringComparison.OrdinalIgnoreCase) == false) + { + value = $"{value}{FileExtensions.Projectfile}"; + } + } + projectFilename = value; + } + } + public override ValidationResult Validate(string property) + { + ValidationStatus validationStatus = ValidationStatus.NoValidation; + List<ValidationMessage>? validationMessages = null; + switch (property) + { + case nameof(CuesheetFilename): + validationStatus = ValidationStatus.Success; + if (string.IsNullOrEmpty(CuesheetFilename)) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} has no value!", nameof(CuesheetFilename))); + } + else + { + var extension = Path.GetExtension(CuesheetFilename); + if (extension.Equals(FileExtensions.Cuesheet, StringComparison.OrdinalIgnoreCase) == false) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} must end with '{1}'!", nameof(CuesheetFilename), FileExtensions.Cuesheet)); + } + var filenameWithoutExtension = Path.GetFileNameWithoutExtension(CuesheetFilename); + if (string.IsNullOrEmpty(filenameWithoutExtension)) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} must have a filename!", nameof(CuesheetFilename))); + } + } + break; + case nameof(ProjectFilename): + validationStatus = ValidationStatus.Success; + if (String.IsNullOrEmpty(ProjectFilename)) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} has no value!", nameof(ProjectFilename))); + } + else + { + var extension = Path.GetExtension(ProjectFilename); + if (extension.Equals(FileExtensions.Projectfile, StringComparison.OrdinalIgnoreCase) == false) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} must end with '{1}'!", nameof(ProjectFilename), FileExtensions.Projectfile)); + } + var filename = Path.GetFileNameWithoutExtension(ProjectFilename); + if (String.IsNullOrEmpty(filename)) + { + validationMessages ??= []; + validationMessages.Add(new ValidationMessage("{0} must have a filename!", nameof(ProjectFilename))); + } + } + break; + } + return ValidationResult.Create(validationStatus, validationMessages); + } + } +} diff --git a/AudioCuesheetEditor/Model/Options/ExportOptions.cs b/AudioCuesheetEditor/Model/Options/ExportOptions.cs index eb36939b..dfa267a2 100644 --- a/AudioCuesheetEditor/Model/Options/ExportOptions.cs +++ b/AudioCuesheetEditor/Model/Options/ExportOptions.cs @@ -67,7 +67,17 @@ public ExportOptions(ICollection<Exportprofile> exportProfiles, Guid? selectedPr public Exportprofile? SelectedExportProfile { get => SelectedProfileId.HasValue ? ExportProfiles.FirstOrDefault(x => x.Id == SelectedProfileId) : null; - set => SelectedProfileId = value?.Id; + set + { + if (ExportProfiles.Any(x => x.Id == value?.Id) == false) + { + if (value != null) + { + ExportProfiles.Add(value); + } + } + SelectedProfileId = value?.Id; + } } public Guid? SelectedProfileId { get; private set; } } diff --git a/AudioCuesheetEditor/Model/Options/ImportOptions.cs b/AudioCuesheetEditor/Model/Options/ImportOptions.cs new file mode 100644 index 00000000..0b186b9f --- /dev/null +++ b/AudioCuesheetEditor/Model/Options/ImportOptions.cs @@ -0,0 +1,75 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//<http: //www.gnu.org/licenses />. +using AudioCuesheetEditor.Model.AudioCuesheet.Import; +using AudioCuesheetEditor.Model.IO.Import; +using System.Text.Json.Serialization; + +namespace AudioCuesheetEditor.Model.Options +{ + public class ImportOptions : IOptions + { + public static readonly Importprofile DefaultSelectedImportprofile = new() + { + Name = "Textfile (common data in first line)", + UseRegularExpression = false, + SchemeCuesheet = $"{nameof(ImportCuesheet.Artist)} - {nameof(ImportCuesheet.Title)}\t{nameof(ImportCuesheet.Audiofile)}", + SchemeTracks = $"{nameof(ImportTrack.Artist)} - {nameof(ImportTrack.Title)}\t{nameof(ImportTrack.End)}" + }; + + public static readonly ICollection<Importprofile> DefaultImportprofiles = + [ + DefaultSelectedImportprofile, + new() + { + Name = "Textfile (just track data)", + UseRegularExpression = false, + SchemeTracks = $"{nameof(ImportTrack.Artist)} - {nameof(ImportTrack.Title)}\t{nameof(ImportTrack.End)}" + }, + new() + { + Name = "Textfile (track data seperated by ~)", + UseRegularExpression = false, + SchemeTracks = $"{nameof(ImportTrack.Artist)}~{nameof(ImportTrack.Title)}~{nameof(ImportTrack.StartDateTime)}" + }, + new() + { + Name = "Traktor history", + UseRegularExpression = true, + SchemeTracks = @$"<tr>\s*<td>(?<{nameof(ImportTrack.Position)}>\d+)</td>\s*<td>(?<{nameof(ImportTrack.Artist)}>.*?)</td>\s*<td>(?<{nameof(ImportTrack.Title)}>.*?)</td>\s*<td>(?<{nameof(ImportTrack.StartDateTime)}>.*?)</td>\s*</tr>" + } + ]; + + [JsonInclude] + public Guid? SelectedImportProfileId { get; private set; } = DefaultSelectedImportprofile.Id; + public ICollection<Importprofile> ImportProfiles { get; set; } = DefaultImportprofiles; + [JsonIgnore] + public Importprofile? SelectedImportProfile + { + get => SelectedImportProfileId.HasValue ? ImportProfiles.FirstOrDefault(x => x.Id == SelectedImportProfileId) : null; + set + { + if (ImportProfiles.Any(x => x.Id == value?.Id) == false) + { + if (value != null) + { + ImportProfiles.Add(value); + } + } + SelectedImportProfileId = value?.Id; + } + } + } +} diff --git a/AudioCuesheetEditor/Model/Options/RecordOptions.cs b/AudioCuesheetEditor/Model/Options/RecordOptions.cs new file mode 100644 index 00000000..ab47a448 --- /dev/null +++ b/AudioCuesheetEditor/Model/Options/RecordOptions.cs @@ -0,0 +1,22 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//<http: //www.gnu.org/licenses />. +namespace AudioCuesheetEditor.Model.Options +{ + public class RecordOptions : IOptions + { + public uint RecordCountdownTimer { get; set; } = 5; + } +} diff --git a/AudioCuesheetEditor/Model/Options/ViewOptions.cs b/AudioCuesheetEditor/Model/Options/ViewOptions.cs new file mode 100644 index 00000000..3c1389e4 --- /dev/null +++ b/AudioCuesheetEditor/Model/Options/ViewOptions.cs @@ -0,0 +1,46 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//<http: //www.gnu.org/licenses />. +using System.Text.Json.Serialization; + +namespace AudioCuesheetEditor.Model.Options +{ + public enum ViewMode + { + DetailView = 0, + RecordView = 1, + ImportView = 2 + } + public class ViewOptions : IOptions + { + [JsonIgnore] + public ViewMode ActiveTab { get; set; } + public String? ActiveTabName + { + get => Enum.GetName(ActiveTab); + set + { + if (value != null) + { + ActiveTab = Enum.Parse<ViewMode>(value); + } + else + { + throw new ArgumentNullException(nameof(value)); + } + } + } + } +} diff --git a/AudioCuesheetEditor/Pages/Index.razor b/AudioCuesheetEditor/Pages/Index.razor index c60b59d9..aaa79b27 100644 --- a/AudioCuesheetEditor/Pages/Index.razor +++ b/AudioCuesheetEditor/Pages/Index.razor @@ -20,36 +20,33 @@ along with Foobar. If not, see @inherits BaseLocalizedComponent -@inject ISessionStateContainer _sessionStateContainer @inject IStringLocalizer<Index> _localizer +@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -<MudTabs Rounded ApplyEffectsToContainer Outlined Color="Color.Success" ActivePanelIndex="(int)GetViewMode()" ActivePanelIndexChanged="this.AsNonRenderingEventHandler<int>(ActiveTabIndexChanged)" KeepPanelsAlive> - <CascadingValue Value="GetViewMode()"> +<CascadingValue Value="currentViewmode"> + <MudTabs Rounded ApplyEffectsToContainer Outlined Color="Color.Success" ActivePanelIndex="(int)currentViewmode" ActivePanelIndexChanged="this.AsNonRenderingEventHandler<int>(ActiveTabIndexChanged)"> <MudTabPanel Text="@_localizer["Detail view"]" Icon="@Icons.Material.Outlined.Edit"> - <CascadingValue Value="_sessionStateContainer.Cuesheet"> - <ViewModeFull /> - </CascadingValue> + <ViewModeFull /> </MudTabPanel> <MudTabPanel Text="@_localizer["Record view"]" Icon="@Icons.Material.Outlined.Mic"> - <CascadingValue Value="_sessionStateContainer.Cuesheet"> - <ViewModeRecord /> - </CascadingValue> + <ViewModeRecord /> </MudTabPanel> <MudTabPanel Text="@_localizer["Import view"]" Icon="@Icons.Material.Outlined.ImportExport"> - <CascadingValue Value="_sessionStateContainer.ImportCuesheet"> - <ViewModeImport /> - </CascadingValue> + <ViewModeImport /> </MudTabPanel> - </CascadingValue> -</MudTabs> + </MudTabs> +</CascadingValue> @code{ + ViewOptions? options; + ViewMode currentViewmode = ViewMode.DetailView; + protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; - _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; - StateHasChanged(); + options = await _localStorageOptionsProvider.GetOptionsAsync<ViewOptions>(); + currentViewmode = options.ActiveTab; + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; } protected override void Dispose(bool disposing) @@ -57,34 +54,23 @@ along with Foobar. If not, see base.Dispose(disposing); if (disposing) { - _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; - _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; } } async Task ActiveTabIndexChanged(int tabIndex) { - var activeViewMode = (ViewMode)tabIndex; - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.ActiveTab, activeViewMode); - } - - void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); - } - - void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) - { - StateHasChanged(); + currentViewmode = (ViewMode)tabIndex; + await LocalStorageOptionsProvider.SaveOptionsValueAsync<ViewOptions>(x => x.ActiveTab, currentViewmode); } - ViewMode GetViewMode() + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) { - ViewMode viewMode = ViewMode.DetailView; - if (ApplicationOptions != null) + if (option is ViewOptions viewOptions) { - viewMode = ApplicationOptions.ActiveTab; + options = viewOptions; + currentViewmode = options.ActiveTab; + StateHasChanged(); } - return viewMode; } } \ No newline at end of file diff --git a/AudioCuesheetEditor/Program.cs b/AudioCuesheetEditor/Program.cs index a6a022fe..8d470618 100644 --- a/AudioCuesheetEditor/Program.cs +++ b/AudioCuesheetEditor/Program.cs @@ -27,10 +27,8 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using MudBlazor.Services; -using System.ComponentModel.Design; using System.Globalization; using System.Reflection; -using System.Reflection.Emit; using Toolbelt.Blazor.Extensions.DependencyInjection; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -52,7 +50,7 @@ builder.Services.AddScoped<ISessionStateContainer, SessionStateContainer>(); builder.Services.AddScoped<ITraceChangeManager, TraceChangeManager>(); builder.Services.AddScoped<ImportManager>(); -builder.Services.AddScoped<TextImportService>(); +builder.Services.AddScoped<ITextImportService, TextImportService>(); builder.Services.AddScoped<CuesheetImportService>(); builder.Services.AddScoped<CuesheetExportService>(); builder.Services.AddScoped<ApplicationOptionsTimeSpanParser>(); @@ -60,7 +58,7 @@ builder.Services.AddScoped<ValidationService>(); builder.Services.AddScoped<IFileInputManager, FileInputManager>(); builder.Services.AddScoped<PlaybackService>(); -builder.Services.AddScoped<EditTrackModalManager>(); +builder.Services.AddScoped<DialogManager>(); builder.Services.AddScoped<ExportfileGenerator>(); builder.Services.AddScoped<AutocompleteManager>(); diff --git a/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs b/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs index 47ad4494..9f6af21d 100644 --- a/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs +++ b/AudioCuesheetEditor/Services/IO/CuesheetImportService.cs @@ -16,16 +16,18 @@ using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO.Import; +using System.Text; using System.Text.RegularExpressions; namespace AudioCuesheetEditor.Services.IO { public class CuesheetImportService { - public static IImportfile Analyse(IEnumerable<String?> fileContent) + public static IImportfile Analyse(string fileContent) { Importfile importfile = new() { + FileContent = fileContent, FileType = ImportFileType.Cuesheet }; try @@ -55,11 +57,9 @@ public static IImportfile Analyse(IEnumerable<String?> fileContent) var regexCDTextfile = new Regex("^" + CuesheetConstants.CuesheetCDTextfile + " \"(?'" + cuesheetCDTextfileGroupName + "'.{0,})\""); var regexCatalogueNumber = new Regex("^" + CuesheetConstants.CuesheetCatalogueNumber + " (?'" + cuesheetCatalogueNumberGroupName + "'.{0,})"); ImportTrack? track = null; - List<String?> lines = []; - List<String?>? recognizedLines = []; - foreach (var line in fileContent) + StringBuilder recognizedContent = new(); + foreach (var line in fileContent.Split(Environment.NewLine)) { - lines.Add(line); String? recognizedLine = line; if (String.IsNullOrEmpty(line) == false) { @@ -277,10 +277,9 @@ public static IImportfile Analyse(IEnumerable<String?> fileContent) } } } - recognizedLines.Add(recognizedLine); + recognizedContent.AppendLine(recognizedLine); } - importfile.FileContent = lines.AsReadOnly(); - importfile.FileContentRecognized = recognizedLines.AsReadOnly(); + importfile.FileContentRecognized = recognizedContent.ToString().TrimEnd(Environment.NewLine.ToCharArray()); } catch (Exception ex) { diff --git a/AudioCuesheetEditor/Services/IO/FileInputManager.cs b/AudioCuesheetEditor/Services/IO/FileInputManager.cs index d3ef5d12..96129e13 100644 --- a/AudioCuesheetEditor/Services/IO/FileInputManager.cs +++ b/AudioCuesheetEditor/Services/IO/FileInputManager.cs @@ -27,7 +27,7 @@ public class FileInputManager(IJSRuntime jsRuntime, HttpClient httpClient, ILogg private readonly HttpClient _httpClient = httpClient; private readonly ILogger<FileInputManager> _logger = logger; - public static AudioCodec? GetAudioCodec(IBrowserFile browserFile) + public AudioCodec? GetAudioCodec(IBrowserFile browserFile) { AudioCodec? foundAudioCodec = null; var extension = Path.GetExtension(browserFile.Name); @@ -37,7 +37,7 @@ public class FileInputManager(IJSRuntime jsRuntime, HttpClient httpClient, ILogg { foundAudioCodec = audioCodecsFound.FirstOrDefault(); } - else + if (foundAudioCodec == null) { // Second search with mime type or file extension audioCodecsFound = Audiofile.AudioCodecs.Where(x => x.MimeType.Equals(browserFile.ContentType, StringComparison.OrdinalIgnoreCase) || x.FileExtension.Equals(extension, StringComparison.OrdinalIgnoreCase)); @@ -46,21 +46,35 @@ public class FileInputManager(IJSRuntime jsRuntime, HttpClient httpClient, ILogg return foundAudioCodec; } - public Boolean CheckFileMimeType(IBrowserFile file, String mimeType, String fileExtension) + public bool IsValidAudiofile(IBrowserFile browserFile) { - _logger.LogDebug("CheckFileMimeType called with file: file.Name: '{FileName}', file.ContentType: '{ContentType}', mimeType: '{MimeType}', fileExtension: '{FileExtension}'", file.Name, file.ContentType, mimeType, fileExtension); + var codec = GetAudioCodec(browserFile); + return codec != null; + } + + public Boolean CheckFileMimeType(IBrowserFile file, String mimeType, IEnumerable<String> fileExtensions) + { + _logger.LogDebug("CheckFileMimeType called with file: file.Name: '{FileName}', file.ContentType: '{ContentType}', mimeType: '{MimeType}', fileExtensions: '{fileExtensions}'", file.Name, file.ContentType, mimeType, fileExtensions); Boolean fileMimeTypeMatches = false; - if ((file != null) && (String.IsNullOrEmpty(mimeType) == false) && (String.IsNullOrEmpty(fileExtension) == false)) + if ((file != null) && (String.IsNullOrEmpty(mimeType) == false)) { if (String.IsNullOrEmpty(file.ContentType) == false) { - fileMimeTypeMatches = file.ContentType.Equals(mimeType, StringComparison.CurrentCultureIgnoreCase); + if (mimeType.EndsWith("/*")) + { + var mainType = mimeType.Substring(0, mimeType.Length - 1); + fileMimeTypeMatches = file.ContentType.StartsWith(mainType, StringComparison.CurrentCultureIgnoreCase); + } + else + { + fileMimeTypeMatches = file.ContentType.Equals(mimeType, StringComparison.CurrentCultureIgnoreCase); + } } - if (fileMimeTypeMatches == false) + if ((fileMimeTypeMatches == false) && (fileExtensions.Any())) { //Try to find by file extension var extension = Path.GetExtension(file.Name); - fileMimeTypeMatches = extension.Equals(fileExtension, StringComparison.CurrentCultureIgnoreCase); + fileMimeTypeMatches = fileExtensions.Any(x => x.Equals(extension, StringComparison.CurrentCultureIgnoreCase)); } } return fileMimeTypeMatches; @@ -101,7 +115,7 @@ public Boolean CheckFileMimeType(IBrowserFile file, String mimeType, String file CDTextfile? cdTextfile = null; if (browserFile != null) { - if (CheckFileMimeType(browserFile, FileMimeTypes.CDTextfile, FileExtensions.CDTextfile)) + if (CheckFileMimeType(browserFile, FileMimeTypes.Text, [FileExtensions.CDTextfile])) { cdTextfile = new CDTextfile(browserFile.Name); } @@ -112,5 +126,11 @@ public Boolean CheckFileMimeType(IBrowserFile file, String mimeType, String file } return cdTextfile; } + + /// <inheritdoc/> + public bool IsValidForImportView(IBrowserFile browserFile) + { + return CheckFileMimeType(browserFile, FileMimeTypes.Text, [FileExtensions.Text, FileExtensions.HTML]); + } } } diff --git a/AudioCuesheetEditor/Services/IO/IFileInputManager.cs b/AudioCuesheetEditor/Services/IO/IFileInputManager.cs index 7a7326c8..6bf15fef 100644 --- a/AudioCuesheetEditor/Services/IO/IFileInputManager.cs +++ b/AudioCuesheetEditor/Services/IO/IFileInputManager.cs @@ -22,8 +22,16 @@ namespace AudioCuesheetEditor.Services.IO { public interface IFileInputManager { - bool CheckFileMimeType(IBrowserFile file, string mimeType, string fileExtension); + bool IsValidAudiofile(IBrowserFile browserFile); + AudioCodec? GetAudioCodec(IBrowserFile browserFile); + bool CheckFileMimeType(IBrowserFile file, string mimeType, IEnumerable<string> fileExtensions); Task<Audiofile?> CreateAudiofileAsync(string? fileInputId, IBrowserFile? browserFile, Action<Task<Stream>>? afterContentStreamLoaded = null); CDTextfile? CreateCDTextfile(IBrowserFile? browserFile); + /// <summary> + /// Checks if the file can be used for the import view + /// </summary> + /// <param name="browserFile"></param> + /// <returns></returns> + bool IsValidForImportView(IBrowserFile browserFile); } } \ No newline at end of file diff --git a/AudioCuesheetEditor/Services/IO/ITextImportService.cs b/AudioCuesheetEditor/Services/IO/ITextImportService.cs new file mode 100644 index 00000000..3f4f31a6 --- /dev/null +++ b/AudioCuesheetEditor/Services/IO/ITextImportService.cs @@ -0,0 +1,25 @@ +//This file is part of AudioCuesheetEditor. + +//AudioCuesheetEditor is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. + +//AudioCuesheetEditor is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. + +//You should have received a copy of the GNU General Public License +//along with Foobar. If not, see +//<http: //www.gnu.org/licenses />. + +using AudioCuesheetEditor.Model.IO.Import; + +namespace AudioCuesheetEditor.Services.IO +{ + public interface ITextImportService + { + Task<IImportfile> AnalyseAsync(string fileContent); + } +} \ No newline at end of file diff --git a/AudioCuesheetEditor/Services/IO/ImportManager.cs b/AudioCuesheetEditor/Services/IO/ImportManager.cs index eef38039..bedeb330 100644 --- a/AudioCuesheetEditor/Services/IO/ImportManager.cs +++ b/AudioCuesheetEditor/Services/IO/ImportManager.cs @@ -13,16 +13,14 @@ //You should have received a copy of the GNU General Public License //along with Foobar. If not, see //<http: //www.gnu.org/licenses />. -using AudioCuesheetEditor.Data.Options; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO; using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Import; -using AudioCuesheetEditor.Model.Options; -using AudioCuesheetEditor.Model.Utility; using AudioCuesheetEditor.Services.UI; using Microsoft.AspNetCore.Components.Forms; +using System.Diagnostics; namespace AudioCuesheetEditor.Services.IO { @@ -34,73 +32,105 @@ public enum ImportFileType Textfile, Audiofile } - public class ImportManager(ISessionStateContainer sessionStateContainer, ILocalStorageOptionsProvider localStorageOptionsProvider, ITraceChangeManager traceChangeManager, IFileInputManager fileInputManager) + public class ImportManager(ISessionStateContainer sessionStateContainer, ITraceChangeManager traceChangeManager, IFileInputManager fileInputManager, ITextImportService textImportService, ILogger<ImportManager> logger) { + private readonly ILogger<ImportManager> _logger = logger; private readonly ISessionStateContainer _sessionStateContainer = sessionStateContainer; - private readonly ILocalStorageOptionsProvider _localStorageOptionsProvider = localStorageOptionsProvider; private readonly ITraceChangeManager _traceChangeManager = traceChangeManager; private readonly IFileInputManager _fileInputManager = fileInputManager; + private readonly ITextImportService _textImportService = textImportService; - public async Task<Dictionary<IBrowserFile, ImportFileType>> ImportFilesAsync(IEnumerable<IBrowserFile> files) + public async Task ImportFilesAsync(IEnumerable<IBrowserFile> files) { - Dictionary<IBrowserFile, ImportFileType> importFileTypes = []; + var stopwatch = Stopwatch.StartNew(); foreach (var file in files) { - if (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Projectfile, FileExtensions.Projectfile)) + if (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Projectfile, [FileExtensions.Projectfile])) { - //TODO: can not be undone var fileContent = await ReadFileContentAsync(file); - var cuesheet = Projectfile.ImportFile(fileContent.ToArray()); - if (cuesheet != null) + fileContent.Position = 0; + using var reader = new StreamReader(fileContent); + var stringFileContent = reader.ReadToEnd(); + _sessionStateContainer.Importfile = new Importfile() { - _sessionStateContainer.Cuesheet = cuesheet; - } - importFileTypes.Add(file, ImportFileType.ProjectFile); + FileContent = stringFileContent, + FileContentRecognized = stringFileContent, + FileType = ImportFileType.ProjectFile + }; } - if (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Cuesheet, FileExtensions.Cuesheet)) + if (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Cuesheet, [FileExtensions.Cuesheet])) { var fileContent = await ReadFileContentAsync(file); fileContent.Position = 0; using var reader = new StreamReader(fileContent); - List<String?> lines = []; - while (reader.EndOfStream == false) + var stringFileContent = reader.ReadToEnd(); + _sessionStateContainer.Importfile = new Importfile() { - lines.Add(reader.ReadLine()); - } - ImportCuesheet(lines); - importFileTypes.Add(file, ImportFileType.Cuesheet); + FileContent = stringFileContent, + FileContentRecognized = stringFileContent, + FileType = ImportFileType.Cuesheet + }; } - if (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Text, FileExtensions.Text)) + if (_fileInputManager.IsValidForImportView(file)) { var fileContent = await ReadFileContentAsync(file); fileContent.Position = 0; using var reader = new StreamReader(fileContent); - List<String?> lines = []; - while (reader.EndOfStream == false) + var stringFileContent = reader.ReadToEnd(); + _sessionStateContainer.Importfile = new Importfile() { - lines.Add(reader.ReadLine()); - } - var options = await _localStorageOptionsProvider.GetOptionsAsync<ApplicationOptions>(); - ImportText([.. lines], options.ImportScheme, options.ImportTimeSpanFormat); - importFileTypes.Add(file, ImportFileType.Textfile); + FileContent = stringFileContent, + FileContentRecognized = stringFileContent, + FileType = ImportFileType.Textfile + }; } } - return importFileTypes; + stopwatch.Stop(); + _logger.LogDebug("ImportFilesAsync duration: {stopwatch.Elapsed}", stopwatch.Elapsed); } - public void ImportText(IEnumerable<String?> fileContent, TextImportScheme textImportScheme, TimeSpanFormat timeSpanFormat) + public async Task AnalyseImportfile() { - _sessionStateContainer.Importfile = TextImportService.Analyse(textImportScheme, fileContent, timeSpanFormat); - if (_sessionStateContainer.Importfile.AnalysedCuesheet != null) + var stopwatch = Stopwatch.StartNew(); + var fileContent = _sessionStateContainer.Importfile?.FileContent; + if (String.IsNullOrEmpty(fileContent) == false) { - var importCuesheet = new Cuesheet(); - CopyCuesheet(importCuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); - _sessionStateContainer.ImportCuesheet = importCuesheet; + switch (_sessionStateContainer.Importfile?.FileType) + { + case ImportFileType.ProjectFile: + _sessionStateContainer.Cuesheet = Projectfile.ImportFile(fileContent)!; + break; + case ImportFileType.Textfile: + _sessionStateContainer.Importfile = await _textImportService.AnalyseAsync(fileContent); + break; + case ImportFileType.Cuesheet: + _sessionStateContainer.Importfile = CuesheetImportService.Analyse(fileContent); + break; + } } + if (_sessionStateContainer.Importfile?.AnalysedCuesheet != null) + { + switch (_sessionStateContainer.Importfile?.FileType) + { + case ImportFileType.Textfile: + var importCuesheet = new Cuesheet(); + CopyCuesheet(importCuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); + _sessionStateContainer.ImportCuesheet = importCuesheet; + break; + case ImportFileType.Cuesheet: + _traceChangeManager.BulkEdit = true; + CopyCuesheet(_sessionStateContainer.Cuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); + _traceChangeManager.BulkEdit = false; + break; + } + } + stopwatch.Stop(); + _logger.LogDebug("ImportTextAsync duration: {stopwatch.Elapsed}", stopwatch.Elapsed); } public void ImportCuesheet() { + var stopwatch = Stopwatch.StartNew(); if (_sessionStateContainer.ImportCuesheet != null) { _traceChangeManager.BulkEdit = true; @@ -108,18 +138,10 @@ public void ImportCuesheet() _traceChangeManager.BulkEdit = false; } _sessionStateContainer.ResetImport(); + stopwatch.Stop(); + _logger.LogDebug("ImportCuesheet duration: {stopwatch.Elapsed}", stopwatch.Elapsed); } - private void ImportCuesheet(IEnumerable<String?> fileContent) - { - _sessionStateContainer.Importfile = CuesheetImportService.Analyse(fileContent); - if (_sessionStateContainer.Importfile.AnalysedCuesheet != null) - { - _traceChangeManager.BulkEdit = true; - CopyCuesheet(_sessionStateContainer.Cuesheet, _sessionStateContainer.Importfile.AnalysedCuesheet); - _traceChangeManager.BulkEdit = false; - } - } private static async Task<MemoryStream> ReadFileContentAsync(IBrowserFile file) { var fileContent = new MemoryStream(); diff --git a/AudioCuesheetEditor/Services/IO/TextImportService.cs b/AudioCuesheetEditor/Services/IO/TextImportService.cs index 5d46e3c4..c1ff815e 100644 --- a/AudioCuesheetEditor/Services/IO/TextImportService.cs +++ b/AudioCuesheetEditor/Services/IO/TextImportService.cs @@ -14,120 +14,184 @@ //along with Foobar. If not, see //<http: //www.gnu.org/licenses />. +using AudioCuesheetEditor.Data.Options; using AudioCuesheetEditor.Model.AudioCuesheet; using AudioCuesheetEditor.Model.AudioCuesheet.Import; using AudioCuesheetEditor.Model.IO.Audio; using AudioCuesheetEditor.Model.IO.Import; +using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Model.Utility; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; namespace AudioCuesheetEditor.Services.IO { - public class TextImportService + public class TextImportService(ILocalStorageOptionsProvider localStorageOptionsProvider) : ITextImportService { - public static IImportfile Analyse(TextImportScheme textImportScheme, IEnumerable<String?> fileContent, TimeSpanFormat? timeSpanFormat = null) + private readonly ILocalStorageOptionsProvider _localStorageOptionsProvider = localStorageOptionsProvider; + + public async Task<IImportfile> AnalyseAsync(string fileContent) { Importfile importfile = new() { + FileContent = fileContent, + FileContentRecognized = fileContent, + AnalysedCuesheet = new ImportCuesheet(), FileType = ImportFileType.Textfile }; try { - importfile.FileContent = fileContent; - importfile.AnalysedCuesheet = new ImportCuesheet(); - Boolean cuesheetRecognized = false; - List<String?> recognizedFileContent = []; - Regex? regExCuesheet = null, regExTracks = null; - if (String.IsNullOrEmpty(textImportScheme.SchemeCuesheet) == false) + var options = await _localStorageOptionsProvider.GetOptionsAsync<ImportOptions>(); + var importprofile = options.SelectedImportProfile ?? throw new InvalidOperationException("Selected import profiles is not set!"); + SearchForCuesheetData(ref importfile, fileContent, importprofile); + SearchForTrackData(ref importfile, fileContent, importprofile); + } + catch (Exception ex) + { + importfile.FileContentRecognized = fileContent; + importfile.AnalyseException = ex; + importfile.AnalysedCuesheet = null; + } + return importfile; + } + + private static string ApplyRegexAndMarkGroups(object entity, Regex regex, string input, TimeSpanFormat? timeSpanFormat) + { + return regex.Replace(input, match => + { + string result = match.Value; + var groupInfos = new List<(int RelIndex, int Length, string Value, string Key)>(); + for (int matchCounter = 1; matchCounter < match.Groups.Count; matchCounter++) + { + var group = match.Groups[matchCounter]; + var key = regex.GroupNameFromNumber(matchCounter); + if (!string.IsNullOrEmpty(key) && key != matchCounter.ToString()) + { + var property = entity.GetType().GetProperty(key, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (property != null) + { + SetValue(entity, property, group.Value, timeSpanFormat); + } + int relIndex = group.Index - match.Index; + groupInfos.Add((relIndex, group.Length, group.Value, key)); + } + } + if (groupInfos.Count == 0) + { + return result; + } + groupInfos.Sort((a, b) => b.RelIndex.CompareTo(a.RelIndex)); + var sb = new StringBuilder(result); + foreach (var (RelIndex, Length, Value, Key) in groupInfos) + { + sb.Remove(RelIndex, Length); + sb.Insert(RelIndex, string.Format(CuesheetConstants.RecognizedMarkHTML, Value)); + } + return sb.ToString(); + }); + } + + private static void SearchForCuesheetData(ref Importfile importfile, string fileContent, Importprofile importprofile) + { + if (string.IsNullOrWhiteSpace(importprofile.SchemeCuesheet) == false) + { + var cuesheet = importfile.AnalysedCuesheet; + Regex regex; + if (importprofile.UseRegularExpression == true) { - regExCuesheet = CreateCuesheetRegexPattern(textImportScheme.SchemeCuesheet); + regex = new Regex(importprofile.SchemeCuesheet, RegexOptions.Multiline); } - if (String.IsNullOrEmpty(textImportScheme.SchemeTracks) == false) + else { - regExTracks = CreateTrackRegexPattern(textImportScheme.SchemeTracks); + regex = CreateCuesheetRegexPattern(importprofile.SchemeCuesheet); } - foreach (var line in fileContent) + + if (importprofile.UseRegularExpression) { - var recognizedLine = line; - if (String.IsNullOrEmpty(line) == false) + importfile.FileContentRecognized = ApplyRegexAndMarkGroups(cuesheet!, regex, fileContent, importprofile.TimeSpanFormat); + } + else + { + var sb = new StringBuilder(); + using (var reader = new StringReader(fileContent)) { - Boolean recognized = false; - if ((recognized == false) && (cuesheetRecognized == false) && (regExCuesheet != null)) - { - recognizedLine = AnalyseLine(line, importfile.AnalysedCuesheet, regExCuesheet, timeSpanFormat); - recognized = recognizedLine != null; - cuesheetRecognized = recognizedLine != null; - } - if ((recognized == false) && (regExTracks != null)) + string? line; + while ((line = reader.ReadLine()) != null) { - var track = new ImportTrack(); - recognizedLine = AnalyseLine(line, track, regExTracks, timeSpanFormat); - recognized = recognizedLine != null; - importfile.AnalysedCuesheet.Tracks.Add(track); + var markedLine = ApplyRegexAndMarkGroups(cuesheet!, regex, line, importprofile.TimeSpanFormat); + sb.AppendLine(markedLine); + if (!string.Equals(markedLine, line)) + { + //We found the first occurence, break the loop + //Attach the rest of the file to FileContentRecognized + sb.Append(reader.ReadToEnd()); + break; + } } } - recognizedFileContent.Add(recognizedLine); - } - if (recognizedFileContent.Count > 0) - { - importfile.FileContentRecognized = recognizedFileContent.AsReadOnly(); + importfile.FileContentRecognized = sb.ToString(); } } - catch (Exception ex) - { - importfile.FileContent = fileContent; - importfile.FileContentRecognized = fileContent; - importfile.AnalyseException = ex; - importfile.AnalysedCuesheet = null; - } - return importfile; } - private static String? AnalyseLine(String line, object entity, Regex regex, TimeSpanFormat? timeSpanFormat) + private static void SearchForTrackData(ref Importfile importfile, string fileContent, Importprofile importprofile) { - String? recognized = null; - string? recognizedLine = line; - if (String.IsNullOrEmpty(line) == false) + if (string.IsNullOrWhiteSpace(importprofile.SchemeTracks) == false) { - var match = regex.Match(line); - if (match.Success) + Regex regex; + if (importprofile.UseRegularExpression == true) + { + regex = new Regex(importprofile.SchemeTracks, RegexOptions.Multiline); + } + else + { + regex = CreateTrackRegexPattern(importprofile.SchemeTracks); + } + + var cuesheet = importfile.AnalysedCuesheet; + importfile.FileContentRecognized ??= fileContent; + if (importprofile.UseRegularExpression) + { + + importfile.FileContentRecognized = regex.Replace(importfile.FileContentRecognized, + match => + { + var track = new ImportTrack(); + string marked = ApplyRegexAndMarkGroups(track, regex, match.Value, importprofile.TimeSpanFormat); + cuesheet!.Tracks.Add(track); + return marked; + } + ); + } + else { - for (int groupCounter = 1; groupCounter < match.Groups.Count; groupCounter++) + var sb = new StringBuilder(); + using (var reader = new StringReader(importfile.FileContentRecognized)) { - var key = match.Groups.Keys.ElementAt(groupCounter); - var group = match.Groups.GetValueOrDefault(key); - if ((group != null) && (group.Success)) + string? line; + while ((line = reader.ReadLine()) != null) { - var property = entity.GetType().GetProperty(key, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (property != null) + // Check if this line is already analyzed + if (line.Contains(CuesheetConstants.MarkHTMLStart) == false) { - SetValue(entity, property, group.Value, timeSpanFormat); - recognizedLine = string.Concat(recognizedLine.AsSpan(0, group.Index + (13 * (groupCounter - 1))) - , String.Format(CuesheetConstants.RecognizedMarkHTML, group.Value) - , recognizedLine.AsSpan(group.Index + (13 * (groupCounter - 1)) + group.Length)); + var track = new ImportTrack(); + var markedLine = ApplyRegexAndMarkGroups(track, regex, line, importprofile.TimeSpanFormat); + if (!string.Equals(markedLine, line)) + { + cuesheet!.Tracks.Add(track); + } + sb.AppendLine(markedLine); } else { - throw new NullReferenceException(String.Format("Property '{0}' was not found for line content {1}", key, line)); + sb.AppendLine(line); } } - else - { - throw new NullReferenceException(String.Format("Group '{0}' could not be found!", key)); - } - } - if (recognizedLine.Contains(CuesheetConstants.RecognizedMarkHTML.Substring(0, CuesheetConstants.RecognizedMarkHTML.IndexOf("{0}")))) - { - recognized = recognizedLine; } - } - else - { - recognized = line; + importfile.FileContentRecognized = sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()); } } - return recognized; } private static void SetValue(object entity, PropertyInfo property, string value, TimeSpanFormat? timeSpanFormat) @@ -163,57 +227,135 @@ private static void SetValue(object entity, PropertyInfo property, string value, private static Regex CreateCuesheetRegexPattern(string scheme) { - var regex = new Regex(scheme); - var groupNames = regex.GetGroupNames(); - //GroupNames always has a group "0", so we count for more than one group - if (groupNames.Any(x => x != "0")) + string[] fieldNames = + [ + nameof(ImportCuesheet.Artist), + nameof(ImportCuesheet.Title), + nameof(ImportCuesheet.Audiofile), + nameof(ImportCuesheet.CDTextfile), + nameof(ImportCuesheet.Cataloguenumber) + ]; + var parts = new List<string>(); + int idx = 0; + while (idx < scheme.Length) { - return regex; + var field = fieldNames.FirstOrDefault(fn => scheme.IndexOf(fn, idx, StringComparison.Ordinal) == idx); + if (field != null) + { + parts.Add(field); + idx += field.Length; + } + else + { + int nextFieldIdx = scheme.Length; + foreach (var fn in fieldNames) + { + int pos = scheme.IndexOf(fn, idx, StringComparison.Ordinal); + if (pos >= 0 && pos < nextFieldIdx) + { + nextFieldIdx = pos; + } + } + string separator = scheme.Substring(idx, nextFieldIdx - idx); + parts.Add(separator); + idx = nextFieldIdx; + } } - else - { - var regexString = Regex.Escape(scheme); - regexString = regexString.Replace(nameof(Cuesheet.Artist), $"(?<{nameof(Cuesheet.Artist)}>.+)"); - regexString = regexString.Replace(nameof(Cuesheet.Title), $"(?<{nameof(Cuesheet.Title)}>.+)"); - regexString = regexString.Replace(nameof(Cuesheet.Audiofile), $"(?<{nameof(Cuesheet.Audiofile)}>.+)"); - regexString = regexString.Replace(nameof(Cuesheet.CDTextfile), $"(?<{nameof(Cuesheet.CDTextfile)}>.+)"); - regexString = regexString.Replace(nameof(Cuesheet.Cataloguenumber), $"(?<{nameof(Cuesheet.Cataloguenumber)}>.+)"); - //Replace tab with non matching group - regexString = regexString.Replace("\\t", "(?:...\\t)"); - - return new Regex(regexString); + var regexBuilder = new StringBuilder("^"); + for (int i = 0; i < parts.Count; i++) + { + var part = parts[i]; + if (fieldNames.Contains(part)) + { + bool isLast = i == parts.Count - 1 || parts.Skip(i + 1).All(p => !fieldNames.Contains(p)); + if (isLast) + { + regexBuilder.Append($@"(?<{part}>.+)"); + } + else + { + regexBuilder.Append($@"(?<{part}>.+?)"); + } + } + else + { + string sep = Regex.Escape(part).Replace("\\t", @"\t{1,}"); + regexBuilder.Append(sep); + } } + regexBuilder.Append('$'); + + return new Regex(regexBuilder.ToString()); } private static Regex CreateTrackRegexPattern(string scheme) { - var regex = new Regex(scheme); - var groupNames = regex.GetGroupNames(); - //GroupNames always has a group "0", so we count for more than one group - if (groupNames.Any(x => x != "0")) + string[] fieldNames = + [ + nameof(ImportTrack.Artist), + nameof(ImportTrack.Title), + nameof(ImportTrack.Begin), + nameof(ImportTrack.End), + nameof(ImportTrack.Length), + nameof(ImportTrack.Position), + nameof(ImportTrack.Flags), + nameof(ImportTrack.PreGap), + nameof(ImportTrack.PostGap), + nameof(ImportTrack.StartDateTime) + ]; + var parts = new List<string>(); + int idx = 0; + while (idx < scheme.Length) { - return regex; + var field = fieldNames.FirstOrDefault(fn => scheme.IndexOf(fn, idx, StringComparison.Ordinal) == idx); + if (field != null) + { + parts.Add(field); + idx += field.Length; + } + else + { + int nextFieldIdx = scheme.Length; + foreach (var fn in fieldNames) + { + int pos = scheme.IndexOf(fn, idx, StringComparison.Ordinal); + if (pos >= 0 && pos < nextFieldIdx) + { + nextFieldIdx = pos; + } + } + string separator = scheme.Substring(idx, nextFieldIdx - idx); + parts.Add(separator); + idx = nextFieldIdx; + } } - else - { - var regexString = Regex.Escape(scheme); - - regexString = regexString.Replace(nameof(ImportTrack.Artist), $"(?<{nameof(ImportTrack.Artist)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.Title), $"(?<{nameof(ImportTrack.Title)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.Begin), $"(?<{nameof(ImportTrack.Begin)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.End), $"(?<{nameof(ImportTrack.End)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.Length), $"(?<{nameof(ImportTrack.Length)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.Position), $"(?<{nameof(ImportTrack.Position)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.Flags), $"(?<{nameof(ImportTrack.Flags)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.PreGap), $"(?<{nameof(ImportTrack.PreGap)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.PostGap), $"(?<{nameof(ImportTrack.PostGap)}>.+)"); - regexString = regexString.Replace(nameof(ImportTrack.StartDateTime), $"(?<{nameof(ImportTrack.StartDateTime)}>.+)"); - //Replace tab with non matching group - regexString = regexString.Replace("\\t", "(?:...\\t)"); - - return new Regex(regexString); + + var regexBuilder = new StringBuilder("^"); + for (int i = 0; i < parts.Count; i++) + { + var part = parts[i]; + if (fieldNames.Contains(part)) + { + bool isLast = i == parts.Count - 1 || parts.Skip(i + 1).All(p => !fieldNames.Contains(p)); + if (isLast) + { + regexBuilder.Append($@"(?<{part}>.+)"); + } + else + { + regexBuilder.Append($@"(?<{part}>.+?)"); + } + } + else + { + string sep = Regex.Escape(part).Replace("\\t", @"\t{1,}"); + regexBuilder.Append(sep); + } } + regexBuilder.Append('$'); + + return new Regex(regexBuilder.ToString()); } } } diff --git a/AudioCuesheetEditor/Services/UI/EditTrackModalManager.cs b/AudioCuesheetEditor/Services/UI/DialogManager.cs similarity index 94% rename from AudioCuesheetEditor/Services/UI/EditTrackModalManager.cs rename to AudioCuesheetEditor/Services/UI/DialogManager.cs index 3320eeb1..cf10997f 100644 --- a/AudioCuesheetEditor/Services/UI/EditTrackModalManager.cs +++ b/AudioCuesheetEditor/Services/UI/DialogManager.cs @@ -20,13 +20,16 @@ namespace AudioCuesheetEditor.Services.UI { - public class EditTrackModalManager(IDialogService dialogService, ITraceChangeManager traceChangeManager) + public class DialogManager(IDialogService dialogService, ITraceChangeManager traceChangeManager) { private readonly IDialogService _dialogService = dialogService; private readonly ITraceChangeManager _traceChangeManager = traceChangeManager; + private IDialogReference? loadingDialog; + public async Task ShowAndHandleModalEditDialogAsync(IEnumerable<Track> tracks) { + _traceChangeManager.BulkEdit = true; if (tracks.Count() == 1) { var parameters = new DialogParameters<EditTrackModal> { { x => x.EditedTrack, tracks.First().Clone() } }; @@ -46,7 +49,6 @@ public async Task ShowAndHandleModalEditDialogAsync(IEnumerable<Track> tracks) var result = await dialog.Result; if ((result?.Canceled == false) && (result.Data is EditMultipleTracksModalResult editMultipleTracksModalResult)) { - _traceChangeManager.BulkEdit = true; foreach (var track in tracks) { var position = editMultipleTracksModalResult.EditedTrack.Position; @@ -173,9 +175,27 @@ public async Task ShowAndHandleModalEditDialogAsync(IEnumerable<Track> tracks) //Now copy all values track.CopyValues(editMultipleTracksModalResult.EditedTrack, setCuesheet: false, setIsLinkedToPreviousTrack: editMultipleTracksModalResult.IsLinkedToPreviousTrackChanged, setPosition: copyTrackPosition, setArtist: editMultipleTracksModalResult.ArtistChanged, setTitle: editMultipleTracksModalResult.TitleChanged, setBegin: copyTrackBegin, setEnd: copyTrackEnd, setLength: copyTrackLength, setFlags: editMultipleTracksModalResult.FlagsChanged, setPreGap: copyTrackPreGap, setPostGap: copyTrackPostGap); } - _traceChangeManager.BulkEdit = false; } } + _traceChangeManager.BulkEdit = false; + } + + public async Task ShowLoadingDialogAsync() + { + if (loadingDialog == null) + { + var options = new DialogOptions() { BackdropClick = false, FullWidth = true, MaxWidth = MaxWidth.ExtraSmall, NoHeader = true }; + loadingDialog = await _dialogService.ShowAsync<LoadingDialog>(null, options); + } + } + + public void HideLoadingDialog() + { + if (loadingDialog != null) + { + loadingDialog.Close(); + loadingDialog = null; + } } } } diff --git a/AudioCuesheetEditor/Services/Validation/ValidationService.cs b/AudioCuesheetEditor/Services/Validation/ValidationService.cs index 5c005ed0..1857efbd 100644 --- a/AudioCuesheetEditor/Services/Validation/ValidationService.cs +++ b/AudioCuesheetEditor/Services/Validation/ValidationService.cs @@ -23,32 +23,35 @@ public class ValidationService(IStringLocalizer<ValidationMessage> localizer) private readonly IStringLocalizer<ValidationMessage> _localizer = localizer; public Func<object, string, Task<IEnumerable<string>>> ValidateProperty => (model, propertyName) => Task.FromResult(Validate(model, propertyName)); - public IEnumerable<string> Validate(object model, string propertyName) + public IEnumerable<string> Validate(object? model, string propertyName) { - List<string> errors = []; - if (model is IValidateable validateable) + IEnumerable<string> errors = []; + if (model != null) { - var validationResult = validateable.Validate(propertyName); - switch (validationResult?.Status) + if (model is IValidateable validateable) { - case ValidationStatus.NoValidation: - case ValidationStatus.Success: - // Nothing to do - break; + var validationResult = validateable.Validate(propertyName); + switch (validationResult?.Status) + { + case ValidationStatus.NoValidation: + case ValidationStatus.Success: + // Nothing to do + break; - case ValidationStatus.Error: - errors = validationResult.ValidationMessages.Select(x => x.GetMessageLocalized(_localizer)).ToList(); - break; + case ValidationStatus.Error: + errors = [.. validationResult.ValidationMessages.Select(x => x.GetMessageLocalized(_localizer))]; + break; - default: - throw new InvalidOperationException("Unknown validation status."); + default: + throw new InvalidOperationException("Unknown validation status."); + } + } + else + { + throw new NotSupportedException(string.Format("Model was not of supposed type '{0}'", nameof(IValidateable))); } } - else - { - throw new NotSupportedException(string.Format("Model was not of supposed type '{0}'", nameof(IValidateable))); - } - return errors.AsEnumerable(); + return errors; } } } diff --git a/AudioCuesheetEditor/Shared/AppBar.razor b/AudioCuesheetEditor/Shared/AppBar.razor index c4413df8..2f3ff923 100644 --- a/AudioCuesheetEditor/Shared/AppBar.razor +++ b/AudioCuesheetEditor/Shared/AppBar.razor @@ -24,6 +24,8 @@ along with Foobar. If not, see @inject ISessionStateContainer _sessionStateContainer @inject IJSRuntime _jsRuntime @inject HotKeys _hotKeys +@inject DialogManager _dialogManager +@inject ImportManager _importManager <MudAppBar Elevation="1" Color="Color.Dark" Fixed> <MudButton StartIcon="@Icons.Material.Outlined.Home" Color="Color.Inherit" OnClick="() => _navigationManager.NavigateTo(_navigationManager.BaseUri)"> @@ -35,10 +37,10 @@ along with Foobar. If not, see { <MudButtonGroup Color="Color.Primary" Variant="Variant.Filled" OverrideStyles> <MudTooltip Text="@_localizer["Undo last edit"]"> - <MudIconButton Icon="@Icons.Material.Outlined.Undo" Disabled="!TraceChangeManager.CanUndo" OnClick="() => TraceChangeManager.Undo()" /> + <MudIconButton Icon="@Icons.Material.Outlined.Undo" aria-label="undo" Disabled="!TraceChangeManager.CanUndo" OnClick="() => TraceChangeManager.Undo()" /> </MudTooltip> <MudTooltip Text="@_localizer["Redo last edit"]"> - <MudIconButton Icon="@Icons.Material.Outlined.Redo" Disabled="!TraceChangeManager.CanRedo" OnClick="() => TraceChangeManager.Redo()" /> + <MudIconButton Icon="@Icons.Material.Outlined.Redo" aria-label="redo" Disabled="!TraceChangeManager.CanRedo" OnClick="() => TraceChangeManager.Redo()" /> </MudTooltip> </MudButtonGroup> } @@ -185,7 +187,14 @@ along with Foobar. If not, see async Task OpenFileClicked() { var options = new DialogOptions() { BackdropClick = false, CloseButton = true }; - await _dialogService.ShowAsync<SelectFileDialog>(_localizer["Select file"], options); + var dialog = await _dialogService.ShowAsync<SelectFileDialog>(_localizer["Select file"], options); + var result = await dialog.Result; + if (result?.Canceled == false) + { + await _dialogManager.ShowLoadingDialogAsync(); + await _importManager.AnalyseImportfile(); + _dialogManager.HideLoadingDialog(); + } } async Task DisplayHotkeys() diff --git a/AudioCuesheetEditor/Shared/BaseLocalizedComponent.cs b/AudioCuesheetEditor/Shared/BaseLocalizedComponent.cs index f4c32d05..5a31acad 100644 --- a/AudioCuesheetEditor/Shared/BaseLocalizedComponent.cs +++ b/AudioCuesheetEditor/Shared/BaseLocalizedComponent.cs @@ -14,7 +14,6 @@ //along with Foobar. If not, see //<http: //www.gnu.org/licenses />. using AudioCuesheetEditor.Data.Options; -using AudioCuesheetEditor.Model.Options; using AudioCuesheetEditor.Services.UI; using Microsoft.AspNetCore.Components; @@ -31,8 +30,6 @@ public abstract class BaseLocalizedComponent : ComponentBase, IDisposable [Inject] protected ILocalStorageOptionsProvider LocalStorageOptionsProvider { get; set; } = default!; - public ApplicationOptions? ApplicationOptions { get; private set; } - protected override void OnInitialized() { base.OnInitialized(); @@ -42,22 +39,6 @@ protected override void OnInitialized() TraceChangeManager.RedoDone += TraceChangeManager_RedoDone; } - protected override async Task OnInitializedAsync() - { - await base.OnInitializedAsync(); - ApplicationOptions = await LocalStorageOptionsProvider.GetOptionsAsync<ApplicationOptions>(); - LocalStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; - } - - void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) - { - if (option is ApplicationOptions applicationOptions) - { - ApplicationOptions = applicationOptions; - StateHasChanged(); - } - } - void TraceChangeManager_RedoDone(object? sender, EventArgs e) { StateHasChanged(); @@ -85,9 +66,9 @@ protected virtual void Dispose(bool disposing) if (disposing) { LocalizationService.LocalizationChanged -= LocalizationService_LocalizationChanged; + TraceChangeManager.TracedObjectHistoryChanged -= TraceChangeManager_TracedObjectHistoryChanged; TraceChangeManager.UndoDone -= TraceChangeManager_UndoDone; TraceChangeManager.RedoDone -= TraceChangeManager_RedoDone; - LocalStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; } disposedValue = true; diff --git a/AudioCuesheetEditor/Shared/Cuesheet/CuesheetData.razor b/AudioCuesheetEditor/Shared/Cuesheet/CuesheetData.razor index d9ce11b1..fbd724d0 100644 --- a/AudioCuesheetEditor/Shared/Cuesheet/CuesheetData.razor +++ b/AudioCuesheetEditor/Shared/Cuesheet/CuesheetData.razor @@ -15,7 +15,6 @@ You should have received a copy of the GNU General Public License along with Foobar. If not, see <http: //www.gnu.org/licenses />. --> - @inherits BaseLocalizedComponent @inject IStringLocalizer<CuesheetData> _localizer @@ -23,6 +22,7 @@ along with Foobar. If not, see @inject IFileInputManager _fileInputManager @inject IBlazorDownloadFileService _blazorDownloadFileService @inject IDialogService _dialogService +@inject ISessionStateContainer _sessionStateContainer @if (Cuesheet != null) { @@ -36,15 +36,26 @@ along with Foobar. If not, see <FileInput Id="@fileInputAudiofileId" Label="@_localizer["Audiofile"]" FileName="@Cuesheet.Audiofile?.Name" OnFileSelected="OnAudiofileSelected" Error="@fileInputAudiofileErrorText" Filter="@String.Join(",", Audiofile.AudioCodecs.Select(x => x.MimeType))" DisplayDownloadFile OnDownloadFileClicked="DownloadAudio" OnFileRenameClicked="AudioFileRename" FileRenameDisabled="Cuesheet.Audiofile == null" FileDownloadDisabled="Cuesheet.Audiofile != null ? Cuesheet.Audiofile.IsContentStreamLoaded == false : true" /> <FileInput Label="@_localizer["CD Textfile"]" FileName="@Cuesheet.CDTextfile?.Name" OnFileSelected="OnCDTextfileSelected" Filter="@FileExtensions.CDTextfile" Error="@fileInputCDTextfileErrorText" OnFileRenameClicked="CDTextFileRename" FileRenameDisabled="Cuesheet.CDTextfile == null" /> - <MudTextField @bind-Value="Cuesheet.Cataloguenumber" For="(() => Cuesheet.Cataloguenumber)" Validation="_validationService.ValidateProperty" Label="@_localizer["Cataloguenumber"]" Placeholder="@_localizer["Enter the cuesheet catalogue number here"]" Variant="Variant.Outlined" /> + <MudTextField @ref="catalogueNumberTextField" @bind-Value="catalogueNumber" For="(() => Cuesheet.Cataloguenumber)" Validation="_validationService.ValidateProperty" + Label="@_localizer["Cataloguenumber"]" Placeholder="@_localizer["Enter the cuesheet catalogue number here"]" Variant="Variant.Outlined" + Mask="@(new PatternMask("00 00000 00000 0"))" OnBlur="CataloguenumberOnBlur" /> break; } </MudForm> } @code { - [CascadingParameter] - public Cuesheet? Cuesheet { get; set; } + public Cuesheet? Cuesheet + { + get + { + if (CurrentViewMode == ViewMode.ImportView) + { + return _sessionStateContainer.ImportCuesheet; + } + return _sessionStateContainer.Cuesheet; + } + } [CascadingParameter] public ViewMode CurrentViewMode { get; set; } @@ -52,7 +63,9 @@ along with Foobar. If not, see string? fileInputAudiofileId; string? fileInputAudiofileErrorText; string? fileInputCDTextfileErrorText; + string? catalogueNumber; MudForm? form; + MudTextField<string>? catalogueNumberTextField; protected override void OnInitialized() { @@ -61,6 +74,19 @@ along with Foobar. If not, see { fileInputAudiofileId = $"Input_Audiofile_{Guid.NewGuid()}"; } + _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; + _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; + TraceChangeManager.UndoDone += TraceChangeManager_UndoDone; + TraceChangeManager.RedoDone += TraceChangeManager_RedoDone; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; + _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; + TraceChangeManager.UndoDone -= TraceChangeManager_UndoDone; + TraceChangeManager.RedoDone -= TraceChangeManager_RedoDone; } protected override void OnParametersSet() @@ -70,6 +96,7 @@ along with Foobar. If not, see { SetAudiofileValidationText(); } + catalogueNumber = Cuesheet?.Cataloguenumber; } async Task OnAudiofileSelected(IBrowserFile? browserFile) @@ -191,4 +218,34 @@ along with Foobar. If not, see return null; } } + + void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) + { + StateHasChanged(); + } + + void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) + { + StateHasChanged(); + } + + void CataloguenumberOnBlur() + { + if (Cuesheet != null) + { + Cuesheet.Cataloguenumber = catalogueNumber?.Replace(" ", string.Empty); + } + } + + void TraceChangeManager_RedoDone(object? sender, EventArgs e) + { + catalogueNumber = Cuesheet?.Cataloguenumber; + catalogueNumberTextField?.ResetAsync(); + } + + void TraceChangeManager_UndoDone(object? sender, EventArgs e) + { + catalogueNumber = Cuesheet?.Cataloguenumber; + catalogueNumberTextField?.ResetAsync(); + } } diff --git a/AudioCuesheetEditor/Shared/Cuesheet/EditSections.razor b/AudioCuesheetEditor/Shared/Cuesheet/EditSections.razor index 412434c1..9bf716f4 100644 --- a/AudioCuesheetEditor/Shared/Cuesheet/EditSections.razor +++ b/AudioCuesheetEditor/Shared/Cuesheet/EditSections.razor @@ -21,6 +21,8 @@ along with Foobar. If not, see @inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser @inject ValidationService _validationService @inject IDialogService _dialogService +@inject IFileInputManager _fileInputManager +@inject ISessionStateContainer _sessionStateContainer <MudButtonGroup OverrideStyles="false"> <MudTooltip Text="@_localizer["Add new section"]"> @@ -83,7 +85,19 @@ along with Foobar. If not, see HashSet<CuesheetSection> selectedSections = new(); [CascadingParameter] - public Cuesheet? Cuesheet { get; set; } + public ViewMode CurrentViewMode { get; set; } + + public Cuesheet? Cuesheet + { + get + { + if (CurrentViewMode == ViewMode.ImportView) + { + return _sessionStateContainer.ImportCuesheet; + } + return _sessionStateContainer.Cuesheet; + } + } void AddSectionClicked() { @@ -128,13 +142,9 @@ along with Foobar. If not, see void AudiofileSelected(CuesheetSection section, IBrowserFile? browserFile) { - if (browserFile != null) + if ((browserFile != null) && (_fileInputManager.IsValidAudiofile(browserFile) == true)) { - var codec = FileInputManager.GetAudioCodec(browserFile); - if (codec != null) - { - section.AudiofileName = browserFile?.Name; - } + section.AudiofileName = browserFile?.Name; } else { diff --git a/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor index a9726b40..df89f7cb 100644 --- a/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor +++ b/AudioCuesheetEditor/Shared/Dialogs/DownloadProjectfileDialog.razor @@ -21,10 +21,11 @@ along with Foobar. If not, see @inject ISessionStateContainer _sessionStateContainer @inject IBlazorDownloadFileService _blazorDownloadFileService @inject ValidationService _validationService +@inject ILocalStorageOptionsProvider _localStorageOptionsProvider <MudDialog> <DialogContent> - <MudTextField T="string" Label="@_localizer["Filename"]" Variant="Variant.Outlined" Text="@ApplicationOptions?.ProjectFilename" TextChanged="FilenameChanged" + <MudTextField T="string" Label="@_localizer["Filename"]" Variant="Variant.Outlined" Text="@options?.ProjectFilename" TextChanged="FilenameChanged" Error="String.IsNullOrEmpty(GetValidationErrorMessage()) == false" ErrorText="@GetValidationErrorMessage()" /> </DialogContent> <DialogActions> @@ -37,25 +38,40 @@ along with Foobar. If not, see [CascadingParameter] private IMudDialogInstance? MudDialog { get; set; } + DownloadOptions? options; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + options = await _localStorageOptionsProvider.GetOptionsAsync<DownloadOptions>(); + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; + } + async Task DownloadProjectClick() { MudDialog?.Close(); var projectFile = new Projectfile(_sessionStateContainer.Cuesheet); var fileData = projectFile.GenerateFile(); - await _blazorDownloadFileService.DownloadFile(ApplicationOptions?.ProjectFilename, fileData, "text/plain"); + await _blazorDownloadFileService.DownloadFile(options?.ProjectFilename, fileData, "text/plain"); } async Task FilenameChanged(string newFilename) { - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.ProjectFilename, newFilename); + await LocalStorageOptionsProvider.SaveOptionsValueAsync<DownloadOptions>(x => x.ProjectFilename, newFilename); } String? GetValidationErrorMessage() { String? validationErrorMessage = null; - if (ApplicationOptions != null) + if (options != null) { - var validationMessages = _validationService.Validate(ApplicationOptions, nameof(ApplicationOptions.ProjectFilename)); + var validationMessages = _validationService.Validate(options, nameof(options.ProjectFilename)); if (validationMessages.Count() > 0) { validationErrorMessage = String.Join(Environment.NewLine, validationMessages); @@ -63,4 +79,13 @@ along with Foobar. If not, see } return validationErrorMessage; } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is DownloadOptions downloadOptions) + { + options = downloadOptions; + StateHasChanged(); + } + } } diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor index a83a3abb..5dec7351 100644 --- a/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateCuesheetDialog.razor @@ -22,6 +22,7 @@ along with Foobar. If not, see @inject ValidationService _validationService @inject IBlazorDownloadFileService _blazorDownloadFileService @inject CuesheetExportService _cuesheetExportService +@inject ILocalStorageOptionsProvider _localStorageOptionsProvider <MudDialog> <DialogContent> @@ -32,7 +33,7 @@ along with Foobar. If not, see @((MarkupString)GetGenerationValidationMessages()!) </MudAlert> } - <MudTextField T="string" Label="@_localizer["Filename"]" Variant="Variant.Outlined" Text="@ApplicationOptions?.CuesheetFilename" TextChanged="CuesheetFilenameChanged" + <MudTextField T="string" Label="@_localizer["Filename"]" Variant="Variant.Outlined" Text="@options?.CuesheetFilename" TextChanged="CuesheetFilenameChanged" Error="String.IsNullOrEmpty(GetValidationErrorMessage()) == false" ErrorText="@GetValidationErrorMessage()" /> <br /> <MudTable Items="exportfiles"> @@ -55,19 +56,28 @@ along with Foobar. If not, see @code { IEnumerable<Exportfile> exportfiles = []; + DownloadOptions? options; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - exportfiles = _cuesheetExportService.GenerateExportfiles(ApplicationOptions?.CuesheetFilename); + options = await _localStorageOptionsProvider.GetOptionsAsync<DownloadOptions>(); + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + exportfiles = _cuesheetExportService.GenerateExportfiles(options?.CuesheetFilename); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; } String? GetValidationErrorMessage() { String? validationErrorMessage = null; - if (ApplicationOptions != null) + if (options != null) { - var validationMessages = _validationService.Validate(ApplicationOptions, nameof(ApplicationOptions.CuesheetFilename)); + var validationMessages = _validationService.Validate(options, nameof(DownloadOptions.CuesheetFilename)); if (validationMessages.Count() > 0) { validationErrorMessage = String.Join(Environment.NewLine, validationMessages); @@ -79,7 +89,7 @@ along with Foobar. If not, see String? GetGenerationValidationMessages() { String? validationErrorMessage = null; - var messages = _cuesheetExportService.CanGenerateExportfiles(ApplicationOptions?.CuesheetFilename); + var messages = _cuesheetExportService.CanGenerateExportfiles(options?.CuesheetFilename); if (messages.Count() > 0) { validationErrorMessage = String.Join("<br />", messages.Select(x => x.GetMessageLocalized(_validationMessageLocalizer))); @@ -89,7 +99,16 @@ along with Foobar. If not, see async Task CuesheetFilenameChanged(string newFilename) { - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.CuesheetFilename, newFilename); - exportfiles = _cuesheetExportService.GenerateExportfiles(ApplicationOptions?.CuesheetFilename); + await LocalStorageOptionsProvider.SaveOptionsValueAsync<DownloadOptions>(x => x.CuesheetFilename, newFilename); + exportfiles = _cuesheetExportService.GenerateExportfiles(options?.CuesheetFilename); + } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is DownloadOptions downloadOptions) + { + options = downloadOptions; + StateHasChanged(); + } } } diff --git a/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor index 5dff4a2e..a854140a 100644 --- a/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor +++ b/AudioCuesheetEditor/Shared/Dialogs/GenerateExportDialog.razor @@ -48,59 +48,66 @@ along with Foobar. If not, see </MudButtonGroup> <br /> <MudDivider /> - <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.Name" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.Name, newValue)" Error="String.IsNullOrEmpty(GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.Name))) == false" ErrorText="@GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.Name))" Label="@_localizer["Name"]" Placeholder="@_localizer["Enter the name for this profile here"]" Variant="Variant.Outlined" /> - <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.Filename" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.Filename, newValue)" Error="String.IsNullOrEmpty(GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.Filename))) == false" ErrorText="@GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.Filename))" Label="@_localizer["Filename"]" Placeholder="@_localizer["Enter the filename for this profile here"]" Variant="Variant.Outlined" /> - <MudStack Row> - <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.SchemeHead" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.SchemeHead, newValue)" Error="String.IsNullOrEmpty(GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeHead))) == false" ErrorText="@GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeHead))" Label="@_localizer["Scheme head"]" Placeholder="@_localizer["Enter the header scheme for this profile here"]" Variant="Variant.Outlined" Clearable /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown" Class="mt-1" Style="height: 56px;"> - @foreach (var placeholder in Exportprofile.AvailableCuesheetSchemes) + <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.Name" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.Name, newValue)" + Label="@_localizer["Name"]" Placeholder="@_localizer["Enter the name for this profile here"]" Variant="Variant.Outlined" + Validation="(string? newValue) => _validationService.Validate(exportOptions.SelectedExportProfile, nameof(Exportprofile.Name))" /> + <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.Filename" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.Filename, newValue)" + Label="@_localizer["Filename"]" Placeholder="@_localizer["Enter the filename for this profile here"]" Variant="Variant.Outlined" + Validation="(string? newValue) => _validationService.Validate(exportOptions.SelectedExportProfile, nameof(Exportprofile.Filename))" /> + <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.SchemeHead" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.SchemeHead, newValue)" + Label="@_localizer["Scheme head"]" Placeholder="@_localizer["Enter the header scheme for this profile here"]" Variant="Variant.Outlined" Clearable + Validation="(string? newValue) => _validationService.Validate(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeHead))" + Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddBox" OnAdornmentClick="(args) => AdornmentClickAsync(schemeHeadMenu, args)" /> + <MudMenu @ref="schemeHeadMenu" PositionAtCursor> + @foreach (var placeholder in Exportprofile.AvailableCuesheetSchemes) + { + <MudMenuItem OnClick="() => { - <MudMenuItem OnClick="() => + if (exportOptions.SelectedExportProfile != null) { - if (exportOptions.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.SchemeHead += placeholder.Value; - } - }"> - @_localizer[placeholder.Key] - </MudMenuItem> - } - </MudMenu> - </MudStack> - <MudStack Row> - <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.SchemeTracks" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.SchemeTracks, newValue)" Error="String.IsNullOrEmpty(GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeTracks))) == false" ErrorText="@GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeTracks))" Label="@_localizer["Scheme tracks"]" Placeholder="@_localizer["Enter the tracks scheme for this profile here"]" Variant="Variant.Outlined" Clearable /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown" Class="mt-1" Style="height: 56px;"> - @foreach (var placeholder in Exportprofile.AvailableTrackSchemes) + exportOptions.SelectedExportProfile.SchemeHead += placeholder.Value; + } + }"> + @_localizer[placeholder.Key] + </MudMenuItem> + } + </MudMenu> + <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.SchemeTracks" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.SchemeTracks, newValue)" + Label="@_localizer["Scheme tracks"]" Placeholder="@_localizer["Enter the tracks scheme for this profile here"]" Variant="Variant.Outlined" Clearable + Validation="(string? newValue) => _validationService.Validate(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeTracks))" + Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddBox" OnAdornmentClick="(args) => AdornmentClickAsync(schemeTracksMenu, args)" /> + <MudMenu @ref="schemeTracksMenu" PositionAtCursor> + @foreach (var placeholder in Exportprofile.AvailableTrackSchemes) + { + <MudMenuItem OnClick="() => { - <MudMenuItem OnClick="() => + if (exportOptions.SelectedExportProfile != null) { - if (exportOptions.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.SchemeTracks += placeholder.Value; - } - }"> - @_localizer[placeholder.Key] - </MudMenuItem> - } - </MudMenu> - </MudStack> - <MudStack Row> - <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.SchemeFooter" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.SchemeFooter, newValue)" Error="String.IsNullOrEmpty(GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeFooter))) == false" ErrorText="@GetValidationErrorMessage(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeFooter))" Label="@_localizer["Scheme footer"]" Placeholder="@_localizer["Enter the footer scheme for this profile here"]" Variant="Variant.Outlined" Clearable /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown" Class="mt-1" Style="height: 56px;"> - @foreach (var placeholder in Exportprofile.AvailableCuesheetSchemes) + exportOptions.SelectedExportProfile.SchemeTracks += placeholder.Value; + } + }"> + @_localizer[placeholder.Key] + </MudMenuItem> + } + </MudMenu> + <MudTextField T="string" Value="exportOptions.SelectedExportProfile?.SchemeFooter" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ExportOptions, Exportprofile?, String>(x => x.SelectedExportProfile, x => x!.SchemeFooter, newValue)" + Label="@_localizer["Scheme footer"]" Placeholder="@_localizer["Enter the footer scheme for this profile here"]" Variant="Variant.Outlined" Clearable + Validation="(string? newValue) => _validationService.Validate(exportOptions.SelectedExportProfile, nameof(Exportprofile.SchemeFooter))" + Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddBox" OnAdornmentClick="(args) => AdornmentClickAsync(schemeFooterMenu, args)" /> + <MudMenu @ref="schemeFooterMenu" PositionAtCursor> + @foreach (var placeholder in Exportprofile.AvailableCuesheetSchemes) + { + <MudMenuItem OnClick="() => { - <MudMenuItem OnClick="() => + if (exportOptions.SelectedExportProfile != null) { - if (exportOptions.SelectedExportProfile != null) - { - exportOptions.SelectedExportProfile.SchemeFooter += placeholder.Value; - } - }"> - @_localizer[placeholder.Key] - </MudMenuItem> - } - </MudMenu> - </MudStack> + exportOptions.SelectedExportProfile.SchemeFooter += placeholder.Value; + } + }"> + @_localizer[placeholder.Key] + </MudMenuItem> + } + </MudMenu> </MudStep> <MudStep Title="@_localizer["Download export"]" Disabled="String.IsNullOrEmpty(GetGenerationValidationMessages()) == false"> <MudTable Items="exportFiles"> @@ -127,6 +134,7 @@ along with Foobar. If not, see ExportOptions? exportOptions; IEnumerable<Exportfile> exportFiles = []; Boolean configureExportCompleted = false; + MudMenu? schemeHeadMenu, schemeTracksMenu, schemeFooterMenu; protected override void Dispose(bool disposing) { @@ -171,20 +179,6 @@ along with Foobar. If not, see } } - String? GetValidationErrorMessage(object? model, string propertyName) - { - String? validationErrorMessage = null; - if (model != null) - { - var validationMessages = _validationService.Validate(model, propertyName); - if (validationMessages.Count() > 0) - { - validationErrorMessage = String.Join(Environment.NewLine, validationMessages); - } - } - return validationErrorMessage; - } - String? GetGenerationValidationMessages() { String? validationErrorMessage = null; @@ -213,4 +207,12 @@ along with Foobar. If not, see configureExportCompleted = exportFiles.Any(); } } + + async Task AdornmentClickAsync(MudMenu? menu, MouseEventArgs args) + { + if (menu != null) + { + await menu.OpenMenuAsync(args); + } + } } diff --git a/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.de.resx b/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.de.resx new file mode 100644 index 00000000..0822beff --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.de.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="Loading ..." xml:space="preserve"> + <value>Lade ...</value> + </data> +</root> \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.razor new file mode 100644 index 00000000..c3230b13 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.razor @@ -0,0 +1,29 @@ +<!-- +This file is part of AudioCuesheetEditor. + +AudioCuesheetEditor is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +AudioCuesheetEditor is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see +<http: //www.gnu.org/licenses />. +--> +@inherits BaseLocalizedComponent + +@inject IStringLocalizer<LoadingDialog> _localizer + +<MudDialog> + <DialogContent> + <div style="display:flex;flex-direction:column;justify-content:center;align-items:center;padding:40px;"> + <MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate /> + <MudText Typo="Typo.h6" Class="mt-4">@_localizer["Loading ..."]</MudText> + </div> + </DialogContent> +</MudDialog> \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.resx b/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.resx new file mode 100644 index 00000000..fb78fbbd --- /dev/null +++ b/AudioCuesheetEditor/Shared/Dialogs/LoadingDialog.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="Loading ..." xml:space="preserve"> + <value>Loading ...</value> + </data> +</root> \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor index 68e64b89..8198bb4f 100644 --- a/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor +++ b/AudioCuesheetEditor/Shared/Dialogs/SelectFileDialog.razor @@ -51,8 +51,8 @@ along with Foobar. If not, see invalidDropFileNames.Clear(); foreach (var file in files) { - if ((_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Projectfile, FileExtensions.Projectfile) == false) - && (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Cuesheet, FileExtensions.Cuesheet) == false)) + if ((_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Projectfile, [FileExtensions.Projectfile]) == false) + && (_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Cuesheet, [FileExtensions.Cuesheet]) == false)) { invalidDropFileNames.Add(file.Name); } diff --git a/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor index 51edf63a..3d65b4b2 100644 --- a/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor +++ b/AudioCuesheetEditor/Shared/Dialogs/SettingsDialog.razor @@ -19,27 +19,26 @@ along with Foobar. If not, see @inject IStringLocalizer<SettingsDialog> _localizer @inject ValidationService _validationService +@inject ILocalStorageOptionsProvider _localStorageOptionsProvider <MudDialog> <DialogContent> <MudText Typo="Typo.subtitle1"><b>@_localizer["Input"]</b></MudText> - <MudSwitch T="Boolean?" Value="ApplicationOptions?.LinkTracks" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.LinkTracks, newValue)" - Label="@_localizer["Automatically link tracks"]" Color="Color.Primary" /> - <MudStack Row AlignItems="AlignItems.Baseline"> - <MudTextField T="string" @ref="timeInputFormatTextField" Label="@_localizer["Time input format"]" Text="@ApplicationOptions?.TimeSpanFormat?.Scheme" TextChanged="TimeInputFormatChangedAsync" - Clearable Error="String.IsNullOrEmpty(GetValidationErrorMessage(ApplicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))) == false" - ErrorText="@GetValidationErrorMessage(ApplicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))" Variant="Variant.Outlined" /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown" Class="mt-1" Style="height: 56px;"> - @foreach(var scheme in TimeSpanFormat.AvailableTimespanScheme) - { - <MudMenuItem OnClick="() => AppendPlaceholderToTimeInputFormatTextField(scheme)">@_localizer[scheme]</MudMenuItem> - } - </MudMenu> - </MudStack> + <MudSwitch T="Boolean?" Value="applicationOptions?.DefaultIsLinkedToPreviousTrack" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.DefaultIsLinkedToPreviousTrack, newValue)" + Label="@_localizer["Automatically link tracks"]" Color="Color.Primary" /> + <MudTextField T="string" @ref="timeInputFormatTextField" Label="@_localizer["Time input format"]" Text="@applicationOptions?.TimeSpanFormat?.Scheme" TextChanged="TimeInputFormatChangedAsync" + Clearable Variant="Variant.Outlined" Validation="(string? newValue) => _validationService.Validate(applicationOptions?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))" + Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddBox" OnAdornmentClick="(args) => AdornmentClickAsync(timespanFormatMenu, args)" /> + <MudMenu @ref="timespanFormatMenu" PositionAtCursor> + @foreach(var scheme in TimeSpanFormat.AvailableTimespanScheme) + { + <MudMenuItem OnClick="() => AppendPlaceholderToTimeInputFormatTextField(scheme)">@_localizer[scheme]</MudMenuItem> + } + </MudMenu> <MudText Typo="Typo.subtitle1"><b>@_localizer["Display"]</b></MudText> - <MudTextField T="string" Label="@_localizer["Time display format"]" Text="@ApplicationOptions?.DisplayTimeSpanFormat" TextChanged="DisplayTimeSpanFormatChangedAsync" - Clearable Variant="Variant.Outlined" HelperText="@_localizer["Uses .NET format, check help for more information"]" /> - <MudSelect T="LogLevel" Label="@_localizer["Minimum Loglevel"]" Variant="Variant.Outlined" Value="ApplicationOptions != null ? ApplicationOptions.MinimumLogLevel : ApplicationOptions.DefaultLogLevel" ValueChanged="LogLevelChanged"> + <MudTextField T="string" Label="@_localizer["Time display format"]" Text="@applicationOptions?.DisplayTimeSpanFormat" TextChanged="DisplayTimeSpanFormatChangedAsync" + Clearable Variant="Variant.Outlined" HelperText="@_localizer["Uses .NET format, check help for more information"]" /> + <MudSelect T="LogLevel" Label="@_localizer["Minimum Loglevel"]" Variant="Variant.Outlined" Value="applicationOptions != null ? applicationOptions.MinimumLogLevel : ApplicationOptions.DefaultLogLevel" ValueChanged="LogLevelChanged"> @foreach (var level in Enum.GetValues<LogLevel>()) { <MudSelectItem Value="level">@level</MudSelectItem> @@ -50,10 +49,25 @@ along with Foobar. If not, see @code { MudTextField<string>? timeInputFormatTextField; + ApplicationOptions? applicationOptions; + MudMenu? timespanFormatMenu; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + applicationOptions = await LocalStorageOptionsProvider.GetOptionsAsync<ApplicationOptions>(); + LocalStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + LocalStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; + } async Task TimeInputFormatChangedAsync(string newValue) { - TimeSpanFormat? timeSpanFormat = ApplicationOptions?.TimeSpanFormat; + TimeSpanFormat? timeSpanFormat = applicationOptions?.TimeSpanFormat; if (string.IsNullOrEmpty(newValue)) { timeSpanFormat = null; @@ -79,22 +93,25 @@ along with Foobar. If not, see timeInputFormatTextField?.SetText($"{timeInputFormatTextField.Text}{placeholder}"); } - String? GetValidationErrorMessage(object? model, string propertyName) + async Task LogLevelChanged(LogLevel logLevel) { - String? validationErrorMessage = null; - if (model != null) + await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.MinimumLogLevel, logLevel); + } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is ApplicationOptions applicationOption) { - var validationMessages = _validationService.Validate(model, propertyName); - if (validationMessages.Count() > 0) - { - validationErrorMessage = String.Join(Environment.NewLine, validationMessages); - } + applicationOptions = applicationOption; + StateHasChanged(); } - return validationErrorMessage; } - async Task LogLevelChanged(LogLevel logLevel) + async Task AdornmentClickAsync(MudMenu? menu, MouseEventArgs args) { - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.MinimumLogLevel, logLevel); + if (menu != null) + { + await menu.OpenMenuAsync(args); + } } } diff --git a/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor index 5fe65643..f8a5507d 100644 --- a/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor +++ b/AudioCuesheetEditor/Shared/Dialogs/StartRecordCountdownDialog.razor @@ -18,24 +18,47 @@ along with Foobar. If not, see @inherits BaseLocalizedComponent @inject IStringLocalizer<StartRecordCountdownDialog> _localizer +@inject ILocalStorageOptionsProvider _localStorageOptionsProvider -@if (ApplicationOptions != null) -{ - <MudDialog> - <TitleContent> - @_localizer["Start record countdown timer"] - </TitleContent> - <DialogContent> - <MudNumericField T="uint" Placeholder="@_localizer["Seconds till record starts"]" Label="@_localizer["Record countdown in sconds"]" Variant="Variant.Outlined" Value="ApplicationOptions.RecordCountdownTimer" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.RecordCountdownTimer, newValue)" /> - </DialogContent> - <DialogActions> - <MudButton Color="Color.Primary" Variant="Variant.Filled" StartIcon="@Icons.Material.Outlined.Timer" OnClick="() => MudDialog?.Close()">@_localizer["Start countdown"]</MudButton> - <MudButton Color="Color.Error" Variant="Variant.Filled" OnClick="() => MudDialog?.Cancel()">@_localizer["Abort"]</MudButton> - </DialogActions> - </MudDialog> -} +<MudDialog> + <TitleContent> + @_localizer["Start record countdown timer"] + </TitleContent> + <DialogContent> + <MudNumericField T="uint?" Placeholder="@_localizer["Seconds till record starts"]" Label="@_localizer["Record countdown in sconds"]" Variant="Variant.Outlined" + Value="options?.RecordCountdownTimer" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveOptionsValueAsync<RecordOptions>(x => x.RecordCountdownTimer, newValue)" /> + </DialogContent> + <DialogActions> + <MudButton Color="Color.Primary" Variant="Variant.Filled" StartIcon="@Icons.Material.Outlined.Timer" OnClick="() => MudDialog?.Close(DialogResult.Ok(options?.RecordCountdownTimer))">@_localizer["Start countdown"]</MudButton> + <MudButton Color="Color.Error" Variant="Variant.Filled" OnClick="() => MudDialog?.Cancel()">@_localizer["Abort"]</MudButton> + </DialogActions> +</MudDialog> @code { + RecordOptions? options; + [CascadingParameter] private IMudDialogInstance? MudDialog { get; set; } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + options = await _localStorageOptionsProvider.GetOptionsAsync<RecordOptions>(); + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; + } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is RecordOptions recordOptions) + { + options = recordOptions; + StateHasChanged(); + } + } } diff --git a/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor b/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor index d90881ca..c8c5c83a 100644 --- a/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor +++ b/AudioCuesheetEditor/Shared/Import/ImportFileContent.razor @@ -21,43 +21,34 @@ along with Foobar. If not, see @inject ISessionStateContainer _sessionStateContainer <MudTabs Rounded ApplyEffectsToContainer Outlined Color="Color.Success"> - @if (FileContentRecognized != null) - { - <MudTabPanel Text="@_localizer["Analyzed file content"]" Icon="@Icons.Material.Outlined.Analytics"> - <MudField Class="ml-2 mr-2"> - <pre> - @foreach (var line in FileContentRecognized) - { - @((MarkupString)String.Format("{0}<br />", line)) - } - </pre> - </MudField> - </MudTabPanel> - <MudTabPanel Text="@_localizer["Edit"]" Icon="@Icons.Material.Outlined.Edit"> - <MudTextField Class="ml-2 mr-2" T="string" Lines="@FileContentRecognized.Count()" AutoGrow Text="@FileContent" TextChanged="FileContent_TextChangedAsync" /> - </MudTabPanel> - } + <MudTabPanel Text="@_localizer["Analyzed file content"]" Icon="@Icons.Material.Outlined.Analytics"> + <MudField Class="ml-2 mr-2"> + <pre> + @if (FileContentRecognized != null) + { + @((MarkupString)SanitizeHTML(FileContentRecognized)) + } + </pre> + </MudField> + </MudTabPanel> + <MudTabPanel Text="@_localizer["Edit"]" Icon="@Icons.Material.Outlined.Edit"> + <MudTextField Class="ml-2 mr-2" T="string" AutoGrow Text="@_sessionStateContainer.Importfile?.FileContent" TextChanged="FileContent_TextChangedAsync" /> + </MudTabPanel> </MudTabs> @code { [Parameter] public EventCallback<string> FileContentChanged { get; set; } - public IEnumerable<String?>? FileContentRecognized => _sessionStateContainer.Importfile?.FileContentRecognized; - public String FileContent - { - get - { - String fileContent = String.Empty; - if (_sessionStateContainer.Importfile?.FileContent != null) - { - fileContent = String.Join(Environment.NewLine, _sessionStateContainer.Importfile.FileContent); - } - return fileContent; - } - } + public String? FileContentRecognized => _sessionStateContainer.Importfile?.FileContentRecognized; async Task FileContent_TextChangedAsync(string newFileContent) { await FileContentChanged.InvokeAsync(newFileContent); } + + string SanitizeHTML(string input) + { + var sanitizer = new HtmlSanitizer(); + return sanitizer.Sanitize(input); + } } diff --git a/AudioCuesheetEditor/Shared/Import/ImportSchemes.razor b/AudioCuesheetEditor/Shared/Import/ImportSchemes.razor deleted file mode 100644 index 29de29ff..00000000 --- a/AudioCuesheetEditor/Shared/Import/ImportSchemes.razor +++ /dev/null @@ -1,133 +0,0 @@ -@using System.Linq.Expressions -<!-- -This file is part of AudioCuesheetEditor. - -AudioCuesheetEditor is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -AudioCuesheetEditor is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with Foobar. If not, see -<http: //www.gnu.org/licenses />. ---> -@inherits BaseLocalizedComponent - -@inject IStringLocalizer<ImportSchemes> _localizer -@inject ValidationService _validationService -@inject IDialogService _dialogService - -<MudStack Row AlignItems="AlignItems.Baseline" Justify="Justify.SpaceBetween"> - <MudText Typo="Typo.h5">@_localizer["Import schemes"]</MudText> - <MudIconButton Icon="@Icons.Material.Outlined.LockReset" Color="Color.Warning" Variant="Variant.Filled" OnClick="ResetSchemes" /> -</MudStack> -<MudStack Row AlignItems="AlignItems.Baseline"> - <MudTextField T="string" @ref="importSchemeCuesheetTextField" Label="@_localizer["Textimport scheme cuesheet"]" Text="@ApplicationOptions?.ImportScheme.SchemeCuesheet" - TextChanged="ImportSchemeCuesheetTextChangedAsync" Clearable Error="String.IsNullOrEmpty(GetValidationErrorMessage(ApplicationOptions?.ImportScheme, nameof(TextImportScheme.SchemeCuesheet))) == false" - ErrorText="@GetValidationErrorMessage(ApplicationOptions?.ImportScheme, nameof(TextImportScheme.SchemeCuesheet))" /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown"> - @foreach (var scheme in TextImportScheme.AvailableSchemeCuesheet) - { - <MudMenuItem OnClick="() => AppendPlaceholderToTextField(importSchemeCuesheetTextField, scheme)">@_localizer[scheme]</MudMenuItem> - } - </MudMenu> -</MudStack> -<MudStack Row AlignItems="AlignItems.Baseline"> - <MudTextField T="string" @ref="importSchemeTracksTextField" Label="@_localizer["Textimport scheme tracks"]" Text="@ApplicationOptions?.ImportScheme.SchemeTracks" - TextChanged="ImportSchemeTracksTextChangedAsync" Clearable Error="String.IsNullOrEmpty(GetValidationErrorMessage(ApplicationOptions?.ImportScheme, nameof(TextImportScheme.SchemeTracks))) == false" - ErrorText="@GetValidationErrorMessage(ApplicationOptions?.ImportScheme, nameof(TextImportScheme.SchemeTracks))" /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown"> - @foreach (var scheme in TextImportScheme.AvailableSchemesTrack) - { - <MudMenuItem OnClick="() => AppendPlaceholderToTextField(importSchemeTracksTextField, scheme)">@_localizer[scheme]</MudMenuItem> - } - </MudMenu> -</MudStack> -<MudStack Row AlignItems="AlignItems.Baseline"> - <MudTextField T="string" @ref="importTimeInputFormatTextField" Label="@_localizer["Time input format for import"]" Text="@ApplicationOptions?.ImportTimeSpanFormat?.Scheme" - TextChanged="ImportTimeInputFormatChangedAsync" Clearable Error="String.IsNullOrEmpty(GetValidationErrorMessage(ApplicationOptions?.ImportTimeSpanFormat, nameof(TimeSpanFormat.Scheme))) == false" - ErrorText="@GetValidationErrorMessage(ApplicationOptions?.ImportTimeSpanFormat, nameof(TimeSpanFormat.Scheme))" /> - <MudMenu Label="@_localizer["Add placeholder"]" Color="Color.Primary" Variant="Variant.Filled" EndIcon="@Icons.Material.Outlined.KeyboardArrowDown"> - @foreach (var scheme in TimeSpanFormat.AvailableTimespanScheme) - { - <MudMenuItem OnClick="() => AppendPlaceholderToTextField(importTimeInputFormatTextField, scheme)">@_localizer[scheme]</MudMenuItem> - } - </MudMenu> -</MudStack> - -@code { - [Parameter] - public EventCallback<string> ImportSchemeCuesheetChanged { get; set; } - - [Parameter] - public EventCallback<string> ImportSchemeTracksChanged { get; set; } - - [Parameter] - public EventCallback<string> ImportTimeInputFormatChanged { get; set; } - - MudTextField<string>? importSchemeCuesheetTextField, importSchemeTracksTextField, importTimeInputFormatTextField; - - void AppendPlaceholderToTextField(MudTextField<string>? mudTextField, string placeholder) - { - mudTextField?.SetText($"{mudTextField.Text}{placeholder}"); - } - - async Task ImportSchemeCuesheetTextChangedAsync(string newScheme) - { - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.ImportScheme.SchemeCuesheet, newScheme); - await ImportSchemeCuesheetChanged.InvokeAsync(newScheme); - } - - async Task ImportSchemeTracksTextChangedAsync(string newScheme) - { - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.ImportScheme.SchemeTracks, newScheme); - await ImportSchemeTracksChanged.InvokeAsync(newScheme); - } - - async Task ImportTimeInputFormatChangedAsync(string newScheme) - { - await LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.ImportTimeSpanFormat.Scheme, newScheme); - await ImportTimeInputFormatChanged.InvokeAsync(newScheme); - } - - String? GetValidationErrorMessage(object? model, string propertyName) - { - String? validationErrorMessage = null; - if (model != null) - { - var validationMessages = _validationService.Validate(model, propertyName); - if (validationMessages.Count() > 0) - { - validationErrorMessage = String.Join(Environment.NewLine, validationMessages); - } - } - return validationErrorMessage; - } - - async Task ResetSchemes() - { - if (ApplicationOptions != null) - { - var parameters = new DialogParameters<ConfirmDialog> - { - { x => x.ConfirmText, _localizer["Are you sure you want to reset the import schemes to factory default?"] }, - }; - var dialog = await _dialogService.ShowAsync<ConfirmDialog>(_localizer["Confirm"], parameters); - var result = await dialog.Result; - if (result?.Canceled == false) - { - ApplicationOptions.ImportScheme = TextImportScheme.DefaultTextImportScheme; - ApplicationOptions.ImportTimeSpanFormat = new(); - await LocalStorageOptionsProvider.SaveOptionsAsync(ApplicationOptions); - await ImportSchemeCuesheetChanged.InvokeAsync(ApplicationOptions.ImportScheme.SchemeCuesheet); - await ImportSchemeTracksChanged.InvokeAsync(ApplicationOptions.ImportScheme.SchemeTracks); - await ImportTimeInputFormatChanged.InvokeAsync(ApplicationOptions.ImportTimeSpanFormat.Scheme); - } - } - } -} diff --git a/AudioCuesheetEditor/Shared/Import/ImportSchemes.de.resx b/AudioCuesheetEditor/Shared/Import/Importprofiles.de.resx similarity index 82% rename from AudioCuesheetEditor/Shared/Import/ImportSchemes.de.resx rename to AudioCuesheetEditor/Shared/Import/Importprofiles.de.resx index 1262de70..a7736980 100644 --- a/AudioCuesheetEditor/Shared/Import/ImportSchemes.de.resx +++ b/AudioCuesheetEditor/Shared/Import/Importprofiles.de.resx @@ -117,11 +117,8 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> - <data name="Add placeholder" xml:space="preserve"> - <value>Platzhalter hinzufügen</value> - </data> - <data name="Are you sure you want to reset the import schemes to factory default?" xml:space="preserve"> - <value>Möchten Sie wirklich die Importschemata auf Werkseinstellungen zurücksetzen?</value> + <data name="Are you sure you want to reset the import profiles to factory default?" xml:space="preserve"> + <value>Möchten Sie wirklich die Importprofile auf Werkseinstellungen zurücksetzen?</value> </data> <data name="Artist" xml:space="preserve"> <value>Künstler</value> @@ -130,7 +127,7 @@ <value>Audiodatei</value> </data> <data name="Begin" xml:space="preserve"> - <value>Start</value> + <value>Beginn</value> </data> <data name="Cataloguenumber" xml:space="preserve"> <value>Katalognummer</value> @@ -147,14 +144,26 @@ <data name="End" xml:space="preserve"> <value>Ende</value> </data> + <data name="Enter the common data scheme for this profile here" xml:space="preserve"> + <value>Geben Sie hier das Allgemeine Informationen Schema für das Profil an</value> + </data> + <data name="Enter the name for this profile here" xml:space="preserve"> + <value>Geben Sie hier den Namen für das Profil an</value> + </data> + <data name="Enter the time format for this profile here" xml:space="preserve"> + <value>Geben Sie hier das Zeitformat für das Profil an</value> + </data> + <data name="Enter the track scheme for this profile here" xml:space="preserve"> + <value>Geben Sie hier das Titel Schema für das Profil an</value> + </data> <data name="Flags" xml:space="preserve"> <value>Markierungen</value> </data> <data name="Hours" xml:space="preserve"> <value>Stunden</value> </data> - <data name="Import schemes" xml:space="preserve"> - <value>Importschemata</value> + <data name="Import profile" xml:space="preserve"> + <value>Import Profil</value> </data> <data name="Length" xml:space="preserve"> <value>Länge</value> @@ -165,6 +174,12 @@ <data name="Minutes" xml:space="preserve"> <value>Minuten</value> </data> + <data name="Name" xml:space="preserve"> + <value>Name</value> + </data> + <data name="New import profile" xml:space="preserve"> + <value>Neues Importprofil</value> + </data> <data name="Position" xml:space="preserve"> <value>Position</value> </data> @@ -174,18 +189,24 @@ <data name="PreGap" xml:space="preserve"> <value>Vorlücke</value> </data> + <data name="Profile name" xml:space="preserve"> + <value>Profil Name</value> + </data> + <data name="Scheme common data" xml:space="preserve"> + <value>Schema Allgemeine Informationen</value> + </data> + <data name="Scheme tracks" xml:space="preserve"> + <value>Schema Titel</value> + </data> + <data name="Search using regular expressions" xml:space="preserve"> + <value>Suche mit regulären Ausdrücken</value> + </data> <data name="Seconds" xml:space="preserve"> <value>Sekunden</value> </data> <data name="StartDateTime" xml:space="preserve"> <value>Startzeitpunkt</value> </data> - <data name="Textimport scheme cuesheet" xml:space="preserve"> - <value>Textimportschema Cuesheet</value> - </data> - <data name="Textimport scheme tracks" xml:space="preserve"> - <value>Textimportschema Titel</value> - </data> <data name="Time input format for import" xml:space="preserve"> <value>Zeitformat für den Import</value> </data> diff --git a/AudioCuesheetEditor/Shared/Import/Importprofiles.razor b/AudioCuesheetEditor/Shared/Import/Importprofiles.razor new file mode 100644 index 00000000..f61fc649 --- /dev/null +++ b/AudioCuesheetEditor/Shared/Import/Importprofiles.razor @@ -0,0 +1,232 @@ +@using System.Linq.Expressions +<!-- +This file is part of AudioCuesheetEditor. + +AudioCuesheetEditor is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +AudioCuesheetEditor is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see +<http: //www.gnu.org/licenses />. +--> +@inherits BaseLocalizedComponent + +@inject IStringLocalizer<Importprofiles> _localizer +@inject ValidationService _validationService +@inject IDialogService _dialogService +@inject ILocalStorageOptionsProvider _localStorageOptionsProvider + +@if (importOptions != null) +{ + <MudStack Row> + <MudSelect T="Importprofile" Variant="Variant.Outlined" Label="@_localizer["Import profile"]" Value="importOptions.SelectedImportProfile" ValueChanged="SelectedImportProfileChangedAsync"> + @foreach (var profile in importOptions.ImportProfiles) + { + <MudSelectItem Value="profile">@profile.Name</MudSelectItem> + } + </MudSelect> + <MudButtonGroup OverrideStyles="false" Class="mt-2" Style="height: 56px;"> + <MudIconButton Color="Color.Primary" Variant="Variant.Filled" Icon="@Icons.Material.Outlined.Add" OnClick="AddImportprofileClick" /> + <MudIconButton Color="Color.Warning" Variant="Variant.Filled" Icon="@Icons.Material.Outlined.Delete" Disabled="importOptions.SelectedImportProfile == null" OnClick="DeleteImportprofileClick" /> + <MudIconButton Color="Color.Error" Variant="Variant.Filled" Icon="@Icons.Material.Outlined.LockReset" OnClick="ResetSchemes" /> + </MudButtonGroup> + </MudStack> + <MudTextField T="string" Value="importOptions.SelectedImportProfile?.Name" Disabled="importOptions.SelectedImportProfile == null" ValueChanged="(newValue) => LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ImportOptions, Importprofile?, String?>(x => x.SelectedImportProfile, x => x!.Name, newValue)" + Validation="(string newValue) => _validationService.Validate(importOptions.SelectedImportProfile, nameof(Importprofile.Name))" + Label="@_localizer["Profile name"]" Placeholder="@_localizer["Enter the name for this profile here"]" Variant="Variant.Outlined" /> + <MudSwitch T="Boolean?" Color="Color.Secondary" Value="importOptions.SelectedImportProfile?.UseRegularExpression" Disabled="importOptions.SelectedImportProfile == null" ValueChanged="UseRegularExpressionChangedAsync" Label="@_localizer["Search using regular expressions"]" /> + <MudTextField T="string" @ref="schemeCuesheetTextField" Text="@importOptions.SelectedImportProfile?.SchemeCuesheet" TextChanged="SchemeCuesheetChangedAsync" + Validation="(string newValue) => _validationService.Validate(importOptions.SelectedImportProfile, nameof(Importprofile.SchemeCuesheet))" + Label="@_localizer["Scheme common data"]" Placeholder="@_localizer["Enter the common data scheme for this profile here"]" Variant="Variant.Outlined" + Clearable Disabled="importOptions.SelectedImportProfile == null" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddCircleOutline" + OnAdornmentClick="(args) => AdornmentClickAsync(schemeCuesheetMenu, args)" /> + <MudMenu @ref="schemeCuesheetMenu" Disabled="importOptions.SelectedImportProfile == null" PositionAtCursor> + @foreach (var scheme in Importprofile.AvailableSchemeCuesheet) + { + <MudMenuItem OnClick="() => AppendPlaceholderToTextField(schemeCuesheetTextField, scheme)">@_localizer[scheme]</MudMenuItem> + } + </MudMenu> + <MudTextField T="string" @ref="schemeTracksTextField" Text="@importOptions.SelectedImportProfile?.SchemeTracks" TextChanged="SchemeTracksChangedAsync" + Validation="(string newValue) => _validationService.Validate(importOptions.SelectedImportProfile, nameof(Importprofile.SchemeTracks))" + Label="@_localizer["Scheme tracks"]" Placeholder="@_localizer["Enter the track scheme for this profile here"]" Variant="Variant.Outlined" + Clearable Disabled="importOptions.SelectedImportProfile == null" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddCircleOutline" + OnAdornmentClick="(args) => AdornmentClickAsync(schemeTracksMenu, args)" /> + <MudMenu @ref="schemeTracksMenu" Disabled="importOptions.SelectedImportProfile == null" PositionAtCursor> + @foreach (var scheme in Importprofile.AvailableSchemesTrack) + { + <MudMenuItem OnClick="() => AppendPlaceholderToTextField(schemeTracksTextField, scheme)">@_localizer[scheme]</MudMenuItem> + } + </MudMenu> + <MudTextField T="string" @ref="timeSpanFormatTextField" Text="@importOptions.SelectedImportProfile?.TimeSpanFormat?.Scheme" TextChanged="ImportTimeInputFormatChangedAsync" + Validation="(string newValue) => _validationService.Validate(importOptions.SelectedImportProfile?.TimeSpanFormat, nameof(TimeSpanFormat.Scheme))" + Label="@_localizer["Time input format for import"]" Placeholder="@_localizer["Enter the time format for this profile here"]" Variant="Variant.Outlined" + Clearable Disabled="importOptions.SelectedImportProfile == null" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.AddCircleOutline" + OnAdornmentClick="(args) => AdornmentClickAsync(timeSpanFormatMenu, args)" /> + <MudMenu @ref="timeSpanFormatMenu" Disabled="importOptions.SelectedImportProfile == null" PositionAtCursor> + @foreach (var scheme in TimeSpanFormat.AvailableTimespanScheme) + { + <MudMenuItem OnClick="() => AppendPlaceholderToTextField(timeSpanFormatTextField, scheme)">@_localizer[scheme]</MudMenuItem> + } + </MudMenu> +} + +@code { + [Parameter] + public EventCallback ImportprofileChanged { get; set; } + + ImportOptions? importOptions; + MudMenu? schemeCuesheetMenu, schemeTracksMenu, timeSpanFormatMenu; + MudTextField<string>? schemeCuesheetTextField, schemeTracksTextField, timeSpanFormatTextField; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + importOptions = await _localStorageOptionsProvider.GetOptionsAsync<ImportOptions>(); + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; + } + + void AppendPlaceholderToTextField(MudTextField<string>? mudTextField, string placeholder) + { + mudTextField?.SetText($"{mudTextField.Text}{placeholder}"); + } + + async Task SelectedImportProfileChangedAsync(Importprofile? newSelectedProfile) + { + await LocalStorageOptionsProvider.SaveOptionsValueAsync<ImportOptions>(x => x.SelectedImportProfile, newSelectedProfile); + await ImportprofileChanged.InvokeAsync(); + } + + async Task UseRegularExpressionChangedAsync(Boolean? newValue) + { + await LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ImportOptions, Importprofile?, Boolean>(x => x.SelectedImportProfile, x => x!.UseRegularExpression, newValue!.Value); + await ImportprofileChanged.InvokeAsync(); + } + + async Task SchemeCuesheetChangedAsync(string newScheme) + { + await LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ImportOptions, Importprofile?, String?>(x => x.SelectedImportProfile, x => x!.SchemeCuesheet, newScheme); + await ImportprofileChanged.InvokeAsync(); + } + + async Task SchemeTracksChangedAsync(string newScheme) + { + await LocalStorageOptionsProvider.SaveNestedOptionValueAsync<ImportOptions, Importprofile?, String?>(x => x.SelectedImportProfile, x => x!.SchemeTracks, newScheme); + await ImportprofileChanged.InvokeAsync(); + } + + async Task ImportTimeInputFormatChangedAsync(string newScheme) + { + var profile = importOptions?.SelectedImportProfile; + if (profile != null) + { + TimeSpanFormat? format = null; + if (string.IsNullOrEmpty(newScheme)) + { + format = null; + } + else + { + format = new() { Scheme = newScheme }; + } + await LocalStorageOptionsProvider.SaveOptionsValueAsync<ImportOptions>(x => x.SelectedImportProfile!.TimeSpanFormat, format); + await ImportprofileChanged.InvokeAsync(); + } + } + + String? GetValidationErrorMessage(object? model, string propertyName) + { + String? validationErrorMessage = null; + if (model != null) + { + var validationMessages = _validationService.Validate(model, propertyName); + if (validationMessages.Count() > 0) + { + validationErrorMessage = String.Join(Environment.NewLine, validationMessages); + } + } + return validationErrorMessage; + } + + async Task ResetSchemes() + { + var parameters = new DialogParameters<ConfirmDialog> + { + { x => x.ConfirmText, _localizer["Are you sure you want to reset the import profiles to factory default?"] }, + }; + var dialog = await _dialogService.ShowAsync<ConfirmDialog>(_localizer["Confirm"], parameters); + var result = await dialog.Result; + if (result?.Canceled == false) + { + importOptions!.ImportProfiles = ImportOptions.DefaultImportprofiles; + importOptions!.SelectedImportProfile = ImportOptions.DefaultSelectedImportprofile; + await LocalStorageOptionsProvider.SaveOptionsAsync(importOptions); + await ImportprofileChanged.InvokeAsync(); + } + } + + async Task AddImportprofileClick() + { + var newProfilesNames = importOptions!.ImportProfiles.Where(x => x.Name?.StartsWith(_localizer["New import profile"]) == true).Select(x => x.Name); + var regex = new System.Text.RegularExpressions.Regex(@"(\d+)$"); + int maxNumber = newProfilesNames + .Select(name => + { + var match = regex.Match(name ?? ""); + return match.Success ? int.Parse(match.Value) : 0; + }) + .DefaultIfEmpty(0) + .Max(); + var profile = new Importprofile() + { + Name = $"{_localizer["New import profile"]} {maxNumber + 1}" + }; + importOptions!.SelectedImportProfile = profile; + await LocalStorageOptionsProvider.SaveOptionsAsync(importOptions); + await ImportprofileChanged.InvokeAsync(); + } + + async Task DeleteImportprofileClick() + { + var selectedProfile = importOptions?.SelectedImportProfile; + if ((selectedProfile != null) && (importOptions != null)) + { + importOptions.ImportProfiles.Remove(selectedProfile); + var lastProfile = importOptions.ImportProfiles.LastOrDefault(); + if (lastProfile != null) + { + importOptions.SelectedImportProfile = lastProfile; + } + await LocalStorageOptionsProvider.SaveOptionsAsync(importOptions); + } + } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is ImportOptions importOption) + { + importOptions = importOption; + StateHasChanged(); + } + } + + async Task AdornmentClickAsync(MudMenu? menu, MouseEventArgs args) + { + if (menu != null) + { + await menu.OpenMenuAsync(args); + } + } +} diff --git a/AudioCuesheetEditor/Shared/Import/ImportSchemes.resx b/AudioCuesheetEditor/Shared/Import/Importprofiles.resx similarity index 83% rename from AudioCuesheetEditor/Shared/Import/ImportSchemes.resx rename to AudioCuesheetEditor/Shared/Import/Importprofiles.resx index e6597bc9..5bd62cc2 100644 --- a/AudioCuesheetEditor/Shared/Import/ImportSchemes.resx +++ b/AudioCuesheetEditor/Shared/Import/Importprofiles.resx @@ -117,11 +117,8 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> - <data name="Add placeholder" xml:space="preserve"> - <value>Add placeholder</value> - </data> - <data name="Are you sure you want to reset the import schemes to factory default?" xml:space="preserve"> - <value>Are you sure you want to reset the import schemes to factory default?</value> + <data name="Are you sure you want to reset the import profiles to factory default?" xml:space="preserve"> + <value>Are you sure you want to reset the import profiles to factory default?</value> </data> <data name="Artist" xml:space="preserve"> <value>Artist</value> @@ -147,14 +144,26 @@ <data name="End" xml:space="preserve"> <value>End</value> </data> + <data name="Enter the common data scheme for this profile here" xml:space="preserve"> + <value>Enter the common data scheme for this profile here</value> + </data> + <data name="Enter the name for this profile here" xml:space="preserve"> + <value>Enter the name for this profile here</value> + </data> + <data name="Enter the time format for this profile here" xml:space="preserve"> + <value>Enter the time format for this profile here</value> + </data> + <data name="Enter the track scheme for this profile here" xml:space="preserve"> + <value>Enter the track scheme for this profile here</value> + </data> <data name="Flags" xml:space="preserve"> <value>Flags</value> </data> <data name="Hours" xml:space="preserve"> <value>Hours</value> </data> - <data name="Import schemes" xml:space="preserve"> - <value>Import schemes</value> + <data name="Import profile" xml:space="preserve"> + <value>Import profile</value> </data> <data name="Length" xml:space="preserve"> <value>Length</value> @@ -165,6 +174,12 @@ <data name="Minutes" xml:space="preserve"> <value>Minutes</value> </data> + <data name="Name" xml:space="preserve"> + <value>Name</value> + </data> + <data name="New import profile" xml:space="preserve"> + <value>New import profile</value> + </data> <data name="Position" xml:space="preserve"> <value>Position</value> </data> @@ -174,18 +189,24 @@ <data name="PreGap" xml:space="preserve"> <value>PreGap</value> </data> + <data name="Profile name" xml:space="preserve"> + <value>Profile name</value> + </data> + <data name="Scheme common data" xml:space="preserve"> + <value>Scheme common data</value> + </data> + <data name="Scheme tracks" xml:space="preserve"> + <value>Scheme tracks</value> + </data> + <data name="Search using regular expressions" xml:space="preserve"> + <value>Search using regular expressions</value> + </data> <data name="Seconds" xml:space="preserve"> <value>Seconds</value> </data> <data name="StartDateTime" xml:space="preserve"> <value>StartDateTime</value> </data> - <data name="Textimport scheme cuesheet" xml:space="preserve"> - <value>Textimport scheme cuesheet</value> - </data> - <data name="Textimport scheme tracks" xml:space="preserve"> - <value>Textimport scheme tracks</value> - </data> <data name="Time input format for import" xml:space="preserve"> <value>Time input format for import</value> </data> diff --git a/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor index 590b8275..4c1f617e 100644 --- a/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor +++ b/AudioCuesheetEditor/Shared/Import/SelectImportFiles.razor @@ -29,7 +29,7 @@ along with Foobar. If not, see </CardHeaderContent> </MudCardHeader> <MudCardContent id="@dropFileInputId"> - <DropFileInput OnFilesSelected="InputFilesChanged" Filter="@FileMimeTypes.Text" /> + <DropFileInput OnFilesSelected="InputFilesChanged" Filter="@FileMimeTypes.TextPlain" /> @foreach (var invalidFileName in invalidDropFileNames) { <MudAlert Variant="Variant.Filled" Severity="Severity.Error" ShowCloseIcon CloseIconClicked="() => CloseInvalidFileClicked(invalidFileName)"> @@ -44,7 +44,7 @@ along with Foobar. If not, see List<String> invalidDropFileNames = new(); [Parameter] - public EventCallback<Dictionary<IBrowserFile, ImportFileType>> FilesImported { get; set; } + public EventCallback FilesImported { get; set; } [Parameter] public EventCallback<List<String>> InvalidFilesChanged { get; set; } @@ -54,8 +54,8 @@ along with Foobar. If not, see invalidDropFileNames.Clear(); foreach (var file in files) { - if ((_fileInputManager.CheckFileMimeType(file, FileMimeTypes.Text, FileExtensions.Text) == false) - && (FileInputManager.GetAudioCodec(file) == null)) + if ((_fileInputManager.IsValidForImportView(file) == false) + && (_fileInputManager.IsValidAudiofile(file) == false)) { invalidDropFileNames.Add(file.Name); } @@ -73,19 +73,17 @@ along with Foobar. If not, see async Task ImportFiles(IReadOnlyCollection<IBrowserFile> files) { _sessionStateContainer.ResetImport(); - var importedFiles = await _importManager.ImportFilesAsync(files); + await _importManager.ImportFilesAsync(files); // Audio file is handled seperatly foreach (var file in files) { - var codec = FileInputManager.GetAudioCodec(file); - if (codec != null) + if (_fileInputManager.IsValidAudiofile(file)) { var audiofile = await _fileInputManager.CreateAudiofileAsync(dropFileInputId, file); _sessionStateContainer.ImportAudiofile = audiofile; - importedFiles.Add(file, ImportFileType.Audiofile); } } - await FilesImported.InvokeAsync(importedFiles); + await FilesImported.InvokeAsync(); } async Task CloseInvalidFileClicked(string invalidFile) diff --git a/AudioCuesheetEditor/Shared/Record/AddTrack.razor b/AudioCuesheetEditor/Shared/Record/AddTrack.razor index 0b914962..77e07b32 100644 --- a/AudioCuesheetEditor/Shared/Record/AddTrack.razor +++ b/AudioCuesheetEditor/Shared/Record/AddTrack.razor @@ -20,6 +20,7 @@ along with Foobar. If not, see @inject IStringLocalizer<AddTrack> _localizer @inject AutocompleteManager _autocompleteManager @inject ValidationService _validationService +@inject ISessionStateContainer _sessionStateContainer <MudCard Outlined> <MudCardContent> @@ -79,7 +80,19 @@ along with Foobar. If not, see @code { [CascadingParameter] - public Cuesheet? Cuesheet { get; set; } + public ViewMode CurrentViewMode { get; set; } + + public Cuesheet? Cuesheet + { + get + { + if (CurrentViewMode == ViewMode.ImportView) + { + return _sessionStateContainer.ImportCuesheet; + } + return _sessionStateContainer.Cuesheet; + } + } protected override void OnParametersSet() { diff --git a/AudioCuesheetEditor/Shared/Record/ControlRecording.razor b/AudioCuesheetEditor/Shared/Record/ControlRecording.razor index aeb7d160..3b28ecae 100644 --- a/AudioCuesheetEditor/Shared/Record/ControlRecording.razor +++ b/AudioCuesheetEditor/Shared/Record/ControlRecording.razor @@ -20,6 +20,7 @@ along with Foobar. If not, see @inject IStringLocalizer<ControlRecording> _localizer @inject IDialogService _dialogService @inject IBlazorDownloadFileService _blazorDownloadFileService +@inject ISessionStateContainer _sessionStateContainer @if (Cuesheet?.IsRecording == false) { @@ -86,8 +87,20 @@ along with Foobar. If not, see Timer? startRecordTimer; DateTime recordTimerStarted; + public Cuesheet? Cuesheet + { + get + { + if (CurrentViewMode == ViewMode.ImportView) + { + return _sessionStateContainer.ImportCuesheet; + } + return _sessionStateContainer.Cuesheet; + } + } + [CascadingParameter] - public Cuesheet? Cuesheet { get; set; } + public ViewMode CurrentViewMode { get; set; } public Boolean StartRecordingDisabled => startRecordTimer?.Enabled == true || Cuesheet?.IsRecordingPossible.Any() == true; @@ -127,9 +140,10 @@ along with Foobar. If not, see var options = new DialogOptions() { CloseOnEscapeKey = true, BackdropClick = false }; var dialog = await _dialogService.ShowAsync<StartRecordCountdownDialog>(null, options); var result = await dialog.Result; - if (result?.Canceled == false) + var recordCountdownTimer = result?.Data as uint?; + if ((result?.Canceled == false) && (recordCountdownTimer.HasValue)) { - startRecordTimer = new Timer(ApplicationOptions!.RecordCountdownTimer * 1000); + startRecordTimer = new Timer(recordCountdownTimer.Value * 1000); startRecordTimer.Elapsed += delegate { StartRecording(); diff --git a/AudioCuesheetEditor/Shared/TrackList/ArtistColumn.razor b/AudioCuesheetEditor/Shared/TrackList/ArtistColumn.razor new file mode 100644 index 00000000..9fbd38d0 --- /dev/null +++ b/AudioCuesheetEditor/Shared/TrackList/ArtistColumn.razor @@ -0,0 +1,78 @@ +<!-- +This file is part of AudioCuesheetEditor. + +AudioCuesheetEditor is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +AudioCuesheetEditor is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see +<http: //www.gnu.org/licenses />. +--> +@inherits BaseLocalizedComponent + +@inject AutocompleteManager _autocompleteManager +@inject ValidationService _validationService + +<MudAutocomplete T="MusicBrainzArtist" SearchFunc="_autocompleteManager.SearchArtistsAsync" ToStringFunc="(value) => value.Name" @bind-Text="artist" + Value="autocompleteArtist" ValueChanged="ValueChanged" Clearable ShowProgressIndicator Validation="(string? newArtist) => _validationService.Validate(Track, nameof(Track.Artist))" + MaxItems="null" CoerceText="false" OnBlur="OnBlur"> + <ItemTemplate> + @if (context.Disambiguation != null) + { + <MudText>@String.Format("{0} ({1})", context.Name, context.Disambiguation)</MudText> + } + else + { + <MudText>@context.Name</MudText> + } + </ItemTemplate> +</MudAutocomplete> + +@code { + [Parameter] + [EditorRequired] + public Track Track { get; set; } = default!; + + MusicBrainzArtist? autocompleteArtist; + string? artist; + Timer? debounceTimer; + + protected override void OnParametersSet() + { + base.OnParametersSet(); + autocompleteArtist = new() { Name = Track.Artist }; + artist = autocompleteArtist.Name; + } + + void ValueChanged(MusicBrainzArtist? newValue) + { + autocompleteArtist = newValue; + artist = autocompleteArtist?.Name; + StartChangeTimer(); + } + + void OnBlur(FocusEventArgs args) + { + StartChangeTimer(); + } + + void StartChangeTimer() + { + debounceTimer = new Timer(100); + debounceTimer.Elapsed += ChangeArtist; + debounceTimer.AutoReset = false; + debounceTimer.Start(); + } + + void ChangeArtist(object? sender, System.Timers.ElapsedEventArgs e) + { + Track.Artist = artist; + } +} diff --git a/AudioCuesheetEditor/Shared/TrackList/TitleColumn.razor b/AudioCuesheetEditor/Shared/TrackList/TitleColumn.razor new file mode 100644 index 00000000..1f9205f4 --- /dev/null +++ b/AudioCuesheetEditor/Shared/TrackList/TitleColumn.razor @@ -0,0 +1,98 @@ +<!-- +This file is part of AudioCuesheetEditor. + +AudioCuesheetEditor is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +AudioCuesheetEditor is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Foobar. If not, see +<http: //www.gnu.org/licenses />. +--> +@inherits BaseLocalizedComponent + +@inject AutocompleteManager _autocompleteManager +@inject ValidationService _validationService + +<MudAutocomplete T="MusicBrainzTrack" SearchFunc="(value, token) => _autocompleteManager.SearchTitlesAsync(value, Track.Artist, token)" ToStringFunc="(value) => value.Title" + @bind-Text="title" Value="autocompleteTrack" ValueChanged="TitleSelected" ResetValueOnEmptyText Clearable ShowProgressIndicator + Validation="(string? newTitle) => _validationService.Validate(Track, nameof(Track.Title))" MaxItems="null" CoerceText="false" OnBlur="OnBlur"> + <ItemTemplate Context="autocompleteContext"> + @if (autocompleteContext.Disambiguation != null) + { + <MudText>@String.Format("{0} ({1})", autocompleteContext.Title, autocompleteContext.Disambiguation)</MudText> + } + else + { + <MudText>@autocompleteContext.Title</MudText> + } + </ItemTemplate> +</MudAutocomplete> + +@code { + [Parameter] + [EditorRequired] + public Track Track { get; set; } = default!; + + [Parameter] + [EditorRequired] + public ViewMode CurrentViewMode { get; set; } + + MusicBrainzTrack? autocompleteTrack; + string? title; + Timer? debounceTimer; + + protected override void OnParametersSet() + { + base.OnParametersSet(); + autocompleteTrack = new() { Artist = Track.Artist, Title = Track.Title }; + title = autocompleteTrack?.Title; + } + + void TitleSelected(MusicBrainzTrack? newValue) + { + autocompleteTrack = newValue; + title = autocompleteTrack?.Title; + StartChangeTimer(); + } + + void OnBlur(FocusEventArgs args) + { + StartChangeTimer(); + } + + void StartChangeTimer() + { + debounceTimer = new Timer(100); + debounceTimer.Elapsed += ChangeTitle; + debounceTimer.AutoReset = false; + debounceTimer.Start(); + } + + void ChangeTitle(object? sender, System.Timers.ElapsedEventArgs e) + { + base.TraceChangeManager.BulkEdit = true; + Track.Title = title; + switch (CurrentViewMode) + { + case ViewMode.DetailView: + case ViewMode.ImportView: + if ((String.IsNullOrEmpty(Track.Artist)) && (String.IsNullOrEmpty(autocompleteTrack?.Artist) == false)) + { + Track.Artist = autocompleteTrack.Artist; + } + if ((Track.Length.HasValue == false) && (autocompleteTrack?.Length.HasValue == true)) + { + Track.Length = autocompleteTrack?.Length; + } + break; + } + base.TraceChangeManager.BulkEdit = false; + } +} diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx index 0dcee2ab..c780f878 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.de.resx @@ -138,12 +138,6 @@ <data name="Length" xml:space="preserve"> <value>Länge</value> </data> - <data name="Remarks" xml:space="preserve"> - <value>Bemerkungen</value> - </data> - <data name="Currently playing this track" xml:space="preserve"> - <value>Dieser Titel wird gerade abgespielt</value> - </data> <data name="A section is beginning inside this track" xml:space="preserve"> <value>Ein Abschnitt beginnt innerhalb dieses Titels</value> </data> @@ -159,4 +153,16 @@ <data name="Confirm" xml:space="preserve"> <value>Bestätigen</value> </data> + <data name="Status" xml:space="preserve"> + <value>Status</value> + </data> + <data name="Section" xml:space="preserve"> + <value>Abschnitt</value> + </data> + <data name="Unlink this track from previous track" xml:space="preserve"> + <value>Track von vorherigem abkoppeln</value> + </data> + <data name="Link this track to previous track" xml:space="preserve"> + <value>Track mit vorherigem verknüpfen</value> + </data> </root> \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor index dcc356ac..3cb44403 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.razor +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.razor @@ -21,11 +21,12 @@ along with Foobar. If not, see @inject IStringLocalizer<ValidationMessage> _validationMessageLocalizer @inject ApplicationOptionsTimeSpanParser _applicationOptionsTimeSpanParser @inject AutocompleteManager _autocompleteManager -@inject EditTrackModalManager _editTrackModalManager +@inject DialogManager _editTrackModalManager @inject ValidationService _validationService @inject ILocalStorageOptionsProvider _localStorageOptionsProvider @inject PlaybackService _playbackService @inject IDialogService _dialogService +@inject ISessionStateContainer _sessionStateContainer @if (CurrentViewMode == ViewMode.DetailView) { @@ -46,119 +47,81 @@ along with Foobar. If not, see MoveTracksUpDisabled="!(Cuesheet?.MoveTracksPossible(selectedTracks, MoveDirection.Up) == true)" MoveTracksUpClicked="() => Cuesheet?.MoveTracks(selectedTracks, MoveDirection.Up)" MoveTracksDownDisabled="!(Cuesheet?.MoveTracksPossible(selectedTracks, MoveDirection.Down) == true)" MoveTracksDownClicked="() => Cuesheet?.MoveTracks(selectedTracks, MoveDirection.Down)" CopySelectedTracksDisabled="selectedTracks.Count != 1" CopySelectedTracksClicked="() => CopyTrackClicked()" - FixedHeader="ApplicationOptions?.FixedTracksTableHeader == true" FixedHeaderClicked="() => LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.FixedTracksTableHeader, !ApplicationOptions?.FixedTracksTableHeader)" /> + FixedHeader="applicationOptions?.FixedTracksTableHeader == true" FixedHeaderClicked="() => _localStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.FixedTracksTableHeader, !applicationOptions?.FixedTracksTableHeader)" /> } - -<MudDataGrid T="Track" Items="Cuesheet?.Tracks" ReadOnly="false" Bordered MultiSelection SelectOnRowClick="false" @bind-SelectedItems="selectedTracks" EditTrigger="DataGridEditTrigger.OnRowClick" - EditMode="DataGridEditMode.Cell" ColumnResizeMode="ResizeMode.Column" RowContextMenuClick="OpenMenuContent" Height="@(ApplicationOptions?.FixedTracksTableHeader == true ? "600px" : null)" FixedHeader="ApplicationOptions?.FixedTracksTableHeader == true"> - <Columns> - <SelectColumn Hidden="CurrentViewMode == ViewMode.RecordView" /> - <TemplateColumn Title="@_localizer["Controls"]" Hidden="CurrentViewMode != ViewMode.RecordView"> - <EditTemplate> - <MudIconButton Icon="@Icons.Material.Outlined.Delete" Variant="Variant.Filled" Color="Color.Error" OnClick="() => Cuesheet?.RemoveTrack(context.Item)" /> - </EditTemplate> - </TemplateColumn> - <PropertyColumn Property="x => x.Position" Title="#" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> - <EditTemplate> - <MudNumericField @bind-Value="context.Item.Position" Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Position)))" - ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Position))" /> - </EditTemplate> - </PropertyColumn> - <PropertyColumn Property="x => x.Artist" Title="@_localizer["Artist"]"> - <EditTemplate> - @{ - MusicBrainzArtist? autocompleteArtist = new() +<MudForm @ref="form"> + <MudDataGrid T="Track" Items="Cuesheet?.Tracks" ReadOnly="false" Bordered MultiSelection SelectOnRowClick="false" @bind-SelectedItems="selectedTracks" EditTrigger="DataGridEditTrigger.OnRowClick" + EditMode="DataGridEditMode.Cell" ColumnResizeMode="ResizeMode.Column" RowContextMenuClick="OpenMenuContent" Height="@(applicationOptions?.FixedTracksTableHeader == true ? "600px" : null)" + FixedHeader="applicationOptions?.FixedTracksTableHeader == true" Validator="form" Virtualize> + <Columns> + <SelectColumn Hidden="CurrentViewMode == ViewMode.RecordView" /> + <TemplateColumn Title="@_localizer["Controls"]" Hidden="CurrentViewMode != ViewMode.RecordView"> + <EditTemplate> + <MudIconButton Icon="@Icons.Material.Outlined.Delete" Variant="Variant.Filled" Color="Color.Error" OnClick="() => Cuesheet?.RemoveTrack(context.Item)" /> + </EditTemplate> + </TemplateColumn> + <PropertyColumn Property="x => x.Position" Title="#" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> + <EditTemplate> + <MudNumericField Validation="(uint? newPosition) => _validationService.Validate(context.Item, nameof(Track.Position))" Value="context.Item.Position" + ValueChanged="(uint? newPosition) => PositionChanged(context.Item, newPosition)" /> + </EditTemplate> + </PropertyColumn> + <PropertyColumn Property="x => x.Artist" Title="@_localizer["Artist"]"> + <EditTemplate> + <ArtistColumn Track="context.Item" /> + </EditTemplate> + </PropertyColumn> + <PropertyColumn Property="x => x.Title" Title="@_localizer["Title"]"> + <EditTemplate> + <TitleColumn Track="context.Item" CurrentViewMode="CurrentViewMode" /> + </EditTemplate> + </PropertyColumn> + <PropertyColumn Property="x => x.Begin" Title="@_localizer["Begin"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> + <CellTemplate> + @_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin) + </CellTemplate> + <EditTemplate> + <MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin)" ValueChanged="(string value) => TimespanChanged(context.Item, x => x.Begin, value)" + Validation="(string? newBegin) => _validationService.Validate(context.Item, nameof(Track.Begin))" /> + </EditTemplate> + </PropertyColumn> + <PropertyColumn Property="x => x.End" Title="@_localizer["End"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> + <CellTemplate> + @_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End) + </CellTemplate> + <EditTemplate> + <MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End)" ValueChanged="(string value) => TimespanChanged(context.Item, x => x.End, value)" + Validation="(string? newEnd) => _validationService.Validate(context.Item, nameof(Track.End))" /> + </EditTemplate> + </PropertyColumn> + <PropertyColumn Property="x => x.Length" Title="@_localizer["Length"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> + <CellTemplate> + @_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Length) + </CellTemplate> + <EditTemplate> + <MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Length)" ValueChanged="(string value) => TimespanChanged(context.Item, x => x.Length, value)" + Validation="(string? newLength) => _validationService.Validate(context.Item, nameof(Track.Length))" /> + </EditTemplate> + </PropertyColumn> + <TemplateColumn Title="@_localizer["Status"]" Hidden="CurrentViewMode != ViewMode.DetailView" HeaderStyle="width: 1px;"> + <EditTemplate> + @if (Cuesheet?.GetSection(context.Item) != null) { - Name = context.Item.Artist - }; - } - <MudAutocomplete T="MusicBrainzArtist" SearchFunc="_autocompleteManager.SearchArtistsAsync" ToStringFunc="(value) => value.Name" @bind-Text="context.Item.Artist" - @bind-Value="autocompleteArtist" Clearable ShowProgressIndicator Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Artist)))" - ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Artist))" MaxItems="null" CoerceText="false"> - <ItemTemplate Context="autocompleteContext"> - @if (autocompleteContext.Disambiguation != null) - { - <MudText>@String.Format("{0} ({1})", autocompleteContext.Name, autocompleteContext.Disambiguation)</MudText> - } - else - { - <MudText>@autocompleteContext.Name</MudText> - } - </ItemTemplate> - </MudAutocomplete> - </EditTemplate> - </PropertyColumn> - <PropertyColumn Property="x => x.Title" Title="@_localizer["Title"]"> - <EditTemplate> - @{ - MusicBrainzTrack? autocompleteTrack = new() + <MudTooltip Text="@_localizer["A section is beginning inside this track"]"> + <MudChip Icon="@Icons.Material.Outlined.ContentCut" Color="Color.Info">@_localizer["Section"]</MudChip> + </MudTooltip> + } + @if (Cuesheet?.Tracks.FirstOrDefault() != context.Item) { - Artist = context.Item.Artist, - Title = context.Item.Title - }; - } - <MudAutocomplete T="MusicBrainzTrack" SearchFunc="(value, token) => _autocompleteManager.SearchTitlesAsync(value, context.Item.Artist, token)" ToStringFunc="(value) => value.Title" - @bind-Text="context.Item.Title" Value="autocompleteTrack" ValueChanged="(value) => TitleSelected(context.Item, value)" - ResetValueOnEmptyText Clearable ShowProgressIndicator Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Title)))" - ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Title))" MaxItems="null" CoerceText="false"> - <ItemTemplate Context="autocompleteContext"> - @if (autocompleteContext.Disambiguation != null) - { - <MudText>@String.Format("{0} ({1})", autocompleteContext.Title, autocompleteContext.Disambiguation)</MudText> - } - else - { - <MudText>@autocompleteContext.Title</MudText> - } - </ItemTemplate> - </MudAutocomplete> - </EditTemplate> - </PropertyColumn> - <PropertyColumn Property="x => x.Begin" Title="@_localizer["Begin"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> - <CellTemplate> - @_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin) - </CellTemplate> - <EditTemplate> - <MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Begin)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.Begin, value)" - Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Begin)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Begin))" /> - </EditTemplate> - </PropertyColumn> - <PropertyColumn Property="x => x.End" Title="@_localizer["End"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> - <CellTemplate> - @_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End) - </CellTemplate> - <EditTemplate> - <MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.End)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.End, value)" - Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.End)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.End))" /> - </EditTemplate> - </PropertyColumn> - <PropertyColumn Property="x => x.Length" Title="@_localizer["Length"]" Editable="CurrentViewMode != ViewMode.RecordView" HeaderStyle="width: 1px;"> - <CellTemplate> - @_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Length) - </CellTemplate> - <EditTemplate> - <MudTextField Value="@_applicationOptionsTimeSpanParser.GetTimespanFormatted(context.Item.Length)" ValueChanged="(string value) => _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TimeSpan?>(context.Item, x => x.Length, value)" - Error="!String.IsNullOrEmpty(GetValidationErrorMessage(context.Item, nameof(Track.Length)))" ErrorText="@GetValidationErrorMessage(context.Item, nameof(Track.Length))" /> - </EditTemplate> - </PropertyColumn> - <TemplateColumn Title="@_localizer["Remarks"]" Hidden="CurrentViewMode != ViewMode.DetailView" HeaderStyle="width: 1px;"> - <EditTemplate> - @if (_playbackService.CurrentlyPlayingTrack == context.Item) - { - <MudIcon Icon="@Icons.Material.Outlined.Audiotrack" Title="@_localizer["Currently playing this track"]" /> - } - @if (Cuesheet?.GetSection(context.Item) != null) - { - <MudIcon Icon="@Icons.Material.Outlined.ContentCut" Title="@_localizer["A section is beginning inside this track"]" /> - } - @if (Cuesheet?.Tracks.FirstOrDefault() != context.Item) - { - <MudIconButton Icon="@GetLinkedTrackIcon(context.Item)" OnClick="() => context.Item.IsLinkedToPreviousTrack = !context.Item.IsLinkedToPreviousTrack" /> - } - </EditTemplate> - </TemplateColumn> - </Columns> -</MudDataGrid> + <MudTooltip Text="@GetLinkedTrackTooltip(context.Item)"> + <MudChip Icon="@GetLinkedTrackIcon(context.Item)" Color="Color.Secondary" OnClick="() => context.Item.IsLinkedToPreviousTrack = !context.Item.IsLinkedToPreviousTrack" /> + </MudTooltip> + } + </EditTemplate> + </TemplateColumn> + </Columns> + </MudDataGrid> +</MudForm> <MudMenu PositionAtCursor @ref="trackContextMenu"> @if (contextMenuTrack != null) @@ -172,30 +135,46 @@ along with Foobar. If not, see HashSet<Track> selectedTracks = new(); MudMenu? trackContextMenu; Track? contextMenuTrack; + ApplicationOptions? applicationOptions; + MudForm? form; - [CascadingParameter] - public ViewMode CurrentViewMode { get; set; } + public Cuesheet? Cuesheet + { + get + { + if (CurrentViewMode == ViewMode.ImportView) + { + return _sessionStateContainer.ImportCuesheet; + } + return _sessionStateContainer.Cuesheet; + } + } [CascadingParameter] - public Cuesheet? Cuesheet { get; set; } + public ViewMode CurrentViewMode { get; set; } protected override void Dispose(bool disposing) { base.Dispose(disposing); - _playbackService.CurrentPositionChanged -= PlaybackService_CurrentPositionChanged; + _localStorageOptionsProvider.OptionSaved -= LocalStorageOptionsProvider_OptionSaved; + _sessionStateContainer.ImportCuesheetChanged -= SessionStateContainer_ImportCuesheetChanged; + _sessionStateContainer.CuesheetChanged -= SessionStateContainer_CuesheetChanged; } - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - base.OnInitialized(); - _playbackService.CurrentPositionChanged += PlaybackService_CurrentPositionChanged; + await base.OnInitializedAsync(); + applicationOptions = await _localStorageOptionsProvider.GetOptionsAsync<ApplicationOptions>(); + _localStorageOptionsProvider.OptionSaved += LocalStorageOptionsProvider_OptionSaved; + _sessionStateContainer.ImportCuesheetChanged += SessionStateContainer_ImportCuesheetChanged; + _sessionStateContainer.CuesheetChanged += SessionStateContainer_CuesheetChanged; } void AddTrackClicked() { var newTrack = new Track() { - IsLinkedToPreviousTrack = ApplicationOptions!.LinkTracks + IsLinkedToPreviousTrack = applicationOptions!.DefaultIsLinkedToPreviousTrack }; Cuesheet?.AddTrack(newTrack); TraceChangeManager.TraceChanges(newTrack); @@ -246,17 +225,6 @@ along with Foobar. If not, see } } - String? GetValidationErrorMessage(object model, string propertyName) - { - String? validationErrorMessage = null; - var validationMessages = _validationService.Validate(model, propertyName); - if (validationMessages.Count() > 0) - { - validationErrorMessage = String.Join(Environment.NewLine, validationMessages); - } - return validationErrorMessage; - } - async Task CopyTrackClicked(Track? trackToCopy = null) { var trackThatWillBeCopied = trackToCopy; @@ -299,7 +267,51 @@ along with Foobar. If not, see } } - void PlaybackService_CurrentPositionChanged() + String GetLinkedTrackTooltip(Track track) + { + if (track.IsLinkedToPreviousTrack) + { + return _localizer["Unlink this track from previous track"]; + } + else + { + return _localizer["Link this track to previous track"]; + } + } + + void LocalStorageOptionsProvider_OptionSaved(object? sender, IOptions option) + { + if (option is ApplicationOptions applicationOption) + { + applicationOptions = applicationOption; + StateHasChanged(); + } + } + + async Task PositionChanged(Track track, uint? newPosition) + { + track.Position = newPosition; + if (form != null) + { + await form.Validate(); + } + } + + async Task TimespanChanged<TProperty>(Track track, System.Linq.Expressions.Expression<Func<Track, TProperty>> expression, String value) + { + await _applicationOptionsTimeSpanParser.TimespanTextChanged<Track, TProperty>(track, expression, value); + if (form != null) + { + await form.Validate(); + } + } + + void SessionStateContainer_ImportCuesheetChanged(object? sender, EventArgs args) + { + StateHasChanged(); + } + + void SessionStateContainer_CuesheetChanged(object? sender, EventArgs args) { StateHasChanged(); } diff --git a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx index 7c24b95a..7ac488bb 100644 --- a/AudioCuesheetEditor/Shared/TrackList/TrackList.resx +++ b/AudioCuesheetEditor/Shared/TrackList/TrackList.resx @@ -138,12 +138,6 @@ <data name="Length" xml:space="preserve"> <value>Length</value> </data> - <data name="Remarks" xml:space="preserve"> - <value>Remarks</value> - </data> - <data name="Currently playing this track" xml:space="preserve"> - <value>Currently playing this track</value> - </data> <data name="A section is beginning inside this track" xml:space="preserve"> <value>A section is beginning inside this track</value> </data> @@ -159,4 +153,16 @@ <data name="Confirm" xml:space="preserve"> <value>Confirm</value> </data> + <data name="Status" xml:space="preserve"> + <value>Status</value> + </data> + <data name="Section" xml:space="preserve"> + <value>Section</value> + </data> + <data name="Unlink this track from previous track" xml:space="preserve"> + <value>Unlink this track from previous track</value> + </data> + <data name="Link this track to previous track" xml:space="preserve"> + <value>Link this track to previous track</value> + </data> </root> \ No newline at end of file diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor index 470d5c84..96aba8df 100644 --- a/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeImport.razor @@ -17,9 +17,11 @@ along with Foobar. If not, see --> @inherits BaseLocalizedComponent +@inject ILogger<ViewModeImport> _logger @inject IStringLocalizer<ViewModeImport> _localizer @inject ISessionStateContainer _sessionStateContainer @inject ImportManager _importManager +@inject DialogManager _dialogManager <MudStepper @ref="mudStepper" NonLinear OnPreviewInteraction="PreviewInteraction" @bind-ActiveIndex="activeStepIndex"> <MudStep Title="@_localizer["Select inputfiles"]" Completed="selectFilesStepCompleted" HasError="selectFilesStepError"> @@ -33,7 +35,7 @@ along with Foobar. If not, see </TitleContent> <ChildContent> <ImportFileContent FileContentChanged="ImportFileContent_FileContentChanged" /> - <ImportSchemes ImportSchemeCuesheetChanged="ReanalyseImportfile" ImportSchemeTracksChanged="ReanalyseImportfile" ImportTimeInputFormatChanged="ReanalyseImportfile" /> + <Importprofiles ImportprofileChanged="AnalyseImportfile" /> @if (_sessionStateContainer.Importfile?.AnalyseException != null) { <MudAlert Severity="Severity.Error" Variant="Variant.Filled"> @@ -67,30 +69,57 @@ along with Foobar. If not, see int activeStepIndex; Boolean fileContentExpanded = false, cuesheetDataExpanded = false, cuesheetTracksExpanded = false; Boolean selectFilesStepCompleted = false, selectFilesStepError = false; + long? renderBegin; - void ImportFileContent_FileContentChanged(string newFileContent) + protected override async Task OnInitializedAsync() { - var textToAnalyse = newFileContent.Split(Environment.NewLine); - _importManager.ImportText(textToAnalyse, ApplicationOptions!.ImportScheme, ApplicationOptions!.ImportTimeSpanFormat); + await base.OnInitializedAsync(); + if (_sessionStateContainer.Importfile?.FileType == ImportFileType.Textfile) + { + await FilesImported(); + } } - void ReanalyseImportfile() + protected override void OnAfterRender(bool firstRender) { - var fileContent = _sessionStateContainer.Importfile?.FileContent; - if (fileContent != null) + if (renderBegin.HasValue) { - _importManager.ImportText(fileContent, ApplicationOptions!.ImportScheme, ApplicationOptions.ImportTimeSpanFormat); + var renderDuration = System.Diagnostics.Stopwatch.GetElapsedTime(renderBegin.Value); + _logger.LogDebug("ViewModeImport render duration: {renderDuration}", renderDuration); } + base.OnAfterRender(firstRender); + } + + async Task ImportFileContent_FileContentChanged(string newFileContent) + { + _sessionStateContainer.Importfile!.FileContent = newFileContent; + await AnalyseImportfile(); + } + + async Task AnalyseImportfile() + { + await _importManager.AnalyseImportfile(); + renderBegin = System.Diagnostics.Stopwatch.GetTimestamp(); } - void FilesImported(Dictionary<IBrowserFile, ImportFileType> files) + async Task FilesImported() { + await _dialogManager.ShowLoadingDialogAsync(); + try + { + await AnalyseImportfile(); + } + finally + { + _dialogManager.HideLoadingDialog(); + } fileContentExpanded = true; cuesheetDataExpanded = true; cuesheetTracksExpanded = true; activeStepIndex = 1; selectFilesStepCompleted = true; selectFilesStepError = false; + renderBegin = System.Diagnostics.Stopwatch.GetTimestamp(); } async Task CompleteImportAsync() @@ -98,7 +127,7 @@ along with Foobar. If not, see _importManager.ImportCuesheet(); await ResetAsync(); // Don't await since otherwise the rendering will stop and this view will not be reset - _ = LocalStorageOptionsProvider.SaveOptionsValueAsync<ApplicationOptions>(x => x.ActiveTab, ViewMode.DetailView); + _ = LocalStorageOptionsProvider.SaveOptionsValueAsync<ViewOptions>(x => x.ActiveTab, ViewMode.DetailView); } Task PreviewInteraction(StepperInteractionEventArgs arg) diff --git a/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor index 9055797e..f734e7d6 100644 --- a/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor +++ b/AudioCuesheetEditor/Shared/ViewModes/ViewModeRecord.razor @@ -47,7 +47,4 @@ along with Foobar. If not, see @code { Boolean cuesheetDataExpanded = false, tracksExpanded = true; - - [CascadingParameter] - public Cuesheet? Cuesheet { get; set; } } diff --git a/AudioCuesheetEditor/_Imports.razor b/AudioCuesheetEditor/_Imports.razor index 4cd9df9e..4dd0e275 100644 --- a/AudioCuesheetEditor/_Imports.razor +++ b/AudioCuesheetEditor/_Imports.razor @@ -43,4 +43,5 @@ @using Howler.Blazor.Components @using Markdig @using Toolbelt.Blazor.HotKeys2 -@using MudBlazor \ No newline at end of file +@using MudBlazor +@using Ganss.Xss \ No newline at end of file diff --git a/AudioCuesheetEditor/wwwroot/index.html b/AudioCuesheetEditor/wwwroot/index.html index 9a4763e7..89c2fd6c 100644 --- a/AudioCuesheetEditor/wwwroot/index.html +++ b/AudioCuesheetEditor/wwwroot/index.html @@ -13,7 +13,7 @@ <link rel="shortcut icon" type="image/x-icon" href="favicon.ico"> <link href="css/app.css" rel="stylesheet" /> - <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" /> + <link href="_content/MudBlazor/MudBlazor.min.css?v=1" rel="stylesheet" /> <script src="scripts/howler.core.min.js"></script> <script src="_content/Howler.Blazor/JsInteropHowl.js"></script> @@ -56,9 +56,8 @@ </div> <script src="_framework/blazor.webassembly.js"></script> <script>navigator.serviceWorker.register('service-worker.js');</script> - <script src="scripts/fix-webm-duration.js"></script> <script src="scripts/library.js"></script> - <script src="_content/MudBlazor/MudBlazor.min.js"></script> + <script src="_content/MudBlazor/MudBlazor.min.js?v=1"></script> </body> </html>