1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-09-14 10:32:10 +02:00

Compare commits

..

24 Commits
1.9 ... 1.9.1

Author SHA1 Message Date
52f1f4c4eb Release 1.9.1 2017-09-05 22:55:32 +02:00
6c1782a038 Fix some twitter links (/signup, /tos, /privacy) having context menu for accounts 2017-09-05 22:43:07 +02:00
8b8f5f5473 Fix login page links opening in the app instead of external browser 2017-09-05 22:32:11 +02:00
61d3ed891a Update t.co bypass to work for media and bio urls 2017-09-05 21:47:22 +02:00
b1abf87320 Revert TweetDeck scrollbar color & fix notification scrollbar with 'Theme color tweaks' on 2017-09-05 14:55:29 +02:00
9aedfc2799 Fix scrollbar in Options not disappearing when switching tabs while animating 2017-09-04 17:52:01 +02:00
ad6240a067 Refactor delegating multiple events at once in code.js 2017-09-04 03:02:30 +02:00
9539eb076a Fix heart icon size and animation 2017-09-03 01:45:59 +02:00
c808e7bd83 Fix calling OpenExternalBrowser from non-UI threads, causing crashes or errors 2017-09-02 21:49:45 +02:00
13ea388f5e Fix upload dialog to include 'All Supported Formats' instead of separating them 2017-09-02 20:51:58 +02:00
c46dc0f1a3 Fix 'Open link in browser' not bypassing t.co 2017-09-02 20:30:54 +02:00
2ae311007d Make https scheme check first because https rocks 2017-09-02 13:54:54 +02:00
9344e02bff Add a privacy warning when opening a t.co link in case the bypass fails 2017-09-02 13:47:43 +02:00
40ad836fc3 Bypass t.co on click & fix right-clicking t.co links in notifications 2017-09-02 13:07:40 +02:00
e8604a261d Replace 'new EventArgs()' with 'EventArgs.Empty' 2017-09-01 14:34:23 +02:00
2a41d21a29 Add unit tests for UserConfig 2017-08-31 21:38:26 +02:00
4c62aa067b Update unit test generated file names 2017-08-31 19:45:56 +02:00
49db3074c6 Rewrite IO handling in unit tests 2017-08-31 19:33:24 +02:00
f5e3b34f30 Tweak border radius on inputs in column settings and custom timelines 2017-08-31 16:03:38 +02:00
f0affa4aec Implement 'Save all images as...' for images in quoted tweets 2017-08-31 15:34:05 +02:00
4f5075ac54 Fix 'Save image as...' usernames in quoted tweets & more refactoring 2017-08-31 15:26:03 +02:00
20f0445b10 Replace FormNotificationBase.ChirpId with CanViewDetail that checks ColumnId too 2017-08-31 14:52:38 +02:00
c77c974455 Rename more parameters and fields in TweetDeckBridge 2017-08-31 14:40:10 +02:00
44397b2d45 Fix parameter name in TweetDeckBridge 2017-08-31 14:07:46 +02:00
28 changed files with 529 additions and 273 deletions

View File

