|
34 | 34 | <button @onclick="() => AutoSelect(AutoSelectMode.Oldest)">↑ Oldest (keep newest)</button> |
35 | 35 | <button @onclick="() => AutoSelect(AutoSelectMode.Newest)">↑ Newest (keep oldest)</button> |
36 | 36 | <button @onclick="() => AutoSelect(AutoSelectMode.HundredPercentEqual)">= 100% equal groups</button> |
| 37 | + <button @onclick="SelectShortest" |
| 38 | + title="Flags the shortest file in each group — skips groups where all files have the same duration.">↓ Shortest duration</button> |
| 39 | + <button @onclick="SelectLongest" |
| 40 | + title="Flags the longest file in each group — skips groups where all files have the same duration.">↑ Longest duration</button> |
37 | 41 | <hr /> |
38 | 42 | <div class="dropdown-section">By Property</div> |
39 | | - <button @onclick="SelectNoAudio">🔇 No audio track</button> |
| 43 | + <button @onclick="SelectNoAudio" |
| 44 | + title="Selects files with no audio track, group by group. Groups where ALL files have no audio are skipped entirely — if no file in the group has audio, there is no good copy to compare against.">🔇 No audio track</button> |
40 | 45 | <hr /> |
41 | 46 | <button @onclick="InvertSelection">⇄ Invert selection</button> |
42 | 47 | <button @onclick="ClearSelection">✕ Deselect all</button> |
|
158 | 163 | <button class="btn-sm" @onclick="() => ClearGroupSelection(items)" |
159 | 164 | disabled="@(!items.Any(x => _flagged.Contains(x.Path)))">✕ Unselect</button> |
160 | 165 | <button class="btn-sm" @onclick="() => { FlagNoAudio(items); _openGroupDropdown = Guid.Empty; }" |
161 | | - disabled="@(!items.Any(x => !x.IsImage && string.IsNullOrEmpty(x.AudioFormat) && !_deleted.Contains(x.Path)))" |
162 | | - title="Flag files in this group that have no audio track">🔇 Select No Audio Track</button> |
| 166 | + disabled="@(!(items.Any(x => !x.IsImage && string.IsNullOrEmpty(x.AudioFormat) && !_deleted.Contains(x.Path)) && items.Any(x => !x.IsImage && !string.IsNullOrEmpty(x.AudioFormat) && !_deleted.Contains(x.Path))))" |
| 167 | + title="Selects files with no audio track — only works when at least one file in this group has audio. If every file here has no audio, nothing is selected (can't tell which is the degraded copy).">🔇 Select No Audio Track</button> |
| 168 | + <button class="btn-sm" @onclick="() => FlagShortestDuration(items)" |
| 169 | + disabled="@(items.Where(x => !_deleted.Contains(x.Path)).Select(x => x.Duration).Distinct().Count() <= 1)" |
| 170 | + title="Flags the shortest file in this group. Does nothing if all files have the same duration.">↓ Shortest</button> |
| 171 | + <button class="btn-sm" @onclick="() => FlagLongestDuration(items)" |
| 172 | + disabled="@(items.Where(x => !_deleted.Contains(x.Path)).Select(x => x.Duration).Distinct().Count() <= 1)" |
| 173 | + title="Flags the longest file in this group. Does nothing if all files have the same duration.">↑ Longest</button> |
163 | 174 |
|
164 | 175 | <div class="toolbar-sep"></div> |
165 | 176 |
|
|
434 | 445 | _flagged.Add(i.Path); |
435 | 446 | } |
436 | 447 | void FlagNoAudio(List<DuplicateItem> items) { |
| 448 | + var eligible = items.Where(x => !x.IsImage && !_deleted.Contains(x.Path)).ToList(); |
| 449 | + if (!eligible.Any(x => !string.IsNullOrEmpty(x.AudioFormat))) return; // all silent — do nothing |
437 | 450 | ClearGroupSelection(items); |
438 | | - foreach (var i in items.Where(x => !x.IsImage && string.IsNullOrEmpty(x.AudioFormat) && !_deleted.Contains(x.Path))) |
| 451 | + foreach (var i in eligible.Where(x => string.IsNullOrEmpty(x.AudioFormat))) |
| 452 | + _flagged.Add(i.Path); |
| 453 | + } |
| 454 | + |
| 455 | + void FlagShortestDuration(List<DuplicateItem> items) { |
| 456 | + var eligible = items.Where(x => !x.IsImage && !_deleted.Contains(x.Path)).ToList(); |
| 457 | + if (eligible.Count < 2) return; |
| 458 | + var min = eligible.Min(x => x.Duration); |
| 459 | + if (eligible.Max(x => x.Duration) == min) return; // all same — do nothing |
| 460 | + ClearGroupSelection(items); |
| 461 | + foreach (var i in eligible.Where(x => x.Duration == min)) |
| 462 | + _flagged.Add(i.Path); |
| 463 | + } |
| 464 | + |
| 465 | + void FlagLongestDuration(List<DuplicateItem> items) { |
| 466 | + var eligible = items.Where(x => !x.IsImage && !_deleted.Contains(x.Path)).ToList(); |
| 467 | + if (eligible.Count < 2) return; |
| 468 | + var max = eligible.Max(x => x.Duration); |
| 469 | + if (eligible.Min(x => x.Duration) == max) return; // all same — do nothing |
| 470 | + ClearGroupSelection(items); |
| 471 | + foreach (var i in eligible.Where(x => x.Duration == max)) |
439 | 472 | _flagged.Add(i.Path); |
440 | 473 | } |
441 | 474 |
|
|
455 | 488 | void SelectNoAudio() { |
456 | 489 | _autoSelectOpen = false; |
457 | 490 | _flagged.Clear(); |
458 | | - foreach (var item in Scan.Duplicates.Where(d => !d.IsImage && string.IsNullOrEmpty(d.AudioFormat) && !_deleted.Contains(d.Path))) |
459 | | - _flagged.Add(item.Path); |
| 491 | + foreach (var group in Scan.Duplicates.GroupBy(d => d.GroupId)) { |
| 492 | + var items = group.Where(d => !d.IsImage && !_deleted.Contains(d.Path)).ToList(); |
| 493 | + if (!items.Any(d => !string.IsNullOrEmpty(d.AudioFormat))) continue; // all silent — skip group |
| 494 | + foreach (var d in items.Where(d => string.IsNullOrEmpty(d.AudioFormat))) |
| 495 | + _flagged.Add(d.Path); |
| 496 | + } |
| 497 | + } |
| 498 | + |
| 499 | + void SelectShortest() { |
| 500 | + _autoSelectOpen = false; |
| 501 | + _flagged.Clear(); |
| 502 | + foreach (var group in Scan.Duplicates.GroupBy(d => d.GroupId)) { |
| 503 | + var items = group.Where(d => !d.IsImage && !_deleted.Contains(d.Path)).ToList(); |
| 504 | + if (items.Count < 2) continue; |
| 505 | + var min = items.Min(d => d.Duration); |
| 506 | + if (items.Max(d => d.Duration) == min) continue; // all same — skip |
| 507 | + foreach (var d in items.Where(d => d.Duration == min)) |
| 508 | + _flagged.Add(d.Path); |
| 509 | + } |
| 510 | + } |
| 511 | + |
| 512 | + void SelectLongest() { |
| 513 | + _autoSelectOpen = false; |
| 514 | + _flagged.Clear(); |
| 515 | + foreach (var group in Scan.Duplicates.GroupBy(d => d.GroupId)) { |
| 516 | + var items = group.Where(d => !d.IsImage && !_deleted.Contains(d.Path)).ToList(); |
| 517 | + if (items.Count < 2) continue; |
| 518 | + var max = items.Max(d => d.Duration); |
| 519 | + if (items.Min(d => d.Duration) == max) continue; // all same — skip |
| 520 | + foreach (var d in items.Where(d => d.Duration == max)) |
| 521 | + _flagged.Add(d.Path); |
| 522 | + } |
460 | 523 | } |
461 | 524 | void InvertSelection() { |
462 | 525 | _autoSelectOpen = false; |
|
0 commit comments