1515using System . Xml . Linq ;
1616
1717namespace Clockwatcher {
18- internal class Dataset {
18+ internal sealed class Dataset {
1919 internal Dataset ( ) {
20- ClearReadyCommand = new CommandWrapper ( o => ClearReady ( ) , o => true ) ;
21- RefreshCommand = new CommandWrapper ( o => Refresh ( true ) , o => true ) ;
22- RefreshTimer . Elapsed += ( sender , e ) => Refresh ( false ) ;
23- Refresh ( true ) ;
20+ TabPanels . Add ( Settings ) ;
21+
22+ RefreshTimer . Elapsed += ( sender , e ) => Refresh ( ) ;
23+
24+ Refresh ( ) ;
2425 }
2526
26- private void Refresh ( bool reload ) {
27+ private void Refresh ( ) {
2728 RefreshTimer . Enabled = false ;
28- if ( reload ) { // Load settings files
29+ try {
30+ // Load settings files
2931 foreach ( var charFile in FindCharacterFiles ( ) ) {
3032 var charData = ExtractCharacterMissions ( charFile ) ;
3133 if ( charData != null ) {
3234 var existing = ( from e in CharacterMissionLists
33- where e . CharName == charData . CharName
35+ where e . TabName == charData . TabName
3436 select e ) . SingleOrDefault ( ) ;
3537 if ( existing != null ) { // Merge with existing character record
3638 existing . Merge ( charData . TimerList ) ;
3739 } else { // Add new character
3840 CharacterMissionLists . Add ( charData ) ;
41+ TabPanels . Insert ( TabPanels . Count - 1 , charData ) ;
3942 }
4043 }
4144 }
42- }
43- // Update time displays
44- foreach ( var m in from c in CharacterMissionLists
45- from m in c . TimerList
46- select m ) { m . Refresh ( reload ) ; }
47- RefreshTimer . Enabled = true ;
48- }
4945
50- private void ClearReady ( ) {
51- foreach ( var character in CharacterMissionLists ) {
52- character . ClearReady ( ) ;
46+ // Update time displays and trigger alert states
47+ if ( ( from c in CharacterMissionLists
48+ from m in c . TimerList
49+ where m . Refresh ( Settings . AlertFilter )
50+ select m ) . ToList ( ) . Any ( ) ) {
51+ RaiseAlert ? . Invoke ( this , EventArgs . Empty ) ;
52+ }
53+ } finally {
54+ // Unhandled exceptions were causing the refresh timer to stop
55+ // A bit of an issue now that there's no way to restart it
56+ RefreshTimer . Enabled = true ;
5357 }
5458 }
5559
@@ -62,23 +66,25 @@ from character in Directory.EnumerateDirectories(account, CharDirFilter)
6266 private CharacterTimers ExtractCharacterMissions ( string settingsFilePath ) {
6367 Debug . Assert ( File . Exists ( settingsFilePath ) ) ;
6468 try {
65- var trackerData = ( from e in XElement . Load ( settingsFilePath ) . Elements ( "Archive" )
66- where ( string ) e . Attribute ( "name" ) == CWArchiveName
67- select e ) . SingleOrDefault ( ) ;
68- // Mod not in use on this character (no mod archive), no tab required
69- // Characters who only lack mission entries, are listed to show they have no cooldowns active
70- if ( trackerData == null ) { return null ; }
71- // Character name should be in saved data, otherwise use the Char# folder name which isn't so informative
72- var charName = ( ( string ) ( from e in trackerData . Elements ( "String" )
73- where ( string ) e . Attribute ( "name" ) == "CharName"
74- select e ) . SingleOrDefault ( ) ? . Attribute ( "value" ) ) ? . Trim ( '"' )
75- ?? Directory . GetParent ( settingsFilePath ) . Name ;
76- // Coerce the serialization difference between a single and multi element array into a single IEnumerable
77- var missions = from e in EnumerateArchiveEntry ( trackerData , "MissionCD" )
78- select new MissionTimer ( ( ( string ) e . Attribute ( "value" ) ) . Trim ( '"' ) ) ;
79- var agents = from e in EnumerateArchiveEntry ( trackerData , "AgentCD" )
80- select new AgentTimer ( ( ( string ) e . Attribute ( "value" ) ) . Trim ( '"' ) ) ;
81- return new CharacterTimers ( charName , Enumerable . Empty < TimerEntry > ( ) . Concat ( missions ) . Concat ( agents ) ) ;
69+ using ( Stream file = new FileStream ( settingsFilePath , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) ) {
70+ var trackerData = ( from e in XElement . Load ( file ) . Elements ( "Archive" )
71+ where e . Attribute ( "name" ) . Value == CWArchiveName
72+ select e ) . SingleOrDefault ( ) ;
73+ // Mod not in use on this character (no mod archive), no tab required
74+ // Characters who only lack mission entries, are listed to show they have no cooldowns active
75+ if ( trackerData == null ) { return null ; }
76+ // Character name should be in saved data, otherwise use the Char# folder name which isn't so informative
77+ var charName = ( from e in trackerData . Elements ( "String" )
78+ where e . Attribute ( "name" ) . Value == "CharName"
79+ select e ) . SingleOrDefault ( ) ? . Attribute ( "value" ) . Value ? . Trim ( '"' )
80+ ?? Directory . GetParent ( settingsFilePath ) . Name ;
81+ // Coerce the serialization difference between a single and multi element array into a single IEnumerable
82+ var missions = from e in EnumerateArchiveEntry ( trackerData , "MissionCD" )
83+ select new MissionTimer ( ( e . Attribute ( "value" ) . Value ) . Trim ( '"' ) ) ;
84+ var agents = from e in EnumerateArchiveEntry ( trackerData , "AgentCD" )
85+ select new AgentTimer ( ( e . Attribute ( "value" ) . Value ) . Trim ( '"' ) ) ;
86+ return new CharacterTimers ( charName , Enumerable . Empty < TimerEntry > ( ) . Concat ( missions ) . Concat ( agents ) ) ;
87+ }
8288 } catch ( IOException ) {
8389 // File in use or other issue... either way no data from this one, so skip it
8490 return null ;
@@ -95,9 +101,13 @@ private IEnumerable<XElement> EnumerateArchiveEntry(XElement archive, string tag
95101 select e ) ;
96102 }
97103
98- public ICollection < CharacterTimers > CharacterMissionLists { get ; } = new ObservableCollection < CharacterTimers > ( ) ;
104+ public ConfigPanel Settings { get ; } = new ConfigPanel ( ) ;
105+ public IList < TabPanelData > TabPanels { get ; } = new ObservableCollection < TabPanelData > ( ) ;
99106 public ICommand RefreshCommand { get ; }
100- public ICommand ClearReadyCommand { get ; }
107+
108+ public event EventHandler RaiseAlert ;
109+
110+ private ICollection < CharacterTimers > CharacterMissionLists = new List < CharacterTimers > ( ) ;
101111 private readonly Timer RefreshTimer = new Timer ( 5000 ) ;
102112
103113 private static readonly string PrefsPath = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) , "Funcom" , "SWL" , "Prefs" ) ;
@@ -107,19 +117,77 @@ private IEnumerable<XElement> EnumerateArchiveEntry(XElement archive, string tag
107117 private const string CWArchiveName = "efdClockwatcherMissionList" ;
108118 }
109119
110- // Nested data
120+ internal abstract class TabPanelData : INotifyPropertyChanged {
121+ protected internal TabPanelData ( string name ) {
122+ TabName = name ;
123+ }
124+
125+ public string TabName { get ; }
126+
127+ public event PropertyChangedEventHandler PropertyChanged ;
128+ protected virtual void RaisePropertyChanged ( string propertyName ) => PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( propertyName ) ) ;
129+ }
111130
112- internal class CharacterTimers : INotifyPropertyChanged {
113- internal CharacterTimers ( string charName , IEnumerable < TimerEntry > timers ) {
114- CharName = charName ;
131+ internal sealed class ConfigPanel : TabPanelData {
132+ internal ConfigPanel ( ) : base ( "Settings" ) {
133+ LoadConfig ( ) ;
134+ }
135+
136+ private void LoadConfig ( ) {
137+ var settingsFile = Path . Combine ( SettingsDir , SettingsFile ) ;
138+ if ( File . Exists ( settingsFile ) ) {
139+ using ( var stream = new FileStream ( settingsFile , FileMode . Open , FileAccess . Read , FileShare . ReadWrite ) ) {
140+ var settings = XElement . Load ( stream ) ;
141+ EnableAudioAlerts = Boolean . Parse ( settings . Element ( "AgentAudioAlert" ) . Attribute ( "Enabled" ) . Value ) ;
142+ }
143+ }
144+ }
115145
116- TimerList = new ObservableCollection < TimerEntry > ( timers ) ;
146+ private void SaveConfig ( ) {
147+ var settings = new XElement ( "Settings" ,
148+ new XElement ( "AgentAudioAlert" ,
149+ new XAttribute ( "Enabled" , EnableAudioAlerts )
150+ ) ) ;
151+ Directory . CreateDirectory ( SettingsDir ) ;
152+ settings . Save ( Path . Combine ( SettingsDir , SettingsFile ) ) ;
153+ }
154+
155+ protected override sealed void RaisePropertyChanged ( string propertyName ) {
156+ base . RaisePropertyChanged ( propertyName ) ;
157+ SaveConfig ( ) ;
158+ }
159+
160+ public bool EnableAudioAlerts {
161+ get { return _EnableAudioAlerts ; }
162+ set {
163+ if ( value != _EnableAudioAlerts ) {
164+ _EnableAudioAlerts = value ;
165+ RaisePropertyChanged ( nameof ( EnableAudioAlerts ) ) ;
166+ }
167+ }
168+ }
169+ public string AlertSoundFileName { get ; } = "sfx\\ AgentAlert.wav" ;
170+
171+ internal TimerClass AlertFilter { get ; } = TimerClass . Agent ;
172+
173+ private bool _EnableAudioAlerts = true ;
174+ private string SettingsDir = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) , "Funcom" , "SWL" , "Mods" , "Clockwatcher" ) ;
175+ private string SettingsFile = "ViewerSettings.xml" ;
176+ }
177+
178+ // Nested data
179+
180+ internal sealed class CharacterTimers : TabPanelData {
181+ internal CharacterTimers ( string charName , IEnumerable < TimerEntry > timers ) :
182+ base ( charName ) {
117183
118184 var view = CollectionViewSource . GetDefaultView ( TimerList ) ;
119185 view . SortDescriptions . Add ( new SortDescription ( nameof ( TimerEntry . RemainingTime ) , ListSortDirection . Ascending ) ) ;
120186 ( ( ICollectionViewLiveShaping ) view ) . IsLiveSorting = true ;
121187
122- PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( TimerList ) ) ) ;
188+ foreach ( var timer in timers ) { TimerList . Add ( timer ) ; }
189+
190+ ClearReadyCommand = new CommandWrapper ( o => ClearReady ( ) , o => true ) ;
123191 }
124192
125193 internal void Merge ( IEnumerable < TimerEntry > other ) {
@@ -128,25 +196,33 @@ internal void Merge(IEnumerable<TimerEntry> other) {
128196 if ( existing != null ) {
129197 existing . UnlockTime = t . UnlockTime ;
130198 if ( existing . Class != t . Class ) { existing . ChangeTimerClass ( t . Class ) ; }
131- } else { TimerList . Add ( t ) ; }
199+ } else if ( ! t . IsReady ) {
200+ App . Current . Dispatcher . Invoke ( ( ) => TimerList . Add ( t ) ) ;
201+ }
132202 }
133203 }
134204
135- internal void ClearReady ( ) {
205+ private void ClearReady ( ) {
136206 foreach ( var tReady in ( from t in TimerList
137- where t . IsReady
138- select t ) . ToList ( ) ) {
207+ where t . IsReady
208+ select t ) . ToList ( ) ) {
139209 TimerList . Remove ( tReady ) ;
140210 }
141211 }
142212
143- public event PropertyChangedEventHandler PropertyChanged ;
144-
145- public string CharName { get ; }
146- public ICollection < TimerEntry > TimerList { get ; }
213+ public ICommand ClearReadyCommand { get ; }
214+ public ICollection < TimerEntry > TimerList { get ; } = new ObservableCollection < TimerEntry > ( ) ;
147215 }
148216
149- internal enum TimerClass { AgentMission , AgentRecovery , Lair , Mission }
217+ [ Flags ]
218+ internal enum TimerClass {
219+ None = 0 ,
220+ AgentMission = 1 ,
221+ AgentRecovery = 2 ,
222+ Agent = AgentMission | AgentRecovery ,
223+ Lair = 4 ,
224+ Mission = 8
225+ }
150226
151227 internal abstract class TimerEntry : INotifyPropertyChanged {
152228
@@ -157,7 +233,11 @@ protected internal TimerEntry(string entryData) {
157233 UnlockTime = DateTimeOffset . FromUnixTimeSeconds ( long . Parse ( dataFields [ 1 ] ) ) . LocalDateTime ;
158234 }
159235
160- internal void Refresh ( bool reload ) => RaisePropertyChanged ( nameof ( RemainingTime ) ) ;
236+ internal bool Refresh ( TimerClass alertFilter ) {
237+ RaisePropertyChanged ( nameof ( RemainingTime ) ) ;
238+ return IsReady != AlertDone ? ( AlertDone = IsReady ) && ( ( alertFilter & Class ) != TimerClass . None ) : false ;
239+ }
240+
161241 internal abstract void ChangeTimerClass ( TimerClass newClass ) ;
162242
163243 internal int ID { get ; }
@@ -167,12 +247,14 @@ protected internal TimerEntry(string entryData) {
167247 internal bool IsReady => RemainingTime . TotalSeconds <= 0 ;
168248
169249 public event PropertyChangedEventHandler PropertyChanged ;
170- protected void RaisePropertyChanged ( string propertyName ) { PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( propertyName ) ) ; }
250+ protected void RaisePropertyChanged ( string propertyName ) => PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( propertyName ) ) ;
171251
172252 public abstract TimerClass Class { get ; }
253+
254+ private bool AlertDone = false ;
173255 }
174256
175- internal class AgentTimer : TimerEntry , INotifyPropertyChanged {
257+ internal sealed class AgentTimer : TimerEntry , INotifyPropertyChanged {
176258 internal AgentTimer ( string agentInfo ) : base ( agentInfo ) {
177259 IsRecovering = Boolean . Parse ( agentInfo . Split ( '|' ) [ 3 ] ) ;
178260 }
@@ -186,18 +268,18 @@ internal override void ChangeTimerClass(TimerClass newClass) {
186268 public override TimerClass Class => IsRecovering ? TimerClass . AgentRecovery : TimerClass . AgentMission ;
187269 }
188270
189- internal class MissionTimer : TimerEntry {
271+ internal sealed class MissionTimer : TimerEntry {
190272 internal MissionTimer ( string missionInfo ) : base ( missionInfo ) { }
191273
192274 // Mission classes are unique by ID, they should not change
193- internal override void ChangeTimerClass ( TimerClass newClass ) { throw new NotSupportedException ( ) ; }
275+ internal override void ChangeTimerClass ( TimerClass newClass ) => throw new NotSupportedException ( ) ;
194276
195277 public override TimerClass Class => ID > 0 ? TimerClass . Mission : TimerClass . Lair ;
196278 }
197279
198280 // Helper classes
199281
200- internal class CommandWrapper : ICommand {
282+ internal sealed class CommandWrapper : ICommand {
201283 public event EventHandler CanExecuteChanged ;
202284
203285 internal CommandWrapper ( Action < object > execute , Func < object , bool > canExecute ) {
0 commit comments