OSDN Git Service

BurageSnapの起動が阻まれたときにエラーになるのを直す
[kancollesniffer/KancolleSniffer.git] / KancolleSniffer / MainForm.cs
index c7d7a8c..ec4df4c 100644 (file)
 \r
 using System;\r
 using System.Collections.Generic;\r
+using System.ComponentModel;\r
 using System.Diagnostics;\r
 using System.Drawing;\r
 using System.Globalization;\r
 using System.IO;\r
 using System.Linq;\r
-using System.Net.Sockets;\r
+using System.Net;\r
 using System.Runtime.InteropServices;\r
 using System.Text;\r
 using System.Text.RegularExpressions;\r
 using System.Threading.Tasks;\r
 using System.Windows.Forms;\r
 using Microsoft.CSharp.RuntimeBinder;\r
-using Microsoft.Win32;\r
 using static System.Math;\r
-using Timer = System.Windows.Forms.Timer;\r
 \r
 namespace KancolleSniffer\r
 {\r
@@ -37,21 +36,25 @@ namespace KancolleSniffer
         private readonly Sniffer _sniffer = new Sniffer();\r
         private readonly Config _config = new Config();\r
         private readonly ConfigDialog _configDialog;\r
+        private readonly ProxyManager _proxyManager;\r
+        private readonly ResizableToolTip _toolTip = new ResizableToolTip {ShowAlways = true};\r
+        private readonly ResizableToolTip _toolTipQuest = new ResizableToolTip {ShowAlways = true, AutoPopDelay = 10000};\r
+        private readonly ResizableToolTip _tooltipCopy = new ResizableToolTip {AutomaticDelay = 0};\r
         private int _currentFleet;\r
         private bool _combinedFleet;\r
         private readonly Label[] _labelCheckFleets;\r
-        private readonly ShipLabels _shipLabels;\r
+        private readonly MainFormLabels _mainLabels;\r
         private readonly ListForm _listForm;\r
-        private readonly NoticeQueue _noticeQueue;\r
+        private readonly NotificationManager _notificationManager;\r
         private bool _started;\r
+        private bool _timerEnabled;\r
         private string _debugLogFile;\r
         private IEnumerator<string> _playLog;\r
-        private int _prevProxyPort;\r
-        private readonly SystemProxy _systemProxy = new SystemProxy();\r
+        private DateTime _prev, _now;\r
+        private bool _inSortie;\r
+\r
         private readonly ErrorDialog _errorDialog = new ErrorDialog();\r
-        private bool _missionFinishTimeMode;\r
-        private bool _ndockFinishTimeMode;\r
-        private readonly KancolleDb _kancolleDb = new KancolleDb();\r
+        private readonly ErrorLog _errorLog;\r
 \r
         public MainForm()\r
         {\r
@@ -66,23 +69,111 @@ namespace KancolleSniffer
                 CurrentAutoScaleDimensions.Height / 12f);\r
 \r
             SetupFleetClick();\r
-            _shipLabels = new ShipLabels();\r
-            _shipLabels.CreateAkashiTimers(panelShipInfo);\r
-            _shipLabels.CreateShipLabels(panelShipInfo, ShowShipOnShipList);\r
-            _shipLabels.CreateCombinedShipLabels(panelCombinedFleet, ShowShipOnShipList);\r
-            _shipLabels.CreateRepairList(panelRepairList, panelRepairList_Click);\r
-            _shipLabels.CreateNDockLabels(panelDock, labelNDock_Click);\r
-            labelPresetAkashiTimer.BackColor = ShipLabels.ColumnColors[1];\r
+            _mainLabels = new MainFormLabels();\r
+            _mainLabels.CreateAkashiTimers(panelShipInfo);\r
+            _mainLabels.CreateShipLabels(panelShipInfo, ShowShipOnShipList);\r
+            _mainLabels.CreateAkashiTimers7(panel7Ships);\r
+            _mainLabels.CreateShipLabels7(panel7Ships, ShowShipOnShipList);\r
+            _mainLabels.CreateCombinedShipLabels(panelCombinedFleet, ShowShipOnShipList);\r
+            _mainLabels.CreateNDockLabels(panelDock, labelNDock_Click);\r
+            panelRepairList.CreateLabels(panelRepairList_Click);\r
+            labelPresetAkashiTimer.BackColor = ShipLabel.ColumnColors[1];\r
             _listForm = new ListForm(_sniffer, _config) {Owner = this};\r
-            _noticeQueue = new NoticeQueue(Ring);\r
+            _notificationManager = new NotificationManager(Alarm);\r
             _config.Load();\r
+            _proxyManager = new ProxyManager(_config, this);\r
+            _errorLog = new ErrorLog(_sniffer);\r
+            _proxyManager.UpdatePacFile();\r
             PerformZoom();\r
+            _mainLabels.AdjustAkashiTimers();\r
+            LoadData();\r
+            _sniffer.RepeatingTimerController = new RepeatingTimerController(_notificationManager, _config);\r
+        }\r
+\r
+        /// <summary>\r
+        /// パネルのz-orderがくるうのを避ける\r
+        /// https://stackoverflow.com/a/5777090/1429506\r
+        /// </summary>\r
+        private void MainForm_Shown(object sender, EventArgs e)\r
+        {\r
+            // ReSharper disable once NotAccessedVariable\r
+            IntPtr handle;\r
+            foreach (var panel in new[] {panelShipInfo, panel7Ships, panelCombinedFleet})\r
+                // ReSharper disable once RedundantAssignment\r
+                handle = panel.Handle;\r
+        }\r
+\r
+        private readonly FileSystemWatcher _watcher = new FileSystemWatcher\r
+        {\r
+            Path = AppDomain.CurrentDomain.BaseDirectory,\r
+            NotifyFilter = NotifyFilters.LastWrite\r
+        };\r
+\r
+        private readonly Timer _watcherTimer = new Timer {Interval = 1000};\r
+\r
+        private void LoadData()\r
+        {\r
+            var target = "";\r
             _sniffer.LoadState();\r
+            DataLoader.LoadTpSpec();\r
+            _watcher.SynchronizingObject = this;\r
+            _watcherTimer.Tick += (sender, ev) =>\r
+            {\r
+                _watcherTimer.Stop();\r
+                switch (target)\r
+                {\r
+                    case "status.xml":\r
+                        _sniffer.LoadState();\r
+                        break;\r
+                    case "TP.csv":\r
+                        DataLoader.LoadTpSpec();\r
+                        break;\r
+                }\r
+            };\r
+            _watcher.Changed += (sender, ev) =>\r
+            {\r
+                target = ev.Name;\r
+                _watcherTimer.Stop();\r
+                _watcherTimer.Start();\r
+            };\r
+            _watcher.EnableRaisingEvents = true;\r
+        }\r
+\r
+        private class RepeatingTimerController : Sniffer.IRepeatingTimerController\r
+        {\r
+            private readonly NotificationManager _manager;\r
+            private readonly Config _config;\r
+\r
+            public RepeatingTimerController(NotificationManager manager, Config config)\r
+            {\r
+                _manager = manager;\r
+                _config = config;\r
+            }\r
+\r
+            public void Stop(string key)\r
+            {\r
+                _manager.StopRepeat(key,\r
+                    (key == "入渠終了" || key == "遠征終了") &&\r
+                    (_config.Notifications[key].Flags & NotificationType.Cont) != 0);\r
+            }\r
+\r
+            public void Stop(string key, int fleet) => _manager.StopRepeat(key, fleet);\r
+\r
+            public void Suspend() => _manager.SuspendRepeat();\r
+\r
+            public void Resume() => _manager.ResumeRepeat();\r
+        }\r
+\r
+        public class ConfigFileException : Exception\r
+        {\r
+            public ConfigFileException(string message, Exception innerException) : base(message, innerException)\r
+            {\r
+            }\r
         }\r
 \r
         private void HttpProxy_AfterSessionComplete(HttpProxy.Session session)\r
         {\r
-            Invoke(new Action<HttpProxy.Session>(ProcessRequest), session);\r
+            BeginInvoke(new Action<HttpProxy.Session>(ProcessRequest), session);\r
         }\r
 \r
         private void ProcessRequest(HttpProxy.Session session)\r
