12 Commits

Author SHA1 Message Date
Martin Barker
45d4dad22a Updated Ignore form to be a little cleaner, and hyperlinks now work. 2023-02-21 22:34:56 +00:00
Martin Barker
49ea2ba2a6 Added Support for Debug Logging
Updated a couple of variables for better memory use.
Added Support for ignoring specific streamers
Moved to using a SingletonFactory for Singlton Classes
No Longer Using MessageBox for disconnected message
Added better detection for disconnected to stop it poping with other problems
Added Window and menu option to open window to manage ignored streamers
2023-02-21 21:25:38 +00:00
Martin Barker
03b620ecec Started work on #1 2023-02-03 18:59:07 +00:00
Martin Barker
0af2d24b4c Fixed the Clean up system so that is actually cleans up the stream thumbnails 2023-01-28 15:32:59 +00:00
Martin Barker
7946c8ec37 Added some Checks to make sure we're not contantly downloading profile images if we have it cached already, and clean up thumbnails when the toast is finished. 2023-01-28 15:24:45 +00:00
Martin Barker
e9f291f6f7 Updates to finally secure the Twitch codes so i don't have to keep adding them for compile and removing them for Source Distribution. fixed a Bug in the notification system that ment dismiss did not work. 2023-01-28 15:17:50 +00:00
Martin Barker (Keatran)
77cae0c9c8 Update Readme.md 2023-01-28 03:02:26 +00:00
Martin Barker (Keatran)
6ff5c89e45 Update Readme.md 2023-01-28 03:02:11 +00:00
Martin Barker (Keatran)
afc4aa0d29 Update Readme.md 2023-01-28 03:01:47 +00:00
Martin Barker
3ccdbbdf57 Updated to fix some bugs 2023-01-28 02:47:54 +00:00
Martin Barker
b09eb0a641 Remvoed secrets for Twitch since they don't work 2023-01-28 02:13:17 +00:00
Martin Barker
36ad4fff02 Added Protection for the Twitch Secrets 2023-01-28 00:39:34 +00:00
27 changed files with 670 additions and 203 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@
/TwitchDesktopNotifications/obj /TwitchDesktopNotifications/obj
/.vs /.vs
/TwitchDesktopNotifications/App.config /TwitchDesktopNotifications/App.config
/TwitchDesktopNotifications/TwitchDetails.cs

View File

