From bd92fc6ee070424b3340ffe9be1ce83878b9b3c0 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Sat, 13 Jan 2018 22:59:34 +0100
Subject: [PATCH] Use <audio> for custom sound notifications & allow volume
 control for default one

Closes #195
---
 Configuration/UserConfig.cs                   | 13 +++-
 Core/Bridge/PropertyBridge.cs                 |  1 -
 Core/Bridge/TweetDeckBridge.cs                |  2 +-
 Core/FormBrowser.cs                           | 37 ++---------
 Core/Notification/SoundNotification.cs        | 61 ++++++++++++-------
 Core/Other/FormSettings.cs                    |  2 +-
 .../Settings/TabSettingsSounds.Designer.cs    |  7 +++
 Core/Other/Settings/TabSettingsSounds.cs      | 53 ++++++++--------
 Core/TweetDeckBrowser.cs                      | 31 ++++++++++
 Resources/Scripts/code.js                     | 27 +++++++-
 TweetDuck.csproj                              |  4 --
 11 files changed, 149 insertions(+), 89 deletions(-)

diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index 0fc8b38a..6c982d3a 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -83,8 +83,8 @@ static UserConfig(){
         public Size CustomNotificationSize             { get; set; } = Size.Empty;
         public int NotificationScrollSpeed             { get; set; } = 100;
 
-        public int NotificationSoundVolume { get; set; } = 100;
         private string _notificationSoundPath;
+        private int _notificationSoundVolume = 100;
 
         public string CustomCefArgs         { get; set; } = null;
         public string CustomBrowserCSS      { get; set; } = null;
@@ -94,12 +94,18 @@ static UserConfig(){
 
         public bool IsCustomNotificationPositionSet => CustomNotificationPosition != ControlExtensions.InvisibleLocation;
         public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
+        public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty;
 
         public TwitterUtils.ImageQuality TwitterImageQuality => BestImageQuality ? TwitterUtils.ImageQuality.Orig : TwitterUtils.ImageQuality.Default;
         
         public string NotificationSoundPath{
-            get => string.IsNullOrEmpty(_notificationSoundPath) ? string.Empty : _notificationSoundPath;
-            set => _notificationSoundPath = value;
+            get => _notificationSoundPath ?? string.Empty;
+            set => UpdatePropertyWithEvent(ref _notificationSoundPath, value, SoundNotificationChanged);
+        }
+        
+        public int NotificationSoundVolume{
+            get => _notificationSoundVolume;
+            set => UpdatePropertyWithEvent(ref _notificationSoundVolume, value, SoundNotificationChanged);
         }
 
         public bool MuteNotifications{
@@ -122,6 +128,7 @@ public TrayIcon.Behavior TrayBehavior{
         public event EventHandler MuteToggled;
         public event EventHandler ZoomLevelChanged;
         public event EventHandler TrayBehaviorChanged;
+        public event EventHandler SoundNotificationChanged;
 
         // END OF CONFIG
         
diff --git a/Core/Bridge/PropertyBridge.cs b/Core/Bridge/PropertyBridge.cs
index e43800c2..cfcbafdf 100644
--- a/Core/Bridge/PropertyBridge.cs
+++ b/Core/Bridge/PropertyBridge.cs
@@ -19,7 +19,6 @@ public static string GenerateScript(Environment environment){
                 build.Append("x.openSearchInFirstColumn=").Append(Bool(Program.UserConfig.OpenSearchInFirstColumn));
                 build.Append("x.keepLikeFollowDialogsOpen=").Append(Bool(Program.UserConfig.KeepLikeFollowDialogsOpen));
                 build.Append("x.muteNotifications=").Append(Bool(Program.UserConfig.MuteNotifications));
-                build.Append("x.hasCustomNotificationSound=").Append(Bool(Program.UserConfig.NotificationSoundPath.Length > 0));
                 build.Append("x.notificationMediaPreviews=").Append(Bool(Program.UserConfig.NotificationMediaPreviews));
                 build.Append("x.translationTarget=").Append(Str(Program.UserConfig.TranslationTarget));
             }
diff --git a/Core/Bridge/TweetDeckBridge.cs b/Core/Bridge/TweetDeckBridge.cs
index aa376266..48b95893 100644
--- a/Core/Bridge/TweetDeckBridge.cs
+++ b/Core/Bridge/TweetDeckBridge.cs
@@ -126,7 +126,7 @@ public void OnTweetPopup(string columnId, string chirpId, string columnName, str
         public void OnTweetSound(){
             form.InvokeAsyncSafe(() => {
                 form.OnTweetNotification();
-                form.PlayNotificationSound();
+                form.OnTweetSound();
             });
         }
 
diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index ed964d6c..0e437218 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -15,7 +15,6 @@
 using TweetDuck.Plugins;
 using TweetDuck.Plugins.Events;
 using TweetDuck.Updates;
-using TweetLib.Audio;
 
 namespace TweetDuck.Core{
     sealed partial class FormBrowser : Form{
@@ -50,7 +49,6 @@ public bool IsWaiting{
         private FormWindowState prevState;
         
         private TweetScreenshotManager notificationScreenshotManager;
-        private SoundNotification soundNotification;
         private VideoPlayer videoPlayer;
         private AnalyticsManager analytics;
 
@@ -82,7 +80,6 @@ public FormBrowser(UpdaterSettings updaterSettings){
                 contextMenu.Dispose();
 
                 notificationScreenshotManager?.Dispose();
-                soundNotification?.Dispose();
                 videoPlayer?.Dispose();
                 analytics?.Dispose();
             };
@@ -291,22 +288,6 @@ private void updates_UpdateDismissed(object sender, UpdateEventArgs e){
             });
         }
 
-        private void soundNotification_PlaybackError(object sender, PlaybackErrorEventArgs e){
-            e.Ignore = true;
-
-            using(FormMessage form = new FormMessage("Notification Sound Error", "Could not play custom notification sound.\n"+e.Message, MessageBoxIcon.Error)){
-                form.AddButton(FormMessage.Ignore, ControlType.Cancel | ControlType.Focused);
-
-                Button btnOpenSettings = form.AddButton("View Options");
-                btnOpenSettings.Width += 16;
-                btnOpenSettings.Location = new Point(btnOpenSettings.Location.X-16, btnOpenSettings.Location.Y);
-
-                if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnOpenSettings){
-                    OpenSettings(typeof(TabSettingsSounds));
-                }
-            }
-        }
-
         protected override void WndProc(ref Message m){
             if (isLoaded && m.Msg == Program.WindowRestoreMessage){
                 if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
@@ -357,6 +338,10 @@ public void ReloadColumns(){
             browser.ReloadColumns();
         }
 
+        public void PlaySoundNotification(){
+            browser.PlaySoundNotification();
+        }
+
         public void ApplyROT13(){
             browser.ApplyROT13();
         }
@@ -459,19 +444,7 @@ public void OpenPlugins(){
             }
         }
 
-        public void PlayNotificationSound(){
-            if (Config.NotificationSoundPath.Length == 0){
-                return;
-            }
-
-            if (soundNotification == null){
-                soundNotification = new SoundNotification();
-                soundNotification.PlaybackError += soundNotification_PlaybackError;
-            }
-
-            soundNotification.SetVolume(Config.NotificationSoundVolume);
-            soundNotification.Play(Config.NotificationSoundPath);
-
+        public void OnTweetSound(){
             TriggerAnalyticsEvent(AnalyticsFile.Event.SoundNotification);
         }
 
diff --git a/Core/Notification/SoundNotification.cs b/Core/Notification/SoundNotification.cs
index 2d6f1436..d5664010 100644
--- a/Core/Notification/SoundNotification.cs
+++ b/Core/Notification/SoundNotification.cs
@@ -1,32 +1,49 @@
-using System;
-using TweetLib.Audio;
+using System.Drawing;
+using System.IO;
+using System.Windows.Forms;
+using CefSharp;
+using TweetDuck.Core.Controls;
+using TweetDuck.Core.Other;
+using TweetDuck.Core.Other.Settings;
 
 namespace TweetDuck.Core.Notification{
-    sealed class SoundNotification : IDisposable{
-        public string SupportedFormats => player.SupportedFormats;
-        public event EventHandler<PlaybackErrorEventArgs> PlaybackError;
+    static class SoundNotification{
+        public const string SupportedFormats = "*.wav;*.ogg;*.flac;*.opus;*.weba;*.webm"; // TODO add mp3 when supported
+        
+        public static IResourceHandler CreateFileHandler(string path){
+            string mimeType;
 
-        private readonly AudioPlayer player;
+            switch(Path.GetExtension(path)){
+                case "weba":
+                case "webm": mimeType = "audio/webm"; break;
+                case "wav": mimeType = "audio/wav"; break;
+                case "ogg": mimeType = "audio/ogg"; break;
+                case "flac": mimeType = "audio/flac"; break;
+                case "opus": mimeType = "audio/ogg; codecs=opus"; break;
+                default: mimeType = null; break;
+            }
 
-        public SoundNotification(){
-            this.player = AudioPlayer.New();
-            this.player.PlaybackError += Player_PlaybackError;
-        }
+            try{
+                return ResourceHandler.FromFilePath(path, mimeType);
+            }catch{
+                FormBrowser browser = FormManager.TryFind<FormBrowser>();
 
-        public void Play(string file){
-            player.Play(file);
-        }
+                browser?.InvokeAsyncSafe(() => {
+                    using(FormMessage form = new FormMessage("Sound Notification Error", "Could not find custom notification sound file:\n"+path, MessageBoxIcon.Error)){
+                        form.AddButton(FormMessage.Ignore, ControlType.Cancel | ControlType.Focused);
+                        
+                        Button btnViewOptions = form.AddButton("View Options");
+                        btnViewOptions.Width += 16;
+                        btnViewOptions.Location = new Point(btnViewOptions.Location.X-16, btnViewOptions.Location.Y);
 
-        public bool SetVolume(int volume){
-            return player.SetVolume(volume);
-        }
+                        if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnViewOptions){
+                            browser.OpenSettings(typeof(TabSettingsSounds));
+                        }
+                    }
+                });
 
-        private void Player_PlaybackError(object sender, PlaybackErrorEventArgs e){
-            PlaybackError?.Invoke(this, e);
-        }
-
-        public void Dispose(){
-            player.Dispose();
+                return null;
+            }
         }
     }
 }
diff --git a/Core/Other/FormSettings.cs b/Core/Other/FormSettings.cs
index 60ae3b9a..38fd460d 100644
--- a/Core/Other/FormSettings.cs
+++ b/Core/Other/FormSettings.cs
@@ -40,7 +40,7 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateHandler up
             AddButton("Locales", () => new TabSettingsLocales());
             AddButton("System Tray", () => new TabSettingsTray());
             AddButton("Notifications", () => new TabSettingsNotifications(new FormNotificationExample(this.browser, this.plugins)));
-            AddButton("Sounds", () => new TabSettingsSounds());
+            AddButton("Sounds", () => new TabSettingsSounds(this.browser.PlaySoundNotification));
             AddButton("Feedback", () => new TabSettingsFeedback(analytics, AnalyticsReportGenerator.ExternalInfo.From(this.browser), this.plugins));
             AddButton("Advanced", () => new TabSettingsAdvanced(this.browser.ReinjectCustomCSS));
 
diff --git a/Core/Other/Settings/TabSettingsSounds.Designer.cs b/Core/Other/Settings/TabSettingsSounds.Designer.cs
index d3784de1..b5bf2596 100644
--- a/Core/Other/Settings/TabSettingsSounds.Designer.cs
+++ b/Core/Other/Settings/TabSettingsSounds.Designer.cs
@@ -36,6 +36,7 @@ private void InitializeComponent() {
             this.trackBarVolume = new System.Windows.Forms.TrackBar();
             this.flowPanel = new System.Windows.Forms.FlowLayoutPanel();
             this.panelVolume = new System.Windows.Forms.Panel();
+            this.volumeUpdateTimer = new System.Windows.Forms.Timer(this.components);
             this.panelSoundNotification.SuspendLayout();
             ((System.ComponentModel.ISupportInitialize)(this.trackBarVolume)).BeginInit();
             this.flowPanel.SuspendLayout();
@@ -170,6 +171,11 @@ private void InitializeComponent() {
             this.panelVolume.Size = new System.Drawing.Size(322, 36);
             this.panelVolume.TabIndex = 2;
             // 
+            // volumeUpdateTimer
+            // 
+            this.volumeUpdateTimer.Interval = 250;
+            this.volumeUpdateTimer.Tick += new System.EventHandler(this.volumeUpdateTimer_Tick);
+            // 
             // TabSettingsSounds
             // 
             this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
@@ -201,5 +207,6 @@ private void InitializeComponent() {
         private System.Windows.Forms.TrackBar trackBarVolume;
         private System.Windows.Forms.FlowLayoutPanel flowPanel;
         private System.Windows.Forms.Panel panelVolume;
+        private System.Windows.Forms.Timer volumeUpdateTimer;
     }
 }
diff --git a/Core/Other/Settings/TabSettingsSounds.cs b/Core/Other/Settings/TabSettingsSounds.cs
index 8cb3c45b..759fd8ba 100644
--- a/Core/Other/Settings/TabSettingsSounds.cs
+++ b/Core/Other/Settings/TabSettingsSounds.cs
@@ -4,25 +4,18 @@
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Notification;
-using TweetLib.Audio;
 
 namespace TweetDuck.Core.Other.Settings{
     sealed partial class TabSettingsSounds : BaseTabSettings{
-        private readonly SoundNotification soundNotification;
-        private readonly bool supportsChangingVolume;
+        private readonly Action playSoundNotification;
 
-        public TabSettingsSounds(){
+        public TabSettingsSounds(Action playSoundNotification){
             InitializeComponent();
 
-            soundNotification = new SoundNotification();
-            soundNotification.PlaybackError += sound_PlaybackError;
-            Disposed += (sender, args) => soundNotification.Dispose();
+            this.playSoundNotification = playSoundNotification;
             
-            supportsChangingVolume = soundNotification.SetVolume(Config.NotificationSoundVolume);
-
             toolTip.SetToolTip(tbCustomSound, "When empty, the default TweetDeck sound notification is used.");
-
-            trackBarVolume.Enabled = supportsChangingVolume && !string.IsNullOrEmpty(Config.NotificationSoundPath);
+            
             trackBarVolume.SetValueSafe(Config.NotificationSoundVolume);
             labelVolumeValue.Text = trackBarVolume.Value+"%";
 
@@ -39,22 +32,29 @@ public override void OnReady(){
 
         public override void OnClosing(){
             Config.NotificationSoundPath = tbCustomSound.Text;
+            Config.NotificationSoundVolume = trackBarVolume.Value;
+        }
+
+        private bool RefreshCanPlay(){
+            bool isEmpty = string.IsNullOrEmpty(tbCustomSound.Text);
+            bool canPlay = isEmpty || File.Exists(tbCustomSound.Text);
+
+            tbCustomSound.ForeColor = canPlay ? SystemColors.WindowText : Color.Red;
+            btnPlaySound.Enabled = canPlay;
+            btnResetSound.Enabled = !isEmpty;
+            return canPlay;
         }
 
         private void tbCustomSound_TextChanged(object sender, EventArgs e){
-            bool isEmpty = string.IsNullOrEmpty(tbCustomSound.Text);
-            tbCustomSound.ForeColor = isEmpty || File.Exists(tbCustomSound.Text) ? SystemColors.WindowText : Color.Red;
-            btnPlaySound.Enabled = !isEmpty;
-            btnResetSound.Enabled = !isEmpty;
-            trackBarVolume.Enabled = supportsChangingVolume && !isEmpty;
+            RefreshCanPlay();
         }
 
         private void btnPlaySound_Click(object sender, EventArgs e){
-            soundNotification.Play(tbCustomSound.Text);
-        }
-
-        private void sound_PlaybackError(object sender, PlaybackErrorEventArgs e){
-            FormMessage.Error("Notification Sound Error", "Could not play custom notification sound.\n"+e.Message, FormMessage.OK);
+            if (RefreshCanPlay()){
+                Config.NotificationSoundPath = tbCustomSound.Text;
+                Config.NotificationSoundVolume = trackBarVolume.Value;
+                playSoundNotification();
+            }
         }
 
         private void btnBrowseSound_Click(object sender, EventArgs e){
@@ -62,7 +62,7 @@ private void btnBrowseSound_Click(object sender, EventArgs e){
                 AutoUpgradeEnabled = true,
                 DereferenceLinks = true,
                 Title = "Custom Notification Sound",
-                Filter = "Sound file ("+soundNotification.SupportedFormats+")|"+soundNotification.SupportedFormats+"|All files (*.*)|*.*"
+                Filter = $"Sound file ({SoundNotification.SupportedFormats})|{SoundNotification.SupportedFormats}|All files (*.*)|*.*"
             }){
                 if (dialog.ShowDialog() == DialogResult.OK){
                     tbCustomSound.Text = dialog.FileName;
@@ -75,9 +75,14 @@ private void btnResetSound_Click(object sender, EventArgs e){
         }
 
         private void trackBarVolume_ValueChanged(object sender, EventArgs e){
+            volumeUpdateTimer.Stop();
+            volumeUpdateTimer.Start();
+            labelVolumeValue.Text = trackBarVolume.Value+"%";
+        }
+
+        private void volumeUpdateTimer_Tick(object sender, EventArgs e){
             Config.NotificationSoundVolume = trackBarVolume.Value;
-            soundNotification.SetVolume(Config.NotificationSoundVolume);
-            labelVolumeValue.Text = Config.NotificationSoundVolume+"%";
+            volumeUpdateTimer.Stop();
         }
     }
 }
diff --git a/Core/TweetDeckBrowser.cs b/Core/TweetDeckBrowser.cs
index 0b0623a4..47fb7ab6 100644
--- a/Core/TweetDeckBrowser.cs
+++ b/Core/TweetDeckBrowser.cs
@@ -7,6 +7,7 @@
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Handling;
 using TweetDuck.Core.Handling.General;
+using TweetDuck.Core.Notification;
 using TweetDuck.Core.Utils;
 using TweetDuck.Plugins;
 using TweetDuck.Plugins.Enums;
@@ -36,6 +37,8 @@ public bool IsTweetDeckWebsite{
         private readonly ChromiumWebBrowser browser;
         private readonly PluginManager plugins;
 
+        private string prevSoundNotificationPath = null;
+
         public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge bridge){
             this.browser = new ChromiumWebBrowser(TwitterUtils.TweetDeckURL){
                 DialogHandler = new FileDialogHandler(),
@@ -70,6 +73,7 @@ public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridg
             
             Program.UserConfig.MuteToggled += UserConfig_MuteToggled;
             Program.UserConfig.ZoomLevelChanged += UserConfig_ZoomLevelChanged;
+            Program.UserConfig.SoundNotificationChanged += UserConfig_SoundNotificationInfoChanged;
         }
 
         // setup and management
@@ -91,6 +95,7 @@ public void Dispose(){
 
             Program.UserConfig.MuteToggled -= UserConfig_MuteToggled;
             Program.UserConfig.ZoomLevelChanged -= UserConfig_ZoomLevelChanged;
+            Program.UserConfig.SoundNotificationChanged -= UserConfig_SoundNotificationInfoChanged;
             
             browser.Dispose();
         }
@@ -129,6 +134,7 @@ private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
                 ScriptLoader.ExecuteFile(e.Frame, "code.js");
                 InjectBrowserCSS();
                 ReinjectCustomCSS(Program.UserConfig.CustomBrowserCSS);
+                UserConfig_SoundNotificationInfoChanged(null, EventArgs.Empty);
                 plugins.ExecutePlugins(e.Frame, PluginEnvironment.Browser);
 
                 TweetDeckBridge.ResetStaticProperties();
@@ -167,6 +173,27 @@ private void UserConfig_ZoomLevelChanged(object sender, EventArgs e){
             BrowserUtils.SetZoomLevel(browser.GetBrowser(), Program.UserConfig.ZoomLevel);
         }
 
+        private void UserConfig_SoundNotificationInfoChanged(object sender, EventArgs e){
+            const string soundUrl = "https://ton.twimg.com/tduck/updatesnd";
+            bool hasCustomSound = Program.UserConfig.IsCustomSoundNotificationSet;
+
+            if (prevSoundNotificationPath != Program.UserConfig.NotificationSoundPath){
+                DefaultResourceHandlerFactory handlerFactory = browser.GetHandlerFactory();
+                IResourceHandler resourceHandler = hasCustomSound ? SoundNotification.CreateFileHandler(Program.UserConfig.NotificationSoundPath) : null;
+            
+                if (resourceHandler != null){
+                    handlerFactory.RegisterHandler(soundUrl, resourceHandler);
+                }
+                else{
+                    handlerFactory.UnregisterHandler(soundUrl);
+                }
+
+                prevSoundNotificationPath = Program.UserConfig.NotificationSoundPath;
+            }
+
+            browser.ExecuteScriptAsync("TDGF_setSoundNotificationData", hasCustomSound, Program.UserConfig.NotificationSoundVolume);
+        }
+
         // external handling
 
         public UpdateHandler CreateUpdateHandler(UpdaterSettings settings){
@@ -215,6 +242,10 @@ public void ReloadColumns(){
             browser.ExecuteScriptAsync("TDGF_reloadColumns()");
         }
 
+        public void PlaySoundNotification(){
+            browser.ExecuteScriptAsync("TDGF_playSoundNotification()");
+        }
+
         public void ApplyROT13(){
             browser.ExecuteScriptAsync("TDGF_applyROT13()");
         }
diff --git a/Resources/Scripts/code.js b/Resources/Scripts/code.js
index d3526bbf..1d66c8c4 100644
--- a/Resources/Scripts/code.js
+++ b/Resources/Scripts/code.js
@@ -507,10 +507,35 @@
   //
   // Block: Hook into the notification sound effect.
   //
+  
   HTMLAudioElement.prototype.play = prependToFunction(HTMLAudioElement.prototype.play, function(){
-    return $TDX.muteNotifications || $TDX.hasCustomNotificationSound;
+    return $TDX.muteNotifications;
   });
   
+  window.TDGF_setSoundNotificationData = function(custom, volume){
+    let audio = document.getElementById("update-sound");
+    audio.volume = volume/100;
+    
+    const sourceId = "tduck-custom-sound-source";
+    let source = document.getElementById(sourceId);
+    
+    if (custom && !source){
+      source = document.createElement("source");
+      source.id = sourceId;
+      source.src = "https://ton.twimg.com/tduck/updatesnd";
+      audio.prepend(source);
+    }
+    else if (!custom && source){
+      audio.removeChild(source);
+    }
+    
+    audio.load();
+  };
+  
+  window.TDGF_playSoundNotification = function(){
+    document.getElementById("update-sound").play();
+  };
+  
   //
   // Block: Update highlighted column and tweet for context menu and other functionality.
   //
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 23423925..038ed7f3 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -378,10 +378,6 @@
     <Content Include="Resources\Scripts\update.js" />
   </ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="lib\TweetLib.Audio\TweetLib.Audio.csproj">
-      <Project>{E9E1FD1B-F480-45B7-9970-BE2ECFD309AC}</Project>
-      <Name>TweetLib.Audio</Name>
-    </ProjectReference>
     <ProjectReference Include="subprocess\TweetDuck.Browser.csproj">
       <Project>{b10b0017-819e-4f71-870f-8256b36a26aa}</Project>
       <Name>TweetDuck.Browser</Name>