@@ -97,38 +188,58 @@ namespace KancolleSniffer
                 WriteDebugLog(url, request, response);\r
                 return;\r
             }\r
-            if (_config.KancolleDb.On)\r
-                _kancolleDb.Send(url, request, response);\r
             response = UnescapeString(response.Remove(0, "svdata=".Length));\r
             WriteDebugLog(url, request, response);\r
+            ProcessRequestMain(url, request, response);\r
+        }\r
+\r
+        private void ProcessRequestMain(string url, string request, string response)\r
+        {\r
             try\r
             {\r
                 UpdateInfo(_sniffer.Sniff(url, request, JsonParser.Parse(response)));\r
+                _errorLog.CheckBattleApi(url, request, response);\r
             }\r
+\r
             catch (RuntimeBinderException e)\r
             {\r
                 if (_errorDialog.ShowDialog(this,\r
-                    "このバージョンは現在の艦これに対応していません。\r\n新しいバージョンを利用してください。", e.ToString()) == DialogResult.Abort)\r
-                    Application.Exit();\r
+                        "艦これに仕様変更があったか、受信内容が壊れています。",\r
+                        _errorLog.GenerateErrorLog(url, request, response, e.ToString())) == DialogResult.Abort)\r
+                    Exit();\r
             }\r
             catch (LogIOException e)\r
             {\r
+                // ReSharper disable once PossibleNullReferenceException\r
                 if (_errorDialog.ShowDialog(this, e.Message, e.InnerException.ToString()) == DialogResult.Abort)\r
-                    Application.Exit();\r
+                    Exit();\r
+            }\r
+            catch (BattleResultError)\r
+            {\r
+                if (_errorDialog.ShowDialog(this, "戦闘結果の計算に誤りがあります。",\r
+                        _errorLog.GenerateBattleErrorLog()) == DialogResult.Abort)\r
+                    Exit();\r
             }\r
             catch (Exception e)\r
             {\r
-                if (_errorDialog.ShowDialog(this, "エラーが発生しました。", e.ToString()) == DialogResult.Abort)\r
-                    Application.Exit();\r
+                if (_errorDialog.ShowDialog(this, "エラーが発生しました。",\r
+                        _errorLog.GenerateErrorLog(url, request, response, e.ToString())) == DialogResult.Abort)\r
+                    Exit();\r
             }\r
         }\r
 \r
+        private void Exit()\r
+        {\r
+            _proxyManager.Shutdown();\r
+            Environment.Exit(1);\r
+        }\r
+\r
         private void WriteDebugLog(string url, string request, string response)\r
         {\r
             if (_debugLogFile != null)\r
             {\r
                 File.AppendAllText(_debugLogFile,\r
-                    $"url: {url}\nrequest: {request}\nresponse: {response ?? "(null)"}\n");\r
+                    $"date: {DateTime.Now:g}\nurl: {url}\nrequest: {request}\nresponse: {response ?? "(null)"}\n");\r
             }\r
         }\r
 \r
@@ -151,12 +262,14 @@ namespace KancolleSniffer
             if (update == Sniffer.Update.Start)\r
             {\r
                 labelLogin.Visible = false;\r
-                labelGuide.Visible = false;\r
+                linkLabelGuide.Visible = false;\r
                 _started = true;\r
                 return;\r
             }\r
             if (!_started)\r
                 return;\r
+            if (_now == DateTime.MinValue)\r
+                _now = DateTime.Now;\r
             if ((update & Sniffer.Update.Item) != 0)\r
                 UpdateItemInfo();\r
             if ((update & Sniffer.Update.Timer) != 0)\r
@@ -178,37 +291,68 @@ namespace KancolleSniffer
             RestoreLocation();\r
             if (_config.HideOnMinimized && WindowState == FormWindowState.Minimized)\r
                 ShowInTaskbar = false;\r
+            if (_config.ShowHpInPercent)\r
+                _mainLabels.ToggleHpPercent();\r
+            if (_config.ShipList.Visible)\r
+                _listForm.Show();\r
             ApplyConfig();\r
             ApplyDebugLogSetting();\r
             ApplyLogSetting();\r
             ApplyProxySetting();\r
-            SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;\r
-            if (_config.KancolleDb.On)\r
-                _kancolleDb.Start(_config.KancolleDb.Token);\r
+            CheckVersionUp((current, latest) =>\r
+            {\r
+                if (double.Parse(latest) <= double.Parse(current))\r
+                    return;\r
+                linkLabelGuide.Text = $"バージョン{latest}があります。";\r
+                linkLabelGuide.LinkArea = new LinkArea(0, linkLabelGuide.Text.Length);\r
+                linkLabelGuide.Click += (obj, ev) =>\r
+                {\r
+                    Process.Start("https://ja.osdn.net/rel/kancollesniffer/" + latest);\r
+                };\r
+            });\r
         }\r
 \r
-        private void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)\r
+        public async void CheckVersionUp(Action<string, string> action)\r
         {\r
-            if (e.Mode != PowerModes.Resume || !_config.Proxy.Auto)\r
-                return;\r
-            SystemProxy.Refresh();\r
+            var current = string.Join(".", Application.ProductVersion.Split('.').Take(2));\r
+            try\r
+            {\r
+                var latest = (await new WebClient().DownloadStringTaskAsync("http://kancollesniffer.osdn.jp/version"))\r
+                    .TrimEnd();\r
+                try\r
+                {\r
+                    action(current, latest);\r
+                }\r
+                catch (InvalidOperationException)\r
+                {\r
+                }\r
+            }\r
+            catch (WebException)\r
+            {\r
+            }\r
         }\r
 \r
         private void MainForm_FormClosing(object sender, FormClosingEventArgs e)\r
         {\r
+            if (!_config.ExitSilently)\r
+            {\r
+                using (var dialog = new ConfirmDialog())\r
+                {\r
+                    if (dialog.ShowDialog(this) != DialogResult.Yes)\r
+                    {\r
+                        e.Cancel = true;\r
+                        return;\r
+                    }\r
+                }\r
+            }\r
             e.Cancel = false;\r
+            _sniffer.FlashLog();\r
             _config.Location = (WindowState == FormWindowState.Normal ? Bounds : RestoreBounds).Location;\r
+            _config.ShowHpInPercent = _mainLabels.ShowHpInPercent;\r
+            _config.ShipList.Visible = _listForm.Visible && _listForm.WindowState == FormWindowState.Normal;\r
             _config.Save();\r
-            Task.Run(() => ShutdownProxy());\r
-            if (_config.Proxy.Auto)\r
-                _systemProxy.RestoreSettings();\r
-            SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;\r
-            _kancolleDb.Stop();\r
-        }\r
-\r
-        private void ShutdownProxy()\r
-        {\r
-            HttpProxy.Shutdown();\r
+            _sniffer.SaveState();\r
+            _proxyManager.Shutdown();\r
         }\r
 \r
         private void MainForm_Resize(object sender, EventArgs e)\r