@@ -1,7 +1,5 @@
# Twitch Notify # Twitch Notify
## not affiliated with Twitch or Amazon ## Not affiliated with Twitch or Amazon
[![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](https://travis-ci.org/joemccann/dillinger)
## Installation ## Installation
Twitch Notify Requires [.NET 6 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.13-windows-x64-installer). Twitch Notify Requires [.NET 6 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-6.0.13-windows-x64-installer).
@@ -11,19 +9,33 @@
Want to contribute? Great! Want to contribute? Great!
Project is built using Visual Studios 2022, must have Windows 10.0.17763 SDK installed Project is built using Visual Studios 2022,
You must create a `App.config` file inside `TwitchDesktopNotifications` project. You need to create Application to obtain a ID and Secret on [Twitch Developer Console](https://dev.twitch.tv/console)
```xml Add a new C# Class to the project named `TwitchDetails.cs` add the following code with your ID and Secret
<?xml version="1.0" encoding="utf-8" ?> ```cs
<configuration> namespace TwitchDesktopNotifications
<appSettings> {
<add key="TwitchClientID" value="" /> static public class TwitchDetails
<add key="TwitchClientSecret" value="" /> {
</appSettings> public static string TwitchClientID = "";
</configuration> public static string TwitchClientSecret = "";
}
}
``` ```
### CommunityToolkit 8.0.0 Pre-release
Project Requests `CommunityToolkit-MainLatest` NuGET Package Source
1. Tool > NuGET Package Manager > Package Manager Settings
2. Click on Package Source (just below the select General in the Left hand column
3. Click the + icon top right
5. 4. Enter the Name `CommunityToolkit-MainLatest` and Source `https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-MainLatest/nuget/v3/index.json`
6. Click Update
7. Click Ok
## License ## License
MIT MIT

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TwitchDesktopNotifications.Core
{
public class Logger : SingletonFactory<Logger>, Singleton
{
private string _name;
private StreamWriter sw;
public Logger()
{
_name = DateTime.Now.ToString("dd_mm_yyyy HH_mm")+".log";
}
~Logger()
{
if (sw != null)
{
sw.Flush();
sw.Close();
}
}
public StreamWriter Writer {
get {
if(sw == null)
{
sw = new StreamWriter(_name);
}
return sw;
}
}
}
}

View File

@@ -9,12 +9,15 @@ using System.IO;
using Windows.Foundation.Collections; using Windows.Foundation.Collections;
using System.Diagnostics; using System.Diagnostics;
using System.ComponentModel; using System.ComponentModel;
using Windows.UI.Notifications;
using System.Net.Http;
namespace TwitchDesktopNotifications.Core namespace TwitchDesktopNotifications.Core
{ {
internal class Notification public class Notification : SingletonFactory<Notification>, Singleton
{ {
private Notification() { private WebClient webClient = new WebClient();
public Notification() {
ToastNotificationManagerCompat.OnActivated += toastArgs => ToastNotificationManagerCompat.OnActivated += toastArgs =>
{ {
// Obtain the arguments from the notification // Obtain the arguments from the notification
@@ -22,25 +25,27 @@ namespace TwitchDesktopNotifications.Core
try try
{ {
if (
// action is defined and set to watch
( args.Contains("action") && args["action"] == "watch" )
||
// action is not defined so the user just generally clicked on the notification
!args.Contains("action")
){
Process myProcess = new Process(); Process myProcess = new Process();
myProcess.StartInfo.UseShellExecute = true; myProcess.StartInfo.UseShellExecute = true;
myProcess.StartInfo.FileName = args["streamerUrl"]; myProcess.StartInfo.FileName = args["streamerUrl"];
myProcess.Start(); myProcess.Start();
}catch(Exception ex) { } }else if( args.Contains("action") && args["action"] == "ignore")
{
NotifyManager.AddStreamerToIgnoreList(args["streamerName"]);
}
}catch(Exception ex) {
Logger.GetInstance().Writer.WriteLine(ex.ToString());
}
}; };
} }
private static Notification Instance;
public static Notification GetInstance()
{
if(Instance == null)
{
Instance = new Notification();
}
return Instance;
}
public void sendNotification(String streamerName, String streamerUrl, String profilePic, String streamThumbnail, String title) public void sendNotification(String streamerName, String streamerUrl, String profilePic, String streamThumbnail, String title)
{ {
String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify"); String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify");
@@ -49,37 +54,66 @@ namespace TwitchDesktopNotifications.Core
// download there profile picture // download there profile picture
string fileNameProfilePic = profilePic.Split("/").Last(); string fileNameProfilePic = profilePic.Split("/").Last();
(new WebClient()).DownloadFile(new Uri(profilePic), FilePath+"/"+ fileNameProfilePic); if (!File.Exists(FilePath + "/" + fileNameProfilePic))
{
webClient.DownloadFile(new Uri(profilePic), FilePath + "/" + fileNameProfilePic);
}
// download there profile picture // download there profile picture
string fileNameThumbnailPic = streamThumbnail.Split("/").Last(); string fileNameThumbnailPic = streamThumbnail.Split("/").Last();
(new WebClient()).DownloadFile(new Uri(streamThumbnail), webClient.DownloadFile(new Uri(streamThumbnail),
FilePath + "/" + fileNameThumbnailPic FilePath + "/" + fileNameThumbnailPic
); );
if (NotifyManager.ShouldNotify(streamerName))
{
var builder = new ToastContentBuilder() var builder = new ToastContentBuilder()
.AddArgument("streamerUrl", streamerUrl) .AddArgument("streamerUrl", streamerUrl)
.AddArgument("streamerName", streamerName)
.AddArgument("thumbnail_path", FilePath + "/" + fileNameThumbnailPic)
.AddText(streamerName + " is now live on Twitch") .AddText(streamerName + " is now live on Twitch")
.AddHeroImage(new Uri("file://" + (FilePath + "/" + fileNameThumbnailPic).Replace("\\", "/"))) .AddHeroImage(new Uri("file://" + (FilePath + "/" + fileNameThumbnailPic).Replace("\\", "/")))
.AddAppLogoOverride(new Uri("file://" + (FilePath + "/" + fileNameProfilePic).Replace("\\", "/")), ToastGenericAppLogoCrop.Circle) .AddAppLogoOverride(new Uri("file://" + (FilePath + "/" + fileNameProfilePic).Replace("\\", "/")), ToastGenericAppLogoCrop.Circle)
.AddButton(new ToastButton() .AddButton(new ToastButton()
.SetContent("Watch " + streamerName) .SetContent("Watch ")
.AddArgument("action", "watch") .AddArgument("action", "watch")
.SetBackgroundActivation()) .SetBackgroundActivation())
.AddButton(new ToastButton() .AddButton(new ToastButton()
.SetContent("Dismiss") .SetContent("Dismiss")
.AddArgument("action", "nothing") .AddArgument("action", "nothing")
.SetBackgroundActivation())
.AddButton(new ToastButton()
.SetContent("Ignore")
.AddArgument("action", "ignore")
.SetBackgroundActivation()); .SetBackgroundActivation());
if(title != "") { if (title != "")
{
builder.AddText(title); builder.AddText(title);
} }
builder.Show(toast => builder.Show(toast =>
{ {
toast.ExpirationTime = DateTime.Now.AddSeconds(15); toast.ExpirationTime = DateTime.Now.AddSeconds(15);
toast.Dismissed += (ToastNotification sender, ToastDismissedEventArgs args) =>
{
try
{
File.Delete(FilePath + "/" + fileNameThumbnailPic);
}
catch (Exception) { }
builder = null;
};
toast.Activated += (ToastNotification sender, object args) =>
{
try
{
File.Delete(FilePath + "/" + fileNameThumbnailPic);
}
catch (Exception) { }
builder = null;
};
}); });
} }
}
} }
} }

View File

@@ -0,0 +1,29 @@
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using TwitchDesktopNotifications.JsonStructure.Helix;
namespace TwitchDesktopNotifications.Core
{
internal class NotifyManager
{
public static Boolean ShouldNotify(String streamerName)
{
if(DataStore.GetInstance().Store.SteamersToIgnore == null || DataStore.GetInstance().Store.SteamersToIgnore.Streamers == null) {
return true;
}
return !UIStreamer.GetCreateStreamer(streamerName).IsIgnored;
}
public static void AddStreamerToIgnoreList(string streamerName)
{
UIStreamer.GetCreateStreamer(streamerName).IsIgnored = true;
DataStore.GetInstance().Save();
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TwitchDesktopNotifications.Core
{
public interface Singleton
{
}
public class SingletonFactory<T> where T : Singleton, new()
{
public static Dictionary<String, Singleton> store = new Dictionary<String, Singleton>();
public static T GetInstance(){
string name = typeof(T).FullName;
if (!store.ContainsKey(name))
{
store.Add(name, new T());
}
return (T)store[name];
}
}
}

View File

@@ -1,33 +1,40 @@
using ABI.System; using System.Diagnostics;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Web; using System.Web;
using TwitchDesktopNotifications.JsonStructure; using TwitchDesktopNotifications.JsonStructure;
using TwitchDesktopNotifications.JsonStructure.Helix; using TwitchDesktopNotifications.JsonStructure.Helix;
using Windows.ApplicationModel.Background;
namespace TwitchDesktopNotifications.Core namespace TwitchDesktopNotifications.Core
{ {
internal class TwitcherRefreshException : Exception
{
public TwitcherRefreshException(string? message, Exception? innerException) : base(message, innerException) {
}
}
internal class TwitchFetcher internal class TwitchFetcher
{ {
private TwitchFetcher() { } private TwitchFetcher() {
}
ReconnectionNeeded rnFrm;
public void OpenFailedNotification()
{
if (rnFrm == null)
{
rnFrm = new ReconnectionNeeded();
}
if (rnFrm.IsActive)
{
rnFrm.Show();
}
}
public static TwitchFetcher instance { get; private set; } public static TwitchFetcher instance { get; private set; }
string TwitchClientID = ConfigurationManager.AppSettings["TwitchClientID"];
string TwitchClientSecret = ConfigurationManager.AppSettings["TwitchClientSecret"];
List <StreamsData> currentlyLive = null; List <StreamsData> currentlyLive = null;
public string guid { get; private set; } public string guid { get; private set; }
@@ -54,16 +61,21 @@ namespace TwitchDesktopNotifications.Core
private T MakeRequest<T>(string endpoint) private T MakeRequest<T>(string endpoint)
{ {
if (DataStore.GetInstance().Store == null)
{
throw new Exception("Not Authenticated");
}
if (DataStore.GetInstance().Store.Authentication.ExpiresAsDate <= DateTime.UtcNow) if (DataStore.GetInstance().Store.Authentication.ExpiresAsDate <= DateTime.UtcNow)
{ {
Refresh(); Refresh();
} }
try
{
WebRequest request = WebRequest.Create("https://api.twitch.tv/" + endpoint); WebRequest request = WebRequest.Create("https://api.twitch.tv/" + endpoint);
request.Method = "GET"; request.Method = "GET";
request.Headers[HttpRequestHeader.Authorization] = String.Format("Bearer {0}", DataStore.GetInstance().Store.Authentication.AccessToken); request.Headers[HttpRequestHeader.Authorization] = String.Format("Bearer {0}", DataStore.GetInstance().Store.Authentication.AccessToken);
request.Headers["Client-ID"] = TwitchClientID; request.Headers["Client-ID"] = TwitchDetails.TwitchClientID;
WebResponse response = request.GetResponse(); WebResponse response = request.GetResponse();
Stream dataStream = response.GetResponseStream(); Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream); StreamReader reader = new StreamReader(dataStream);
@@ -74,16 +86,34 @@ namespace TwitchDesktopNotifications.Core
return JsonSerializer.Deserialize<T>(responseFromServer); return JsonSerializer.Deserialize<T>(responseFromServer);
} }
catch (TwitcherRefreshException ex)
{
OpenFailedNotification();
}
catch(Exception ex)
{
Logger.GetInstance().Writer.WriteLineAsync(ex.ToString());
}
return default(T);
}
public void FetchCurrentUser() public void FetchCurrentUser()
{ {
try try
{ {
DataStore.GetInstance().Store.UserData = MakeRequest<User>("helix/users").Data[0]; var UserList = MakeRequest<User>("helix/users");
if (UserList.Data.Count > 0) {
DataStore.GetInstance().Store.UserData = UserList.Data[0];
DataStore.GetInstance().Save(); DataStore.GetInstance().Save();
}catch(System.Exception ex) }
}
catch (TwitcherRefreshException ex)
{ {
Environment.Exit(1); OpenFailedNotification();
}
catch (Exception ex)
{
Logger.GetInstance().Writer.WriteLineAsync(ex.ToString());
} }
} }
@@ -91,10 +121,19 @@ namespace TwitchDesktopNotifications.Core
{ {
try try
{ {
return MakeRequest<User>("helix/users?id=" + user_id).Data[0]; var Response = MakeRequest<User>("helix/users?id=" + user_id);
}catch(System.Exception ex) if (Response.Data.Count > 0)
{ {
Environment.Exit(1); return Response.Data[0];
}
}
catch (TwitcherRefreshException ex)
{
OpenFailedNotification();
}
catch (Exception ex)
{
Logger.GetInstance().Writer.WriteLineAsync(ex.ToString());
} }
return null; return null;
} }
@@ -112,7 +151,7 @@ namespace TwitchDesktopNotifications.Core
string QueryUrl = "helix/streams/followed?first=100&user_id=" + DataStore.GetInstance().Store.UserData.UserId; string QueryUrl = "helix/streams/followed?first=100&user_id=" + DataStore.GetInstance().Store.UserData.UserId;
Streams following = MakeRequest<Streams>(QueryUrl); Streams following = MakeRequest<Streams>(QueryUrl);
if (currentlyLive != null) if (following != null && currentlyLive != null)
{ {
following.Data.ForEach(x => following.Data.ForEach(x =>
{ {
@@ -126,24 +165,32 @@ namespace TwitchDesktopNotifications.Core
if (!found) if (!found)
{ {
UserData streamer = FetchUserData(x.UserId); UserData streamer = FetchUserData(x.UserId);
UIStreamer.GetCreateStreamer(x.DisplayName);
Notification.GetInstance().sendNotification(streamer.DisplayName, "https://twitch.tv/" + streamer.UserName, streamer.ProfileImage, x.ThumbnailImg, x.Title); Notification.GetInstance().sendNotification(streamer.DisplayName, "https://twitch.tv/" + streamer.UserName, streamer.ProfileImage, x.ThumbnailImg, x.Title);
} }
}); });
} }
currentlyLive = following.Data; currentlyLive = following.Data;
}catch(System.Exception ex)
{
Environment.Exit(1);
} }
} catch (TwitcherRefreshException ex)
{
OpenFailedNotification();
}
catch (Exception ex)
{
Logger.GetInstance().Writer.WriteLineAsync(ex.ToString());
}
}
public void Refresh() public void Refresh()
{
try
{ {
Dictionary<string, string> postData = new Dictionary<string, string>(); Dictionary<string, string> postData = new Dictionary<string, string>();
postData["client_id"] = TwitchClientID; postData["client_id"] = TwitchDetails.TwitchClientID;
postData["client_secret"] = TwitchClientSecret; postData["client_secret"] = TwitchDetails.TwitchClientSecret;
postData["grant_type"] = "refresh_token"; postData["grant_type"] = "refresh_token";
postData["refresh_token"] = DataStore.GetInstance().Store.Authentication.RefreshToken; postData["refresh_token"] = DataStore.GetInstance().Store.Authentication.RefreshToken;
@@ -168,6 +215,10 @@ namespace TwitchDesktopNotifications.Core
DateTime unixStart = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc); DateTime unixStart = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc);
DataStore.GetInstance().Store.Authentication.ExpiresAt = (long)Math.Floor((DateTime.Now.AddSeconds(DataStore.GetInstance().Store.Authentication.ExpiresSeconds) - unixStart).TotalMilliseconds); DataStore.GetInstance().Store.Authentication.ExpiresAt = (long)Math.Floor((DateTime.Now.AddSeconds(DataStore.GetInstance().Store.Authentication.ExpiresSeconds) - unixStart).TotalMilliseconds);
DataStore.GetInstance().Save(); DataStore.GetInstance().Save();
}catch(Exception e)
{
throw new TwitcherRefreshException("Unable to refresh", e);
}
} }
async public void BeginConnection() async public void BeginConnection()
@@ -176,7 +227,7 @@ namespace TwitchDesktopNotifications.Core
WebServer.GetInstance().TwitchState = guid; WebServer.GetInstance().TwitchState = guid;
Process myProcess = new Process(); Process myProcess = new Process();
myProcess.StartInfo.UseShellExecute = true; myProcess.StartInfo.UseShellExecute = true;
myProcess.StartInfo.FileName = String.Format("https://id.twitch.tv/oauth2/authorize?&redirect_uri=http://localhost:32584/twitchRedirect&scope=user:read:subscriptions%20user:read:follows%20user:read:email%20openid&response_type=code&state={0}&nonce={1}&client_id={2}", guid, guid, TwitchClientID); myProcess.StartInfo.FileName = String.Format("https://id.twitch.tv/oauth2/authorize?&redirect_uri=http://localhost:32584/twitchRedirect&scope=user:read:subscriptions%20user:read:follows%20user:read:email%20openid&response_type=code&state={0}&nonce={1}&client_id={2}", guid, guid, TwitchDetails.TwitchClientID);
myProcess.Start(); myProcess.Start();
} }
@@ -184,8 +235,8 @@ namespace TwitchDesktopNotifications.Core
{ {
Dictionary<string, string> postData = new Dictionary<string, string>(); Dictionary<string, string> postData = new Dictionary<string, string>();
postData["client_id"] = TwitchClientID; postData["client_id"] = TwitchDetails.TwitchClientID;
postData["client_secret"] = TwitchClientSecret; postData["client_secret"] = TwitchDetails.TwitchClientSecret;
postData["grant_type"] = "authorization_code"; postData["grant_type"] = "authorization_code";
postData["redirect_uri"] = "http://localhost:32584/twitchRedirect"; postData["redirect_uri"] = "http://localhost:32584/twitchRedirect";
postData["code"] = code; postData["code"] = code;

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Documents;
namespace TwitchDesktopNotifications.Core
{
public class UIStreamer
{
public UIStreamer() {
}
public static UIStreamer GetCreateStreamer(string name)
{
if (DataStore.GetInstance().Store.SteamersToIgnore == null)
{
DataStore.GetInstance().Store.SteamersToIgnore = new JsonStructure.SteamersToIgnore();
}
if (DataStore.GetInstance().Store.SteamersToIgnore.Streamers == null)
{
DataStore.GetInstance().Store.SteamersToIgnore.Streamers = new List<UIStreamer>();
}
UIStreamer strmr = null;
try
{
strmr = DataStore.GetInstance().Store.SteamersToIgnore.Streamers.Where((UIStreamer strmr) => strmr.Name == name).First();
}
catch { }
finally
{
if (strmr == null)
{
strmr = new UIStreamer() { IsIgnored = false, Name = name };
DataStore.GetInstance().Store.SteamersToIgnore.Streamers.Add(strmr);
DataStore.GetInstance().Save();
}
}
return strmr;
}
[JsonIgnore]
public List<UIStreamer> StreamersToIgnore
{
get
{
return DataStore.GetInstance().Store.SteamersToIgnore.Streamers;
}
}
[JsonPropertyName("IsIgnored")]
public bool IsIgnored { get; set; } = false;
[JsonPropertyName("StreamerName")]
public string Name { get; set; }
[JsonIgnore]
public string Link {
get {
return String.Format("https://www.twitch.tv/{0}", Name);
}
}
}
}

View File

@@ -5,23 +5,12 @@ using System.IO;
namespace TwitchDesktopNotifications.Core namespace TwitchDesktopNotifications.Core
{ {
internal class WebServer public class WebServer : SingletonFactory<WebServer>, Singleton
{ {
public int Port = 32584; public int Port = 32584;
private HttpListener listener; private HttpListener listener;
private static WebServer Instance;
public static WebServer GetInstance()
{
if(Instance == null)
{
Instance= new WebServer();
}
return Instance;
}
public String TwitchCode { get; private set; } public String TwitchCode { get; private set; }
public String TwitchState { get; set; } public String TwitchState { get; set; }

View File

@@ -1,14 +1,12 @@
using System.Text.Json; using System.Text.Json;
using TwitchDesktopNotifications.JsonStructure; using TwitchDesktopNotifications.JsonStructure;
using System.IO; using System.IO;
using TwitchDesktopNotifications.Core;
namespace TwitchDesktopNotifications namespace TwitchDesktopNotifications
{ {
internal class DataStore public class DataStore : SingletonFactory<DataStore>, Singleton
{ {
private DataStore() { }
public static DataStore Instance { get; private set; }
private Store _store; private Store _store;
public JsonStructure.Store Store { public JsonStructure.Store Store {
get { get {
@@ -25,41 +23,37 @@ namespace TwitchDesktopNotifications
public bool isLoaded { get; private set; } public bool isLoaded { get; private set; }
public static DataStore GetInstance()
{
if(Instance == null)
{
Instance = new DataStore();
}
return Instance;
}
public void Save() public void Save()
{ {
String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify"); String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify");
String FileName = "store.json"; String FileName = "store.json";
string fileContent = JsonSerializer.Serialize<JsonStructure.Store>(Store); string fileContent = JsonSerializer.Serialize<JsonStructure.Store>(Store);
Directory.CreateDirectory(FilePath); Directory.CreateDirectory(FilePath);
File.WriteAllText(FilePath + "/" + FileName, fileContent); File.WriteAllText(FilePath + "/" + FileName, fileContent);
} }
public void Load() { public void Load() {
if (!isLoaded)
{
String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify"); String FilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TwitchNotify");
String FileName = "store.json"; String FileName = "store.json";
Directory.CreateDirectory(FilePath); Directory.CreateDirectory(FilePath);
if(File.Exists(FilePath+"/"+ FileName)) { if (File.Exists(FilePath + "/" + FileName))
{
string fileContent = File.ReadAllText(FilePath+"/"+ FileName); string fileContent = File.ReadAllText(FilePath + "/" + FileName);
Store = JsonSerializer.Deserialize<JsonStructure.Store>(fileContent); Store = JsonSerializer.Deserialize<JsonStructure.Store>(fileContent);
} }
else else
{ {
Store = new JsonStructure.Store(); Store = new JsonStructure.Store();
} }
isLoaded= true; isLoaded = true;
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure namespace TwitchDesktopNotifications.JsonStructure
{ {
internal class Authentication public class Authentication
{ {
[JsonPropertyName("access_token")] [JsonPropertyName("access_token")]
public string AccessToken { get; set; } public string AccessToken { get; set; }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class Pagination public class Pagination
{ {
public Pagination() { } public Pagination() { }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class Streams public class Streams
{ {
public Streams() { } public Streams() { }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class StreamsData public class StreamsData
{ {
public StreamsData() { } public StreamsData() { }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class User public class User
{ {
public User() { } public User() { }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class UserData public class UserData
{ {
public UserData() { } public UserData() { }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class UserFollows public class UserFollows
{ {
public UserFollows() { } public UserFollows() { }

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace TwitchDesktopNotifications.JsonStructure.Helix namespace TwitchDesktopNotifications.JsonStructure.Helix
{ {
internal class UsersFollowsData public class UsersFollowsData
{ {
public UsersFollowsData() { } public UsersFollowsData() { }

View File

@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
using TwitchDesktopNotifications.Core;
namespace TwitchDesktopNotifications.JsonStructure
{
public class SteamersToIgnore
{
[JsonPropertyName("IgnoredStreamers")]
public List<UIStreamer> Streamers { get; set; } = new List<UIStreamer>();
}
}

View File

@@ -8,14 +8,28 @@ using TwitchDesktopNotifications.JsonStructure.Helix;
namespace TwitchDesktopNotifications.JsonStructure namespace TwitchDesktopNotifications.JsonStructure
{ {
internal class Store public class Store
{ {
public Store() { } public Store() { }
[JsonPropertyName("ignore")]
public SteamersToIgnore ignore { get; set; }
[JsonPropertyName("authentication")] [JsonPropertyName("authentication")]
public Authentication Authentication { get; set; } public Authentication Authentication { get; set; }
[JsonPropertyName("user_data")] [JsonPropertyName("user_data")]
public UserData UserData { get; set; } public UserData UserData { get; set; }
[JsonIgnore]
public SteamersToIgnore SteamersToIgnore {
get {
if(ignore == null) { ignore = new SteamersToIgnore(); }
return ignore;
}
set {
ignore = value;
}
}
} }
} }

View File

@@ -0,0 +1,50 @@
<Window x:Class="TwitchDesktopNotifications.ManageIgnores"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TwitchDesktopNotifications"
xmlns:core="clr-namespace:TwitchDesktopNotifications.Core"
mc:Ignorable="d"
Title="Manage Ignored Streamers" Height="435" Width="395" ResizeMode="CanResizeWithGrip">
<Window.DataContext>
<core:UIStreamer />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="30" />
<RowDefinition Height="10" />
<RowDefinition Height="*" />
<RowDefinition Height="10" />
<RowDefinition Height="25" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<DataGrid
Grid.Column="1"
Grid.RowSpan="1" Grid.Row="3"
CanUserResizeColumns="False"
x:Name="dgrdIgnore" ItemsSource="{Binding StreamersToIgnore}" SelectionChanged="dgrdIgnore_SelectionChanged" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="Ignore" Binding="{Binding IsIgnored}" IsReadOnly="False" Width="50">
</DataGridCheckBoxColumn>
<DataGridTextColumn Header="Streamer Name" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridHyperlinkColumn Header="Streamer Link" Binding="{Binding Link}" Width="*">
<DataGridHyperlinkColumn.ElementStyle>
<Style TargetType="TextBlock">
<EventSetter Event="Hyperlink.Click" Handler="HyperLink_Click"/>
</Style>
</DataGridHyperlinkColumn.ElementStyle>
</DataGridHyperlinkColumn>
</DataGrid.Columns>
</DataGrid>
<Button Content="Close" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Right" Width="100" Grid.Row="5" Click="CloseBtn_Click" />
<TextBlock Grid.Column="1" Grid.Row="1" TextWrapping="Wrap" Text="Changes to the ignore list are automatically saved." VerticalAlignment="Top"/>
<TextBlock Grid.Column="1" Grid.Row="1" TextWrapping="Wrap" Text="If you can't see a streamer Twitch Notify has not seen them." VerticalAlignment="Bottom"/>
</Grid>
</Window>

View File

@@ -0,0 +1,45 @@
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Forms;
using System.Windows.Threading;
using TwitchDesktopNotifications.Core;
namespace TwitchDesktopNotifications
{
/// <summary>
/// Interaction logic for ManageIgnores.xaml
/// </summary>
public partial class ManageIgnores : Window
{
public ManageIgnores()
{
InitializeComponent();
}
List<UIStreamer> StreamersToIgnore = DataStore.GetInstance().Store.SteamersToIgnore.Streamers;
private void CloseBtn_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
private void dgrdIgnore_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(() => dgrdIgnore.UnselectAll()));
}
private void HyperLink_Click(object sender, RoutedEventArgs e)
{
string link = ((Hyperlink)e.OriginalSource).NavigateUri.OriginalString;
var psi = new ProcessStartInfo
{
FileName = link,
UseShellExecute = true
};
Process.Start(psi);
}
}
}

View File

@@ -1,5 +1,6 @@
// See https://aka.ms/new-console-template for more information // See https://aka.ms/new-console-template for more information
using System.Drawing; using System.Drawing;
using System.Reflection.Metadata;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json; using System.Text.Json;
using System.Windows.Controls; using System.Windows.Controls;
@@ -17,7 +18,7 @@ internal class Program
private static NotifyIcon notifyIcon; private static NotifyIcon notifyIcon;
private static ContextMenuStrip cms; private static ContextMenuStrip cms;
private static ManageIgnores manageIgnores;
public static void Ws_CodeRecived(object? sender, EventArgs e) public static void Ws_CodeRecived(object? sender, EventArgs e)
{ {
@@ -45,6 +46,21 @@ internal class Program
TriggerAuthentication(); TriggerAuthentication();
} }
protected static void ManageIgnores_Click(object? sender, System.EventArgs e)
{
if (manageIgnores == null) {
manageIgnores = new ManageIgnores();
manageIgnores.Closed += ManageIgnores_Closed;
}
manageIgnores.Show();
manageIgnores.Focus();
}
private static void ManageIgnores_Closed(object? sender, EventArgs e)
{
manageIgnores = null;
}
protected static void Quit_Click(object? sender, System.EventArgs e) protected static void Quit_Click(object? sender, System.EventArgs e)
{ {
notifyIcon.Visible = false; notifyIcon.Visible = false;
@@ -60,28 +76,27 @@ internal class Program
TwitchFetcher.GetInstance().BeginConnection(); TwitchFetcher.GetInstance().BeginConnection();
if (DataStore.GetInstance().Store.Authentication == null) if (DataStore.GetInstance().Store.Authentication == null)
{ {
var timerForCrash = new PeriodicTimer(TimeSpan.FromSeconds(10));
await timerForCrash.WaitForNextTickAsync();
if (isConnecting) if (isConnecting)
{ {
MessageBox.Show("Twitch Connection not authenticated Exiting for saftey.", "Twitch Notify"); TwitchFetcher.GetInstance().OpenFailedNotification();
notifyIcon.Visible = false;
notifyIcon.Dispose();
Environment.Exit(1);
} }
} }
} }
private static async Task Main(string[] args) [STAThread]
private static void Main(string[] args)
{
try
{ {
var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
notifyIcon = new NotifyIcon(); notifyIcon = new NotifyIcon();
notifyIcon.Icon = new Icon("Assets/icon.ico"); notifyIcon.Icon = new Icon("Assets/icon.ico");
notifyIcon.Text = "Notify"; notifyIcon.Text = "Twitch Notify";
cms = new ContextMenuStrip(); cms = new ContextMenuStrip();
cms.Items.Add(new ToolStripMenuItem("Manage Ignores", null, new EventHandler(ManageIgnores_Click)));
cms.Items.Add(new ToolStripSeparator());
cms.Items.Add(new ToolStripMenuItem("Reconnect", null, new EventHandler(Reconnect_Click))); cms.Items.Add(new ToolStripMenuItem("Reconnect", null, new EventHandler(Reconnect_Click)));
cms.Items.Add(new ToolStripSeparator()); cms.Items.Add(new ToolStripSeparator());
cms.Items.Add(new ToolStripMenuItem("Quit", null, new EventHandler(Quit_Click), "Quit")); cms.Items.Add(new ToolStripMenuItem("Quit", null, new EventHandler(Quit_Click), "Quit"));
@@ -94,16 +109,25 @@ internal class Program
TriggerAuthentication(); TriggerAuthentication();
} }
new Thread(() => Thread thread = new Thread(() =>
{ {
while (true) while (true)
{ {
Thread.Sleep(10000); Thread.Sleep(10000);
if (DataStore.GetInstance().Store != null)
{
TwitchFetcher.GetInstance().GetLiveFollowingUsers(); TwitchFetcher.GetInstance().GetLiveFollowingUsers();
} }
}).Start(); }
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Application.Run(); Application.Run();
}
catch (Exception e) {
Logger.GetInstance().Writer.WriteLineAsync(e.ToString());
}
} }
} }

View File

@@ -0,0 +1,27 @@
<Window x:Class="TwitchDesktopNotifications.ReconnectionNeeded"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TwitchDesktopNotifications"
mc:Ignorable="d"
Title="Twitch Disconnected" Height="140" Width="335" x:Name="reconnectionNeededWin">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="10" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="40" />
<RowDefinition Height="10" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Center" Text="We have been unable to refresh your twitch connection" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="3"/>
<TextBlock TextWrapping="Wrap" VerticalAlignment="Center" TextAlignment="Center" Text="Don't worry you just need to reconnect" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="3"/>
<Button Content="Thanks" HorizontalAlignment="Center" VerticalAlignment="Top" Grid.Column="2" Grid.Row="3" Padding="5" Click="Button_Click"/>
</Grid>
</Window>

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace TwitchDesktopNotifications
{
/// <summary>
/// Interaction logic for ReconnectionNeeded.xaml
/// </summary>
public partial class ReconnectionNeeded : Window
{
public ReconnectionNeeded()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
@@ -11,6 +11,7 @@
<AssemblyName>Twitch Notify</AssemblyName> <AssemblyName>Twitch Notify</AssemblyName>
<UseWPF>True</UseWPF> <UseWPF>True</UseWPF>
<UseWindowsForms>True</UseWindowsForms> <UseWindowsForms>True</UseWindowsForms>
<UserSecretsId>2dfb7064-609b-41c3-a80d-a9e4d842a55d</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
<ItemGroup>
<Compile Update="ManageIgnores.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="ReconnectionNeeded.xaml.cs">
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Page Update="ManageIgnores.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="ReconnectionNeeded.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>