diff --git a/DistFiles/aboutBox.htm b/DistFiles/aboutBox.htm index 718f9034..d251279a 100644 --- a/DistFiles/aboutBox.htm +++ b/DistFiles/aboutBox.htm @@ -7,7 +7,7 @@ -

Copyright © 2011-2025 SIL Global

+

Copyright © 2011-2026 SIL Global

License

Open source (MIT).

diff --git a/SayMore.sln.DotSettings b/SayMore.sln.DotSettings index 95865b2c..2b0d594d 100644 --- a/SayMore.sln.DotSettings +++ b/SayMore.sln.DotSettings @@ -1,6 +1,8 @@  + HTML IE IMDI + ISO MRU OK RAMP diff --git a/build/SayMore.proj b/build/SayMore.proj index 41f09ed1..67dd4937 100644 --- a/build/SayMore.proj +++ b/build/SayMore.proj @@ -14,7 +14,7 @@ $(RootDir)/packages/ 16.2.0 $(NuGetPackageRoot)SIL.libpalaso.l10ns/$(PalasoL10nsVersion)/ - $(NuGetPackageRoot)SIL.ReleaseTasks/3.1.1/build/SIL.ReleaseTasks.props + $(NuGetPackageRoot)SIL.ReleaseTasks/3.2.0/build/SIL.ReleaseTasks.props $(LocalPackagesRoot)SIL.BuildTasks/ $(BuildTasksVersionFolder)tools/SIL.BuildTasks.dll $(BuildTasksVersionFolder)build/SIL.BuildTasks.props diff --git a/build/TestInstallerBuild.bat b/build/TestInstallerBuild.bat index c78ef84d..f92a4bfb 100644 --- a/build/TestInstallerBuild.bat +++ b/build/TestInstallerBuild.bat @@ -14,5 +14,5 @@ GOTO pauseforusertoseeoutput REM :unexpectedsystemvariableinuse REM @ECHO Unexpected system variable msbuildpath is in use. Value: %msbuildpath% :pauseforusertoseeoutput -ECHO %CMDCMDLINE% | findstr /i "/c" >nul +ECHO %CMDCMDLINE% | findstr /i /c:"/c" >nul IF NOT errorlevel 1 PAUSE \ No newline at end of file diff --git a/src/Installer/Installer.wxs b/src/Installer/Installer.wxs index 01071197..d0bf3ed8 100644 --- a/src/Installer/Installer.wxs +++ b/src/Installer/Installer.wxs @@ -7,6 +7,7 @@ + @@ -200,6 +201,10 @@ are trying to support, you're better off using non-advertised shortcuts. "--> + + + + @@ -341,22 +346,22 @@ are trying to support, you're better off using non-advertised shortcuts. "--> - + - + - + - + - + - + @@ -378,6 +383,7 @@ are trying to support, you're better off using non-advertised shortcuts. "--> + diff --git a/src/SayMore/Model/Files/DataGathering/BackgroundFileProcessor.cs b/src/SayMore/Model/Files/DataGathering/BackgroundFileProcessor.cs index 8010fe6d..c3e984d6 100644 --- a/src/SayMore/Model/Files/DataGathering/BackgroundFileProcessor.cs +++ b/src/SayMore/Model/Files/DataGathering/BackgroundFileProcessor.cs @@ -11,14 +11,6 @@ namespace SayMore.Model.Files.DataGathering { - /// ---------------------------------------------------------------------------------------- - public interface ISingleListDataGatherer - { - event EventHandler NewDataAvailable; - event EventHandler FinishedProcessingAllFiles; - IEnumerable GetValues(); - } - /// ---------------------------------------------------------------------------------------- /// /// Gives lists of data, indexed by a key into a dictionary @@ -27,7 +19,6 @@ public interface ISingleListDataGatherer public interface IMultiListDataProvider { event EventHandler NewDataAvailable; - event EventHandler FinishedProcessingAllFiles; Dictionary> GetValueLists(bool includeUnattestedFactoryChoices); } @@ -35,7 +26,7 @@ public interface IMultiListDataProvider /// /// This is the base class for processes which live in the background, /// gathering data about the files in the collection so that this data - /// is quickly accesible when needed. + /// is quickly accessible when needed. /// /// ---------------------------------------------------------------------------------------- public abstract class BackgroundFileProcessor : IDisposable where T : class @@ -49,17 +40,17 @@ public abstract class BackgroundFileProcessor : IDisposable where T : class protected readonly IEnumerable _typesOfFilesToProcess; protected readonly Func _fileDataFactory; protected bool _restartRequested = true; - protected Dictionary _fileToDataDictionary = new Dictionary(); + protected Dictionary _fileToDataDictionary = new(); private readonly Queue _pendingFileEvents; private volatile int _suspendEventProcessingCount; - private readonly object _lockObj = new object(); - private readonly object _lockSuspendObj = new object(); + private readonly object _lockObj = new(); + private readonly object _lockSuspendObj = new(); public event EventHandler NewDataAvailable; public event EventHandler FinishedProcessingAllFiles; /// ------------------------------------------------------------------------------------ - public BackgroundFileProcessor(string rootDirectoryPath, + protected BackgroundFileProcessor(string rootDirectoryPath, IEnumerable typesOfFilesToProcess, Func fileDataFactory) { RootDirectoryPath = rootDirectoryPath; @@ -72,8 +63,7 @@ public BackgroundFileProcessor(string rootDirectoryPath, /// ------------------------------------------------------------------------------------ public void Dispose() { - if (_workerThread != null) - _workerThread.Abort(); //will eventually lead to it stopping + _workerThread?.Abort(); //will eventually lead to it stopping _workerThread = null; } @@ -122,22 +112,21 @@ public virtual void ResumeProcessing(bool processAllPendingEventsNow) protected virtual bool GetDoIncludeFile(string path) { var fileName = Path.GetFileName(path); - return (fileName != null && !fileName.StartsWith(".") && - (_typesOfFilesToProcess.Any(t => t.IsMatch(path)))); + return fileName != null && !fileName.StartsWith(".") && + _typesOfFilesToProcess.Any(t => t.IsMatch(path)); } /// ------------------------------------------------------------------------------------ - protected virtual ThreadPriority ThreadPriority - { - get { return ThreadPriority.Lowest; } - } + protected virtual ThreadPriority ThreadPriority => ThreadPriority.Lowest; /// ------------------------------------------------------------------------------------ public virtual void Start() { - _workerThread = new Thread(StartWorking); - _workerThread.Name = GetType().Name; - _workerThread.Priority = ThreadPriority; + _workerThread = new Thread(StartWorking) + { + Name = GetType().Name, + Priority = ThreadPriority + }; _workerThread.TrySetApartmentState(ApartmentState.STA);//needed in case we eventually show an error & need to talk to email. _workerThread.Start(); } @@ -145,8 +134,7 @@ public virtual void Start() /// ------------------------------------------------------------------------------------ protected virtual void OnNewDataAvailable(T fileData) { - if (NewDataAvailable != null) - NewDataAvailable(this, EventArgs.Empty); + NewDataAvailable?.Invoke(this, EventArgs.Empty); } /// ------------------------------------------------------------------------------------ @@ -154,7 +142,7 @@ private void StartWorking() { try { - Status = kWorkingStatus; //NB: this helps simplify unit tests, if go to the busy state before returning + Status = kWorkingStatus; //NB: this helps simplify unit tests, if we go to the busy state before returning using (var watcher = new FileSystemWatcher(RootDirectoryPath)) { @@ -189,11 +177,11 @@ private void StartWorking() } catch (ThreadAbortException) { - //this is fine, it happens when we quit + // This is fine, it happens when we quit } catch (Exception error) { - SIL.Reporting.ErrorReport.NotifyUserOfProblem(error, "Background file watching failed."); + ErrorReport.NotifyUserOfProblem(error, "Background file watching failed."); } } @@ -216,13 +204,11 @@ private void ProcessFileEvent(FileSystemEventArgs fileEvent) { try { - if (fileEvent is RenamedEventArgs) + if (fileEvent is RenamedEventArgs e) { - var e = fileEvent as RenamedEventArgs; lock (((ICollection)_fileToDataDictionary).SyncRoot) { - T fileData; - if (_fileToDataDictionary.TryGetValue(e.OldFullPath, out fileData)) + if (_fileToDataDictionary.TryGetValue(e.OldFullPath, out var fileData)) { _fileToDataDictionary.Remove(e.OldFullPath); _fileToDataDictionary[e.FullPath] = fileData; @@ -244,7 +230,7 @@ private void ProcessFileEvent(FileSystemEventArgs fileEvent) Debug.WriteLine(e.Message); Logger.WriteEvent("Handled Exception in {0}.ProcessingFileEvent:\r\n{1}", GetType().Name, e.ToString()); #if DEBUG - SIL.Reporting.ErrorReport.NotifyUserOfProblem(e, "Error gathering data"); + ErrorReport.NotifyUserOfProblem(e, "Error gathering data"); #endif //nothing here is worth crashing over } @@ -255,14 +241,13 @@ public T GetFileData(string filePath) { lock (((ICollection)_fileToDataDictionary).SyncRoot) { - T stats; - if (_fileToDataDictionary.TryGetValue(filePath, out stats)) + if (_fileToDataDictionary.TryGetValue(filePath, out var stats)) return stats; if (GetDoIncludeFile(filePath)) { CollectDataForFile(filePath); - return (_fileToDataDictionary.TryGetValue(filePath, out stats) ? stats : null); + return _fileToDataDictionary.TryGetValue(filePath, out stats) ? stats : null; } return null; } @@ -310,7 +295,7 @@ protected virtual void CollectDataForFile(string path) { ErrorReport.NotifyUserOfProblem(new ShowOncePerSessionBasedOnExactMessagePolicy(), e, string.Format(LocalizationManager.GetString("MainWindow.AutoCompleteValueGathererError", - "An error of type {0} ocurred trying to gather information from file: {1}", + "An error of type {0} occurred trying to gather information from file: {1}", "Parameter 0 is an exception type; parameter 1 is a file name"), e.GetType(), path)); } else @@ -356,7 +341,7 @@ public virtual void ProcessAllFilesInFolder(string folder) /// ------------------------------------------------------------------------------------ public virtual void ProcessAllFiles() { - //now that the watcher is up and running, gather up all existing files + // Now that the watcher is up and running, gather all existing files lock (((ICollection)_fileToDataDictionary).SyncRoot) { _fileToDataDictionary.Clear(); @@ -405,8 +390,7 @@ protected virtual void ProcessAllFiles(string topLevelFolder, bool searchSubFold Status = kUpToDataStatus; - if (FinishedProcessingAllFiles != null) - FinishedProcessingAllFiles(this, EventArgs.Empty); + FinishedProcessingAllFiles?.Invoke(this, EventArgs.Empty); } private static List WalkDirectoryTree(string topLevelFolder, SearchOption searchOption) @@ -416,7 +400,7 @@ private static List WalkDirectoryTree(string topLevelFolder, SearchOptio // First, process all the files directly under this folder try { - // SP-879: Crash reading .DS_Store file on MacOS + // SP-879: Crash reading .DS_Store file on macOS files = Directory.GetFiles(topLevelFolder, "*.*").Where(name => { var fileName = Path.GetFileName(name); @@ -435,13 +419,13 @@ private static List WalkDirectoryTree(string topLevelFolder, SearchOptio Debug.Print("Directory not found: " + topLevelFolder); } - if ((files != null) && (searchOption == SearchOption.AllDirectories)) + if (files != null && searchOption == SearchOption.AllDirectories) { // Now find all the subdirectories under this directory. var dirs = Directory.GetDirectories(topLevelFolder); foreach (var dir in dirs) { - // Resursive call for each subdirectory. + // Recursive call for each subdirectory. returnVal.AddRange(WalkDirectoryTree(dir, searchOption)); } } @@ -468,16 +452,10 @@ protected bool ShouldStop } /// ------------------------------------------------------------------------------------ - public bool Busy - { - get { return Status.StartsWith(kWorkingStatus); } - } + public bool Busy => Status.StartsWith(kWorkingStatus); /// ------------------------------------------------------------------------------------ - public bool DataUpToDate - { - get { return Status == kUpToDataStatus; } - } + public bool DataUpToDate => Status == kUpToDataStatus; /// ------------------------------------------------------------------------------------ public string Status diff --git a/src/SayMore/Model/Files/FieldUpdater.cs b/src/SayMore/Model/Files/FieldUpdater.cs index 2aa949b5..976b610c 100644 --- a/src/SayMore/Model/Files/FieldUpdater.cs +++ b/src/SayMore/Model/Files/FieldUpdater.cs @@ -77,8 +77,7 @@ private void FindAndUpdateFiles(ComponentFile file, string idOfFieldToFind, var matchingFiles = GetMatchingFiles(file.FileType); - if (_fieldGatherer != null) - _fieldGatherer.SuspendProcessing(); + _fieldGatherer?.SuspendProcessing(); foreach (var path in matchingFiles) { diff --git a/src/SayMore/Model/Project.cs b/src/SayMore/Model/Project.cs index 5d71ed92..f191b18e 100644 --- a/src/SayMore/Model/Project.cs +++ b/src/SayMore/Model/Project.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Drawing; +using System.IO; using System.IO.Compression; using System.Linq; using System.Text; @@ -13,21 +13,23 @@ using System.Xml.Serialization; using DesktopAnalytics; using L10NSharp; -using SIL.Extensions; -using SIL.Reporting; -using SIL.Windows.Forms; +using SayMore.Model.Files; +using SayMore.Properties; +using SayMore.Transcription.Model; using SayMore.UI.ComponentEditors; using SayMore.UI.Overview; +using SayMore.UI.Overview.Statistics; +using SayMore.Utilities; using SIL.Archiving; using SIL.Archiving.Generic; using SIL.Archiving.IMDI; -using SayMore.Properties; -using SayMore.Transcription.Model; -using SayMore.Model.Files; -using SayMore.Utilities; using SIL.Core.ClearShare; +using SIL.Extensions; using SIL.IO; +using SIL.Reporting; +using SIL.Windows.Forms; using static System.IO.Path; +using static SayMore.Model.Files.ComponentRole.MeasurementTypes; namespace SayMore.Model { @@ -63,6 +65,118 @@ public class Project : IAutoSegmenterSettings, IRAMPArchivable, IDisposable private bool _needToDisposeFreeTranslationFont; private Font _workingLanguageFont; private bool _needToDisposeWorkingLanguageFont; + + private sealed class ProgressStats + { + private readonly StatisticsViewModel _model; + + private readonly int _initialNumberOfSessions; + private readonly int _initialNumberOfPersons; + + private sealed class MediaDurationStats(TimeSpan initial) + { + private TimeSpan Initial { get; } = initial; + public TimeSpan Current { get; set; } = initial; + + public TimeSpan Delta => Current - Initial; + } + + private sealed class RoleCountStats(int initial) + { + private int Initial { get; } = initial; + public int Current { get; set; } = initial; + + public int Delta => Current - Initial; + } + + private readonly Dictionary _mediaDurationStats; + private readonly Dictionary _sessionRoleStats; + + internal ProgressStats(StatisticsViewModel model) + { + _model = model; + + _initialNumberOfSessions = _model.SessionInformant.NumberOfSessions; + _initialNumberOfPersons = _model.PersonInformant.NumberOfPeople; + _mediaDurationStats = _model.GetComponentRoleStatisticsPairs() + .ToDictionary(s => s.Name, s => new MediaDurationStats(s.Length)); + + _sessionRoleStats = _model.SessionInformant.GetSessionsCategorizedByStage() + .Where(s => s.Key.MeasurementType != Time) + .ToDictionary(s => s.Key.Id, s => new RoleCountStats(s.Value.Count())); + } + + internal void ReportUpdatedStatistics() + { + if (_model.IsBusy) + Thread.Sleep(200); // Give it a fighting chance to finish. + + var sessionDelta = _model.SessionInformant.NumberOfSessions - _initialNumberOfSessions; + var personDelta = _model.PersonInformant.NumberOfPeople - _initialNumberOfPersons; + + foreach (var stat in _model.GetComponentRoleStatisticsPairs()) + { + if (_mediaDurationStats.TryGetValue(stat.Name, out var entry)) + entry.Current = stat.Length; + else + { + _mediaDurationStats[stat.Name] = + new MediaDurationStats(TimeSpan.Zero) + { + Current = stat.Length + }; + } + } + + foreach (var stat in _model.SessionInformant.GetSessionsCategorizedByStage() + .Where(s => s.Key.MeasurementType != Time)) + { + if (_sessionRoleStats.TryGetValue(stat.Key.Id, out var entry)) + entry.Current = stat.Value.Count(); + else + { + _sessionRoleStats[stat.Key.Id] = new RoleCountStats(0) + { + Current = stat.Value.Count() + }; + } + } + + var properties = new Dictionary(); + + if (sessionDelta > 0) + properties["SessionsAdded"] = sessionDelta.ToString(); + + if (personDelta > 0) + properties["PersonsAdded"] = personDelta.ToString(); + + foreach (var kvp in _mediaDurationStats) + { + var delta = kvp.Value.Delta; + if (delta > TimeSpan.Zero) + { + properties[$"MediaDurationAdded.{kvp.Key}"] = + delta.TotalSeconds.ToString("F0"); + } + } + + foreach (var kvp in _sessionRoleStats) + { + var delta = kvp.Value.Delta; + if (delta > 0) + { + properties[$"CompletedStages.{kvp.Key}"] = + delta.ToString(); + } + } + + if (properties.Any()) + Analytics.Track("ProjectProgress", properties); + } + } + + private StatisticsViewModel _statisticsViewModel; + private ProgressStats _progressStats; public delegate Project Factory(string desiredOrExistingFilePath); @@ -105,30 +219,40 @@ public Project(string desiredOrExistingSettingsFilePath, throw new ArgumentException("Invalid project path specified", nameof(desiredOrExistingSettingsFilePath)); var saveNeeded = false; + var projectInfo = new Dictionary {{"projectName", Name}}; + if (File.Exists(desiredOrExistingSettingsFilePath)) { RenameEventsToSessions(projectDirectory); Load(); + projectInfo["vernacularISO3CodeAndName"] = VernacularISO3CodeAndName; + projectInfo["analysisISO3CodeAndName"] = AnalysisISO3CodeAndName; + projectInfo["projectLocation"] = Location; + projectInfo["projectRegion"] = Region; + projectInfo["projectCountry"] = Country; + projectInfo["projectContinent"] = Continent; } else { + Analytics.Track("Project Created", projectInfo); Directory.CreateDirectory(projectDirectory); Title = Name; saveNeeded = true; } - if (TranscriptionFont == null) - TranscriptionFont = Program.DialogFont; + TranscriptionFont ??= Program.DialogFont; + projectInfo["transcriptionFont"] = TranscriptionFont.Name; + FreeTranslationFont ??= Program.DialogFont; + projectInfo["freeTranslationFont"] = FreeTranslationFont.Name; - if (FreeTranslationFont == null) - FreeTranslationFont = Program.DialogFont; + Analytics.Track("Project Opened", projectInfo); if (AutoSegmenterMinimumSegmentLengthInMilliseconds < Settings.Default.MinimumSegmentLengthInMilliseconds || - AutoSegmenterMaximumSegmentLengthInMilliseconds <= 0 || - AutoSegmenterMinimumSegmentLengthInMilliseconds >= AutoSegmenterMaximumSegmentLengthInMilliseconds || - AutoSegmenterPreferredPauseLengthInMilliseconds <= 0 || - AutoSegmenterPreferredPauseLengthInMilliseconds > AutoSegmenterMaximumSegmentLengthInMilliseconds || - AutoSegmenterOptimumLengthClampingFactor <= 0) + AutoSegmenterMaximumSegmentLengthInMilliseconds <= 0 || + AutoSegmenterMinimumSegmentLengthInMilliseconds >= AutoSegmenterMaximumSegmentLengthInMilliseconds || + AutoSegmenterPreferredPauseLengthInMilliseconds <= 0 || + AutoSegmenterPreferredPauseLengthInMilliseconds > AutoSegmenterMaximumSegmentLengthInMilliseconds || + AutoSegmenterOptimumLengthClampingFactor <= 0) { saveNeeded = AutoSegmenterMinimumSegmentLengthInMilliseconds != 0 || AutoSegmenterMaximumSegmentLengthInMilliseconds != 0 || AutoSegmenterPreferredPauseLengthInMilliseconds != 0 || !AutoSegmenterOptimumLengthClampingFactor.Equals(0) || saveNeeded; @@ -143,9 +267,36 @@ public Project(string desiredOrExistingSettingsFilePath, Save(); } + /// ------------------------------------------------------------------------------------ + public void ReportProgressIfAny() + { + if (_statisticsViewModel == null) + return; + + _statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics; + + // Really unlikely, but if we get here before the initial gathering is done, we can't do anything. + if (_progressStats == null) + return; + + try + { + _progressStats.ReportUpdatedStatistics(); + } + catch (ObjectDisposedException e) + { + // This probably should be impossible, but just in case, we don't want reporting stats to + // crash the program. + Logger.WriteError(e); + } + _progressStats = null; // This ensures we only report once per project open. + } + /// ------------------------------------------------------------------------------------ public void Dispose() { + _progressStats = null; // Probably already done, but it also had a copy of _statisticsViewModel. + _statisticsViewModel?.Dispose(); _sessionsRepoFactory = null; if (_needToDisposeTranscriptionFont) TranscriptionFont.Dispose(); @@ -328,54 +479,6 @@ public void Save() _accessProtocolChanged = false; } - /// ------------------------------------------------------------------------------------ - public string GetFileDescription(string key, string file) - { - var description = (key == string.Empty ? "SayMore Session File" : "SayMore Contributor File"); - - if (file.ToLower().EndsWith(Settings.Default.SessionFileExtension)) - description = "SayMore Session Metadata (XML)"; - else if (file.ToLower().EndsWith(Settings.Default.PersonFileExtension)) - description = "SayMore Contributor Metadata (XML)"; - else if (file.ToLower().EndsWith(Settings.Default.MetadataFileExtension)) - description = "SayMore File Metadata (XML)"; - - return description; - } - - /// ------------------------------------------------------------------------------------ - public void SetAdditionalMetsData(RampArchivingDlgViewModel model) - { - foreach (var session in GetAllSessions(CancellationToken.None)) - { - model.SetScholarlyWorkType(ScholarlyWorkType.PrimaryData); - model.SetDomains(SilDomain.Ling_LanguageDocumentation); - - var value = session.MetaDataFile.GetStringValue(SessionFileType.kDateFieldName, null); - if (!string.IsNullOrEmpty(value)) - model.SetCreationDate(value); - - // Return the session's note as the abstract portion of the package's description. - value = session.MetaDataFile.GetStringValue(SessionFileType.kSynopsisFieldName, null); - if (!string.IsNullOrEmpty(value)) - model.SetAbstract(value, string.Empty); - - // Set contributors - var contribsVal = session.MetaDataFile.GetValue(SessionFileType.kContributionsFieldName, null); - if (contribsVal is ContributionCollection contributions && contributions.Count > 0) - model.SetContributors(contributions); - - // Return total duration of source audio/video recordings. - TimeSpan totalDuration = session.GetTotalDurationOfSourceMedia(); - if (totalDuration.Ticks > 0) - model.SetAudioVideoExtent($"Total Length of Source Recordings: {totalDuration}"); - - //First session details are enough for "Archive RAMP (SIL)..." from Project menu - break; - } - } - - /// ------------------------------------------------------------------------------------ public void Load() { @@ -483,26 +586,26 @@ private static string GetFontErrorMessage(string description, string settingValu } /// ------------------------------------------------------------------------------------ - private string GetStringSettingValue(XElement project, string elementName, string defaultValue) + private static string GetStringSettingValue(XElement project, string elementName, string defaultValue) { var element = project.Element(elementName); return element == null ? defaultValue : element.Value; } /// ------------------------------------------------------------------------------------ - private int GetIntAttributeValue(XElement project, string attribName, string fallbackAttribName = null) + private static int GetIntAttributeValue(XElement project, string attribName, string fallbackAttribName = null) { var attrib = project.Attribute(attribName); if (attrib == null && fallbackAttribName != null) attrib = project.Attribute(fallbackAttribName); - return (attrib != null && Int32.TryParse(attrib.Value, out var val)) ? val : default; + return attrib != null && Int32.TryParse(attrib.Value, out var val) ? val : 0; } /// ------------------------------------------------------------------------------------ - private double GetDoubleAttributeValue(XElement project, string attribName) + private static double GetDoubleAttributeValue(XElement project, string attribName) { var attrib = project.Attribute(attribName); - return (attrib != null && Double.TryParse(attrib.Value, out var val)) ? val : default; + return attrib != null && Double.TryParse(attrib.Value, out var val) ? val : 0; } /// ------------------------------------------------------------------------------------ @@ -558,6 +661,53 @@ internal IEnumerable GetAllSessions(CancellationToken cancellationToken } #region Archiving + /// ------------------------------------------------------------------------------------ + public string GetFileDescription(string key, string file) + { + var description = key == string.Empty ? "SayMore Session File" : "SayMore Contributor File"; + + if (file.ToLower().EndsWith(Settings.Default.SessionFileExtension)) + description = "SayMore Session Metadata (XML)"; + else if (file.ToLower().EndsWith(Settings.Default.PersonFileExtension)) + description = "SayMore Contributor Metadata (XML)"; + else if (file.ToLower().EndsWith(Settings.Default.MetadataFileExtension)) + description = "SayMore File Metadata (XML)"; + + return description; + } + + /// ------------------------------------------------------------------------------------ + public void SetAdditionalMetsData(RampArchivingDlgViewModel model) + { + foreach (var session in GetAllSessions(CancellationToken.None)) + { + model.SetScholarlyWorkType(ScholarlyWorkType.PrimaryData); + model.SetDomains(SilDomain.Ling_LanguageDocumentation); + + var value = session.MetaDataFile.GetStringValue(SessionFileType.kDateFieldName, null); + if (!string.IsNullOrEmpty(value)) + model.SetCreationDate(value); + + // Return the session's note as the abstract portion of the package's description. + value = session.MetaDataFile.GetStringValue(SessionFileType.kSynopsisFieldName, null); + if (!string.IsNullOrEmpty(value)) + model.SetAbstract(value, string.Empty); + + // Set contributors + var contribsVal = session.MetaDataFile.GetValue(SessionFileType.kContributionsFieldName, null); + if (contribsVal is ContributionCollection contributions && contributions.Count > 0) + model.SetContributors(contributions); + + // Return total duration of source audio/video recordings. + TimeSpan totalDuration = session.GetTotalDurationOfSourceMedia(); + if (totalDuration.Ticks > 0) + model.SetAudioVideoExtent($"Total Length of Source Recordings: {totalDuration}"); + + //First session details are enough for "Archive RAMP (SIL)..." from Project menu + break; + } + } + /// ------------------------------------------------------------------------------------ public string ArchiveInfoDetails => LocalizationManager.GetString("DialogBoxes.ArchivingDlg.ProjectArchivingInfoDetails", @@ -799,5 +949,29 @@ public IEnumerable GetSessionFilesToArchive(Type typeOfArchive, Settings.Default.SessionFileExtension, CancellationToken.None)); } #endregion + + public void TrackStatistics(StatisticsViewModel statisticsViewModel) + { + if (_statisticsViewModel != null) + _statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics; + + _statisticsViewModel = statisticsViewModel; + _statisticsViewModel.FinishedGatheringStatisticsForAllFiles += FinishedGatheringStatistics; + if (_statisticsViewModel.IsDataUpToDate) + { + // It either finished before we could hook the event, or we hit the race condition. + FinishedGatheringStatistics(_statisticsViewModel, null); + } + } + + private void FinishedGatheringStatistics(object sender, EventArgs e) + { + _statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics; + // Check for race condition (see above). + if (_progressStats != null) + return; + + _progressStats = new ProgressStats(_statisticsViewModel); + } } } diff --git a/src/SayMore/Model/SessionWorkflowInformant.cs b/src/SayMore/Model/SessionWorkflowInformant.cs index 7a1d9ca2..1973df83 100644 --- a/src/SayMore/Model/SessionWorkflowInformant.cs +++ b/src/SayMore/Model/SessionWorkflowInformant.cs @@ -14,7 +14,7 @@ namespace SayMore.Model public class SessionWorkflowInformant { private readonly ElementRepository _sessionRepository; - private IEnumerable _componentRoles; + private readonly IEnumerable _componentRoles; [Obsolete("For mocking only")] public SessionWorkflowInformant(){} @@ -28,10 +28,7 @@ public SessionWorkflowInformant(ElementRepository sessionRepository, } /// ------------------------------------------------------------------------------------ - public int NumberOfSessions - { - get { return _sessionRepository.AllItems.Count(); } - } + public int NumberOfSessions => _sessionRepository.AllItems.Count(); /// ------------------------------------------------------------------------------------ /// diff --git a/src/SayMore/Program.cs b/src/SayMore/Program.cs index af639de5..7f2a60dd 100644 --- a/src/SayMore/Program.cs +++ b/src/SayMore/Program.cs @@ -235,6 +235,8 @@ static void Main() { Application.Run(); Settings.Default.Save(); + _projectContext?.Project.ReportProgressIfAny(); + Analytics.FlushClient(); Logger.WriteEvent("SayMore shutting down"); if (s_countOfContiguousFirstChanceOutOfMemoryExceptions > 1) Logger.WriteEvent("Total number of contiguous OutOfMemoryExceptions: {0}", s_countOfContiguousFirstChanceOutOfMemoryExceptions); @@ -596,8 +598,10 @@ static void ChooseAnotherProject(object sender, EventArgs e) } /// ------------------------------------------------------------------------------------ - static void HandleProjectWindowClosed(object sender, EventArgs e) + private static void HandleProjectWindowClosed(object sender, EventArgs e) { + _projectContext?.Project.ReportProgressIfAny(); + SafelyDisposeProjectContext(); ReleaseMutexForThisProject(); diff --git a/src/SayMore/ProjectContext.cs b/src/SayMore/ProjectContext.cs index 31973d0a..7e20c5ab 100644 --- a/src/SayMore/ProjectContext.cs +++ b/src/SayMore/ProjectContext.cs @@ -369,47 +369,37 @@ public void Dispose() /// ------------------------------------------------------------------------------------ public void SuspendAudioVideoBackgroundProcesses() { - if (_audioVideoDataGatherer != null) - _audioVideoDataGatherer.SuspendProcessing(); + _audioVideoDataGatherer?.SuspendProcessing(); } /// ------------------------------------------------------------------------------------ public void ResumeAudioVideoBackgroundProcesses(bool processAllPendingEventsNow) { - if (_audioVideoDataGatherer != null) - _audioVideoDataGatherer.ResumeProcessing(processAllPendingEventsNow); + _audioVideoDataGatherer?.ResumeProcessing(processAllPendingEventsNow); } /// ------------------------------------------------------------------------------------ public void SuspendBackgroundProcesses() { - if (_audioVideoDataGatherer != null) - _audioVideoDataGatherer.SuspendProcessing(); + _audioVideoDataGatherer?.SuspendProcessing(); - if (_autoCompleteValueGatherer != null) - _autoCompleteValueGatherer.SuspendProcessing(); + _autoCompleteValueGatherer?.SuspendProcessing(); - if (_fieldGatherer != null) - _fieldGatherer.SuspendProcessing(); + _fieldGatherer?.SuspendProcessing(); - if (_presetGatherer != null) - _presetGatherer.SuspendProcessing(); + _presetGatherer?.SuspendProcessing(); } /// ------------------------------------------------------------------------------------ public void ResumeBackgroundProcesses(bool processAllPendingEventsNow) { - if (_audioVideoDataGatherer != null) - _audioVideoDataGatherer.ResumeProcessing(processAllPendingEventsNow); + _audioVideoDataGatherer?.ResumeProcessing(processAllPendingEventsNow); - if (_autoCompleteValueGatherer != null) - _autoCompleteValueGatherer.ResumeProcessing(processAllPendingEventsNow); + _autoCompleteValueGatherer?.ResumeProcessing(processAllPendingEventsNow); - if (_fieldGatherer != null) - _fieldGatherer.ResumeProcessing(processAllPendingEventsNow); + _fieldGatherer?.ResumeProcessing(processAllPendingEventsNow); - if (_presetGatherer != null) - _presetGatherer.ResumeProcessing(processAllPendingEventsNow); + _presetGatherer?.ResumeProcessing(processAllPendingEventsNow); } /// ------------------------------------------------------------------------------------ diff --git a/src/SayMore/Properties/AssemblyInfo.cs b/src/SayMore/Properties/AssemblyInfo.cs index dd2e5029..59b87d0a 100644 --- a/src/SayMore/Properties/AssemblyInfo.cs +++ b/src/SayMore/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("SIL Global")] [assembly: AssemblyProduct("SayMore")] -[assembly: AssemblyCopyright("Copyright © 2011-2025 SIL Global")] +[assembly: AssemblyCopyright("Copyright © 2011-2026 SIL Global")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/SayMore/SayMore.csproj b/src/SayMore/SayMore.csproj index 10bcf972..c5a58b90 100644 --- a/src/SayMore/SayMore.csproj +++ b/src/SayMore/SayMore.csproj @@ -40,7 +40,7 @@ - + @@ -51,10 +51,10 @@ - + - + All @@ -62,11 +62,14 @@ + + - + + diff --git a/src/SayMore/UI/Charts/ChartBarInfo.cs b/src/SayMore/UI/Charts/ChartBarInfo.cs index 196820c6..7cc957df 100644 --- a/src/SayMore/UI/Charts/ChartBarInfo.cs +++ b/src/SayMore/UI/Charts/ChartBarInfo.cs @@ -130,7 +130,7 @@ public ChartBarSegmentInfo(string fieldName, string fieldValue, } catch (InvalidOperationException) { - // SP-854: This can happen if the the list is still loading, "Collection was modified; enumeration operation may not execute." + // SP-854: This can happen if the list is still loading, "Collection was modified; enumeration operation may not execute." // Let the other thread continue and try again. Application.DoEvents(); Thread.Sleep(0); diff --git a/src/SayMore/UI/Charts/HTMLChartBuilder.cs b/src/SayMore/UI/Charts/HTMLChartBuilder.cs index 273a0f44..f24ce6c4 100644 --- a/src/SayMore/UI/Charts/HTMLChartBuilder.cs +++ b/src/SayMore/UI/Charts/HTMLChartBuilder.cs @@ -18,7 +18,7 @@ public class HTMLChartBuilder public const string kNonBreakingSpace = " "; private readonly StatisticsViewModel _statsViewModel; - protected readonly StringBuilder _htmlText = new StringBuilder(6000); + protected readonly StringBuilder _htmlText = new(6000); /// ------------------------------------------------------------------------------------ public HTMLChartBuilder(StatisticsViewModel statsViewModel) @@ -61,7 +61,7 @@ public string GetStatisticsCharts() WriteStageChart(); var backColors = GetStatusSegmentColors(); - var textColors = backColors.ToDictionary(kvp => kvp.Key, kvp => Color.Empty); + var textColors = backColors.ToDictionary(kvp => kvp.Key, _ => Color.Empty); text = LocalizationManager.GetString("ProgressView.ByGenreHeadingText", "By Genre"); WriteChartByFieldPair(text, SessionFileType.kGenreFieldName, SessionFileType.kStatusFieldName, backColors, textColors); @@ -91,8 +91,8 @@ private void WriteStageChart() var sessionsByStage = _statsViewModel.SessionInformant.GetSessionsCategorizedByStage() .Where(r => r.Key.Id != ComponentRole.kConsentComponentRoleId); - var barInfoList = (sessionsByStage.Select( - x => new ChartBarInfo(x.Key.Name, x.Value, x.Key.Color, x.Key.TextColor))).ToList(); + var barInfoList = sessionsByStage.Select( + x => new ChartBarInfo(x.Key.Name, x.Value, x.Key.Color, x.Key.TextColor)).ToList(); ChartBarInfo.CalculateBarSizes(barInfoList); var text = LocalizationManager.GetString("ProgressView.ByStagesHeadingText", "Completed Stages"); @@ -104,7 +104,7 @@ private IDictionary GetStatusSegmentColors() { var statusColors = new Dictionary(); - foreach (var statusName in Enum.GetNames(typeof(Session.Status)).Where(x => x != Session.Status.Skipped.ToString())) + foreach (var statusName in Enum.GetNames(typeof(Session.Status)).Where(x => x != nameof(Session.Status.Skipped))) { statusColors[Session.GetLocalizedStatus(statusName)] = (Color)Properties.Settings.Default[statusName + "StatusColor"]; @@ -135,9 +135,10 @@ private void WriteOverviewSection() foreach (var stats in _statsViewModel.GetComponentRoleStatisticsPairs()) { OpenTableRow(); - WriteTableRowHead(string.Format("{0}:", stats.Name)); - WriteTableCell(stats.Length); - WriteTableCell(stats.Size); + WriteTableRowHead($"{stats.Name}:"); + WriteTableCell(stats.Length.ToString()); + var size = stats.Size == 0 ? "---" : ComponentFile.GetDisplayableFileSize(stats.Size, false); + WriteTableCell(size); CloseTableRow(); } diff --git a/src/SayMore/UI/Overview/Statistics/StatisticsView.cs b/src/SayMore/UI/Overview/Statistics/StatisticsView.cs index 24285be1..83bb1d58 100644 --- a/src/SayMore/UI/Overview/Statistics/StatisticsView.cs +++ b/src/SayMore/UI/Overview/Statistics/StatisticsView.cs @@ -60,7 +60,7 @@ private void UpdateDisplay(ILocalizationManager lm = null) _webBrowser.DocumentStream?.Dispose(); UpdateStatusDisplay(true); - Thread updateDisplayThread = new Thread(() => + var updateDisplayThread = new Thread(() => { var htmlData = new MemoryStream(Encoding.UTF8.GetBytes(_model.HTMLString)); @@ -179,8 +179,7 @@ void HandleNewDataAvailable(object sender, EventArgs e) { // Can't actually call UpdateDisplay from here because this event is fired from // a background (data gathering) thread and updating the browser control on the - // background thread is a no-no. UpdateDisplay will be called when the timer - // tick fires. + // background thread is a no-no. BeginInvoke(new Action(() => UpdateDisplay())); } diff --git a/src/SayMore/UI/Overview/Statistics/StatisticsViewModel.cs b/src/SayMore/UI/Overview/Statistics/StatisticsViewModel.cs index 56961419..80205b8b 100644 --- a/src/SayMore/UI/Overview/Statistics/StatisticsViewModel.cs +++ b/src/SayMore/UI/Overview/Statistics/StatisticsViewModel.cs @@ -17,7 +17,7 @@ public class StatisticsViewModel : IDisposable public event EventHandler FinishedGatheringStatisticsForAllFiles; private readonly IEnumerable _componentRoles; - private readonly AudioVideoDataGatherer _backgroundStatisticsGather; + private readonly AudioVideoDataGatherer _backgroundStatisticsGatherer; protected HTMLChartBuilder _chartBuilder; public PersonInformant PersonInformant { get; protected set; } @@ -28,16 +28,18 @@ public class StatisticsViewModel : IDisposable /// ------------------------------------------------------------------------------------ public StatisticsViewModel(Project project, PersonInformant personInformant, SessionWorkflowInformant sessionInformant, IEnumerable componentRoles, - AudioVideoDataGatherer backgroundStatisticsMananager) + AudioVideoDataGatherer backgroundStatisticsManager) { - ProjectName = (project == null ? string.Empty : project.Name); - ProjectPath = (project == null ? string.Empty : project.FolderPath); + ProjectName = project?.Name ?? string.Empty; + ProjectPath = project?.FolderPath ?? string.Empty; PersonInformant = personInformant; SessionInformant = sessionInformant; _componentRoles = componentRoles; - _backgroundStatisticsGather = backgroundStatisticsMananager; - _backgroundStatisticsGather.NewDataAvailable += HandleNewStatistics; - _backgroundStatisticsGather.FinishedProcessingAllFiles += HandleFinishedGatheringStatisticsForAllFiles; + _backgroundStatisticsGatherer = backgroundStatisticsManager; + _backgroundStatisticsGatherer.FinishedProcessingAllFiles += HandleFinishedGatheringStatisticsForAllFiles; + _backgroundStatisticsGatherer.NewDataAvailable += HandleNewStatistics; + + project?.TrackStatistics(this); _chartBuilder = new HTMLChartBuilder(this); } @@ -45,33 +47,21 @@ public StatisticsViewModel(Project project, PersonInformant personInformant, /// ------------------------------------------------------------------------------------ public void Dispose() { - _backgroundStatisticsGather.NewDataAvailable -= HandleNewStatistics; - _backgroundStatisticsGather.FinishedProcessingAllFiles -= HandleFinishedGatheringStatisticsForAllFiles; + _backgroundStatisticsGatherer.NewDataAvailable -= HandleNewStatistics; + _backgroundStatisticsGatherer.FinishedProcessingAllFiles -= HandleFinishedGatheringStatisticsForAllFiles; } /// ------------------------------------------------------------------------------------ - public string Status - { - get { return _backgroundStatisticsGather.Status; } - } + public string Status => _backgroundStatisticsGatherer.Status; /// ------------------------------------------------------------------------------------ - public bool IsDataUpToDate - { - get { return _backgroundStatisticsGather.DataUpToDate; } - } + public bool IsDataUpToDate => _backgroundStatisticsGatherer.DataUpToDate; /// ------------------------------------------------------------------------------------ - public bool IsBusy - { - get { return _backgroundStatisticsGather.Busy; } - } + public bool IsBusy => _backgroundStatisticsGatherer.Busy; /// ------------------------------------------------------------------------------------ - public string HTMLString - { - get { return _chartBuilder.GetStatisticsCharts(); } - } + public string HTMLString => _chartBuilder.GetStatisticsCharts(); /// ------------------------------------------------------------------------------------ public IEnumerable> GetElementStatisticsPairs() @@ -89,13 +79,12 @@ public IEnumerable GetComponentRoleStatisticsPairs() foreach (var role in _componentRoles.Where(def => def.MeasurementType == ComponentRole.MeasurementTypes.Time)) { long bytes = GetTotalComponentRoleFileSizes(role); - var size = (bytes == 0 ? "---" : ComponentFile.GetDisplayableFileSize(bytes, false)); yield return new ComponentRoleStatistics { Name = role.Name, - Length = GetRecordingDurations(role).ToString(), - Size = size + Length = GetRecordingDurations(role), + Size = bytes }; } } @@ -121,7 +110,7 @@ private IEnumerable GetFilteredFileData(ComponentRole role) { var comparer = new SourceAndStandardAudioCoalescingComparer(); // SP-2171: i.MediaFilePath will be empty if the file is zero length (see MediaFileInfo.GetInfo()). This happens often with the generated oral annotation file. - return _backgroundStatisticsGather.GetAllFileData() + return _backgroundStatisticsGatherer.GetAllFileData() .Where(i => !string.IsNullOrEmpty(i.MediaFilePath) && !i.MediaFilePath.EndsWith(Settings.Default.OralAnnotationGeneratedFileSuffix) && role.IsMatch(i.MediaFilePath)) .Distinct(comparer); @@ -130,28 +119,26 @@ private IEnumerable GetFilteredFileData(ComponentRole role) /// ------------------------------------------------------------------------------------ public void Refresh() { - _backgroundStatisticsGather.Restart(); + _backgroundStatisticsGatherer.Restart(); } /// ------------------------------------------------------------------------------------ void HandleNewStatistics(object sender, EventArgs e) { - if (NewStatisticsAvailable != null) - NewStatisticsAvailable(this, EventArgs.Empty); + NewStatisticsAvailable?.Invoke(this, EventArgs.Empty); } /// ------------------------------------------------------------------------------------ void HandleFinishedGatheringStatisticsForAllFiles(object sender, EventArgs e) { - if (FinishedGatheringStatisticsForAllFiles != null) - FinishedGatheringStatisticsForAllFiles(this, EventArgs.Empty); + FinishedGatheringStatisticsForAllFiles?.Invoke(this, EventArgs.Empty); } } public class ComponentRoleStatistics { public string Name { get; set; } - public string Length { get; set; } - public string Size { get; set; } + public TimeSpan Length { get; set; } + public long Size { get; set; } } } \ No newline at end of file diff --git a/src/SayMore/UI/SplashScreenForm.Designer.cs b/src/SayMore/UI/SplashScreenForm.Designer.cs index eaae9199..d7c0771f 100644 --- a/src/SayMore/UI/SplashScreenForm.Designer.cs +++ b/src/SayMore/UI/SplashScreenForm.Designer.cs @@ -188,7 +188,7 @@ private void InitializeComponent() this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(126, 13); this.label1.TabIndex = 7; - this.label1.Text = "© 2011-2025 SIL Global"; + this.label1.Text = "© 2011-2026 SIL Global"; // // SplashScreenForm // diff --git a/src/SayMoreTests/Properties/AssemblyInfo.cs b/src/SayMoreTests/Properties/AssemblyInfo.cs index 0207a0b4..d890f930 100644 --- a/src/SayMoreTests/Properties/AssemblyInfo.cs +++ b/src/SayMoreTests/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("SayMoreTests")] -[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/SayMoreTests/SayMoreTests.csproj b/src/SayMoreTests/SayMoreTests.csproj index 2799d699..b2b02d7b 100644 --- a/src/SayMoreTests/SayMoreTests.csproj +++ b/src/SayMoreTests/SayMoreTests.csproj @@ -67,7 +67,7 @@ - + @@ -78,14 +78,15 @@ - + - - + + +