@@ -237,7 +381,17 @@ namespace KancolleSniffer
         private void ConfigToolStripMenuItem_Click(object sender, EventArgs e)\r
         {\r
             if (_configDialog.ShowDialog(this) == DialogResult.OK)\r
+            {\r
+                _config.Save();\r
                 ApplyConfig();\r
+                StopRepeatingTimer(_configDialog.RepeatSettingsChanged);\r
+            }\r
+        }\r
+\r
+        private void StopRepeatingTimer(IEnumerable<string> names)\r
+        {\r
+            foreach (var name in names)\r
+                _notificationManager.StopRepeat(name);\r
         }\r
 \r
         private void PerformZoom()\r
@@ -245,8 +399,19 @@ namespace KancolleSniffer
             if (_config.Zoom == 100)\r
                 return;\r
             var prev = CurrentAutoScaleDimensions;\r
-            foreach (var control in new Control[] {this, _listForm, labelLogin, labelGuide})\r
+            foreach (var control in new Control[]\r
+            {\r
+                this, _listForm, labelLogin, linkLabelGuide,\r
+                _configDialog, _configDialog.NotificationConfigDialog,\r
+                contextMenuStripMain, _errorDialog\r
+            })\r
+            {\r
                 control.Font = new Font(control.Font.FontFamily, control.Font.Size * _config.Zoom / 100);\r
+            }\r
+            foreach (var toolTip in new[]{_toolTip, _toolTipQuest, _tooltipCopy})\r
+            {\r
+                toolTip.Font = new Font(toolTip.Font.FontFamily, toolTip.Font.Size * _config.Zoom / 100);\r
+            }\r
             ShipLabel.LatinFont = new Font("Tahoma", 8f * _config.Zoom / 100);\r
             var cur = CurrentAutoScaleDimensions;\r
             ShipLabel.ScaleFactor = new SizeF(ShipLabel.ScaleFactor.Width * cur.Width / prev.Width,\r
@@ -257,9 +422,7 @@ namespace KancolleSniffer
         {\r
             if (_config.Location.X == int.MinValue)\r
                 return;\r
-            var newBounds = Bounds;\r
-            newBounds.Location = _config.Location;\r
-            if (IsVisibleOnAnyScreen(newBounds))\r
+            if (IsTitleBarOnAnyScreen(_config.Location))\r
                 Location = _config.Location;\r
         }\r
 \r
@@ -267,12 +430,12 @@ namespace KancolleSniffer
         {\r
             _listForm.TopMost = TopMost = _config.TopMost;\r
             _sniffer.Item.MarginShips = _config.MarginShips;\r
+            UpdateNumOfShips();\r
             _sniffer.Item.MarginEquips = _config.MarginEquips;\r
+            UpdateNumOfEquips();\r
             _sniffer.Achievement.ResetHours = _config.ResetHours;\r
             labelAkashiRepair.Visible = labelAkashiRepairTimer.Visible =\r
                 labelPresetAkashiTimer.Visible = _config.UsePresetAkashi;\r
-            if (_config.KancolleDb.On)\r
-                _kancolleDb.Start(_config.KancolleDb.Token);\r
         }\r
 \r
         public void ApplyDebugLogSetting()\r
@@ -282,86 +445,41 @@ namespace KancolleSniffer
 \r
         public bool ApplyProxySetting()\r
         {\r
-            if (!_config.Proxy.Auto)\r
-                _systemProxy.RestoreSettings();\r
-            if (_config.Proxy.UseUpstream)\r
-            {\r
-                HttpProxy.UpstreamProxyHost = "127.0.0.1";\r
-                HttpProxy.UpstreamProxyPort = _config.Proxy.UpstreamPort;\r
-            }\r
-            HttpProxy.IsEnableUpstreamProxy = _config.Proxy.UseUpstream;\r
-            var result = true;\r
-            if (!HttpProxy.IsInListening || _config.Proxy.Listen != _prevProxyPort)\r
-            {\r
-                ShutdownProxy();\r
-                result = StartProxy();\r
-            }\r
-            if (_config.Proxy.Auto && result)\r
-            {\r
-                _systemProxy.SetAutoProxyUrl($"http://localhost:{_config.Proxy.Listen}/proxy.pac");\r
-            }\r
-            _prevProxyPort = _config.Proxy.Listen;\r
-            return result;\r
-        }\r
-\r
-        private bool StartProxy()\r
-        {\r
-            try\r
-            {\r
-                HttpProxy.Startup(_config.Proxy.Listen, false, false);\r
-            }\r
-            catch (SocketException e)\r
-            {\r
-                if (e.SocketErrorCode != SocketError.AddressAlreadyInUse)\r
-                    throw;\r
-                if (WarnConflictPortNumber("プロキシサーバー", _config.Proxy.Listen, _config.Proxy.Auto) == DialogResult.No ||\r
-                    !_config.Proxy.Auto)\r
-                {\r
-                    _systemProxy.RestoreSettings();\r
-                    return false;\r
-                }\r
-                HttpProxy.Startup(0, false, false);\r
-                _config.Proxy.Listen = HttpProxy.LocalPort;\r
-            }\r
-            return true;\r
-        }\r
-\r
-        private DialogResult WarnConflictPortNumber(string name, int port, bool auto)\r
-        {\r
-            var msg = $"{name}のポート番号{port}は他のアプリケーションが使用中です。";\r
-            var cap = "ポート番号の衝突";\r
-            return auto\r
-                ? MessageBox.Show(this, msg + "自動的に別の番号を割り当てますか?", cap,\r
-                    MessageBoxButtons.YesNo, MessageBoxIcon.Question)\r
-                : MessageBox.Show(this, msg + "設定ダイアログでポート番号を変更してください。", cap,\r
-                    MessageBoxButtons.OK, MessageBoxIcon.Exclamation);\r
+            return _proxyManager.ApplyConfig();\r
         }\r
 \r
         public void ApplyLogSetting()\r
         {\r
             LogServer.OutputDir = _config.Log.OutputDir;\r
+            LogServer.MaterialHistory = _sniffer.Material.MaterialHistory;\r
             _sniffer.EnableLog(_config.Log.On ? LogType.All : LogType.None);\r
             _sniffer.MaterialLogInterval = _config.Log.MaterialLogInterval;\r
             _sniffer.LogOutputDir = _config.Log.OutputDir;\r
         }\r
 \r
-        public static bool IsVisibleOnAnyScreen(Rectangle rect)\r
+        public static bool IsTitleBarOnAnyScreen(Point location)\r
         {\r
-            return Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(rect));\r
+            var rect = new Rectangle(\r
+                new Point(location.X + SystemInformation.IconSize.Width + SystemInformation.HorizontalFocusThickness,\r
+                    location.Y + SystemInformation.CaptionHeight), new Size(60, 1));\r
+            return Screen.AllScreens.Any(screen => screen.WorkingArea.Contains(rect));\r
         }\r
 \r
         private void timerMain_Tick(object sender, EventArgs e)\r
         {\r
-            if (_started)\r
+            if (_timerEnabled)\r
             {\r
                 try\r
                 {\r
+                    _now = DateTime.Now;\r
                     UpdateTimers();\r
+                    NotifyTimers();\r
+                    _prev = _now;\r
                 }\r
                 catch (Exception ex)\r
                 {\r
                     if (_errorDialog.ShowDialog(this, "エラーが発生しました。", ex.ToString()) == DialogResult.Abort)\r
-                        Application.Exit();\r
+                        Exit();\r
                 }\r
             }\r
             if (_playLog == null || _configDialog.Visible)\r
@@ -382,16 +500,18 @@ namespace KancolleSniffer
             var lines = new List<string>();\r
             foreach (var s in new[] {"url: ", "request: ", "response: "})\r
             {\r
-                if (!_playLog.MoveNext() || !_playLog.Current.StartsWith(s))\r
+                do\r
                 {\r
-                    labelPlayLog.Visible = false;\r
-                    return;\r
-                }\r
+                    if (!_playLog.MoveNext() || _playLog.Current == null)\r
+                    {\r
+                        labelPlayLog.Visible = false;\r
+                        return;\r
+                    }\r
+                } while (!_playLog.Current.StartsWith(s));\r
                 lines.Add(_playLog.Current.Substring(s.Length));\r
             }\r
             labelPlayLog.Visible = !labelPlayLog.Visible;\r