@@ -105,7 +105,7 @@ namespace TweetDuck.Configuration{
set{
if (_muteNotifications != value){
_muteNotifications = value;
MuteToggled?.Invoke(this, new EventArgs());
MuteToggled?.Invoke(this, EventArgs.Empty);
}
}
}
@@ -116,7 +116,7 @@ namespace TweetDuck.Configuration{
set{
if (_zoomLevel != value){
_zoomLevel = value;
ZoomLevelChanged?.Invoke(this, new EventArgs());
ZoomLevelChanged?.Invoke(this, EventArgs.Empty);
}
}
}
@@ -129,7 +129,7 @@ namespace TweetDuck.Configuration{
set{
if (_trayBehavior != value){
_trayBehavior = value;
TrayBehaviorChanged?.Invoke(this, new EventArgs());
TrayBehaviorChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -11,15 +11,18 @@ using TweetDuck.Resources;
namespace TweetDuck.Core.Bridge{
sealed class TweetDeckBridge{
public static string LastHighlightedTweet = string.Empty;
public static string LastHighlightedQuotedTweet = string.Empty;
public static string LastHighlightedTweetAuthor = string.Empty;
public static string[] LastHighlightedTweetImages = StringUtils.EmptyArray;
public static Dictionary<string, string> SessionData = new Dictionary<string, string>(2);
public static string LastHighlightedTweetUrl = string.Empty;
public static string LastHighlightedQuoteUrl = string.Empty;
private static string LastHighlightedTweetAuthors = string.Empty;
private static string LastHighlightedTweetImages = string.Empty;
public static string[] LastHighlightedTweetAuthorsArray => LastHighlightedTweetAuthors.Split(';');
public static string[] LastHighlightedTweetImagesArray => LastHighlightedTweetImages.Split(';');
private static readonly Dictionary<string, string> SessionData = new Dictionary<string, string>(2);
public static void ResetStaticProperties(){
LastHighlightedTweet = LastHighlightedQuotedTweet = LastHighlightedTweetAuthor = string.Empty;
LastHighlightedTweetImages = StringUtils.EmptyArray;
LastHighlightedTweetUrl = LastHighlightedQuoteUrl = LastHighlightedTweetAuthors = LastHighlightedTweetImages = string.Empty;
}
public static void RestoreSessionData(IFrame frame){
@@ -59,12 +62,12 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(() => ContextMenuBase.SetContextInfo(type, link));
}
public void SetLastHighlightedTweet(string link, string quotedLink, string author, string imageList){
public void SetLastHighlightedTweet(string tweetUrl, string quoteUrl, string authors, string imageList){
form.InvokeAsyncSafe(() => {
LastHighlightedTweet = link;
LastHighlightedQuotedTweet = quotedLink;
LastHighlightedTweetAuthor = author;
LastHighlightedTweetImages = imageList.Split(';');
LastHighlightedTweetUrl = tweetUrl;
LastHighlightedQuoteUrl = quoteUrl;
LastHighlightedTweetAuthors = authors;
LastHighlightedTweetImages = imageList;
});
}
@@ -72,10 +75,10 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(form.OpenContextMenu);
}
public void OnTweetPopup(string columnKey, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl){
public void OnTweetPopup(string columnId, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl){
notification.InvokeAsyncSafe(() => {
form.OnTweetNotification();
notification.ShowNotification(new TweetNotification(columnKey, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
notification.ShowNotification(new TweetNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
});
}
@@ -117,12 +120,12 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
}
public int GetIdleSeconds(){
return NativeMethods.GetIdleSeconds();
public void OpenBrowser(string url){
form.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
}
public void OpenBrowser(string url){
BrowserUtils.OpenExternalBrowser(url);
public int GetIdleSeconds(){
return NativeMethods.GetIdleSeconds();
}
public void Alert(string type, string contents){

View File

@@ -84,6 +84,7 @@ namespace TweetDuck.Core{
this.notification.Show();
this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
DialogHandler = new FileDialogHandler(),
MenuHandler = new ContextMenuBrowser(this),
JsDialogHandler = new JavaScriptDialogHandler(),
KeyboardHandler = new KeyboardHandlerBrowser(this),
@@ -377,7 +378,7 @@ namespace TweetDuck.Core{
if (isLoaded){
if (m.Msg == Program.WindowRestoreMessage){
if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
trayIcon_ClickRestore(trayIcon, new EventArgs());
trayIcon_ClickRestore(trayIcon, EventArgs.Empty);
}
return;

View File

@@ -7,6 +7,7 @@ using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using System.Collections.Generic;
using System.Linq;
namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{
@@ -35,25 +36,19 @@ namespace TweetDuck.Core.Handling{
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand)26505;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand)26506;
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand)26599;
private readonly Form form;
private string lastHighlightedTweetAuthor;
private string[] lastHighlightedTweetAuthors;
private string[] lastHighlightedTweetImageList;
protected ContextMenuBase(Form form){
this.form = form;
}
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
lastHighlightedTweetAuthor = string.Empty;
lastHighlightedTweetAuthors = StringUtils.EmptyArray;
lastHighlightedTweetImageList = StringUtils.EmptyArray;
ContextInfo = default(KeyValuePair<string, string>);
}
else{
lastHighlightedTweetAuthor = TweetDeckBridge.LastHighlightedTweetAuthor;
lastHighlightedTweetImageList = TweetDeckBridge.LastHighlightedTweetImages;
lastHighlightedTweetAuthors = TweetDeckBridge.LastHighlightedTweetAuthorsArray;
lastHighlightedTweetImageList = TweetDeckBridge.LastHighlightedTweetImagesArray;
}
bool hasTweetImage = IsImage;
@@ -99,24 +94,24 @@ namespace TweetDuck.Core.Handling{
public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
switch(commandId){
case MenuOpenLinkUrl:
BrowserUtils.OpenExternalBrowser(parameters.LinkUrl);
OpenBrowser(browserControl.AsControl(), IsLink ? ContextInfo.Value : parameters.LinkUrl);
break;
case MenuCopyLinkUrl:
SetClipboardText(IsLink ? ContextInfo.Value : parameters.UnfilteredLinkUrl);
SetClipboardText(browserControl.AsControl(), IsLink ? ContextInfo.Value : parameters.UnfilteredLinkUrl);
break;
case MenuCopyUsername:
Match match = TwitterUtils.RegexAccount.Match(parameters.UnfilteredLinkUrl);
SetClipboardText(match.Success ? match.Groups[1].Value : parameters.UnfilteredLinkUrl);
SetClipboardText(browserControl.AsControl(), match.Success ? match.Groups[1].Value : parameters.UnfilteredLinkUrl);
break;
case MenuOpenMediaUrl:
BrowserUtils.OpenExternalBrowser(TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
OpenBrowser(browserControl.AsControl(), TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
break;
case MenuCopyMediaUrl:
SetClipboardText(TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
SetClipboardText(browserControl.AsControl(), TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
break;
case MenuSaveMedia:
@@ -124,13 +119,13 @@ namespace TweetDuck.Core.Handling{
TwitterUtils.DownloadVideo(GetMediaLink(parameters));
}
else{
TwitterUtils.DownloadImage(GetMediaLink(parameters), lastHighlightedTweetAuthor, ImageQuality);
TwitterUtils.DownloadImage(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality);
}
break;
case MenuSaveTweetImages:
TwitterUtils.DownloadImages(lastHighlightedTweetImageList, lastHighlightedTweetAuthor, ImageQuality);
TwitterUtils.DownloadImages(lastHighlightedTweetImageList, lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality);
break;
case MenuOpenDevTools:
@@ -149,8 +144,12 @@ namespace TweetDuck.Core.Handling{
return false;
}
protected void SetClipboardText(string text){
form.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
protected void OpenBrowser(Control control, string url){
control.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
}
protected void SetClipboardText(Control control, string text){
control.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
}
protected static void AddDebugMenuItems(IMenuModel model){

View File

@@ -26,10 +26,10 @@ namespace TweetDuck.Core.Handling{
private readonly FormBrowser form;
private string lastHighlightedTweet;
private string lastHighlightedQuotedTweet;
private string lastHighlightedTweetUrl;
private string lastHighlightedQuoteUrl;
public ContextMenuBrowser(FormBrowser form) : base(form){
public ContextMenuBrowser(FormBrowser form){
this.form = form;
}
@@ -46,20 +46,20 @@ namespace TweetDuck.Core.Handling{
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
lastHighlightedTweet = TweetDeckBridge.LastHighlightedTweet;
lastHighlightedQuotedTweet = TweetDeckBridge.LastHighlightedQuotedTweet;
lastHighlightedTweetUrl = TweetDeckBridge.LastHighlightedTweetUrl;
lastHighlightedQuoteUrl = TweetDeckBridge.LastHighlightedQuoteUrl;
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
lastHighlightedTweet = string.Empty;
lastHighlightedQuotedTweet = string.Empty;
lastHighlightedTweetUrl = string.Empty;
lastHighlightedQuoteUrl = string.Empty;
}
if (!string.IsNullOrEmpty(lastHighlightedTweet) && (parameters.TypeFlags & (ContextMenuType.Editable | ContextMenuType.Selection)) == 0){
if (!string.IsNullOrEmpty(lastHighlightedTweetUrl) && (parameters.TypeFlags & (ContextMenuType.Editable | ContextMenuType.Selection)) == 0){
model.AddItem(MenuOpenTweetUrl, "Open tweet in browser");
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
model.AddItem(MenuScreenshotTweet, "Screenshot tweet to clipboard");
if (!string.IsNullOrEmpty(lastHighlightedQuotedTweet)){
if (!string.IsNullOrEmpty(lastHighlightedQuoteUrl)){
model.AddSeparator();
model.AddItem(MenuOpenQuotedTweetUrl, "Open quoted tweet in browser");
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
@@ -118,11 +118,11 @@ namespace TweetDuck.Core.Handling{
return true;
case MenuOpenTweetUrl:
BrowserUtils.OpenExternalBrowser(lastHighlightedTweet);
OpenBrowser(form, lastHighlightedTweetUrl);
return true;
case MenuCopyTweetUrl:
SetClipboardText(lastHighlightedTweet);
SetClipboardText(form, lastHighlightedTweetUrl);
return true;
case MenuScreenshotTweet:
@@ -130,11 +130,11 @@ namespace TweetDuck.Core.Handling{
return true;
case MenuOpenQuotedTweetUrl:
BrowserUtils.OpenExternalBrowser(lastHighlightedQuotedTweet);
OpenBrowser(form, lastHighlightedQuoteUrl);
return true;
case MenuCopyQuotedTweetUrl:
SetClipboardText(lastHighlightedQuotedTweet);
SetClipboardText(form, lastHighlightedQuoteUrl);
return true;
}

View File

@@ -13,7 +13,7 @@ namespace TweetDuck.Core.Handling{
private readonly FormNotificationBase form;
private readonly bool enableCustomMenu;
public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu) : base(form){
public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu){
this.form = form;
this.enableCustomMenu = enableCustomMenu;
}
@@ -29,7 +29,7 @@ namespace TweetDuck.Core.Handling{
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (enableCustomMenu){
if (!string.IsNullOrEmpty(form.CurrentChirpId)){
if (form.CanViewDetail){
model.AddItem(MenuViewDetail, "View detail");
}
@@ -76,11 +76,11 @@ namespace TweetDuck.Core.Handling{
return true;
case MenuCopyTweetUrl:
SetClipboardText(form.CurrentTweetUrl);
SetClipboardText(form, form.CurrentTweetUrl);
return true;
case MenuCopyQuotedTweetUrl:
SetClipboardText(form.CurrentQuoteUrl);
SetClipboardText(form, form.CurrentQuoteUrl);
return true;
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using CefSharp;
namespace TweetDuck.Core.Handling.General{
sealed class FileDialogHandler : IDialogHandler{
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback){
CefFileDialogMode dialogType = mode & CefFileDialogMode.TypeMask;
if (dialogType == CefFileDialogMode.Open || dialogType == CefFileDialogMode.OpenMultiple){
string allFilters = string.Join(";", acceptFilters.Select(filter => "*"+filter));
using(OpenFileDialog dialog = new OpenFileDialog{
AutoUpgradeEnabled = true,
DereferenceLinks = true,
Multiselect = dialogType == CefFileDialogMode.OpenMultiple,
Title = "Open Files",
Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*"
}){
if (dialog.ShowDialog() == DialogResult.OK){
callback.Continue(acceptFilters.FindIndex(filter => filter == Path.GetExtension(dialog.FileName)), dialog.FileNames.ToList());
}
else{
callback.Cancel();
}
callback.Dispose();
}
return true;
}
else{
callback.Dispose();
return false;
}
}
}
}

View File

@@ -1,4 +1,5 @@
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling.General{
@@ -11,7 +12,7 @@ namespace TweetDuck.Core.Handling.General{
case WindowOpenDisposition.NewForegroundTab:
case WindowOpenDisposition.NewPopup:
case WindowOpenDisposition.NewWindow:
BrowserUtils.OpenExternalBrowser(targetUrl);
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
return true;
default:

View File

@@ -82,9 +82,10 @@ namespace TweetDuck.Core.Notification{
private TweetNotification currentNotification;
private int pauseCounter;
public string CurrentChirpId => currentNotification?.ChirpId;
public string CurrentTweetUrl => currentNotification?.TweetUrl;
public string CurrentQuoteUrl => currentNotification?.QuoteUrl;
public bool CanViewDetail => currentNotification != null && !string.IsNullOrEmpty(currentNotification.ColumnId) && !string.IsNullOrEmpty(currentNotification.ChirpId);
public bool IsPaused => pauseCounter > 0;
public bool FreezeTimer { get; set; }
@@ -144,7 +145,7 @@ namespace TweetDuck.Core.Notification{
private void Browser_IsBrowserInitializedChanged(object sender, IsBrowserInitializedChangedEventArgs e){
if (e.IsBrowserInitialized){
Initialized?.Invoke(this, new EventArgs());
Initialized?.Invoke(this, EventArgs.Empty);
int identifier = browser.GetBrowser().Identifier;
Disposed += (sender2, args2) => BrowserProcesses.Forget(identifier);

View File

@@ -128,11 +128,16 @@ namespace TweetDuck.Core.Other{
tab.Control.OnReady();
}
panelContents.VerticalScroll.Enabled = false; // required to stop animation that would otherwise break everything
panelContents.PerformLayout();
panelContents.SuspendLayout();
panelContents.VerticalScroll.Value = 0; // https://gfycat.com/GrotesqueTastyAstarte
panelContents.Controls.Clear();
panelContents.Controls.Add(tab.Control);
panelContents.ResumeLayout(true);
panelContents.VerticalScroll.Enabled = true;
panelContents.Focus();
currentTab = tab;

View File

@@ -173,7 +173,7 @@ namespace TweetDuck.Core.Other.Management{
}
private void TriggerProcessExitEventUnsafe(){
ProcessExited?.Invoke(this, new EventArgs());
ProcessExited?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -38,7 +38,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
tbDataFolder.TextChanged += control_Change;
}
control_Change(this, new EventArgs());
control_Change(this, EventArgs.Empty);
Text = Program.BrandName+" Arguments";
}

View File

@@ -24,7 +24,7 @@ namespace TweetDuck.Core.Other.Settings{
labelVolumeValue.Text = trackBarVolume.Value+"%";
tbCustomSound.Text = Config.NotificationSoundPath;
tbCustomSound_TextChanged(tbCustomSound, new EventArgs());
tbCustomSound_TextChanged(tbCustomSound, EventArgs.Empty);
Disposed += (sender, args) => soundNotification.Dispose();
}

View File

@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Net;
using System.Windows.Forms;
using CefSharp.WinForms;
using TweetDuck.Core.Other;
namespace TweetDuck.Core.Utils{
@@ -42,23 +43,46 @@ namespace TweetDuck.Core.Utils{
}
}
public static bool IsValidUrl(string url){
public static ChromiumWebBrowser AsControl(this IWebBrowser browserControl){
return (ChromiumWebBrowser)browserControl;
}
private const string TwitterTrackingUrl = "t.co";
public enum UrlCheckResult{
Invalid, Tracking, Fine
}
public static UrlCheckResult CheckUrl(string url){
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)){
string scheme = uri.Scheme;
return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto;
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto){
return uri.Host == TwitterTrackingUrl ? UrlCheckResult.Tracking : UrlCheckResult.Fine;
}
}
return false;
return UrlCheckResult.Invalid;
}
public static void OpenExternalBrowser(string url){
if (string.IsNullOrWhiteSpace(url))return;
if (IsValidUrl(url)){
OpenExternalBrowserUnsafe(url);
}
else{
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
switch(CheckUrl(url)){
case UrlCheckResult.Fine:
OpenExternalBrowserUnsafe(url);
break;
case UrlCheckResult.Tracking:
if (FormMessage.Warning("Blocked URL", "TweetDuck has blocked a tracking url due to privacy concerns. Do you want to visit it anyway?\n"+url, FormMessage.Yes, FormMessage.No)){
OpenExternalBrowserUnsafe(url);
}
break;
case UrlCheckResult.Invalid:
FormMessage.Warning("Blocked URL", "A potentially malicious URL was blocked from opening:\n"+url, FormMessage.OK);
break;
}
}

View File

@@ -14,7 +14,7 @@ namespace TweetDuck.Core.Utils{
public static readonly Color BackgroundColor = Color.FromArgb(28, 99, 153);
public const string BackgroundColorFix = "let e=document.createElement('style');document.head.appendChild(e);e.innerHTML='body::before{background:#1c6399!important}'";
private static readonly Lazy<Regex> RegexAccountLazy = new Lazy<Regex>(() => new Regex(@"^https?://twitter\.com/([^/]+)/?$", RegexOptions.Compiled), false);
private static readonly Lazy<Regex> RegexAccountLazy = new Lazy<Regex>(() => new Regex(@"^https?://twitter\.com/(?!signup$|tos$|privacy$)([^/]+)/?$", RegexOptions.Compiled), false);
public static Regex RegexAccount => RegexAccountLazy.Value;
public static readonly string[] DictionaryWords = {

View File

@@ -21,7 +21,7 @@ namespace TweetDuck{
public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.9";
public const string VersionTag = "1.9.1";
public static readonly bool IsPortable = File.Exists("makeportable");

View File

@@ -334,17 +334,22 @@ enabled(){
this.css.insert(".txt-base-smallest:not(.icon), .txt-base-largest:not(.icon) { font-size: "+this.config.fontSize+" !important }");
this.css.insert(".avatar { border-radius: "+this.config.avatarRadius+"% !important }");
let notificationScrollbarColor = null;
if (this.config.themeColorTweaks){
switch(TD.settings.getTheme()){
case "dark":
this.css.insert(".app-content, .app-columns-container { background-color: #444448 }");
this.css.insert(".column-drag-handle { opacity: 0.5 }");
this.css.insert(".column-drag-handle:hover { opacity: 1 }");
this.css.insert(".scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb { background-color: #666 }");
notificationScrollbarColor = "666";
break;
case "light":
this.css.insert(".scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb { background-color: #d2d6da }");
this.css.insert(".app-columns-container.scroll-styled-h::-webkit-scrollbar-thumb:not(:hover) { background-color: #a5aeb5 }");
notificationScrollbarColor = "a5aeb5";
break;
}
}
@@ -460,6 +465,9 @@ enabled(){
.drawer .btn .icon, .app-header .btn .icon { line-height: 1em !important }
.column-header .column-type-icon { bottom: 26px !important }
.is-options-open .column-type-icon { bottom: 25px !important }
.tweet-action-item .icon-favorite-toggle { font-size: 16px !important; }
.tweet-action-item .heartsprite { top: -260% !important; left: -260% !important; transform: scale(0.4, 0.39) translateY(0.5px) !important; }
.tweet-footer { margin-top: 6px !important }`;
document.head.appendChild(this.icons);
@@ -511,6 +519,10 @@ ${this.config.revertIcons ? `
.icon-user-filled:before{content:"\\f035";font-family:tweetdeckold}
.icon-user-dd:before{content:"\\f01a";font-family:tweetdeckold}
` : ``}
${notificationScrollbarColor ? `
.scroll-styled-v::-webkit-scrollbar-thumb, .scroll-styled-h::-webkit-scrollbar-thumb { background-color: #${notificationScrollbarColor} }
` : ``}
</style>`);
};

View File

@@ -356,10 +356,9 @@
var prevMouseX = -1, prevMouseY = -1;
var tooltipTimer, tooltipDisplayed;
$(document.body).delegate("a[data-full-url]", "mouseenter mouseleave mousemove", function(e){
var me = $(this);
if (e.type === "mouseenter"){
$(document.body).delegate("a[data-full-url]", {
mouseenter: function(){
let me = $(this);
let text = me.text();
return if text.charCodeAt(text.length-1) !== 8230; // horizontal ellipsis
@@ -380,14 +379,13 @@
tooltipDisplayed = true;
}, 400);
}
}
else if (e.type === "mouseleave"){
if ($TDX.expandLinksOnHover){
let prevText = me.attr("td-prev-text");
if (prevText){
me.text(prevText);
}
},
mouseleave: function(){
let me = $(this);
if (me[0].hasAttribute("td-prev-text")){
me.text(me.attr("td-prev-text"));
}
window.clearTimeout(tooltipTimer);
@@ -396,10 +394,11 @@
tooltipDisplayed = false;
$TD.displayTooltip(null, false);
}
}
else if (e.type === "mousemove"){
},
mousemove: function(e){
if (tooltipDisplayed && (prevMouseX !== e.clientX || prevMouseY !== e.clientY)){
$TD.displayTooltip(me.attr("data-full-url"), false);
$TD.displayTooltip($(this).attr("data-full-url"), false);
prevMouseX = e.clientX;
prevMouseY = e.clientY;
}
@@ -408,7 +407,49 @@
})();
//
// Block: Allow bypassing of t.co and include media previews in context menus.
// Block: Bypass t.co when clicking links and media.
//
$(document.body).delegate("a[data-full-url]", "click", function(e){
$TD.openBrowser($(this).attr("data-full-url"));
e.preventDefault();
});
if (ensurePropertyExists(TD, "services", "TwitterUser", "prototype", "fromJSONObject")){
let prevFunc = TD.services.TwitterUser.prototype.fromJSONObject;
TD.services.TwitterUser.prototype.fromJSONObject = function(){
let obj = prevFunc.apply(this, arguments);
let e = arguments[0].entities;
if (e && e.url && e.url.urls && e.url.urls.length && e.url.urls[0].expanded_url){
obj.url = e.url.urls[0].expanded_url;
}
return obj;
};
}
if (ensurePropertyExists(TD, "services", "TwitterMedia", "prototype", "fromMediaEntity")){
let prevFunc = TD.services.TwitterMedia.prototype.fromMediaEntity;
TD.services.TwitterMedia.prototype.fromMediaEntity = function(){
let obj = prevFunc.apply(this, arguments);
let e = arguments[0];
if (e.expanded_url){
if (obj.url === obj.shortUrl){
obj.shortUrl = e.expanded_url;
}
obj.url = e.expanded_url;
}
return obj;
};
}
//
// Block: Include additional information in context menus.
//
$(document.body).delegate("a", "contextmenu", function(){
let me = $(this)[0];
@@ -450,29 +491,34 @@
return !!highlightedColumnObj;
};
var updateHighlightedTweet = function(ele, obj, link, embeddedLink, author, imageList){
var updateHighlightedTweet = function(ele, obj, tweetUrl, quoteUrl, authors, imageList){
highlightedTweetEle = ele;
highlightedTweetObj = obj;
if (lastTweet !== link){
$TD.setLastHighlightedTweet(link, embeddedLink, author, imageList);
lastTweet = link;
if (lastTweet !== tweetUrl){
$TD.setLastHighlightedTweet(tweetUrl, quoteUrl, authors, imageList);
lastTweet = tweetUrl;
}
};
app.delegate("section.js-column", "mouseenter mouseleave", function(e){
if (e.type === "mouseenter"){
var processMedia = function(media){
return media.filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https+":small").join(";");
};
app.delegate("section.js-column", {
mouseenter: function(){
if (!highlightedColumnObj){
updateHighlightedColumn($(this));
}
}
else if (e.type === "mouseleave"){
},
mouseleave: function(){
updateHighlightedColumn(null);
}
});
app.delegate("article.js-stream-item", "mouseenter mouseleave", function(e){
if (e.type === "mouseenter"){
app.delegate("article.js-stream-item", {
mouseenter: function(){
let me = $(this);
return if !me[0].hasAttribute("data-account-key") || (!highlightedColumnObj && !updateHighlightedColumn(me.closest("section.js-column")));
@@ -480,19 +526,19 @@
return if !tweet;
if (tweet.chirpType === TD.services.ChirpBase.TWEET){
let link = tweet.getChirpURL();
let embedded = tweet.quotedTweet ? tweet.quotedTweet.getChirpURL() : "";
let username = tweet.getMainUser().screenName;
let images = tweet.hasImage() ? tweet.getMedia().filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https+":small").join(";") : "";
// TODO maybe handle embedded images too?
updateHighlightedTweet(me, tweet, link || "", embedded || "", username, images);
let tweetUrl = tweet.getChirpURL();
let quoteUrl = tweet.quotedTweet ? tweet.quotedTweet.getChirpURL() : "";
let authors = tweet.quotedTweet ? [ tweet.getMainUser().screenName, tweet.quotedTweet.getMainUser().screenName ].join(";") : tweet.getMainUser().screenName;
let imageList = tweet.quotedTweet ? processMedia(tweet.quotedTweet.getMedia()) : tweet.hasImage() ? processMedia(tweet.getMedia()) : "";
updateHighlightedTweet(me, tweet, tweetUrl || "", quoteUrl || "", authors, imageList);
}
else{
updateHighlightedTweet(me, tweet, "", "", "", "");
}
}
else if (e.type === "mouseleave"){
},
mouseleave: function(){
updateHighlightedTweet(null, null, "", "", "", "");
}
});
@@ -704,9 +750,7 @@
$(".js-drawer[data-drawer='compose']").delegate(".js-account-list > .js-account-item", "click", onAccountClick);
if (!ensurePropertyExists(TD, "components", "AccountSelector", "prototype", "refreshPostingAccounts")){
return;
}
return if !ensurePropertyExists(TD, "components", "AccountSelector", "prototype", "refreshPostingAccounts");
TD.components.AccountSelector.prototype.refreshPostingAccounts = appendToFunction(TD.components.AccountSelector.prototype.refreshPostingAccounts, function(){
if (!this.$node.attr("td-account-selector-hook")){
@@ -764,8 +808,8 @@
addRule(".txt-base-smallest .sprite-verified-mini { width: 13px !important; height: 13px !important; background-position: -223px -99px !important; }"); // fix cut off badge icon when zoomed in
addRule(".txt-base-smallest .badge-verified:before { width: 13px !important; height: 13px !important; background-position: -223px -98px !important; }"); // fix cut off badge icon in notifications
addRule(".btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .media-item, .media-preview, .tooltip-inner { border-radius: 1px !important; }"); // square-ify buttons, inputs, dialogs, menus, media previews
addRule(".compose-text-container, .dropdown-menu, .list-item-last, .quoted-tweet { border-radius: 0 !important; }"); // square-ify dropdowns, quoted tweets, and account selectors
addRule(".btn, .mdl, .mdl-content, .app-search-fake, .app-search-input, .popover, .lst-modal, .media-item, .media-preview, .tooltip-inner { border-radius: 1px !important; }"); // square-ify buttons, dialogs, menus, media previews
addRule(".compose-text-container, .dropdown-menu, .list-item-last, .quoted-tweet, .input-group-button, input, textarea, select { border-radius: 0 !important; }"); // square-ify dropdowns, inputs, quoted tweets, and account selectors
addRule(".prf-header { border-radius: 0; }"); // fix user account header border
addRule(".accs li, .accs img { border-radius: 0 !important; }"); // square-ify retweet account selector

View File

@@ -14,14 +14,16 @@
};
//
// Block: Hook into links to bypass default open function.
// Block: Hook into links to bypass default open function and t.co.
//
addEventListener(links, "click", function(e){
$TD.openBrowser(e.currentTarget.getAttribute("href"));
let ele = e.currentTarget;
$TD.openBrowser(ele.hasAttribute("data-full-url") ? ele.getAttribute("data-full-url") : ele.getAttribute("href"));
e.preventDefault();
if ($TDX.skipOnLinkClick){
let parentClasses = e.currentTarget.parentNode.classList;
let parentClasses = ele.parentNode.classList;
if (parentClasses.contains("js-tweet-text") || parentClasses.contains("js-quoted-tweet-text") || parentClasses.contains("js-timestamp")){
$TD.loadNextNotification();
@@ -33,7 +35,7 @@
// Block: Allow bypassing of t.co in context menus.
//
addEventListener(links, "contextmenu", function(e){
$TD.setLastRightClickedLink(e.currentTarget.getAttribute("data-full-url") || "");
$TD.setLastRightClickInfo("link", e.currentTarget.getAttribute("data-full-url"));
});
//

View File

@@ -39,4 +39,25 @@
};
setTimeout(injectCSS, 1);
//
// Block: Make login page links external.
//
if (location.pathname === "/login"){
document.addEventListener("DOMContentLoaded", function(){
let openLinkExternally = function(e){
let href = e.currentTarget.getAttribute("href");
$TD.openBrowser(href[0] === '/' ? location.origin+href : href);
e.preventDefault();
e.stopPropagation();
};
let links = document.getElementsByTagName("A");
for(let index = 0; index < links.length; index++){
links[index].addEventListener("click", openLinkExternally);
}
});
}
})();

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.props" Condition="Exists('packages\CefSharp.WinForms.57.0.0\build\CefSharp.WinForms.props')" />
<Import Project="packages\CefSharp.Common.57.0.0\build\CefSharp.Common.props" Condition="Exists('packages\CefSharp.Common.57.0.0\build\CefSharp.Common.props')" />
@@ -97,6 +97,7 @@
<Compile Include="Core\FormBrowser.Designer.cs">
<DependentUpon>FormBrowser.cs</DependentUpon>
</Compile>
<Compile Include="Core\Handling\General\FileDialogHandler.cs" />
<Compile Include="Core\Handling\KeyboardHandlerBrowser.cs" />
<Compile Include="Core\Handling\KeyboardHandlerNotification.cs" />
<Compile Include="Core\Handling\RequestHandlerBrowser.cs" />

View File

@@ -0,0 +1,138 @@
using System;
using System.Diagnostics.Contracts;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TweetDuck.Configuration;
using TweetDuck.Core;
namespace UnitTests.Configuration{
[TestClass]
public class TestUserConfig : UnitTestIO{
private static void WriteTestConfig(string file, bool withBackup){
UserConfig cfg = UserConfig.Load(file);
cfg.ZoomLevel = 123;
cfg.Save();
if (withBackup){
cfg.Save();
}
}
[Pure] // used to display a warning when not using the return value
private static bool CheckTestConfig(string file){
return UserConfig.Load(file).ZoomLevel == 123;
}
[TestMethod]
public void TestMissing(){
Assert.IsNotNull(UserConfig.Load("missing"));
Assert.IsFalse(File.Exists("missing"));
}
[TestMethod]
public void TestBasic(){
Assert.IsFalse(CheckTestConfig("basic"));
WriteTestConfig("basic", false);
Assert.IsTrue(CheckTestConfig("basic"));
}
[TestMethod]
public void TestBackupName(){
Assert.AreEqual("name.bak", UserConfig.GetBackupFile("name"));
Assert.AreEqual("name.cfg.bak", UserConfig.GetBackupFile("name.cfg"));
Assert.AreEqual("name.bak.bak", UserConfig.GetBackupFile("name.bak"));
}
[TestMethod]
public void TestBackupCreate(){
WriteTestConfig("nobackup", false);
Assert.IsTrue(File.Exists("nobackup"));
Assert.IsFalse(File.Exists(UserConfig.GetBackupFile("nobackup")));
WriteTestConfig("withbackup", true);
Assert.IsTrue(File.Exists("withbackup"));
Assert.IsTrue(File.Exists(UserConfig.GetBackupFile("withbackup")));
}
[TestMethod]
public void TestBackupRestore(){
WriteTestConfig("gone", true);
Assert.IsTrue(File.Exists("gone"));
Assert.IsTrue(File.Exists(UserConfig.GetBackupFile("gone")));
File.Delete("gone");
Assert.IsTrue(CheckTestConfig("gone"));
WriteTestConfig("corrupted", true);
Assert.IsTrue(File.Exists("corrupted"));
Assert.IsTrue(File.Exists(UserConfig.GetBackupFile("corrupted")));
File.WriteAllText("corrupted", "oh no corrupt");
Assert.IsTrue(CheckTestConfig("corrupted"));
}
[TestMethod]
public void TestReload(){
UserConfig cfg = UserConfig.Load("reloaded");
cfg.ZoomLevel = 123;
cfg.Save();
cfg.ZoomLevel = 200;
Assert.IsTrue(cfg.Reload());
Assert.AreEqual(123, cfg.ZoomLevel);
}
[TestMethod]
public void TestReset(){
UserConfig cfg = UserConfig.Load("reset");
cfg.ZoomLevel = 123;
cfg.Save();
File.Delete("reset");
Assert.IsTrue(cfg.Reload());
Assert.AreEqual(100, cfg.ZoomLevel);
Assert.IsTrue(File.Exists("reset"));
}
[TestMethod]
public void TestEventsNoTrigger(){
void Fail(object sender, EventArgs args) => Assert.Fail();
UserConfig cfg = UserConfig.Load("events");
cfg.MuteNotifications = true;
cfg.TrayBehavior = TrayIcon.Behavior.Combined;
cfg.ZoomLevel = 99;
cfg.MuteToggled += Fail;
cfg.TrayBehaviorChanged += Fail;
cfg.ZoomLevelChanged += Fail;
cfg.MuteNotifications = true;
cfg.TrayBehavior = TrayIcon.Behavior.Combined;
cfg.ZoomLevel = 99;
}
[TestMethod]
public void TestEventsTrigger(){
int triggers = 0;
void Trigger(object sender, EventArgs args) => ++triggers;
UserConfig cfg = UserConfig.Load("events");
cfg.MuteNotifications = false;
cfg.TrayBehavior = TrayIcon.Behavior.Disabled;
cfg.ZoomLevel = 100;
cfg.MuteToggled += Trigger;
cfg.TrayBehaviorChanged += Trigger;
cfg.ZoomLevelChanged += Trigger;
cfg.MuteNotifications = true;
cfg.TrayBehavior = TrayIcon.Behavior.Combined;
cfg.ZoomLevel = 99;
cfg.MuteNotifications = false;
cfg.TrayBehavior = TrayIcon.Behavior.Disabled;
cfg.ZoomLevel = 100;
Assert.AreEqual(6, triggers);
}
}
}

View File

@@ -6,33 +6,36 @@ namespace UnitTests.Core{
public class TestBrowserUtils{
[TestMethod]
public void TestIsValidUrl(){
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com")); // base
Assert.IsTrue(BrowserUtils.IsValidUrl("http://www.google.com")); // www.
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.co.uk")); // co.uk
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com")); // base
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://www.google.com")); // www.
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.co.uk")); // co.uk
Assert.IsTrue(BrowserUtils.IsValidUrl("https://google.com")); // https
Assert.IsTrue(BrowserUtils.IsValidUrl("ftp://google.com")); // ftp
Assert.IsTrue(BrowserUtils.IsValidUrl("mailto:someone@google.com")); // mailto
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("https://google.com")); // https
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("ftp://google.com")); // ftp
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("mailto:someone@google.com")); // mailto
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/")); // trailing slash
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/?")); // trailing question mark
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/?a=5&b=x")); // parameters
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/#hash")); // parameters + hash
Assert.IsTrue(BrowserUtils.IsValidUrl("http://google.com/?a=5&b=x#hash")); // parameters + hash
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/")); // trailing slash
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/?")); // trailing question mark
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/?a=5&b=x")); // parameters
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/#hash")); // parameters + hash
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://google.com/?a=5&b=x#hash")); // parameters + hash
foreach(string tld in new string[]{ "accountants", "blackfriday", "cancerresearch", "coffee", "cool", "foo", "travelersinsurance" }){
Assert.IsTrue(BrowserUtils.IsValidUrl("http://test."+tld)); // long and unusual TLDs
Assert.AreEqual(BrowserUtils.UrlCheckResult.Fine, BrowserUtils.CheckUrl("http://test."+tld)); // long and unusual TLDs
}
Assert.IsFalse(BrowserUtils.IsValidUrl("explorer")); // file
Assert.IsFalse(BrowserUtils.IsValidUrl("explorer.exe")); // file
Assert.IsFalse(BrowserUtils.IsValidUrl("://explorer.exe")); // file-sorta
Assert.IsFalse(BrowserUtils.IsValidUrl("file://explorer.exe")); // file-proper
Assert.IsFalse(BrowserUtils.IsValidUrl("")); // empty
Assert.IsFalse(BrowserUtils.IsValidUrl("lol")); // random
Assert.AreEqual(BrowserUtils.UrlCheckResult.Tracking, BrowserUtils.CheckUrl("http://t.co/abc")); // tracking
Assert.AreEqual(BrowserUtils.UrlCheckResult.Tracking, BrowserUtils.CheckUrl("https://t.co/abc")); // tracking
Assert.IsFalse(BrowserUtils.IsValidUrl("gopher://nobody.cares")); // lmao rekt
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("explorer")); // file
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("explorer.exe")); // file
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("://explorer.exe")); // file-sorta
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("file://explorer.exe")); // file-proper
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("")); // empty
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("lol")); // random
Assert.AreEqual(BrowserUtils.UrlCheckResult.Invalid, BrowserUtils.CheckUrl("gopher://nobody.cares")); // lmao rekt
}
[TestMethod]

View File

@@ -6,39 +6,39 @@ using TweetDuck.Data;
namespace UnitTests.Data{
[TestClass]
public class TestCombinedFileStream{
public class TestCombinedFileStream : UnitTestIO{
[TestMethod]
public void TestNoFiles(){
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_empty"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("empty"))){
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_empty"));
Assert.IsTrue(File.Exists("empty"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_empty"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenRead("empty"))){
Assert.IsNull(cfs.ReadFile());
}
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_empty"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenRead("empty"))){
Assert.IsNull(cfs.SkipFile());
}
}
[TestMethod]
public void TestEmptyFiles(){
TestUtils.WriteText("cfs_input_empty_1", string.Empty);
TestUtils.WriteText("cfs_input_empty_2", string.Empty);
File.WriteAllText("input_empty_1", string.Empty);
File.WriteAllText("input_empty_2", string.Empty);
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_blank_files"))){
cfs.WriteFile("id1", "cfs_input_empty_1");
cfs.WriteFile("id2", "cfs_input_empty_2");
cfs.WriteFile("id2_clone", "cfs_input_empty_2");
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("blank_files"))){
cfs.WriteFile("id1", "input_empty_1");
cfs.WriteFile("id2", "input_empty_2");
cfs.WriteFile("id2_clone", "input_empty_2");
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_blank_files"));
Assert.IsTrue(File.Exists("blank_files"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_blank_files"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenRead("blank_files"))){
CombinedFileStream.Entry entry1 = cfs.ReadFile();
string entry2key = cfs.SkipFile();
CombinedFileStream.Entry entry3 = cfs.ReadFile();
@@ -56,31 +56,29 @@ namespace UnitTests.Data{
Assert.AreEqual("id2_clone", entry3.Identifier);
CollectionAssert.AreEqual(new string[0], entry3.KeyValue);
entry1.WriteToFile("cfs_blank_file_1");
entry3.WriteToFile("cfs_blank_file_2");
TestUtils.DeleteFileOnExit("cfs_blank_file_1");
TestUtils.DeleteFileOnExit("cfs_blank_file_2");
entry1.WriteToFile("blank_file_1");
entry3.WriteToFile("blank_file_2");
}
Assert.IsTrue(File.Exists("cfs_blank_file_1"));
Assert.IsTrue(File.Exists("cfs_blank_file_2"));
Assert.AreEqual(string.Empty, TestUtils.ReadText("cfs_blank_file_1"));
Assert.AreEqual(string.Empty, TestUtils.ReadText("cfs_blank_file_2"));
Assert.IsTrue(File.Exists("blank_file_1"));
Assert.IsTrue(File.Exists("blank_file_2"));
Assert.AreEqual(string.Empty, File.ReadAllText("blank_file_1"));
Assert.AreEqual(string.Empty, File.ReadAllText("blank_file_2"));
}
[TestMethod]
public void TestTextFilesAndComplexKeys(){
TestUtils.WriteText("cfs_input_text_1", "Hello World!"+Environment.NewLine);
File.WriteAllText("input_text_1", "Hello World!"+Environment.NewLine);
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_text_files"))){
cfs.WriteFile(new string[]{ "key1", "a", "bb", "ccc", "dddd" }, "cfs_input_text_1");
cfs.WriteFile(new string[]{ "key2", "a", "bb", "ccc", "dddd" }, "cfs_input_text_1");
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("text_files"))){
cfs.WriteFile(new string[]{ "key1", "a", "bb", "ccc", "dddd" }, "input_text_1");
cfs.WriteFile(new string[]{ "key2", "a", "bb", "ccc", "dddd" }, "input_text_1");
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_text_files"));
Assert.IsTrue(File.Exists("text_files"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_text_files"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenRead("text_files"))){
CombinedFileStream.Entry entry = cfs.ReadFile();
Assert.AreEqual("key2", cfs.SkipFile());
@@ -91,68 +89,65 @@ namespace UnitTests.Data{
Assert.AreEqual("key1", entry.KeyName);
CollectionAssert.AreEqual(new string[]{ "a", "bb", "ccc", "dddd" }, entry.KeyValue);
entry.WriteToFile("cfs_text_file_1");
TestUtils.DeleteFileOnExit("cfs_text_file_1");
entry.WriteToFile("text_file_1");
}
Assert.IsTrue(File.Exists("cfs_text_file_1"));
Assert.AreEqual("Hello World!"+Environment.NewLine, TestUtils.ReadText("cfs_text_file_1"));
Assert.IsTrue(File.Exists("text_file_1"));
Assert.AreEqual("Hello World!"+Environment.NewLine, File.ReadAllText("text_file_1"));
}
[TestMethod]
public void TestEntryWriteWithDirectory(){
if (Directory.Exists("cfs_directory")){
Directory.Delete("cfs_directory", true);
if (Directory.Exists("directory")){
Directory.Delete("directory", true);
}
TestUtils.WriteText("cfs_input_dir_1", "test");
File.WriteAllText("input_dir_1", "test");
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_dir_test"))){
cfs.WriteFile("key1", "cfs_input_dir_1");
cfs.WriteFile("key2", "cfs_input_dir_1");
cfs.WriteFile("key3", "cfs_input_dir_1");
cfs.WriteFile("key4", "cfs_input_dir_1");
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("dir_test"))){
cfs.WriteFile("key1", "input_dir_1");
cfs.WriteFile("key2", "input_dir_1");
cfs.WriteFile("key3", "input_dir_1");
cfs.WriteFile("key4", "input_dir_1");
cfs.Flush();
}
Assert.IsTrue(File.Exists("cfs_dir_test"));
Assert.IsTrue(File.Exists("dir_test"));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_dir_test"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenRead("dir_test"))){
try{
cfs.ReadFile().WriteToFile("cfs_directory/cfs_dir_test_file", false);
cfs.ReadFile().WriteToFile("directory/dir_test_file", false);
Assert.Fail("WriteToFile did not trigger an exception.");
}catch(DirectoryNotFoundException){}
cfs.ReadFile().WriteToFile("cfs_directory/cfs_dir_test_file", true);
cfs.ReadFile().WriteToFile("cfs_dir_test_file", true);
cfs.ReadFile().WriteToFile("cfs_dir_test_file.txt", true);
TestUtils.DeleteFileOnExit("cfs_dir_test_file");
TestUtils.DeleteFileOnExit("cfs_dir_test_file.txt");
cfs.ReadFile().WriteToFile("directory/dir_test_file", true);
cfs.ReadFile().WriteToFile("dir_test_file", true);
cfs.ReadFile().WriteToFile("dir_test_file.txt", true);
}
Assert.IsTrue(Directory.Exists("cfs_directory"));
Assert.IsTrue(File.Exists("cfs_directory/cfs_dir_test_file"));
Assert.IsTrue(File.Exists("cfs_dir_test_file"));
Assert.IsTrue(File.Exists("cfs_dir_test_file.txt"));
Assert.AreEqual("test", TestUtils.ReadText("cfs_directory/cfs_dir_test_file"));
Assert.AreEqual("test", TestUtils.ReadText("cfs_dir_test_file"));
Assert.AreEqual("test", TestUtils.ReadText("cfs_dir_test_file.txt"));
Assert.IsTrue(Directory.Exists("directory"));
Assert.IsTrue(File.Exists("directory/dir_test_file"));
Assert.IsTrue(File.Exists("dir_test_file"));
Assert.IsTrue(File.Exists("dir_test_file.txt"));
Assert.AreEqual("test", File.ReadAllText("directory/dir_test_file"));
Assert.AreEqual("test", File.ReadAllText("dir_test_file"));
Assert.AreEqual("test", File.ReadAllText("dir_test_file.txt"));
Directory.Delete("cfs_directory", true);
Directory.Delete("directory", true);
}
[TestMethod]
public void TestLongIdentifierSuccess(){
TestUtils.WriteText("cfs_long_identifier_fail_in", "test");
File.WriteAllText("long_identifier_fail_in", "test");
string identifier = string.Join("", Enumerable.Repeat("x", 255));
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_long_identifier_success"))){
cfs.WriteFile(identifier, "cfs_long_identifier_fail_in");
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("long_identifier_success"))){
cfs.WriteFile(identifier, "long_identifier_fail_in");
cfs.Flush();
}
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.ReadFile("cfs_long_identifier_success"))){
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenRead("long_identifier_success"))){
Assert.AreEqual(identifier, cfs.ReadFile().Identifier);
}
}
@@ -160,10 +155,10 @@ namespace UnitTests.Data{
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestLongIdentifierFail(){
TestUtils.WriteText("cfs_long_identifier_fail_in", "test");
File.WriteAllText("long_identifier_fail_in", "test");
using(CombinedFileStream cfs = new CombinedFileStream(TestUtils.WriteFile("cfs_long_identifier_fail"))){
cfs.WriteFile(string.Join("", Enumerable.Repeat("x", 256)), "cfs_long_identifier_fail_in");
using(CombinedFileStream cfs = new CombinedFileStream(File.OpenWrite("long_identifier_fail"))){
cfs.WriteFile(string.Join("", Enumerable.Repeat("x", 256)), "long_identifier_fail_in");
}
}
}

View File

@@ -5,7 +5,7 @@ using TweetDuck.Data.Serialization;
namespace UnitTests.Data{
[TestClass]
public class TestFileSerializer{
public class TestFileSerializer : UnitTestIO{
private enum TestEnum{
A, B, C, D, E
}
@@ -30,13 +30,11 @@ namespace UnitTests.Data{
TestEnum = TestEnum.D
};
serializer.Write("serialized_basic", write);
Assert.IsTrue(File.Exists("serialized_basic"));
TestUtils.DeleteFileOnExit("serialized_basic");
serializer.Write("basic_wr", write);
Assert.IsTrue(File.Exists("basic_wr"));
SerializationTestBasic read = new SerializationTestBasic();
serializer.Read("serialized_basic", read);
serializer.Read("basic_wr", read);
Assert.IsTrue(read.TestBool);
Assert.AreEqual(-100, read.TestInt);

View File

@@ -1,65 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests{
public static class TestUtils{
private static readonly HashSet<string> CreatedFiles = new HashSet<string>();
public static void WriteText(string file, string text){
DeleteFileOnExit(file);
File.WriteAllText(file, text, Encoding.UTF8);
}
public static void WriteLines(string file, IEnumerable<string> lines){
DeleteFileOnExit(file);
File.WriteAllLines(file, lines, Encoding.UTF8);
}
public static FileStream WriteFile(string file){
DeleteFileOnExit(file);
return new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
}
public static string ReadText(string file){
try{
return File.ReadAllText(file, Encoding.UTF8);
}catch(Exception){
return string.Empty;
}
}
public static IEnumerable<string> ReadLines(string file){
try{
return File.ReadLines(file, Encoding.UTF8);
}catch(Exception){
return Enumerable.Empty<string>();
}
}
public static FileStream ReadFile(string file){
return new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None);
}
public static void DeleteFileOnExit(string file){
CreatedFiles.Add(file);
}
[TestClass]
public static class Cleanup{
[AssemblyCleanup]
public static void DeleteFilesOnExit(){
foreach(string file in CreatedFiles){
try{
File.Delete(file);
}catch(Exception){
// ignore
}
}
}
}
}
}

32
tests/UnitTestIO.cs Normal file
View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests{
[TestClass]
public class UnitTestIO{
private static readonly HashSet<string> CreatedFolders = new HashSet<string>();
[TestInitialize]
public void InitTest(){
string folder = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, GetType().Name);
CreatedFolders.Add(folder);
Directory.CreateDirectory(folder);
Directory.SetCurrentDirectory(folder);
}
[AssemblyCleanup]
public static void DeleteFilesOnExit(){
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.SetupInformation.ApplicationBase);
foreach(string folder in CreatedFolders){
try{
Directory.Delete(folder, true);
}catch(Exception){
// ignore
}
}
}
}
}

View File

@@ -47,6 +47,7 @@
</Otherwise>
</Choose>
<ItemGroup>
<Compile Include="Configuration\TestUserConfig.cs" />
<Compile Include="Core\TestStringUtils.cs" />
<Compile Include="Core\TestTwitterUtils.cs" />
<Compile Include="Data\TestCombinedFileStream.cs" />
@@ -56,7 +57,7 @@
<Compile Include="Data\TestInjectedHTML.cs" />
<Compile Include="Data\TestTwoKeyDictionary.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestUtils.cs" />
<Compile Include="UnitTestIO.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TweetDuck.csproj">