diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 819da29..22e5d7e 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "0.16.0" +#define MyAppVersion "0.17.0" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/App.xaml.cs b/src/PSLauncher.App/App.xaml.cs index 24ee7d6..ca5ef35 100644 --- a/src/PSLauncher.App/App.xaml.cs +++ b/src/PSLauncher.App/App.xaml.cs @@ -16,6 +16,7 @@ using PSLauncher.Core.Integrity; using PSLauncher.Core.Licensing; using PSLauncher.Core.Localization; using PSLauncher.Core.Manifests; +using PSLauncher.Core.DocTool; using PSLauncher.Core.Migrations; using PSLauncher.Core.Process; using PSLauncher.Core.ReportTool; @@ -182,6 +183,13 @@ public partial class App : Application configProvider: () => sp.GetRequiredService().ReportTool, sp.GetRequiredService>())); + // Déploiement de l'outil Documentation (parallèle à Report) : + // copie atomique de _doc/ → C:\xampp\htdocs\ProserveDoc\ + services.AddSingleton(sp => + new DocToolDeployer( + configProvider: () => sp.GetRequiredService().DocTool, + sp.GetRequiredService>())); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 987f2e2..fb6f37f 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -15,9 +15,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 0.16.0 - 0.16.0.0 - 0.16.0.0 + 0.17.0 + 0.17.0.0 + 0.17.0.0 true diff --git a/src/PSLauncher.App/ViewModels/MainViewModel.cs b/src/PSLauncher.App/ViewModels/MainViewModel.cs index fdca5e0..2ccad32 100644 --- a/src/PSLauncher.App/ViewModels/MainViewModel.cs +++ b/src/PSLauncher.App/ViewModels/MainViewModel.cs @@ -14,6 +14,7 @@ using PSLauncher.Core.Installations; using PSLauncher.Core.Licensing; using PSLauncher.Core.Localization; using PSLauncher.Core.Manifests; +using PSLauncher.Core.DocTool; using PSLauncher.Core.Migrations; using PSLauncher.Core.Process; using PSLauncher.Core.ReportTool; @@ -43,6 +44,7 @@ public sealed partial class MainViewModel : ObservableObject private readonly IToastService _toastService; private readonly IDatabaseMigrationService _migrationService; private readonly IReportToolDeployer _reportDeployer; + private readonly IDocToolDeployer _docDeployer; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; @@ -226,6 +228,7 @@ public sealed partial class MainViewModel : ObservableObject IToastService toastService, IDatabaseMigrationService migrationService, IReportToolDeployer reportDeployer, + IDocToolDeployer docDeployer, IServiceProvider serviceProvider, ILogger logger) { @@ -242,6 +245,7 @@ public sealed partial class MainViewModel : ObservableObject _toastService = toastService; _migrationService = migrationService; _reportDeployer = reportDeployer; + _docDeployer = docDeployer; _serviceProvider = serviceProvider; _logger = logger; @@ -793,6 +797,49 @@ public sealed partial class MainViewModel : ObservableObject } } + // 7) Déploiement de l'outil Documentation (parallèle à Report). + // Cherche _doc/ dans le ZIP extrait et copie vers htdocs en + // atomic rename. Skip silencieux si _doc/ absent du ZIP. + if (_config.DocTool.AutoDeploy) + { + StatusMessage = Strings.StatusDeployingDoc(row.Version); + ProgressDetail = null; + ProgressPercent = 0; + var ddProgress = new Progress(dp => + { + if (dp.FilesTotal > 0) + { + var pct = (double)dp.FilesDone / dp.FilesTotal * 100.0; + ProgressPercent = pct; + row.ProgressPercent = pct; + } + var label = Strings.ProgressDocDeploy(dp.FilesDone, dp.FilesTotal, dp.CurrentFilename); + ProgressDetail = label; + row.ProgressDetail = label; + }); + var ddResult = await _docDeployer.DeployAsync(target, ddProgress, ct); + switch (ddResult.Status) + { + case DeployStatus.Deployed: + _logger.LogInformation("Doc tool deployed ({N} files) for v{Version}", ddResult.FilesDeployed, row.Version); + break; + case DeployStatus.SkippedNoSourceFolder: + _logger.LogInformation("No _doc/ in v{Version} ZIP, doc tool unchanged", row.Version); + break; + case DeployStatus.XamppNotFound: + // Le warning Report a déjà été affiché si nécessaire ; on reste silencieux ici + // (même cause = même symptôme, inutile de faire 2 popups identiques). + _logger.LogWarning("Doc tool deploy : XAMPP htdocs not found at {Root}", _config.DocTool.HtdocsRoot); + break; + case DeployStatus.Failed: + ThemedMessageBox.Show( + Strings.MsgDocDeployFailed(ddResult.ErrorMessage ?? "?"), + Strings.MsgBoxDocDeployFailed, + MessageBoxButton.OK, MessageBoxImage.Warning); + break; + } + } + StatusMessage = Strings.StatusInstalledSuccess(row.Version); ProgressDetail = null; ProgressPercent = 0; diff --git a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs index 3a9bdc9..8cc218a 100644 --- a/src/PSLauncher.App/ViewModels/SettingsViewModel.cs +++ b/src/PSLauncher.App/ViewModels/SettingsViewModel.cs @@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; using PSLauncher.App.Views; using PSLauncher.Core.Configuration; +using PSLauncher.Core.DocTool; using PSLauncher.Core.Downloads; using PSLauncher.Core.Installations; using PSLauncher.Core.Licensing; @@ -33,6 +34,7 @@ public sealed partial class SettingsViewModel : ObservableObject private readonly IDownloadStateStore _downloadStore; private readonly IDatabaseMigrationService _migrationService; private readonly IReportToolDeployer _reportDeployer; + private readonly IDocToolDeployer _docDeployer; private readonly IInstallationRegistry _registry; private readonly HttpClient _http; private readonly ILogger _logger; @@ -71,6 +73,17 @@ public sealed partial class SettingsViewModel : ObservableObject = new(); public bool HasReportBackups => ReportBackups.Count > 0; + // ----- Doc Tool deploy settings (mirror of Report) ----- + [ObservableProperty] private string _docHtdocsRoot; + [ObservableProperty] private string _docFolderName; + [ObservableProperty] private bool _docAutoDeploy; + [ObservableProperty] private int _docMaxBackups; + [ObservableProperty] private string? _docDeployStatus; + [ObservableProperty] private bool _isDeployingDoc; + public System.Collections.ObjectModel.ObservableCollection DocBackups { get; } + = new(); + public bool HasDocBackups => DocBackups.Count > 0; + public IReadOnlyList AvailableLanguages { get; } = PSLauncher.Core.Localization.Strings.Available .Select(t => new LanguageOption(t.Code, t.Name)) @@ -139,6 +152,7 @@ public sealed partial class SettingsViewModel : ObservableObject IDownloadStateStore downloadStore, IDatabaseMigrationService migrationService, IReportToolDeployer reportDeployer, + IDocToolDeployer docDeployer, IInstallationRegistry registry, HttpClient http, ILogger logger) @@ -149,6 +163,7 @@ public sealed partial class SettingsViewModel : ObservableObject _downloadStore = downloadStore; _migrationService = migrationService; _reportDeployer = reportDeployer; + _docDeployer = docDeployer; _registry = registry; _http = http; _logger = logger; @@ -170,8 +185,15 @@ public sealed partial class SettingsViewModel : ObservableObject _reportFolderName = config.ReportTool.FolderName; _reportAutoDeploy = config.ReportTool.AutoDeploy; _reportMaxBackups = config.ReportTool.MaxBackups; - // Charge la liste de backups en arrière-plan (UI immédiate puis remplie quand prête) + + _docHtdocsRoot = config.DocTool.HtdocsRoot; + _docFolderName = config.DocTool.FolderName; + _docAutoDeploy = config.DocTool.AutoDeploy; + _docMaxBackups = config.DocTool.MaxBackups; + + // Charge les listes de backups en arrière-plan (UI immédiate puis remplies quand prêtes) _ = LoadReportBackupsAsync(); + _ = LoadDocBackupsAsync(); LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0"; MachineId = licenseService.GetMachineId(); @@ -297,6 +319,11 @@ public sealed partial class SettingsViewModel : ObservableObject _config.ReportTool.AutoDeploy = ReportAutoDeploy; _config.ReportTool.MaxBackups = Math.Max(0, ReportMaxBackups); + _config.DocTool.HtdocsRoot = DocHtdocsRoot.Trim(); + _config.DocTool.FolderName = DocFolderName.Trim(); + _config.DocTool.AutoDeploy = DocAutoDeploy; + _config.DocTool.MaxBackups = Math.Max(0, DocMaxBackups); + _configStore.Save(_config); _logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged); @@ -553,6 +580,96 @@ public sealed partial class SettingsViewModel : ObservableObject IsDeployingReport = false; } } + + // ===================== DOC TOOL BACKUPS (mirror of Report) ===================== + + private async Task LoadDocBackupsAsync() + { + try + { + var list = await _docDeployer.ListBackupsAsync(CancellationToken.None); + DocBackups.Clear(); + foreach (var b in list) + DocBackups.Add(new DocBackupViewModel(b, RevertDocToBackupAsync)); + OnPropertyChanged(nameof(HasDocBackups)); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Could not list doc backups"); + } + } + + private async Task RevertDocToBackupAsync(BackupInfo backup) + { + var localDate = backup.TimestampUtc.ToLocalTime(); + var dateLabel = Strings.FormatLongDate(localDate); + var confirm = ThemedMessageBox.Show( + Strings.MsgRevertReportConfirm(dateLabel), + Strings.MsgBoxRevertReport, + MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (confirm != MessageBoxResult.Yes) return; + + IsDeployingDoc = true; + DocDeployStatus = $"Revert vers {dateLabel}…"; + try + { + var result = await _docDeployer.RevertAsync(backup.FolderName, CancellationToken.None); + DocDeployStatus = result.Status == DeployStatus.Deployed + ? Strings.MsgRevertReportSuccess(dateLabel) + : $"✗ {result.ErrorMessage}"; + await LoadDocBackupsAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Revert doc failed"); + DocDeployStatus = $"✗ {ex.Message}"; + } + finally + { + IsDeployingDoc = false; + } + } + + [RelayCommand] + private async Task RedeployDocAsync() + { + var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault(); + if (installed is null) + { + DocDeployStatus = "Aucune version installée — installe d'abord une version qui contient _doc/."; + return; + } + IsDeployingDoc = true; + DocDeployStatus = $"Déploiement depuis v{installed.Version}…"; + try + { + Save(); + var progress = new Progress(dp => + { + if (dp.FilesTotal > 0) + DocDeployStatus = $"{dp.FilesDone}/{dp.FilesTotal} : {dp.CurrentFilename}"; + }); + var result = await _docDeployer.DeployAsync(installed.FolderPath, progress, CancellationToken.None); + DocDeployStatus = result.Status switch + { + DeployStatus.Deployed => $"✓ {result.FilesDeployed} fichiers déployés vers {result.TargetPath}", + DeployStatus.SkippedNoSourceFolder => $"⚠ La version v{installed.Version} ne contient pas de _doc/ — rien à déployer", + DeployStatus.XamppNotFound => $"✗ {result.ErrorMessage}", + DeployStatus.Failed => $"✗ {result.ErrorMessage}", + _ => "?", + }; + await LoadDocBackupsAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Redeploy doc failed"); + DocDeployStatus = $"✗ {ex.Message}"; + } + finally + { + IsDeployingDoc = false; + } + } } /// @@ -576,3 +693,22 @@ public sealed partial class ReportBackupViewModel : ObservableObject [RelayCommand] private Task Revert() => _revertHandler(Info); } + +/// Variante doc — même structure que . +public sealed partial class DocBackupViewModel : ObservableObject +{ + public BackupInfo Info { get; } + public string DateDisplay => Strings.FormatLongDate(Info.TimestampUtc.ToLocalTime()); + public string SizeDisplay => Strings.SettingsReportBackupSize(Strings.FormatSize(Info.SizeBytes)); + public string FolderName => Info.FolderName; + + private readonly Func _revertHandler; + public DocBackupViewModel(BackupInfo info, Func revertHandler) + { + Info = info; + _revertHandler = revertHandler; + } + + [RelayCommand] + private Task Revert() => _revertHandler(Info); +} diff --git a/src/PSLauncher.App/Views/SettingsDialog.xaml b/src/PSLauncher.App/Views/SettingsDialog.xaml index 3193531..59cdc27 100644 --- a/src/PSLauncher.App/Views/SettingsDialog.xaml +++ b/src/PSLauncher.App/Views/SettingsDialog.xaml @@ -525,6 +525,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +public sealed class DocToolDeployer : IDocToolDeployer +{ + private const string SourceSubdir = "_doc"; + private const string BackupSuffix = ".backup-"; + private const string TimestampFormat = "yyyyMMdd-HHmmss"; + + private readonly Func _configProvider; + private readonly ILogger _logger; + + public DocToolDeployer( + Func configProvider, + ILogger logger) + { + _configProvider = configProvider; + _logger = logger; + } + + public async Task DeployAsync( + string installFolder, + IProgress? progress, + CancellationToken ct) + { + var cfg = _configProvider(); + var source = Path.Combine(installFolder, SourceSubdir); + var target = Path.Combine(cfg.HtdocsRoot, cfg.FolderName); + + if (!Directory.Exists(source)) + { + _logger.LogInformation("No {Sub}/ directory in {Folder}, skipping doc deploy", SourceSubdir, installFolder); + return new DeployResult(DeployStatus.SkippedNoSourceFolder, 0, 0, target); + } + + if (!Directory.Exists(cfg.HtdocsRoot)) + { + _logger.LogWarning("HtdocsRoot {Root} not found — XAMPP not installed at expected location", cfg.HtdocsRoot); + return new DeployResult(DeployStatus.XamppNotFound, 0, 0, target, + $"Le dossier {cfg.HtdocsRoot} est introuvable. XAMPP est-il installé à cet emplacement ? (Settings → Avancés)"); + } + + var stagingTarget = target + ".new"; + var backupTarget = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture); + + try + { + if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true); + + var allFiles = Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories).ToList(); + int total = allFiles.Count; + long bytesTotal = 0; + + await Task.Run(() => + { + Directory.CreateDirectory(stagingTarget); + int done = 0; + foreach (var srcFile in allFiles) + { + ct.ThrowIfCancellationRequested(); + var rel = Path.GetRelativePath(source, srcFile); + var dstFile = Path.Combine(stagingTarget, rel); + var dstDir = Path.GetDirectoryName(dstFile); + if (!string.IsNullOrEmpty(dstDir) && !Directory.Exists(dstDir)) + Directory.CreateDirectory(dstDir); + File.Copy(srcFile, dstFile, overwrite: true); + bytesTotal += new FileInfo(srcFile).Length; + done++; + progress?.Report(new DeployProgress(done, total, rel)); + } + }, ct).ConfigureAwait(false); + + if (Directory.Exists(target)) + Directory.Move(target, backupTarget); + Directory.Move(stagingTarget, target); + + PruneOldBackups(cfg); + + _logger.LogInformation("Doc tool deployed to {Target} ({Files} files, {Bytes} bytes), backup at {Backup}", + target, total, bytesTotal, backupTarget); + return new DeployResult(DeployStatus.Deployed, total, bytesTotal, target); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to deploy doc tool to {Target}", target); + try { if (Directory.Exists(stagingTarget)) Directory.Delete(stagingTarget, recursive: true); } + catch { /* ignore */ } + return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message); + } + } + + public Task> ListBackupsAsync(CancellationToken ct) + { + var cfg = _configProvider(); + var prefix = cfg.FolderName + BackupSuffix; + + if (!Directory.Exists(cfg.HtdocsRoot)) + return Task.FromResult>(Array.Empty()); + + var list = new List(); + foreach (var dir in Directory.EnumerateDirectories(cfg.HtdocsRoot, prefix + "*", SearchOption.TopDirectoryOnly)) + { + ct.ThrowIfCancellationRequested(); + var name = Path.GetFileName(dir); + var tsPart = name.Substring(prefix.Length); + if (!DateTime.TryParseExact(tsPart, TimestampFormat, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ts)) + { + continue; + } + long size = 0; + try + { + foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) + size += new FileInfo(f).Length; + } + catch { /* size best-effort */ } + list.Add(new BackupInfo(name, ts, size)); + } + list.Sort((a, b) => b.TimestampUtc.CompareTo(a.TimestampUtc)); + return Task.FromResult>(list); + } + + public async Task RevertAsync(string backupFolderName, CancellationToken ct) + { + var cfg = _configProvider(); + var backupPath = Path.Combine(cfg.HtdocsRoot, backupFolderName); + var target = Path.Combine(cfg.HtdocsRoot, cfg.FolderName); + + if (!Directory.Exists(backupPath)) + { + return new DeployResult(DeployStatus.BackupNotFound, 0, 0, target, + $"Le backup {backupFolderName} n'existe plus."); + } + + var newBackupForCurrent = target + BackupSuffix + DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture); + + try + { + await Task.Run(() => + { + ct.ThrowIfCancellationRequested(); + if (Directory.Exists(target)) + Directory.Move(target, newBackupForCurrent); + Directory.Move(backupPath, target); + }, ct).ConfigureAwait(false); + + int files = 0; + long bytes = 0; + try + { + foreach (var f in Directory.EnumerateFiles(target, "*", SearchOption.AllDirectories)) + { + files++; + bytes += new FileInfo(f).Length; + } + } + catch { /* best-effort */ } + + PruneOldBackups(cfg); + _logger.LogInformation("Reverted doc to backup {Backup} ({Files} files), previous current saved as {New}", + backupFolderName, files, newBackupForCurrent); + return new DeployResult(DeployStatus.Deployed, files, bytes, target); + } + catch (Exception ex) + { + _logger.LogError(ex, "Doc revert to {Backup} failed", backupFolderName); + return new DeployResult(DeployStatus.Failed, 0, 0, target, ex.Message); + } + } + + private void PruneOldBackups(DocToolConfig cfg) + { + try + { + var prefix = cfg.FolderName + BackupSuffix; + var dirs = Directory.EnumerateDirectories(cfg.HtdocsRoot, prefix + "*", SearchOption.TopDirectoryOnly) + .Select(d => new { Path = d, Name = Path.GetFileName(d) }) + .OrderByDescending(d => d.Name, StringComparer.Ordinal) + .ToList(); + + int keep = Math.Max(0, cfg.MaxBackups); + for (int i = keep; i < dirs.Count; i++) + { + try + { + Directory.Delete(dirs[i].Path, recursive: true); + _logger.LogDebug("Pruned old doc backup {Dir}", dirs[i].Name); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Could not prune doc backup {Dir} (will retry next deploy)", dirs[i].Name); + } + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Doc backup pruning skipped"); + } + } +} diff --git a/src/PSLauncher.Core/DocTool/IDocToolDeployer.cs b/src/PSLauncher.Core/DocTool/IDocToolDeployer.cs new file mode 100644 index 0000000..3078c3d --- /dev/null +++ b/src/PSLauncher.Core/DocTool/IDocToolDeployer.cs @@ -0,0 +1,25 @@ +using PSLauncher.Core.ReportTool; + +namespace PSLauncher.Core.DocTool; + +/// +/// Déploie l'outil Documentation (page web hébergée dans XAMPP) depuis le +/// sous-dossier _doc/ bundled dans chaque ZIP PROSERVE vers le htdocs +/// local. Mêmes garanties que : copy + atomic +/// rename + backups horodatés + revert manuel. +/// +/// On réutilise les types , , +/// et du namespace ReportTool +/// pour ne pas dupliquer la modélisation — le mécanisme est strictement le même. +/// +public interface IDocToolDeployer +{ + Task DeployAsync( + string installFolder, + IProgress? progress, + CancellationToken ct); + + Task> ListBackupsAsync(CancellationToken ct); + + Task RevertAsync(string backupFolderName, CancellationToken ct); +} diff --git a/src/PSLauncher.Core/Localization/Strings.cs b/src/PSLauncher.Core/Localization/Strings.cs index 1136afb..5e66921 100644 --- a/src/PSLauncher.Core/Localization/Strings.cs +++ b/src/PSLauncher.Core/Localization/Strings.cs @@ -515,6 +515,42 @@ public static class Strings ); public static string SettingsReportRedeploy => T("📂 Re-déployer le Report maintenant", "📂 Re-deploy Report now", "📂 立即重新部署报告", "📂 ปรับใช้รายงานใหม่เดี๋ยวนี้", "📂 إعادة نشر التقرير الآن"); + // ---- Doc tool (mêmes mécanique que Report, autres libellés) ---- + public static string StatusDeployingDoc(string version) => T( + $"Déploiement de la Documentation v{version}…", + $"Deploying Documentation for v{version}…", + $"正在部署 v{version} 的文档…", + $"กำลังปรับใช้เอกสาร v{version}…", + $"جارٍ نشر التوثيق v{version}…" + ); + public static string ProgressDocDeploy(int done, int total, string filename) => T( + $"📖 Déploiement Doc : {done}/{total} — {filename}", + $"📖 Deploying Doc: {done}/{total} — {filename}", + $"📖 部署文档:{done}/{total} — {filename}", + $"📖 ปรับใช้เอกสาร: {done}/{total} — {filename}", + $"📖 نشر التوثيق: {done}/{total} — {filename}" + ); + public static string MsgBoxDocDeployFailed => T("Déploiement Doc échoué", "Doc deploy failed", "文档部署失败", "การปรับใช้เอกสารล้มเหลว", "فشل نشر التوثيق"); + public static string MsgDocDeployFailed(string detail) => T( + $"Le déploiement de la Documentation a échoué :\n\n{detail}\n\nLa version PROSERVE est installée mais l'onglet Documentation pourrait être désynchronisé. Tu peux retenter via Paramètres → Avancés → Re-déployer la Doc.", + $"Documentation deploy failed:\n\n{detail}\n\nThe PROSERVE version is installed but the Documentation tab might be out of sync. You can retry via Settings → Advanced → Re-deploy Doc.", + $"文档部署失败:\n\n{detail}\n\nPROSERVE 版本已安装,但文档选项卡可能不同步。您可以通过 设置 → 高级 → 重新部署文档 重试。", + $"การปรับใช้เอกสารล้มเหลว:\n\n{detail}\n\nเวอร์ชัน PROSERVE ติดตั้งแล้วแต่แท็บเอกสารอาจไม่ซิงค์ คุณสามารถลองใหม่ผ่าน ตั้งค่า → ขั้นสูง → ปรับใช้เอกสารใหม่", + $"فشل نشر التوثيق:\n\n{detail}\n\nنسخة PROSERVE مثبتة لكن علامة التبويب التوثيق قد لا تكون متزامنة. يمكنك إعادة المحاولة عبر الإعدادات → متقدم → إعادة نشر التوثيق." + ); + + public static string SettingsDocTool => T("OUTIL DOCUMENTATION (XAMPP htdocs)", "DOCUMENTATION TOOL (XAMPP htdocs)", "文档工具 (XAMPP htdocs)", "เครื่องมือเอกสาร (XAMPP htdocs)", "أداة التوثيق (XAMPP htdocs)"); + public static string SettingsDocAutoDeploy => T( + "Déployer automatiquement la Documentation à l'install d'une nouvelle version", + "Automatically deploy the Documentation when installing a new version", + "安装新版本时自动部署文档", + "ปรับใช้เอกสารโดยอัตโนมัติเมื่อติดตั้งเวอร์ชันใหม่", + "نشر التوثيق تلقائياً عند تثبيت نسخة جديدة" + ); + public static string SettingsDocRedeploy => T("📖 Re-déployer la Doc maintenant", "📖 Re-deploy Doc now", "📖 立即重新部署文档", "📖 ปรับใช้เอกสารใหม่เดี๋ยวนี้", "📖 إعادة نشر التوثيق الآن"); + public static string SettingsDocBackups => SettingsReportBackups; + public static string SettingsDocMaxBackups => SettingsReportMaxBackups; + // ---- Backups Report tool ---- public static string SettingsReportBackups => T("Backups disponibles", "Available backups", "可用备份", "สำรองข้อมูลที่มี", "النسخ الاحتياطية المتاحة"); public static string SettingsReportMaxBackups => T("Conserver N backups", "Keep N backups", "保留 N 个备份", "เก็บสำรอง N ชุด", "الاحتفاظ بـ N نسخة احتياطية"); diff --git a/src/PSLauncher.Models/LocalConfig.cs b/src/PSLauncher.Models/LocalConfig.cs index db5264f..7cf0dbc 100644 --- a/src/PSLauncher.Models/LocalConfig.cs +++ b/src/PSLauncher.Models/LocalConfig.cs @@ -36,12 +36,11 @@ public sealed class LocalConfig public string ReportUrl { get; set; } = "http://localhost/ProserveReport/"; /// - /// URL de la documentation locale (onglet "Documentation"). Vide par défaut : - /// l'utilisateur peut pointer vers une URL locale ou web. Si vide, l'onglet - /// affiche un placeholder avec les liens vers les fichiers .pptx du dossier - /// d'install du launcher. + /// URL de la documentation locale (onglet "Documentation"). Pointe par défaut + /// vers l'install standard ASTERION dans XAMPP. Surchargeable dans Settings → + /// Avancés. Si vide, l'onglet affiche un placeholder. /// - public string DocumentationUrl { get; set; } = string.Empty; + public string DocumentationUrl { get; set; } = "http://localhost/ProserveDoc/"; public LicenseConfig License { get; set; } = new(); @@ -53,6 +52,14 @@ public sealed class LocalConfig /// public ReportToolConfig ReportTool { get; set; } = new(); + /// + /// Configuration du déploiement de l'outil Documentation (page web locale + /// embarquée dans l'onglet "Documentation"). Même mécanisme que ReportTool : + /// copie de _doc/ bundled dans le ZIP PROSERVE vers + /// {HtdocsRoot}\{FolderName} avec backups horodatés et revert. + /// + public DocToolConfig DocTool { get; set; } = new(); + /// /// Connexion à la base MySQL locale (XAMPP) utilisée par PROSERVE pour ses /// statistiques. Le launcher s'en sert pour appliquer les migrations bundled @@ -118,6 +125,19 @@ public sealed class ReportToolConfig public int MaxBackups { get; set; } = 3; } +/// +/// Mêmes paramètres que mais pour la +/// documentation locale (sous-dossier _doc/ du ZIP, déployé vers +/// {HtdocsRoot}\ProserveDoc par défaut). +/// +public sealed class DocToolConfig +{ + public string HtdocsRoot { get; set; } = @"C:\xampp\htdocs"; + public string FolderName { get; set; } = "ProserveDoc"; + public bool AutoDeploy { get; set; } = true; + public int MaxBackups { get; set; } = 3; +} + public sealed class LicenseConfig { public string? EncryptedKey { get; set; } diff --git a/src/PSLauncher.Updater/PSLauncher.Updater.csproj b/src/PSLauncher.Updater/PSLauncher.Updater.csproj index d5d5ae3..34da7ab 100644 --- a/src/PSLauncher.Updater/PSLauncher.Updater.csproj +++ b/src/PSLauncher.Updater/PSLauncher.Updater.csproj @@ -8,7 +8,7 @@ latest PS_Launcher.Updater PSLauncher.Updater - 0.16.0 + 0.17.0 ..\PSLauncher.App\Resources\favicon.ico ASTERION VR © 2026 ASTERION VR — All rights reserved