-            var json = JsonParser.Parse(lines[2]);\r
-            UpdateInfo(_sniffer.Sniff(lines[0], lines[1], json));\r
+            ProcessRequestMain(lines[0], lines[1], lines[2]);\r
         }\r
 \r
         private void ShowShipOnShipList(object sender, EventArgs ev)\r
@@ -409,13 +529,14 @@ namespace KancolleSniffer
         {\r
             UpdateNumOfShips();\r
             UpdateNumOfEquips();\r
+            _notificationManager.Flash();\r
             labelNumOfBuckets.Text = _sniffer.Material.MaterialHistory[(int)Material.Bucket].Now.ToString("D");\r
             UpdateBucketHistory();\r
             var ac = _sniffer.Achievement.Value;\r
             if (ac >= 10000)\r
                 ac = 9999;\r
             labelAchievement.Text = ac >= 1000 ? ((int)ac).ToString("D") : ac.ToString("F1");\r
-            toolTipAchievement.SetToolTip(labelAchievement,\r
+            _toolTip.SetToolTip(labelAchievement,\r
                 "今月 " + _sniffer.Achievement.ValueOfMonth.ToString("F1") + "\n" +\r
                 "EO " + _sniffer.ExMap.Achievement);\r
             UpdateMaterialHistry();\r
@@ -427,12 +548,12 @@ namespace KancolleSniffer
         {\r
             var item = _sniffer.Item;\r
             labelNumOfShips.Text = $"{item.NowShips:D}/{item.MaxShips:D}";\r
-            labelNumOfShips.ForeColor = item.TooManyShips ? Color.Red : Color.Black;\r
-            if (item.RingShips)\r
+            labelNumOfShips.ForeColor = item.TooManyShips ? CUDColor.Red : Color.Black;\r
+            if (item.AlarmShips)\r
             {\r
                 var message = $"残り{_sniffer.Item.MaxShips - _sniffer.Item.NowShips:D}隻";\r
-                _noticeQueue.Enqueue("艦娘が多すぎます", message, _config.Sounds["艦娘数超過"]);\r
-                item.RingShips = false;\r
+                _notificationManager.Enqueue("艦娘数超過", message);\r
+                item.AlarmShips = false;\r
             }\r
         }\r
 \r
@@ -440,12 +561,12 @@ namespace KancolleSniffer
         {\r
             var item = _sniffer.Item;\r
             labelNumOfEquips.Text = $"{item.NowEquips:D}/{item.MaxEquips:D}";\r
-            labelNumOfEquips.ForeColor = item.TooManyEquips ? Color.Red : Color.Black;\r
-            if (item.RingEquips)\r
+            labelNumOfEquips.ForeColor = item.TooManyEquips ? CUDColor.Red : Color.Black;\r
+            if (item.AlarmEquips)\r
             {\r
                 var message = $"残り{_sniffer.Item.MaxEquips - _sniffer.Item.NowEquips:D}個";\r
-                _noticeQueue.Enqueue("装備が多すぎます", message, _config.Sounds["装備数超過"]);\r
-                item.RingEquips = false;\r
+                _notificationManager.Enqueue("装備数超過", message);\r
+                item.AlarmEquips = false;\r
             }\r
         }\r
 \r
@@ -482,6 +603,7 @@ namespace KancolleSniffer
 \r
         private void UpdateShipInfo()\r
         {\r
+            SetCurrentFleet();\r
             UpdatePanelShipInfo();\r
             NotifyDamagedShip();\r
             UpdateChargeInfo();\r
@@ -490,90 +612,126 @@ namespace KancolleSniffer
                 _listForm.UpdateList();\r
         }\r
 \r
+        private void SetCurrentFleet()\r
+        {\r
+            var inSortie = _sniffer.InSortie;\r
+            if (_inSortie || !inSortie.Any(x => x))\r
+            {\r
+                _inSortie = inSortie.Any(x => x);\r
+                return;\r
+            }\r
+            _inSortie = true;\r
+            if (inSortie[0] && inSortie[1])\r
+            {\r
+                _combinedFleet = true;\r
+                _currentFleet = 0;\r
+            }\r
+            else\r
+            {\r
+                _combinedFleet = false;\r
+                _currentFleet = Array.FindIndex(inSortie, x => x);\r
+            }\r
+        }\r
+\r
         private void UpdatePanelShipInfo()\r
         {\r
             var statuses = _sniffer.GetShipStatuses(_currentFleet);\r
-            _shipLabels.SetShipLabels(statuses);\r
+            panel7Ships.Visible = statuses.Length == 7;\r
+            _mainLabels.SetShipLabels(statuses);\r
             if (_sniffer.CombinedFleetType == 0)\r
                 _combinedFleet = false;\r
             labelFleet1.Text = _combinedFleet ? "連合" : "第一";\r
             panelCombinedFleet.Visible = _combinedFleet;\r
             if (_combinedFleet)\r
-                _shipLabels.SetCombinedShipLabels(_sniffer.GetShipStatuses(0), _sniffer.GetShipStatuses(1));\r
+                _mainLabels.SetCombinedShipLabels(_sniffer.GetShipStatuses(0), _sniffer.GetShipStatuses(1));\r
+            for (var i = 0; i < _labelCheckFleets.Length; i++)\r
+                _labelCheckFleets[i].Visible = _currentFleet == i;\r
             UpdateAkashiTimer();\r
-            UpdateFighterPower();\r
+            var battle = _sniffer.Battle;\r
+            UpdateFighterPower(_combinedFleet && (battle.BattleState == BattleState.None || battle.EnemyIsCombined));\r
             UpdateLoS();\r
             UpdateCondTimers();\r
         }\r
 \r
         private void NotifyDamagedShip()\r
         {\r
-            if (_sniffer.BadlyDamagedShips.Any())\r
-                _noticeQueue.Enqueue("大破した艦娘がいます", string.Join(" ", _sniffer.BadlyDamagedShips),\r
-                    _config.Sounds["大破警告"]);\r
-        }\r
-\r
-        private void NotifyAkashiTimer()\r
-        {\r
-            var akashi = _sniffer.AkashiTimer;\r
-            var msgs = akashi.GetNotice();\r
-            if (msgs.Length == 0)\r
+            if (!_sniffer.BadlyDamagedShips.Any())\r
                 return;\r
-            if (!akashi.CheckReparing() && !(akashi.CheckPresetReparing() && _config.UsePresetAkashi))\r
-                return;\r
-            if (msgs[0].Proceeded == "20分経過しました。")\r
-            {\r
-                _noticeQueue.Enqueue("泊地修理", msgs[0].Proceeded, _config.Sounds["泊地修理20分経過"]);\r
-                msgs[0].Proceeded = "";\r
-                // 修理完了がいるかもしれないので続ける\r
-            }\r
-            var fn = new[] {"第一艦隊", "第二艦隊", "第三艦隊", "第四艦隊"};\r
-            for (var i = 0; i < fn.Length; i++)\r
-            {\r
-                if (msgs[i].Proceeded != "")\r
-                    _noticeQueue.Enqueue("泊地修理 " + fn[i], "修理進行:" + msgs[i].Proceeded, _config.Sounds["泊地修理進行"]);\r
-                if (msgs[i].Completed != "")\r
-                    _noticeQueue.Enqueue("泊地修理 " + fn[i], "修理完了:" + msgs[i].Completed, _config.Sounds["泊地修理完了"]);\r
-            }\r
+            _notificationManager.Enqueue("大破警告", string.Join(" ", _sniffer.BadlyDamagedShips));\r
+            _notificationManager.Flash();\r
         }\r
 \r
-        public void UpdateFighterPower()\r
+        public void UpdateFighterPower(bool combined)\r
         {\r
-            var fp = _sniffer.GetFighterPower(_currentFleet);\r
+            var fp = combined\r
+                ? _sniffer.GetFighterPower(0).Zip(_sniffer.GetFighterPower(1), (a, b) => a + b).ToArray()\r
+                : _sniffer.GetFighterPower(_currentFleet);\r
             labelFighterPower.Text = fp[0].ToString("D");\r
-            var cr = _sniffer.GetContactTriggerRate(_currentFleet) * 100;\r
+            var cr = combined\r
+                ? _sniffer.GetContactTriggerRate(0) + _sniffer.GetContactTriggerRate(1)\r
+                : _sniffer.GetContactTriggerRate(_currentFleet);\r
             var text = "制空: " + (fp[0] == fp[1] ? $"{fp[0]}" : $"{fp[0]}~{fp[1]}") +\r
-                       $" 触接: {cr:f1}";\r
-            toolTipFighterPower.SetToolTip(labelFighterPower, text);\r
-            toolTipFighterPower.SetToolTip(labelFighterPowerCaption, text);\r
+                       $" 触接: {cr * 100:f1}";\r
+            _toolTip.SetToolTip(labelFighterPower, text);\r
+            _toolTip.SetToolTip(labelFighterPowerCaption, text);\r
         }\r
 \r
         private void UpdateLoS()\r
         {\r
-            labelLoS.Text = (Floor(_sniffer.GetFleetLineOfSights(_currentFleet) * 10) / 10.0).ToString("F1");\r
+            labelLoS.Text = RoundDown(_sniffer.GetFleetLineOfSights(_currentFleet, 1)).ToString("F1");\r
+            var text = $"係数3: {RoundDown(_sniffer.GetFleetLineOfSights(_currentFleet, 3)):F1}\r\n" +\r
+                       $"係数4: {RoundDown(_sniffer.GetFleetLineOfSights(_currentFleet, 4)):F1}";\r
+            _toolTip.SetToolTip(labelLoS, text);\r
+            _toolTip.SetToolTip(labelLoSCaption, text);\r
+        }\r
+\r
+        private double RoundDown(double number)\r
+        {\r
+            return Floor(number * 10) / 10.0;\r
         }\r
 \r
         private void UpdateBattleInfo()\r
         {\r
-            labelFormation.Text = "";\r
-            labelEnemyFighterPower.Text = "";\r
-            labelFighterPower.ForeColor = DefaultForeColor;\r
-            labelResultRank.Text = "判定";\r
-            panelBattleInfo.Visible = _sniffer.Battle.BattleState != BattleState.None;\r
+            ResetBattleInfo();\r
+            _listForm.UpdateBattleResult();\r
             if (_sniffer.Battle.BattleState == BattleState.None)\r
                 return;\r
             panelBattleInfo.BringToFront();\r
             var battle = _sniffer.Battle;\r
-            var color = new[] {DefaultForeColor, DefaultForeColor, Color.Blue, Color.Green, Color.Orange, Color.Red};\r
-            labelFormation.Text = battle.Formation;\r
-            labelEnemyFighterPower.Text = battle.EnemyFighterPower;\r
-            labelFighterPower.ForeColor = color[battle.AirControlLevel + 1];\r
-            if (_config.AlwaysShowResultRank)\r
+            labelFormation.Text = new[] {"同航戦", "反航戦", "T字有利", "T字不利"}[battle.Formation[2] - 1];\r
+            UpdateBattleFighterPower();\r
+            if ((_config.Spoilers & Spoiler.ResultRank) != 0)\r
                 ShowResultRank();\r
             if (_sniffer.Battle.BattleState == BattleState.Day)\r
                 _listForm.UpdateAirBattleResult();\r
         }\r
 \r
+        private void ResetBattleInfo()\r
+        {\r
+            labelFormation.Text = "";\r
+            labelEnemyFighterPower.Text = "";\r
+            labelFighterPower.ForeColor = DefaultForeColor;\r
+            labelResultRank.Text = "判定";\r
+            panelBattleInfo.Visible = _sniffer.Battle.BattleState != BattleState.None;\r
+        }\r
+\r
+        private void UpdateBattleFighterPower()\r
+        {\r
+            var battle = _sniffer.Battle;\r
+            var power = battle.EnemyFighterPower;\r
+            labelEnemyFighterPower.Text = power.AirCombat + power.UnknownMark;\r
+            if (power.AirCombat != power.Interception)\r
+            {\r
+                var text = "防空: " + power.Interception + power.UnknownMark;\r
+                _toolTip.SetToolTip(labelEnemyFighterPower, text);\r
+                _toolTip.SetToolTip(labelEnemyFighterPowerCaption, text);\r
+            }\r
+            UpdateFighterPower(_sniffer.CombinedFleetType > 0 && battle.EnemyIsCombined);\r
+            labelFighterPower.ForeColor = new[]\r
+                {DefaultForeColor, DefaultForeColor, CUDColor.Blue, CUDColor.Green, CUDColor.Orange, CUDColor.Red}[\r
+                battle.AirControlLevel + 1];\r
+        }\r
+\r
         private void ShowResultRank()\r
         {\r
             var result = new[] {"完全S", "勝利S", "勝利A", "勝利B", "敗北C", "敗北D", "敗北E"};\r
@@ -600,13 +758,13 @@ namespace KancolleSniffer
 \r
         private void UpdateNDocLabels()\r
         {\r
-            _shipLabels.SetNDockLabels(_sniffer.NDock);\r
+            _mainLabels.SetNDockLabels(_sniffer.NDock);\r
         }\r
 \r
 \r
         private void labelNDock_Click(object sender, EventArgs e)\r
         {\r
-            _ndockFinishTimeMode = !_ndockFinishTimeMode;\r
+            _config.ShowEndTime ^= TimerKind.NDock;\r
             UpdateTimers();\r
         }\r
 \r
@@ -620,54 +778,75 @@ namespace KancolleSniffer
 \r
         private void labelMission_Click(object sender, EventArgs e)\r
         {\r
-            _missionFinishTimeMode = !_missionFinishTimeMode;\r
+            _config.ShowEndTime ^= TimerKind.Mission;\r
             UpdateTimers();\r
         }\r
 \r
         private void UpdateTimers()\r
         {\r
-            foreach (var entry in\r
-                new[] {labelMission1, labelMission2, labelMission3}.Zip(_sniffer.Missions,\r
-                    (label, mission) => new {label, mission.Name, mission.Timer}))\r
+            var mission = new[] {labelMission1, labelMission2, labelMission3};\r
+            for (var i = 0; i < mission.Length; i++)\r
             {\r
-                entry.Timer.Update();\r
-                SetTimerColor(entry.label, entry.Timer);\r
-                entry.label.Text = entry.Timer.ToString(_missionFinishTimeMode);\r
-                if (!entry.Timer.NeedRing)\r
-                    continue;\r
-                _noticeQueue.Enqueue("遠征が終わりました", entry.Name, _config.Sounds["遠征終了"]);\r
-                entry.Timer.NeedRing = false;\r
+                var entry = _sniffer.Missions[i];\r
+                SetTimerColor(mission[i], entry.Timer, _now);\r
+                mission[i].Text = entry.Timer.ToString(_now, (_config.ShowEndTime & TimerKind.Mission) != 0);\r
             }\r
             for (var i = 0; i < _sniffer.NDock.Length; i++)\r
             {\r
                 var entry = _sniffer.NDock[i];\r
-                entry.Timer.Update();\r
-                _shipLabels.SetNDockTimer(i, entry.Timer, _ndockFinishTimeMode);\r
-                if (!entry.Timer.NeedRing)\r
-                    continue;\r
-                _noticeQueue.Enqueue("入渠が終わりました", entry.Name, _config.Sounds["入渠終了"]);\r
-                entry.Timer.NeedRing = false;\r
+                _mainLabels.SetNDockTimer(i, entry.Timer, _now, (_config.ShowEndTime & TimerKind.NDock) != 0);\r
             }\r
             var kdock = new[] {labelConstruct1, labelConstruct2, labelConstruct3, labelConstruct4};\r
             for (var i = 0; i < kdock.Length; i++)\r
             {\r
                 var timer = _sniffer.KDock[i];\r
-                timer.Update();\r
-                SetTimerColor(kdock[i], timer);\r
-\r
-                kdock[i].Text = timer.EndTime == DateTime.MinValue ? "" : timer.Rest.ToString(@"hh\:mm\:ss");\r
-                if (!timer.NeedRing)\r
-                    continue;\r
-                _noticeQueue.Enqueue("建造が終わりました", $"第{i + 1:D}ドック", _config.Sounds["建造完了"]);\r
-                timer.NeedRing = false;\r
+                SetTimerColor(kdock[i], timer, _now);\r
+                kdock[i].Text = timer.ToString(_now);\r
             }\r
             UpdateCondTimers();\r
             UpdateAkashiTimer();\r
+            _timerEnabled = true;\r
+        }\r
+\r
+        private void NotifyTimers()\r
+        {\r
+            for (var i = 0; i < _sniffer.Missions.Length; i++)\r
+            {\r
+                var entry = _sniffer.Missions[i];\r
+                CheckAlarm("遠征終了", entry.Timer, i + 1, entry.Name);\r
+            }\r
+            for (var i = 0; i < _sniffer.NDock.Length; i++)\r
+            {\r
+                var entry = _sniffer.NDock[i];\r
+                CheckAlarm("入渠終了", entry.Timer, i, entry.Name);\r
+            }\r
+            for (var i = 0; i < _sniffer.KDock.Length; i++)\r
+            {\r
+                var timer = _sniffer.KDock[i];\r
+                CheckAlarm("建造完了", timer, 0, $"第{i + 1:D}ドック");\r
+            }\r
+            NotifyCondTimers();\r
+            NotifyAkashiTimer();\r
+            _notificationManager.Flash();\r
+        }\r
+\r
+        private void CheckAlarm(string key, AlarmTimer timer, int fleet, string subject)\r
+        {\r
+            if (timer.CheckAlarm(_prev, _now))\r
+            {\r
+                SetNotification(key, fleet, subject);\r
+                return;\r
+            }\r
+            var pre = TimeSpan.FromSeconds(_config.Notifications[key].PreliminaryPeriod);\r
+            if (pre == TimeSpan.Zero)\r
+                return;\r
+            if (timer.CheckAlarm(_prev + pre, _now + pre))\r
+                SetPreNotification(key, fleet, subject);\r
         }\r
 \r
-        private void SetTimerColor(Label label, RingTimer timer)\r
+        private void SetTimerColor(Label label, AlarmTimer timer, DateTime now)\r
         {\r
-            label.ForeColor = timer.IsFinished ? Color.Red : Color.Black;\r
+            label.ForeColor = timer.IsFinished(now) ? CUDColor.Red : Color.Black;\r
         }\r
 \r
         private void UpdateCondTimers()\r
@@ -683,33 +862,44 @@ namespace KancolleSniffer
             {\r
                 timer = _sniffer.GetConditionTimer(_currentFleet);\r
             }\r
-            var now = DateTime.Now;\r
             if (timer == DateTime.MinValue)\r
             {\r
                 labelCondTimerTitle.Text = "";\r
                 labelCondTimer.Text = "";\r
                 return;\r
             }\r
-            var span = TimeSpan.FromSeconds(Ceiling((timer - now).TotalSeconds));\r
+            var span = TimeSpan.FromSeconds(Ceiling((timer - _now).TotalSeconds));\r
             if (span >= TimeSpan.FromMinutes(9))\r
             {\r
                 labelCondTimerTitle.Text = "cond40まで";\r
                 labelCondTimer.Text = (span - TimeSpan.FromMinutes(9)).ToString(@"mm\:ss");\r
+                labelCondTimer.ForeColor = DefaultForeColor;\r
             }\r
             else\r
             {\r
                 labelCondTimerTitle.Text = "cond49まで";\r
                 labelCondTimer.Text = (span >= TimeSpan.Zero ? span : TimeSpan.Zero).ToString(@"mm\:ss");\r
+                labelCondTimer.ForeColor = span <= TimeSpan.Zero ? CUDColor.Red : DefaultForeColor;\r
             }\r
-            var notice = _sniffer.GetConditionNotice();\r
-            if (notice == null)\r
-                return;\r
-            var fn = new[] {"第一艦隊", "第二艦隊", "第三艦隊", "第四艦隊"};\r
-            for (var i = 0; i < fn.Length; i++)\r
+        }\r
+\r
+        private void NotifyCondTimers()\r
+        {\r
+            var notice = _sniffer.GetConditionNotice(_prev, _now);\r
+            var pre = TimeSpan.FromSeconds(_config.Notifications["疲労回復"].PreliminaryPeriod);\r
+            var preNotice = pre == TimeSpan.Zero\r
+                ? new int[ShipInfo.FleetCount]\r
+                : _sniffer.GetConditionNotice(_prev + pre, _now + pre);\r
+            for (var i = 0; i < ShipInfo.FleetCount; i++)\r
             {\r
-                if (!_config.NotifyConditions.Contains(notice[i]))\r
-                    return;\r
-                _noticeQueue.Enqueue("疲労が回復しました", fn[i] + " cond" + notice[i].ToString("D"), _config.Sounds["疲労回復"]);\r
+                if (_config.NotifyConditions.Contains(notice[i]))\r
+                {\r
+                    SetNotification("疲労回復" + notice[i], i, "cond" + notice[i]);\r
+                }\r
+                else if (_config.NotifyConditions.Contains(preNotice[i]))\r
+                {\r
+                    SetPreNotification("疲労回復" + preNotice[i], i, "cond" + notice[i]);\r
+                }\r
             }\r
         }\r
 \r
@@ -717,20 +907,20 @@ namespace KancolleSniffer
         {\r
             if (_config.UsePresetAkashi)\r
                 UpdatePresetAkashiTimer();\r
-            _shipLabels.SetAkashiTimer(_sniffer.GetShipStatuses(_currentFleet),\r
+            var statuses = _sniffer.GetShipStatuses(_currentFleet);\r
+            _mainLabels.SetAkashiTimer(statuses,\r
                 _sniffer.AkashiTimer.GetTimers(_currentFleet));\r
-            NotifyAkashiTimer();\r
         }\r
 \r
         private void UpdatePresetAkashiTimer()\r
         {\r
             var akashi = _sniffer.AkashiTimer;\r
             var span = akashi.PresetDeckTimer;\r
-            var color = span == TimeSpan.Zero && akashi.CheckPresetReparing() ? Color.Red : DefaultForeColor;\r
+            var color = span == TimeSpan.Zero && akashi.CheckPresetRepairing() ? CUDColor.Red : DefaultForeColor;\r
             var text = span == TimeSpan.MinValue ? "" : span.ToString(@"mm\:ss");\r
             labelAkashiRepairTimer.ForeColor = color;\r
             labelAkashiRepairTimer.Text = text;\r
-            if (akashi.CheckPresetReparing() && !akashi.CheckReparing(_currentFleet))\r
+            if (akashi.CheckPresetRepairing() && !akashi.CheckRepairing(_currentFleet))\r
             {\r
                 labelPresetAkashiTimer.ForeColor = color;\r
                 labelPresetAkashiTimer.Text = text;\r
@@ -742,9 +932,64 @@ namespace KancolleSniffer
             }\r
         }\r
 \r
+        private void NotifyAkashiTimer()\r
+        {\r
+            var akashi = _sniffer.AkashiTimer;\r
+            var msgs = akashi.GetNotice(_prev, _now);\r
+            if (msgs.Length == 0)\r
+            {\r
+                _notificationManager.StopRepeat("泊地修理");\r
+                return;\r
+            }\r
+            if (!akashi.CheckRepairing() && !(akashi.CheckPresetRepairing() && _config.UsePresetAkashi))\r
+            {\r
+                _notificationManager.StopRepeat("泊地修理");\r
+                return;\r
+            }\r
+            var skipPreliminary = false;\r
+            if (msgs[0].Proceeded == "20分経過しました。")\r
+            {\r
+                SetNotification("泊地修理20分経過", msgs[0].Proceeded);\r
+                msgs[0].Proceeded = "";\r
+                skipPreliminary = true;\r
+                // 修理完了がいるかもしれないので続ける\r
+            }\r
+            for (var i = 0; i < ShipInfo.FleetCount; i++)\r
+            {\r
+                if (msgs[i].Proceeded != "")\r
+                    SetNotification("泊地修理進行", i, msgs[i].Proceeded);\r
+                if (msgs[i].Completed != "")\r
+                    SetNotification("泊地修理完了", i, msgs[i].Completed);\r
+            }\r
+            var pre = TimeSpan.FromSeconds(_config.Notifications["泊地修理20分経過"].PreliminaryPeriod);\r
+            if (skipPreliminary || pre == TimeSpan.Zero)\r
+                return;\r
+            if ((msgs = akashi.GetNotice(_prev + pre, _now + pre))[0].Proceeded == "20分経過しました。")\r
+                SetPreNotification("泊地修理20分経過", 0, msgs[0].Proceeded);\r
+        }\r
+\r
+        private void SetNotification(string key, string subject)\r
+        {\r
+            SetNotification(key, 0, subject);\r
+        }\r
+\r
+        private void SetNotification(string key, int fleet, string subject)\r
+        {\r
+            var spec = _config.Notifications[_notificationManager.KeyToName(key)];\r
+            _notificationManager.Enqueue(key, fleet, subject,\r
+                (spec.Flags & _config.NotificationFlags & NotificationType.Repeat) == 0 ? 0 : spec.RepeatInterval);\r
+        }\r
+\r
+        private void SetPreNotification(string key, int fleet, string subject)\r
+        {\r
+            var spec = _config.Notifications[_notificationManager.KeyToName(key)];\r
+            if ((spec.Flags & NotificationType.Preliminary) != 0)\r
+                _notificationManager.Enqueue(key, fleet, subject, 0, true);\r
+        }\r
+\r
         private void UpdateRepairList()\r
         {\r
-            _shipLabels.SetRepairList(_sniffer.RepairList);\r
+            panelRepairList.SetRepairList(_sniffer.RepairList);\r
         }\r
 \r
         private void UpdateQuestList()\r
@@ -755,8 +1000,13 @@ namespace KancolleSniffer
                 labelQuestColor6\r
             };\r
             var name = new[] {labelQuest1, labelQuest2, labelQuest3, labelQuest4, labelQuest5, labelQuest6};\r
+            var count = new[]\r
+            {\r
+                labelQuestCount1, labelQuestCount2, labelQuestCount3, labelQuestCount4, labelQuestCount5,\r
+                labelQuestCount6\r
+            };\r
             var progress = new[]\r
-            {labelProgress1, labelProgress2, labelProgress3, labelProgress4, labelProgress5, labelProgress6};\r
+                {labelProgress1, labelProgress2, labelProgress3, labelProgress4, labelProgress5, labelProgress6};\r
             var quests = _sniffer.Quests;\r
             for (var i = 0; i < name.Length; i++)\r
             {\r
@@ -765,68 +1015,63 @@ namespace KancolleSniffer
                     category[i].BackColor = quests[i].Color;\r
                     name[i].Text = quests[i].Name;\r
                     progress[i].Text = $"{quests[i].Progress:D}%";\r
+                    _toolTipQuest.SetToolTip(name[i], quests[i].ToToolTip());\r
+                    var c = quests[i].Count;\r
+                    if (c.Id == 0)\r
+                    {\r
+                        count[i].Text = "";\r
+                        count[i].ForeColor = Color.Black;\r
+                        _toolTip.SetToolTip(count[i], "");\r
+                        continue;\r
+                    }\r
+                    count[i].Text = " " + c;\r
+                    count[i].ForeColor = c.Cleared ? CUDColor.Green : Color.Black;\r
+                    _toolTip.SetToolTip(count[i], c.ToToolTip());\r
                 }\r
                 else\r
                 {\r
                     category[i].BackColor = DefaultBackColor;\r
-                    name[i].Text = progress[i].Text = "";\r
+                    name[i].Text = count[i].Text = progress[i].Text = "";\r
+                    _toolTipQuest.SetToolTip(name[i], "");\r
+                    _toolTip.SetToolTip(count[i], "");\r
                 }\r
             }\r
         }\r
 \r
