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 @@
-
+
-
-
+
+
+