diff --git a/kr.devany.googleapi.sdPlugin/manifest.json b/kr.devany.googleapi.sdPlugin/manifest.json index 2549f10..4fb7ce6 100644 --- a/kr.devany.googleapi.sdPlugin/manifest.json +++ b/kr.devany.googleapi.sdPlugin/manifest.json @@ -38,6 +38,25 @@ "UUID": "kr.devany.googleapi.adsensemanagement", "PropertyInspectorPath": "propertyInspector/adsensemanagement/index.html" }, + { + "Icon": "images/calendar", + "Name": "Google Calendar", + "States": [ + { + "FontSize": "10", + "TitleAlignment": "bottom", + "Image": "images/calendar" + } + ], + "Controllers": [ + "Keypad", + "Information" + ], + "SupportedInMultiActions": false, + "Tooltip": "구글 캘린더", + "UUID": "kr.devany.googleapi.googlecalendar", + "PropertyInspectorPath": "propertyInspector/googlecalendar/index.html" + }, { "Icon": "images/gmail.png", "Name": "Gmail", diff --git a/kr.devany.googleapi.sdPlugin/propertyInspector/googlecalendar/PluginActionPI.js b/kr.devany.googleapi.sdPlugin/propertyInspector/googlecalendar/PluginActionPI.js new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/kr.devany.googleapi.sdPlugin/propertyInspector/googlecalendar/PluginActionPI.js @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/kr.devany.googleapi.sdPlugin/propertyInspector/googlecalendar/index.html b/kr.devany.googleapi.sdPlugin/propertyInspector/googlecalendar/index.html new file mode 100644 index 0000000..41b66b8 --- /dev/null +++ b/kr.devany.googleapi.sdPlugin/propertyInspector/googlecalendar/index.html @@ -0,0 +1,24 @@ + + + + + Google Calendar + + + + + + + +
+
+
Front Color
+ +
+
+
Back Color
+ +
+
+ + diff --git a/src/GoogleAPI.csproj b/src/GoogleAPI.csproj index e45684b..4a9e145 100644 --- a/src/GoogleAPI.csproj +++ b/src/GoogleAPI.csproj @@ -13,6 +13,21 @@ true true preview + 게시\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true AnyCPU @@ -58,6 +73,9 @@ ..\packages\Google.Apis.Auth.1.68.0\lib\net462\Google.Apis.Auth.dll + + ..\packages\Google.Apis.Calendar.v3.1.35.1.1321\lib\net45\Google.Apis.Calendar.v3.dll + ..\packages\Google.Apis.Core.1.68.0\lib\net462\Google.Apis.Core.dll @@ -74,11 +92,14 @@ ..\packages\System.CodeDom.7.0.0\lib\net462\System.CodeDom.dll + + - + ..\packages\System.Drawing.Common.8.0.1\lib\net462\System.Drawing.Common.dll + True @@ -100,10 +121,14 @@ + + + + + - @@ -119,6 +144,7 @@ + @@ -132,5 +158,17 @@ + + + False + Microsoft .NET Framework 4.8.1%28x86 및 x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + \ No newline at end of file diff --git a/src/GoogleAPIs/AdSenseManagement/PluginAction.cs b/src/GoogleAPIs/AdSenseManagement/PluginAction.cs index af42ca6..c8f0224 100644 --- a/src/GoogleAPIs/AdSenseManagement/PluginAction.cs +++ b/src/GoogleAPIs/AdSenseManagement/PluginAction.cs @@ -44,7 +44,16 @@ public PluginAction(ISDConnection connection, InitialPayload payload) : base(con private async void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs e) { Logger.Instance.LogMessage(TracingLevel.INFO, "OnTitleParametersDidChange Event Handled"); - await DisplayInitialAsync(); + + if (!File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"DevAny\StreamDock.Plugins\GoogleAPI\Google.Apis.Auth.OAuth2.Responses.TokenResponse-user"))) + { + await DisplayInitialAsync(); + } + else + { + await DisplayBusyAsync(); + await UpdateApiDataAsync(); + } } /// diff --git a/src/GoogleAPIs/GoogleAPIs.cs b/src/GoogleAPIs/GoogleAPIs.cs index c12b5ea..d244614 100644 --- a/src/GoogleAPIs/GoogleAPIs.cs +++ b/src/GoogleAPIs/GoogleAPIs.cs @@ -6,6 +6,7 @@ using Google.Apis.Adsense.v2; using Google.Apis.Auth.OAuth2; +using Google.Apis.Calendar.v3; namespace StreamDock.Plugins.GoogleAPIs { @@ -31,7 +32,10 @@ protected async Task GetClientSecretAsync() { credential = await GoogleWebAuthorizationBroker.AuthorizeAsync( GoogleClientSecrets.FromStream(stream).Secrets, - new[] { AdsenseService.Scope.AdsenseReadonly }, + new[] { + AdsenseService.Scope.AdsenseReadonly, + CalendarService.Scope.Calendar + }, "user", CancellationToken.None); } Logger.Instance.LogMessage(TracingLevel.INFO, "Read client_secrets.json"); diff --git a/src/GoogleAPIs/GoogleCalendar/ApiAction.cs b/src/GoogleAPIs/GoogleCalendar/ApiAction.cs new file mode 100644 index 0000000..bd324a4 --- /dev/null +++ b/src/GoogleAPIs/GoogleCalendar/ApiAction.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using BarRaider.SdTools; + +using Google.Apis.Calendar.v3; +using Google.Apis.Services; + +using Newtonsoft.Json.Linq; + +namespace StreamDock.Plugins.GoogleAPIs.GoogleCalendar +{ + /// + /// API 동작 정의 프로시저 + /// + internal class ApiAction : GoogleAPI + { + PluginSettings pluginSettings { get; set; } + Item item { get; set; } + + internal ApiAction(PluginSettings pluginsettings, Item item) + { + this.pluginSettings = pluginsettings; + this.item = item; + } + internal async Task GetService() + { + // 서비스 생성 + var service = new CalendarService(new BaseClientService.Initializer() + { + HttpClientInitializer = await GetClientSecretAsync(), + ApplicationName = "StreamDock Plugin" + }); + + return service; + } + /// + /// 키가 눌렸을 때 동작 정의. Google API 통신. + /// + internal async Task ExecuteAsync() + { + Item _item = this.item; + + try + { + // 서비스 생성 + var service = new CalendarService(new BaseClientService.Initializer() + { + HttpClientInitializer = await GetClientSecretAsync(), + ApplicationName = "StreamDock Plugin" + }); + + // 구글 API 통신 인스턴스 + GoogleAPIQuery googleAPIQuery = new GoogleAPIQuery(service); + + if (pluginSettings.CalendarSummary.IsNullOrEmpty()) + { + item.calendarID = googleAPIQuery.GetPrimaryCalendar().Id; + } + else + { + item.calendarID = googleAPIQuery.GetCalendar(pluginSettings.CalendarSummary).Id; + } + + Logger.Instance.LogMessage(TracingLevel.INFO, item.calendarID); + + // StreamDock 설정에 따른 동작 호출 + item.Events = await googleAPIQuery.CalendarEventsToday(item.calendarID); + + // 디스플레이용 데이터 가공 + _item = SetDisplayValue(); + } + catch (Exception ex) + { + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.Message); + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.StackTrace); + } + return _item; + } + + /// + /// StreamDock 설정에 따라 표시할 데이터를 지정합니다. + /// + /// + internal Item SetDisplayValue() + { + try + { + item.DisplayValues.Clear(); + + foreach (var value in item.Events) { + if (value.End.Date.IsDateTime()) + { + + } + item.DisplayValues.Add(value.Summary); + } + } + catch (Exception ex) + { + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.Message); + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.StackTrace); + } + + return item; + } + } +} diff --git a/src/GoogleAPIs/GoogleCalendar/GoogleAPIQuery.cs b/src/GoogleAPIs/GoogleCalendar/GoogleAPIQuery.cs new file mode 100644 index 0000000..aa4bd31 --- /dev/null +++ b/src/GoogleAPIs/GoogleCalendar/GoogleAPIQuery.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; + +using BarRaider.SdTools; + +using Google.Apis.Calendar.v3; +using Google.Apis.Calendar.v3.Data; + +using System.Threading.Tasks; +using Google.Apis.Adsense.v2.Data; +using System; + +namespace StreamDock.Plugins.GoogleAPIs.GoogleCalendar +{ + internal class GoogleAPIQuery + { + private CalendarService service; + + /// + /// 클래스의 새 인스턴스를 초기화합니다. + /// + /// 요청을 실행할 애드센스 서비스 개체입니다. + /// 검색할 최대 페이지 크기입니다. + internal GoogleAPIQuery(CalendarService service) + { + this.service = service; + } + + /// + /// 계정의 모든 캘린더를 가져옵니다. + /// + /// 검색된 계정의 마지막 페이지입니다. + internal IList GetAllCalendars() + { + string pageToken = null; + CalendarList calendarList = null; + + do + { + var calendarListRequest = service.CalendarList.List(); + calendarListRequest.PageToken = pageToken; + calendarList = calendarListRequest.Execute(); + pageToken = calendarList.NextPageToken; + } while (pageToken != null); + return calendarList.Items; + } + /// + /// 단일 캘린더를 가져옵니다. + /// + /// + internal CalendarListEntry GetCalendar(string summary) + { + return GetAllCalendars().First(s => s.Summary == summary); + } + /// + /// 단일 캘린더를 가져옵니다. + /// + /// + internal CalendarListEntry GetPrimaryCalendar() + { + return GetAllCalendars().First(s => s.Primary == true); + } + /// + /// 오늘 이벤트를 가져옵니다. + /// + /// + internal async Task> CalendarEventsToday(string id) + { + var requeust = service.Events.List(id); + + requeust.MaxResults = 5; + requeust.TimeMin = DateTime.Today; + requeust.TimeMax = DateTime.Today.AddDays(1).AddSeconds(-1); + + var result = await requeust.ExecuteAsync(); + + return result.Items; + } + } +} \ No newline at end of file diff --git a/src/GoogleAPIs/GoogleCalendar/Models/Item.cs b/src/GoogleAPIs/GoogleCalendar/Models/Item.cs new file mode 100644 index 0000000..f0f0d32 --- /dev/null +++ b/src/GoogleAPIs/GoogleCalendar/Models/Item.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Google.Apis.Calendar.v3; +using Google.Apis.Calendar.v3.Data; + +namespace StreamDock.Plugins.GoogleAPIs.GoogleCalendar +{ + /// + /// API 데이터를 보관하는 클래스입니다. static은 스트림독의 모든 키에 공유됩니다. + /// 데이터 형식은 API에서 제공하는 데이터 클래스를 사용하세요. + /// + public class Item + { + // 공유 데이터 + internal static IList Calendars { get; set; } + + // 개별 데이터 + internal CalendarsResource Calendar { get; set; } + internal string calendarID { get; set; } + internal IList Events { get; set; } = new List(); + internal bool CalendarExists => !Calendars.IsNullOrEmpty() && Calendars.Any(); + + internal IList DisplayValues { get; set; } = new List(); + + internal void Init() + { + } + } +} diff --git a/src/GoogleAPIs/GoogleCalendar/Models/PluginSettings.cs b/src/GoogleAPIs/GoogleCalendar/Models/PluginSettings.cs new file mode 100644 index 0000000..6d7b8a0 --- /dev/null +++ b/src/GoogleAPIs/GoogleCalendar/Models/PluginSettings.cs @@ -0,0 +1,47 @@ +using System; +using System.Drawing; + +using BarRaider.SdTools; + +using Newtonsoft.Json; + +using System.ComponentModel; + +namespace StreamDock.Plugins.GoogleAPIs.GoogleCalendar +{ + public class PluginSettings : INotifyPropertyChanged + { + //TODO + [JsonProperty(PropertyName = "CalendarList")] + public string PiCalendarList { get; set; } + public string CalendarSummary { get; set; } // 캘린더 이름 + public string CalendarListJSON { get; set; } + + [JsonProperty(PropertyName = "frontColor")] + public string PiFrontColor { get; set; } + public Color FrontColor => GraphicsTools.ColorFromHex(PiFrontColor); + + [JsonProperty(PropertyName = "backColor")] + public string PiBackColor { get; set; } + public Color BackColor => GraphicsTools.ColorFromHex(PiBackColor); + + public static PluginSettings CreateDefaultSettings() + { + PluginSettings instance = new PluginSettings(); + instance.PiCalendarList = String.Empty; + instance.PiFrontColor = "#FFFFFF"; + instance.PiBackColor = String.Empty; + return instance; + } + + public event PropertyChangedEventHandler PropertyChanged; + protected void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + protected void OnPropertyChanged(string propertyName) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/GoogleAPIs/GoogleCalendar/PluginAction.cs b/src/GoogleAPIs/GoogleCalendar/PluginAction.cs new file mode 100644 index 0000000..e550aa9 --- /dev/null +++ b/src/GoogleAPIs/GoogleCalendar/PluginAction.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using BarRaider.SdTools; +using BarRaider.SdTools.Wrappers; + +using Newtonsoft.Json.Linq; + +namespace StreamDock.Plugins.GoogleAPIs.GoogleCalendar +{ + /// + /// manifest.json 에서 선언한 플러그인 UUID + /// + [PluginActionId("kr.devany.googleapi.googlecalendar")] + public class PluginAction : KeypadBase + { + Item item { get; set; } + PluginSettings pluginSettings { get; set; } + + public PluginAction(ISDConnection connection, InitialPayload payload) : base(connection, payload) + { + pluginSettings = (payload.Settings == null || payload.Settings.Count == 0) ? PluginSettings.CreateDefaultSettings() : payload.Settings.ToObject(); + item = new(); + + Connection.OnApplicationDidLaunch += Connection_OnApplicationDidLaunch; + Connection.OnApplicationDidTerminate += Connection_OnApplicationDidTerminate; + Connection.OnDeviceDidConnect += Connection_OnDeviceDidConnect; + Connection.OnDeviceDidDisconnect += Connection_OnDeviceDidDisconnect; + Connection.OnPropertyInspectorDidAppear += Connection_OnPropertyInspectorDidAppear; + Connection.OnPropertyInspectorDidDisappear += Connection_OnPropertyInspectorDidDisappear; + Connection.OnSendToPlugin += Connection_OnSendToPlugin; + Connection.OnTitleParametersDidChange += Connection_OnTitleParametersDidChange; + } + + /// + /// 제목이 변경되거나 스트림독에 플러그인이 나타날 때 호출됩니다.titleParametersDidChange + /// 하드웨어 기준. + /// + /// + /// + private async void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnTitleParametersDidChange Event Handled"); + + if (!File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"DevAny\StreamDock.Plugins\GoogleAPI\Google.Apis.Auth.OAuth2.Responses.TokenResponse-user"))) + { + await DisplayInitialAsync(); + } + else + { + await DisplayBusyAsync(); + await UpdateApiDataAsync(); + } + } + + /// + /// PropertyInspector가 sendToPlugin 이벤트를 사용할 때 플러그인에서 받은 이벤트입니다. + /// OnPropertyInspectorDidAppear 이벤트와 동시에 호출됩니다. + /// + /// + /// + private void Connection_OnSendToPlugin(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnSendToPlugin Event Handled"); + } + + /// + /// PropertyInspector가 표시될 때마다 호출됩니다. + /// + /// + /// + private void Connection_OnPropertyInspectorDidAppear(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnPropertyInspectorDidAppear Event Handled"); + + } + + /// + /// PropertyInspector가 숨겨질 때마다 호출됩니다. + /// + /// + /// + private void Connection_OnPropertyInspectorDidDisappear(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnPropertyInspectorDidDisappear Event Handled"); + } + + + /// + /// 장치 연결 끊김 이벤트 + /// + /// + /// + private void Connection_OnDeviceDidDisconnect(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnDeviceDidDisconnect Event Handled"); + } + + /// + /// 장치 연결 이벤트 + /// + /// + /// + private void Connection_OnDeviceDidConnect(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnDeviceDidConnect Event Handled"); + } + + /// + /// 앱 종료 이벤트 + /// + /// + /// + private void Connection_OnApplicationDidTerminate(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnApplicationDidTerminate Event Handled"); + } + + /// + /// 앱 실행 이벤트 + /// + /// + /// + private void Connection_OnApplicationDidLaunch(object sender, SDEventReceivedEventArgs e) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "OnApplicationDidLaunch Event Handled"); + } + + /// + /// 스트림독에 플러그인이 표시되지 않으면 호출됩니다. + /// + public override void Dispose() + { + Logger.Instance.LogMessage(TracingLevel.INFO, "Destructor called"); + } + + /// + /// 키를 눌렀을 때 호출됩니다. + /// + /// + public override void KeyPressed(KeyPayload payload) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "KeyPressed called"); + } + + /// + /// 키를 뗄 때 호출됩니다. + /// + /// + public async override void KeyReleased(KeyPayload payload) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "KeyReleased called"); + + if (CheckExistData()) + { + System.Diagnostics.Process.Start(item.Events.First().HtmlLink); + } + else + { + await DisplayBusyAsync(); + await UpdateApiDataAsync(); + } + } + + /// + /// 매 초 호출되는 메서드입니다. + /// + public override void OnTick() + { + } + + /// + /// PropertyInspector에서 값이 변경될 때마다 호출됩니다. + /// + /// + public async override void ReceivedSettings(ReceivedSettingsPayload payload) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedSettings called"); + + Tools.AutoPopulateSettings(pluginSettings, payload.Settings); + + await SaveSettingsAsync(); + await DisplayInitialAsync(); + } + + public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) + { + Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings called"); + } + + #region Private Methods + /// + /// 설정 값을 스트림독으로 전달합니다. + /// + /// + private async Task SaveSettingsAsync() + { + await Connection.SetSettingsAsync(JObject.FromObject(pluginSettings)); + } + + /// + /// 기존 데이터가 있는지 검사합니다. + /// + /// + private bool CheckExistData() + { + return item.Events.Any(); + } + + /// + /// 초기 이미지를 표시합니다. + /// + private async Task DisplayInitialAsync() + { + try + { + if (!CheckExistData()) + { + item.DisplayValues.OnlyOne("Press Key..."); + } + else + { + UpdateValues(); + } + await Connection.SetImageAsync(UpdateKeyImage(item, true), null, true); // 초기 이미지 출력 + } + catch (Exception ex) + { + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.Message); + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.StackTrace); + } + } + /// + /// 작업 중임을 알리는 이미지를 표시합니다. + /// + private async Task DisplayBusyAsync() + { + try + { + TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 12, Color.White, true, TitleVerticalAlignment.Middle); + using (Bitmap image = Tools.GenerateGenericKeyImage(out Graphics graphics)) + { + graphics.FillRectangle(new SolidBrush(Color.Yellow), 0, 0, image.Width, image.Height); + graphics.AddTextPath(tp, image.Height, image.Width, "Loading...", Color.Black, 7); //TODO 지역화 +#if DEBUG + Logger.Instance.LogMessage(TracingLevel.INFO, "DisplayBusyAsync: 스트림독으로 이미지 전송 중..."); +#endif + await Connection.SetImageAsync(image); + graphics.Dispose(); + } + } + catch (Exception ex) + { + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.Message); + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.StackTrace); + } + } + /// + /// PI 설정에 따라 Google API 데이터를 갱신합니다. + /// + private async Task UpdateApiDataAsync() + { + try + { + item = await GetApiInstance().ExecuteAsync(); + Logger.Instance.LogMessage(TracingLevel.INFO, "UpdateApiDataAsync: Sending Image to Stream Dock..."); + await Connection.SetImageAsync(UpdateKeyImage(item), null, true); + } + catch (Exception ex) + { + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.Message); + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.StackTrace); + } + } + /// + /// PI 설정에 따라 이미 수신된 Google API 데이터로 갱신합니다. + /// + private void UpdateValues() + { + GetApiInstance().SetDisplayValue(); + } + /// + /// 키 이미지를 변경합니다. 출력할 정보를 이미지로 변환합니다. + /// + private Bitmap UpdateKeyImage(Item item, bool initial = false) + { + Bitmap bmp = null; + try + { + bmp = new Bitmap(ImageHelper.GetImage(pluginSettings.BackColor)); + + for (int i = 0; i < item.DisplayValues.Count; i++) + { + var font = new Font("Arial", 32, FontStyle.Bold, GraphicsUnit.Pixel); + var stringFormat = new StringFormat + { + Alignment = StringAlignment.Near, + LineAlignment = StringAlignment.Center + }; + var isRGB = stringFormat.Alignment == StringAlignment.Near; + + using (var graphics = Graphics.FromImage(bmp)) + { + //font = ImageHelper.ResizeFont(graphics, item.DisplayValues[i], font); + graphics.DrawString(item.DisplayValues[i], font, new SolidBrush(pluginSettings.FrontColor), !isRGB ? 72 : 5, (144 / (item.DisplayValues.Count + 1)) * (i + 1), stringFormat); + } + + //bmp = new Bitmap(ImageHelper.SetImageText(bmp, item.DisplayValues[i], new SolidBrush(pluginSettings.FrontColor), 72, (144 / (item.DisplayValues.Count + 1)) * (i + 1))); + } + } + catch (Exception ex) + { + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.Message); + Logger.Instance.LogMessage(TracingLevel.ERROR, ex.StackTrace); + } + return bmp; + } + /// + /// Google API 쿼리 클래스의 인스턴스를 가져옵니다. + /// + /// + private ApiAction GetApiInstance() + { + return new ApiAction(pluginSettings, item); + } + #endregion + } +} diff --git a/src/packages.config b/src/packages.config index 762eb59..69edf52 100644 --- a/src/packages.config +++ b/src/packages.config @@ -4,6 +4,7 @@ +