-        private class NoticeQueue\r
+        private void Alarm(string balloonTitle, string balloonMessage, string name)\r
         {\r
-            private readonly Action<string, string, string> _ring;\r
-            private readonly Queue<Tuple<string, string, string>> _queue = new Queue<Tuple<string, string, string>>();\r
-            private readonly Timer _timer = new Timer {Interval = 2000};\r
-\r
-            public NoticeQueue(Action<string, string, string> ring)\r
-            {\r
-                _ring = ring;\r
-                _timer.Tick += TimerOnTick;\r
-            }\r
-\r
-            private void TimerOnTick(object obj, EventArgs e)\r
+            var flags = _config.Notifications[name].Flags;\r
+            var effective = _config.NotificationFlags & _config.Notifications[name].Flags;\r
+            if ((effective & NotificationType.FlashWindow) != 0)\r
+                Win32API.FlashWindow(Handle);\r
+            if ((effective & NotificationType.ShowBaloonTip) != 0)\r
+                notifyIconMain.ShowBalloonTip(20000, balloonTitle, balloonMessage, ToolTipIcon.Info);\r
+            if ((effective & NotificationType.PlaySound) != 0)\r
+                PlaySound(_config.Sounds[name], _config.Sounds.Volume);\r
+            if (_config.Pushbullet.On && (flags & NotificationType.Push) != 0)\r
             {\r
-                if (_queue.Count == 0)\r
+                Task.Run(() =>\r
                 {\r
-                    _timer.Stop();\r
-                    return;\r
-                }\r
-                var notice = _queue.Dequeue();\r
-                _ring(notice.Item1, notice.Item2, notice.Item3);\r
+                    PushNotification.PushToPushbullet(_config.Pushbullet.Token, balloonTitle, balloonMessage);\r
+                });\r
             }\r
