diff --git a/.github/workflows/test-latest.yml b/.github/workflows/test-latest.yml new file mode 100644 index 0000000..65a9a0e --- /dev/null +++ b/.github/workflows/test-latest.yml @@ -0,0 +1,75 @@ +# Постоянная тестовая сборка форка: тег test-latest всегда указывает +# на последний коммит, а SignService.exe по одной и той же ссылке +# заменяется. На zaynullinmi/SignService задание не выполняется. +name: Test build + +on: + push: + branches: + - cursor/v1.3.1-ui-tweaks-cb6f + - cursor/cosign-discovery-gost-cb6f + - main + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: test-latest + cancel-in-progress: false + +jobs: + publish-test-latest: + name: Тестовая сборка test-latest + if: github.repository == 'sozertsatel/SignService' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + # Те же флаги, что у задания «Publish self-contained single-file exe» + # в .github/workflows/build.yml. + - name: Publish self-contained single-file exe + run: > + dotnet publish src/SignService -c Release -r win-x64 + --self-contained true + -p:PublishSingleFile=true + -p:IncludeNativeLibrariesForSelfExtract=true + -p:EnableCompressionInSingleFile=true + -o publish + + - name: SHA-256 + working-directory: publish + run: sha256sum SignService.exe | tee SignService.exe.sha256 + + - name: Обновить pre-release test-latest + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + version="$(sed -n 's/.*\([^<]*\)<\/Version>.*/\1/p' src/SignService/SignService.csproj | head -1)" + sha="$(git rev-parse HEAD)" + notes="$(printf '%s\n' \ + "Тестовая сборка форка, не выпуск для пользователей. Автообновление SignService запрашивает только релизы https://github.com/zaynullinmi/SignService/releases/latest и этот pre-release не видит." \ + "" \ + "- Версия: ${version}" \ + "- Коммит: ${sha}" \ + "- Ветка: ${GITHUB_REF_NAME}" \ + "" \ + "https://github.com/sozertsatel/SignService/releases/download/test-latest/SignService.exe")" + if gh release view test-latest >/dev/null 2>&1; then + gh release delete test-latest --yes --cleanup-tag + fi + # Тег без релиза тоже сдвигаем, иначе create останется на старом коммите. + git push origin ":refs/tags/test-latest" || true + gh release create test-latest \ + publish/SignService.exe \ + publish/SignService.exe.sha256 \ + --target "$sha" \ + --title "Тестовая сборка (последняя)" \ + --prerelease \ + --latest=false \ + --notes "$notes" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3548704..8da9459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # История версий SignService +## v1.3.3 — 12.10.2026 + +- **Резервная копия .sig.** В «Настройках…» — галочка «Всегда создавать + резервную копию .sig (.bak) перед подписанием», по умолчанию выключена. + Без неё копия по-прежнему создаётся только в рискованных случаях: замена + файла подписи без объединения, исключение подписантов или пропажа + документа, вложенного в прикреплённую подпись. С галочкой .bak пишется + перед каждой перезаписью существующего .sig, в том числе при обычном + дописывании. +- **«Подписать»** стоит под списком файлов у правого края, как «Очистить список». +- **«Очистить лог»** перенесена в нижнюю строку, слева, в один ряд с «Настройки…». + Стирает только лог операций; «Очистить список» по-прежнему очищает очередь файлов. +- Если файл подписи успели изменить, пока шло подписание (другая программа, + синхронизация папки), запись отменяется: на диске остаётся более новая версия. +- **«Применить ко всем»** в режиме «Создать новую» прямо в строке пишет, + какие уже найденные подписи не войдут в объединение. +- **Версия на главном окне.** Строка с автором убрана (автор остаётся + в «О программе»). На её месте — версия программы. +- «Обзор…» и «Инструменты» стоят над списком файлов в одной строке, + «Очистить список» — у правого края. +- Пункт «Объединить .sig…» убран из меню «Инструменты»: соподписание + выбирается в строке файла. +- **«Применить ко всем»** рядом с «Инструменты». Все файлы очереди, + включая добавленные позже, берут режим первого файла. Если добавить + к существующей подписи нельзя, строка остаётся на «Создать новую» + и пишет об этом. Состояние галочки запоминается. + ## v1.3.0 — 09.10.2026 - **Проверка ГОСТ-подписей КриптоПро.** Подписи, у которых в signatureAlgorithm diff --git a/src/SignService/Services/AppSettings.cs b/src/SignService/Services/AppSettings.cs index 091c51e..e7c2d2a 100644 --- a/src/SignService/Services/AppSettings.cs +++ b/src/SignService/Services/AppSettings.cs @@ -38,6 +38,20 @@ public AppSettings(string settingsDirectory) /// Объединять свою подпись с существующим .sig (соподписание). public bool MergeWithExisting { get; set; } = true; + /// + /// Сохранять .bak перед любой перезаписью существующего .sig. + /// По умолчанию выключено: копия всё равно создаётся, если файл заменяется + /// без объединения, из него исключаются подписанты или пропадает документ, + /// вложенный в прикреплённую подпись. + /// + public bool CreateSignatureBackup { get; set; } + + /// + /// Все файлы очереди используют режим первого файла + /// («Создать новую» или «Добавить к существующей»). + /// + public bool ApplyCoSignToAll { get; set; } + /// Добавлять штамп времени TSA (CAdES-T) в подпись. public bool UseTimestamp { get; set; } diff --git a/src/SignService/Services/DocumentSigner.cs b/src/SignService/Services/DocumentSigner.cs index de9c126..3b2c3d8 100644 --- a/src/SignService/Services/DocumentSigner.cs +++ b/src/SignService/Services/DocumentSigner.cs @@ -28,6 +28,20 @@ public sealed record SignOptions /// Объединять ли с уже существующим файлом .sig (соподписание). public bool MergeWithExisting { get; init; } + /// + /// Сохранять .bak перед любой перезаписью существующего .sig, в том числе + /// при обычном дописывании. Без этого копия создаётся только в рискованных + /// случаях: замена без объединения, исключение подписантов или потеря + /// документа, вложенного в прикреплённую подпись. + /// + public bool CreateBackup { get; init; } + + /// + /// Вызывается после подготовки новой подписи и до её записи. + /// Нужен тестам, чтобы подменить файл подписи в этот промежуток. + /// + internal Action? BeforePublish { get; init; } + /// Пути к .sig других подписантов для объединения. public IReadOnlyList ExtraSignatures { get; init; } = Array.Empty(); @@ -227,9 +241,15 @@ void AddInputPath(string? path) var appended = inputPaths.Any(path => string.Equals( Path.GetFullPath(path), Path.GetFullPath(signaturePath), StringComparison.OrdinalIgnoreCase)); - var previousSignerCount = appended - ? CmsMerger.CountSigners(await File.ReadAllBytesAsync(signaturePath, cancellationToken)) - : 0; + // Снимок того, что вошло в объединение. Перед записью файл читается ещё раз: + // за время PIN-кода или пакетного подписания его могли обновить. + byte[]? previousBytes = null; + var previousSignerCount = 0; + if (appended) + { + previousBytes = await File.ReadAllBytesAsync(signaturePath, cancellationToken); + previousSignerCount = CmsMerger.CountSigners(previousBytes); + } int signerCount; IReadOnlyList excluded; @@ -256,13 +276,21 @@ void AddInputPath(string? path) cancellationToken.ThrowIfCancellationRequested(); if (!(await File.ReadAllBytesAsync(targetPath, cancellationToken)).AsSpan().SequenceEqual(data)) throw new IOException("Документ изменился во время подписания. Подпись не сохранена; повторите операцию."); - if (poaPackage is not null) PowerOfAttorneyService.CopyPackage(poaPackage, targetPath); - // Резервная копия — только когда прежний файл теряет данные: он заменяется без - // объединения или из него исключаются подписанты. Дописанная подпись сохраняет - // всех прежних подписантов байт в байт, копия тогда не нужна. + options.BeforePublish?.Invoke(); + // Рискованные случаи — всегда: файл заменяется без объединения, из него + // исключаются подписанты или пропадает документ внутри прикреплённой подписи. + // Галочка добавляет копию и при обычном дописывании. Если файл успели + // изменить после чтения — не пишем ничего, в том числе файлы МЧД. string? backupPath = null; - if (File.Exists(signaturePath) && (!appended || excluded.Count > 0)) - backupPath = SignatureBackup.Create(signaturePath); + if (File.Exists(signaturePath)) + { + var latest = await File.ReadAllBytesAsync(signaturePath, cancellationToken); + ThrowIfSignatureChanged(previousBytes, latest); + var riskyOverwrite = !appended || excluded.Count > 0 || LosesEmbeddedDocument(latest, output); + if (riskyOverwrite || options.CreateBackup) + backupPath = SignatureBackup.Create(signaturePath); + } + if (poaPackage is not null) PowerOfAttorneyService.CopyPackage(poaPackage, targetPath); await AtomicFile.WriteAsync(signaturePath, output, overwrite: true, cancellationToken); // Режим «копия отдельно»: штампованная копия БЕЗ подписи, со всеми @@ -294,6 +322,40 @@ void AddInputPath(string? path) previousSignerCount, appended, backupPath); } + /// + /// Отказ от записи, если файл подписи сменился после того, как его прочитали + /// для объединения. Иначе более новая чужая подпись была бы затёрта нашей копией. + /// + internal static void ThrowIfSignatureChanged(byte[]? snapshot, byte[] latest) + { + if (snapshot is not null && !snapshot.AsSpan().SequenceEqual(latest)) + throw new IOException( + "Файл подписи изменился во время подписания. Подпись не сохранена; повторите операцию."); + } + + /// + /// true, если в существующей прикреплённой подписи есть документ, а в новой + /// записи его не будет или он станет другим. Сами значения подписей при этом + /// могут сохраниться — пропадает копия документа внутри .sig. + /// + internal static bool LosesEmbeddedDocument(byte[] existing, byte[] output) + { + byte[]? embedded; + try { embedded = CmsMerger.ExtractContent(existing); } + catch (Exception) { return true; } + if (embedded is null) + return false; + try + { + var kept = CmsMerger.ExtractContent(output); + return kept is null || !kept.AsSpan().SequenceEqual(embedded); + } + catch (Exception) + { + return true; + } + } + /// /// Формирует CMS/PKCS#7 подпись для массива байтов. /// diff --git a/src/SignService/SignService.csproj b/src/SignService/SignService.csproj index d326609..18c13e7 100644 --- a/src/SignService/SignService.csproj +++ b/src/SignService/SignService.csproj @@ -10,7 +10,7 @@ true SignService — подписание документов ЭЦП SignService - 1.3.0 + 1.3.3 Зайнуллин Марат Илгамович Зайнуллин Марат Илгамович © 2026 Зайнуллин Марат Илгамович, +7-963-694-2461, zaynullinmi@gmail.com diff --git a/src/SignService/ViewModels/MainWindowViewModel.cs b/src/SignService/ViewModels/MainWindowViewModel.cs index 3961fe6..55105c8 100644 --- a/src/SignService/ViewModels/MainWindowViewModel.cs +++ b/src/SignService/ViewModels/MainWindowViewModel.cs @@ -38,6 +38,8 @@ public MainWindowViewModel(CertificateProvider certificateProvider, DocumentSign _initializing = true; IsDetached = _settings.DetachedSignature; MergeWithExisting = _settings.MergeWithExisting; + CreateSignatureBackup = _settings.CreateSignatureBackup; + ApplyCoSignToAll = _settings.ApplyCoSignToAll; UseTimestamp = _settings.UseTimestamp; TsaUrl = _settings.TsaUrl; UseStamp = _settings.UseStamp; @@ -59,6 +61,9 @@ public MainWindowViewModel(CertificateProvider certificateProvider, DocumentSign /// Настройки приложения — для окна «О программе». public AppSettings Settings => _settings; + /// Версия программы для нижней строки главного окна. + public string VersionDisplay => "Версия " + UpdateService.CurrentVersion; + // Тихая проверка обновлений при запуске: при наличии новой версии — строка // в статусе/логе, никаких всплывающих окон. private async Task CheckUpdatesOnStartAsync() @@ -102,6 +107,16 @@ private async Task CheckUpdatesOnStartAsync() [ObservableProperty] private bool _mergeWithExisting = true; + /// Сохранять .bak перед любой перезаписью существующего .sig. + [ObservableProperty] + private bool _createSignatureBackup; + + /// Режим первого файла в очереди применяется ко всем остальным. + [ObservableProperty] + private bool _applyCoSignToAll; + + private bool _syncingCoSign; + [ObservableProperty] private bool _includeExpiredCertificates; @@ -163,7 +178,6 @@ private async Task CheckUpdatesOnStartAsync() [NotifyCanExecuteChangedFor(nameof(RemoveFromStoreCommand))] [NotifyCanExecuteChangedFor(nameof(StampOnlyCommand))] [NotifyCanExecuteChangedFor(nameof(ExtractCommand))] - [NotifyCanExecuteChangedFor(nameof(MergeFilesCommand))] [NotifyCanExecuteChangedFor(nameof(BuildContainerCommand))] [NotifyCanExecuteChangedFor(nameof(VerifySignatureCommand))] [NotifyCanExecuteChangedFor(nameof(SplitSignaturesCommand))] @@ -214,9 +228,6 @@ private void Log(string message) /// Запрос диалога выбора контейнеров для извлечения. public event EventHandler? ExtractRequested; - /// Запрос диалога выбора подписей для объединения без подписания. - public event EventHandler? MergeFilesRequested; - /// Запрос диалога выбора PDF для штампа без подписания. public event EventHandler? StampOnlyRequested; @@ -300,6 +311,63 @@ partial void OnMergeWithExistingChanged(bool value) foreach (var file in Files) file.ApplyPreferredMode(value, resetPin: !_initializing); + if (ApplyCoSignToAll) + SyncSharedCoSignMode(); + } + + partial void OnCreateSignatureBackupChanged(bool value) + { + if (!_initializing) + { + _settings.CreateSignatureBackup = value; + _settings.Save(); + } + + foreach (var file in Files) + file.SetBackupMention(value); + } + + partial void OnApplyCoSignToAllChanged(bool value) + { + if (!_initializing) + { + _settings.ApplyCoSignToAll = value; + _settings.Save(); + } + + SyncSharedCoSignMode(); + } + + /// Смена режима в строке: при «Применить ко всем» источник — первый файл. + internal void NotifyCoSignSelectionChanged(SignFileItem item) + { + if (_syncingCoSign || !ApplyCoSignToAll || Files.Count == 0 || !ReferenceEquals(Files[0], item)) + return; + SyncSharedCoSignMode(); + } + + private void SyncSharedCoSignMode() + { + if (_syncingCoSign) + return; + _syncingCoSign = true; + try + { + var add = Files.Count > 0 && Files[0].SelectedCoSignOption is { CreateNew: false }; + for (var i = 0; i < Files.Count; i++) + { + var file = Files[i]; + file.CoSignLocked = ApplyCoSignToAll && i > 0; + if (!ApplyCoSignToAll || i == 0) + file.StopFollowingSharedMode(); + else + file.FollowSharedMode(add); + } + } + finally + { + _syncingCoSign = false; + } } partial void OnUseTimestampChanged(bool value) @@ -739,6 +807,7 @@ public async Task AddFilesAsync(params string[] filePaths) continue; var item = new SignFileItem(path) { Owner = this }; + item.SetBackupMention(CreateSignatureBackup); item.SignsStampedCopy = SignsStampedCopy(item); Files.Add(item); added.Add(item); @@ -845,6 +914,8 @@ private async Task DiscoverAsync(IReadOnlyList t matched.Add(System.IO.Path.GetFullPath(path)); } + if (ApplyCoSignToAll) + SyncSharedCoSignMode(); return new DiscoveryOutcome(withMatches, attached, matched); } finally @@ -873,9 +944,6 @@ public void AttachSignatureFiles(SignFileItem item, IReadOnlyList paths) [RelayCommand(CanExecute = nameof(CanBrowse))] private void Extract() => ExtractRequested?.Invoke(this, EventArgs.Empty); - [RelayCommand(CanExecute = nameof(CanBrowse))] - private void MergeFiles() => MergeFilesRequested?.Invoke(this, EventArgs.Empty); - [RelayCommand(CanExecute = nameof(CanBrowse))] private void BuildContainer() => BuildContainerRequested?.Invoke(this, EventArgs.Empty); @@ -1191,39 +1259,6 @@ private void OpenFolder(SignFileItem item) } } - /// Объединяет выбранные файлы подписей в один — без создания своей подписи. - public async Task MergeSignatureFilesAsync(IReadOnlyList paths) - { - if (paths.Count == 0) - return; - - IsBusy = true; - try - { - var r = await Task.Run(() => CmsExtractor.MergeSignatureFiles(paths)); - var parts = new List - { - $"подписантов: {r.SignerCount}", - r.AttachedOutput ? "прикреплённая (документ внутри)" : "откреплённая", - r.DocumentNote, - }; - if (r.ExcludedSigners.Count > 0) - parts.Add("исключены не соответствующие документу: " + string.Join("; ", r.ExcludedSigners)); - if (r.UnverifiedSigners.Count > 0) - parts.Add("не удалось проверить: " + string.Join("; ", r.UnverifiedSigners)); - - StatusText = $"Объединено → «{System.IO.Path.GetFileName(r.OutputPath)}»: {string.Join(", ", parts)}"; - } - catch (Exception ex) - { - StatusText = "Объединение не выполнено: " + ex.Message; - } - finally - { - IsBusy = false; - } - } - /// Извлекает содержимое выбранных контейнеров в файлы рядом с ними. public async Task ExtractContainersAsync(IReadOnlyList containerPaths) { @@ -1265,7 +1300,12 @@ public async Task ExtractContainersAsync(IReadOnlyList containerPaths) } [RelayCommand] - private void RemoveFile(SignFileItem item) => Files.Remove(item); + private void RemoveFile(SignFileItem item) + { + Files.Remove(item); + if (ApplyCoSignToAll) + SyncSharedCoSignMode(); + } [RelayCommand(CanExecute = nameof(CanClear))] private void Clear() @@ -1316,6 +1356,8 @@ private bool CanSignAll() => } } + WarnIfEmbeddedDocumentDropped(file, blocks); + var inputs = file.PlannedMergeInputs(); if (inputs.Count == 0) continue; @@ -1342,6 +1384,38 @@ private bool CanSignAll() => return string.Join("\n\n", blocks) + "\n\nПродолжить?"; } + // Прикреплённая подпись хранит копию документа внутри. Откреплённый режим + // или другая копия документа эту копию затирают — предупреждаем до записи. + private void WarnIfEmbeddedDocumentDropped(SignFileItem file, List blocks) + { + if (file.SignsStampedCopy || file.SelectedCoSignOption is not { CreateNew: false, SignaturePath: { } path }) + return; + if (!System.IO.File.Exists(path)) + return; + try + { + var embedded = CmsMerger.ExtractContent(System.IO.File.ReadAllBytes(path)); + if (embedded is null) + return; + var name = System.IO.Path.GetFileName(path); + if (IsDetached) + { + blocks.Add($"«{file.FileName}»: «{name}» — прикреплённая подпись, внутри лежит копия документа. " + + "Откреплённый режим уберёт её из файла (подписанты сохранятся, перед заменой создаётся резервная копия)."); + return; + } + + var document = System.IO.File.ReadAllBytes(file.FilePath); + if (!embedded.AsSpan().SequenceEqual(document)) + blocks.Add($"«{file.FileName}»: в «{name}» вложен другой документ. Он будет заменён текущим файлом " + + "(перед заменой создаётся резервная копия)."); + } + catch (Exception e) + { + blocks.Add($"«{file.FileName}»: не удалось проверить, есть ли внутри подписи копия документа ({e.Message})."); + } + } + [RelayCommand(CanExecute = nameof(CanSignAll))] private async Task SignAllAsync() { @@ -1459,6 +1533,7 @@ private async Task SignAllAsync() StampWithDate = StampWithDate, StampLogoPath = StampLogoPath, StampParameters = stampParameters, + CreateBackup = CreateSignatureBackup, PowerOfAttorney = poaPackage?.Info, PreparedPowerOfAttorney = poaPackage, }; diff --git a/src/SignService/ViewModels/SignFileItem.cs b/src/SignService/ViewModels/SignFileItem.cs index 085b012..6f4d1bf 100644 --- a/src/SignService/ViewModels/SignFileItem.cs +++ b/src/SignService/ViewModels/SignFileItem.cs @@ -48,6 +48,9 @@ public partial class SignFileItem : ObservableObject private readonly List _extraSignatures = new(); private IReadOnlyList _discovered = Array.Empty(); private bool _adjustingSelection; + private bool _mentionBackup; + private bool _followingShared; + private bool _sharedWantsAdd; private int _unionCount; private string _planText = "будет создан новый файл подписи"; @@ -143,9 +146,20 @@ public SignFileItem(string filePath) [ObservableProperty] private CoSignOption? _selectedCoSignOption; + /// Режим задаёт первый файл («Применить ко всем») — список в этой строке только для чтения. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanChooseCoSign))] + private bool _coSignLocked; + + /// Общий режим «добавить» для этого файла невозможен. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(WarningDisplay))] + [NotifyPropertyChangedFor(nameof(HasWarning))] + private bool _sharedAddUnavailable; + /// Режим можно менять, пока файл ждёт подписания и программа не занята. public bool CanChooseCoSign => Status is SignStatus.Pending or SignStatus.Failed && !IsDiscovering && !SignsStampedCopy - && Owner?.IsBusy != true; + && !CoSignLocked && Owner?.IsBusy != true; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasExtra))] @@ -177,7 +191,9 @@ public void ApplyDiscovery(SignatureDiscovery.DocumentDiscovery discovery, bool CoSignOptions.Add(new CoSignOption(match.Path, match.SignerCount)); CoSignOptions.Add(new CoSignOption(null, 0)); - if (pinned && previous is null) + if (_followingShared) + ApplySharedSelection(); + else if (pinned && previous is null) SelectedCoSignOption = CoSignOptions.Last(option => option.CreateNew); else if (pinned) SelectedCoSignOption = CoSignOptions.FirstOrDefault(option => @@ -192,6 +208,40 @@ public void ApplyDiscovery(SignatureDiscovery.DocumentDiscovery discovery, bool UpdatePlan(); } + /// Упоминать ли в плане, что перед потерей данных сохранится .bak. + public void SetBackupMention(bool enabled) + { + if (_mentionBackup == enabled) + return; + _mentionBackup = enabled; + UpdatePlan(); + } + + /// + /// Повторяет режим первого файла. Если добавить не к чему, остаётся «Создать новую» + /// и строка явно об этом говорит. + /// + public void FollowSharedMode(bool addToExisting) + { + _followingShared = true; + _sharedWantsAdd = addToExisting; + CoSignPinned = false; + _adjustingSelection = true; + ApplySharedSelection(); + _adjustingSelection = false; + UpdatePlan(); + } + + /// Снимает общий режим, не меняя уже выбранный пункт. + public void StopFollowingSharedMode() + { + _followingShared = false; + if (!SharedAddUnavailable) + return; + SharedAddUnavailable = false; + UpdatePlan(); + } + /// Общий переключатель «добавить / создать новую» для файлов без ручного выбора. public void ApplyPreferredMode(bool addToExisting, bool resetPin) { @@ -312,6 +362,25 @@ partial void OnSelectedCoSignOptionChanged(CoSignOption? value) if (!_adjustingSelection && value is not null) CoSignPinned = true; UpdatePlan(); + if (!_adjustingSelection && value is not null) + Owner?.NotifyCoSignSelectionChanged(this); + } + + private void ApplySharedSelection() + { + var add = _sharedWantsAdd && !SignsStampedCopy + ? CoSignOptions.FirstOrDefault(option => !option.CreateNew) + : null; + if (add is null) + { + SelectedCoSignOption = CoSignOptions.LastOrDefault(option => option.CreateNew) ?? CoSignOptions.LastOrDefault(); + SharedAddUnavailable = _sharedWantsAdd; + } + else + { + SelectedCoSignOption = add; + SharedAddUnavailable = false; + } } private void UpdatePlan() @@ -341,6 +410,8 @@ private string BuildPlan() $"будет добавлена подпись №{baseCount + 1} к «{Path.GetFileName(path)}» ({SelectedCoSignOption.SignerCount} {SignatureDiscovery.SignerWord(SelectedCoSignOption.SignerCount)})"; if (others.Count > 0) sentence += "; также объединятся: " + string.Join(", ", others.Select(match => "«" + Path.GetFileName(match.Path) + "»")); + if (_mentionBackup) + sentence += " (сохранится резервная копия)"; return sentence + extras; } @@ -348,6 +419,7 @@ private string BuildPlan() if (File.Exists(canonical)) return "будет создан новый файл подписи — существующий «" + Path.GetFileName(canonical) + "» будет заменён (сохранится резервная копия)" + extras; + return "будет создан новый файл подписи" + extras; } @@ -358,13 +430,26 @@ private string BuildPlan() private string? DiscoveryWarning() { - if (Status != SignStatus.Pending || IsDiscovering || SignsStampedCopy) + if (Status != SignStatus.Pending || IsDiscovering) return null; + string? fallback = null; + if (SharedAddUnavailable) + fallback = SignsStampedCopy + ? "Для копии со штампом режим «Добавить к существующей» недоступен — будет создан новый файл подписи." + : "Нет подходящей подписи — будет создан новый файл. Режим «Добавить к существующей», как у первого файла, здесь недоступен."; + if (SignsStampedCopy) + return fallback; + if (fallback is null && _followingShared && !_sharedWantsAdd && _discovered.Count > 0) + fallback = "Режим «Создать новую», как у первого файла. Найденные подписи не будут объединены: " + + string.Join(", ", _discovered.Select(match => "«" + Path.GetFileName(match.Path) + "»")) + "."; var names = PlannedDiscovered().SelectMany(match => match.MismatchedSignerNames).Distinct().ToList(); - if (names.Count == 0) - return null; - return "Внимание: при объединении будут исключены подписанты, чья подпись не соответствует документу: " - + string.Join("; ", names) + ". Перед исключением программа запросит подтверждение."; + var discovery = names.Count == 0 + ? null + : "Внимание: при объединении будут исключены подписанты, чья подпись не соответствует документу: " + + string.Join("; ", names) + ". Перед исключением программа запросит подтверждение."; + if (fallback is null) + return discovery; + return discovery is null ? fallback : fallback + " " + discovery; } private string SignedNote => string.IsNullOrEmpty(Message) ? string.Empty : $" ⚠ {Message}"; diff --git a/src/SignService/Views/MainWindow.axaml b/src/SignService/Views/MainWindow.axaml index a10ecd7..5f836b5 100644 --- a/src/SignService/Views/MainWindow.axaml +++ b/src/SignService/Views/MainWindow.axaml @@ -9,7 +9,7 @@ WindowStartupLocation="CenterScreen" DragDrop.AllowDrop="True"> - + @@ -104,8 +104,39 @@ + + + + + -