-\r
-            public void Enqueue(string title, string message, string soundFile)\r
+            if (_config.Pushover.On && (flags & NotificationType.Push) != 0)\r
             {\r
-                if (_timer.Enabled)\r
+                Task.Run(() =>\r
                 {\r
-                    _queue.Enqueue(new Tuple<string, string, string>(title, message, soundFile));\r
-                }\r
-                else\r
-                {\r
-                    _ring(title, message, soundFile);\r
-                    _timer.Start();\r
-                }\r
+                    PushNotification.PushToPushover(_config.Pushover.ApiKey, _config.Pushover.UserKey,\r
+                        balloonTitle, balloonMessage);\r
+                });\r
             }\r
         }\r
 \r
-        private void Ring(string baloonTitle, string baloonMessage, string soundFile)\r
-        {\r
-            if (_config.FlashWindow)\r
-                Win32API.FlashWindow(Handle);\r
-            if (_config.ShowBaloonTip)\r
-                notifyIconMain.ShowBalloonTip(20000, baloonTitle, baloonMessage, ToolTipIcon.Info);\r
-            if (_config.PlaySound)\r
-                PlaySound(soundFile, _config.Sounds.Volume);\r
-        }\r
-\r
         [DllImport("winmm.dll")]\r
         private static extern int mciSendString(String command,\r
             StringBuilder buffer, int bufferSize, IntPtr hwndCallback);\r
 \r
 // ReSharper disable InconsistentNaming\r
         private const int MM_MCINOTIFY = 0x3B9;\r
+\r
         private const int MCI_NOTIFY_SUCCESSFUL = 1;\r
 // ReSharper restore InconsistentNaming\r
 \r
@@ -881,9 +1126,6 @@ namespace KancolleSniffer
             }\r
             _combinedFleet = false;\r
             _currentFleet = fleet;\r
-            foreach (var label in _labelCheckFleets)\r
-                label.Visible = false;\r
-            _labelCheckFleets[fleet].Visible = true;\r
             UpdatePanelShipInfo();\r
         }\r
 \r
@@ -897,6 +1139,8 @@ namespace KancolleSniffer
             labelFleet1.Text = _combinedFleet ? "連合" : "第一";\r
         }\r
 \r
+        private readonly Color _activeButtonColor = Color.FromArgb(152, 179, 208);\r
+\r
         private void labelBucketHistoryButton_Click(object sender, EventArgs e)\r
         {\r
             if (labelBucketHistory.Visible)\r
@@ -908,7 +1152,7 @@ namespace KancolleSniffer
             {\r
                 labelBucketHistory.Visible = true;\r
                 labelBucketHistory.BringToFront();\r
-                labelBucketHistoryButton.BackColor = SystemColors.ActiveCaption;\r
+                labelBucketHistoryButton.BackColor = _activeButtonColor;\r
             }\r
         }\r
 \r
@@ -929,7 +1173,7 @@ namespace KancolleSniffer
             {\r
                 panelMaterialHistory.Visible = true;\r
                 panelMaterialHistory.BringToFront();\r
-                labelMaterialHistoryButton.BackColor = SystemColors.ActiveCaption;\r
+                labelMaterialHistoryButton.BackColor = _activeButtonColor;\r
             }\r
         }\r
 \r
@@ -956,7 +1200,7 @@ namespace KancolleSniffer
             {\r
                 panelRepairList.Visible = true;\r
                 panelRepairList.BringToFront();\r
-                labelRepairListButton.BackColor = SystemColors.ActiveCaption;\r
+                labelRepairListButton.BackColor = _activeButtonColor;\r
             }\r
         }\r
 \r
@@ -980,6 +1224,37 @@ namespace KancolleSniffer
             Process.Start("http://localhost:" + _config.Proxy.Listen + "/");\r
         }\r
 \r
+        private void labelClearQuest_Click(object sender, EventArgs e)\r
+        {\r
+            _sniffer.ClearQuests();\r
+            UpdateQuestList();\r
+        }\r
+\r
+        private void labelClearQuest_MouseDown(object sender, MouseEventArgs e)\r
+        {\r
+            labelClearQuest.BackColor = _activeButtonColor;\r
+        }\r
+\r
+        private void labelClearQuest_MouseUp(object sender, MouseEventArgs e)\r
+        {\r
+            labelClearQuest.BackColor = DefaultBackColor;\r
+        }\r
+\r
+        private void labelQuest_DoubleClick(object sender, EventArgs e)\r
+        {\r
+            var label = (Label)sender;\r
+            if (string.IsNullOrEmpty(label.Text))\r
+                return;\r
+            Clipboard.SetText(label.Text);\r
+            _tooltipCopy.Active = true;\r
+            _tooltipCopy.Show("コピーしました。", label);\r
+            Task.Run(async () =>\r
+            {\r
+                await Task.Delay(1000);\r
+                _tooltipCopy.Active = false;\r
+            });\r
+        }\r
+\r
         private void CaptureToolStripMenuItem_Click(object sender, EventArgs e)\r
         {\r
             try\r
@@ -990,6 +1265,9 @@ namespace KancolleSniffer
             catch (FileNotFoundException)\r
             {\r
             }\r
+            catch (Win32Exception)\r
+            {\r
+            }\r
         }\r
     }\r
 }
\ No newline at end of file