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

Compare commits

..

24 Commits

Author SHA1 Message Date
327ef1cbee Restart TweetDuck if user declines UAC when updating and improve error handling 2017-10-19 00:30:37 +02:00
15eb823c7f Replace OpenExternalBrowserUnsafe with the new OpenAssociatedProgram 2017-10-19 00:24:40 +02:00
54613e5242 Handle errors when opening images in associated viewer 2017-10-19 00:21:53 +02:00
df1352cbe3 Update README.md 2017-10-18 23:50:23 +02:00
0559afd972 Make middle-click tweet actions work in detail view 2017-10-18 16:50:34 +02:00
afffca020e Add context menu item to view images in photo viewer 2017-10-18 13:50:20 +02:00
d663cc3f64 Add username to default video download filename & tweak playback error message 2017-10-17 19:39:35 +02:00
110d41e393 Add function to trigger video playback for plugins 2017-10-17 13:16:19 +02:00
1a8823f592 Fix clipboard html stripping crashing with no text data
Closes #171
2017-10-17 11:22:38 +02:00
6374a852b0 Merge branch 'master' of https://github.com/chylex/TweetDuck 2017-10-16 16:37:24 +02:00
a10c7dd7c3 Fix README heading links 2017-10-16 16:36:30 +02:00
547c7ea417 Update README heading links (still bork on GitHub) 2017-10-16 16:33:41 +02:00
760607995a Update README and post build log message 2017-10-16 16:30:55 +02:00
4704197c09 Add option for larger quote font size to edit-design & update modal layout 2017-10-16 15:38:19 +02:00
093ac1ac40 Make middle-click instant quote work with reply-account plugin 2017-10-16 09:32:52 +02:00
9ed8b0d904 Fix used account in updated middle-click actions 2017-10-15 23:48:56 +02:00
7346ce370d Add middle-click actions (reply popout, RT quote, fav modal) & fix popout in temp columns 2017-10-15 22:59:56 +02:00
adefdadc19 Fix border-radius when quoting a tweet 2017-10-15 22:29:44 +02:00
703bce2d00 Fix PostBuild.ps1 errors not causing failed build & refactor 2017-10-13 13:09:50 +02:00
97928ecd84 Check emoji-ordering.txt for carriage return on build 2017-10-13 12:54:15 +02:00
be9ea7f64a Fuck everything about gitattributes 2017-10-13 12:39:03 +02:00
ec2aaa8789 Add exact emoji name match detection to emoji keyboard 2017-10-13 12:20:54 +02:00
ab14b72526 Force LF in emoji-ordering.txt and other txt/js files 2017-10-13 12:16:39 +02:00
d8e304f3c1 Release 1.10.2 2017-10-12 13:01:47 +02:00
27 changed files with 274 additions and 119 deletions

5
.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
*.txt text eof=lf
*.cs diff=csharp

View File

@@ -117,8 +117,8 @@ namespace TweetDuck.Core.Bridge{
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height)); form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
} }
public void PlayVideo(string url){ public void PlayVideo(string url, string username){
form.InvokeAsyncSafe(() => form.PlayVideo(url)); form.InvokeAsyncSafe(() => form.PlayVideo(url, username));
} }
public void FixClipboard(){ public void FixClipboard(){

View File

@@ -541,7 +541,7 @@ namespace TweetDuck.Core{
soundNotification.Play(Config.NotificationSoundPath); soundNotification.Play(Config.NotificationSoundPath);
} }
public void PlayVideo(string url){ public void PlayVideo(string url, string username){
if (string.IsNullOrEmpty(url)){ if (string.IsNullOrEmpty(url)){
videoPlayer?.Close(); videoPlayer?.Close();
return; return;
@@ -556,7 +556,7 @@ namespace TweetDuck.Core{
}; };
} }
videoPlayer.Launch(url); videoPlayer.Launch(url, username);
} }
public void HideVideoOverlay(){ public void HideVideoOverlay(){

View File

@@ -7,7 +7,9 @@ using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls; using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils; using TweetDuck.Core.Utils;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using TweetDuck.Core.Other;
namespace TweetDuck.Core.Handling{ namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{ abstract class ContextMenuBase : IContextMenuHandler{
@@ -31,10 +33,11 @@ namespace TweetDuck.Core.Handling{
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500; private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501; private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501;
private const CefMenuCommand MenuCopyUsername = (CefMenuCommand)26502; private const CefMenuCommand MenuCopyUsername = (CefMenuCommand)26502;
private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand)26503; private const CefMenuCommand MenuViewImage = (CefMenuCommand)26503;
private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand)26504; private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand)26504;
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand)26505; private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand)26505;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand)26506; private const CefMenuCommand MenuSaveMedia = (CefMenuCommand)26506;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand)26507;
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand)26599; private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand)26599;
private string[] lastHighlightedTweetAuthors; private string[] lastHighlightedTweetAuthors;
@@ -79,6 +82,7 @@ namespace TweetDuck.Core.Handling{
model.AddSeparator(); model.AddSeparator();
} }
else if ((parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) || hasTweetImage){ else if ((parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) || hasTweetImage){
model.AddItem(MenuViewImage, "View image in photo viewer");
model.AddItem(MenuOpenMediaUrl, TextOpen("image")); model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
model.AddItem(MenuCopyMediaUrl, TextCopy("image")); model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
model.AddItem(MenuSaveMedia, TextSave("image")); model.AddItem(MenuSaveMedia, TextSave("image"));
@@ -114,9 +118,35 @@ namespace TweetDuck.Core.Handling{
SetClipboardText(browserControl.AsControl(), TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality)); SetClipboardText(browserControl.AsControl(), TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
break; break;
case MenuViewImage:
string url = GetMediaLink(parameters);
string file = Path.Combine(BrowserCache.CacheFolder, TwitterUtils.GetImageFileName(url));
void ViewFile(){
string ext = Path.GetExtension(file);
if (TwitterUtils.ValidImageExtensions.Contains(ext)){
WindowsUtils.OpenAssociatedProgram(file);
}
else{
FormMessage.Error("Image Download", "Invalid file extension "+ext, FormMessage.OK);
}
}
if (File.Exists(file)){
ViewFile();
}
else{
BrowserUtils.DownloadFileAsync(TwitterUtils.GetMediaLink(url, ImageQuality), file, ViewFile, ex => {
FormMessage.Error("Image Download", "An error occurred while downloading the image: "+ex.Message, FormMessage.OK);
});
}
break;
case MenuSaveMedia: case MenuSaveMedia:
if (IsVideo){ if (IsVideo){
TwitterUtils.DownloadVideo(GetMediaLink(parameters)); TwitterUtils.DownloadVideo(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault());
} }
else{ else{
TwitterUtils.DownloadImage(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality); TwitterUtils.DownloadImage(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality);

View File

@@ -21,7 +21,7 @@ namespace TweetDuck.Core.Other{
} }
private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e){ private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe(e.Link.LinkData as string); BrowserUtils.OpenExternalBrowser(e.Link.LinkData as string);
} }
private void FormAbout_HelpRequested(object sender, HelpEventArgs hlpevent){ private void FormAbout_HelpRequested(object sender, HelpEventArgs hlpevent){

View File

@@ -25,6 +25,7 @@ namespace TweetDuck.Core.Other.Management{
private readonly Form owner; private readonly Form owner;
private string lastUrl; private string lastUrl;
private string lastUsername;
private Process currentProcess; private Process currentProcess;
private DuplexPipe.Server currentPipe; private DuplexPipe.Server currentPipe;
@@ -35,13 +36,14 @@ namespace TweetDuck.Core.Other.Management{
this.owner.FormClosing += owner_FormClosing; this.owner.FormClosing += owner_FormClosing;
} }
public void Launch(string url){ public void Launch(string url, string username){
if (Running){ if (Running){
Destroy(); Destroy();
isClosing = false; isClosing = false;
} }
lastUrl = url; lastUrl = url;
lastUsername = username;
try{ try{
currentPipe = DuplexPipe.CreateServer(); currentPipe = DuplexPipe.CreateServer();
@@ -84,7 +86,7 @@ namespace TweetDuck.Core.Other.Management{
break; break;
case "download": case "download":
TwitterUtils.DownloadVideo(lastUrl); TwitterUtils.DownloadVideo(lastUrl, lastUsername);
break; break;
case "rip": case "rip":
@@ -157,14 +159,14 @@ namespace TweetDuck.Core.Other.Management{
switch(exitCode){ switch(exitCode){
case 3: // CODE_LAUNCH_FAIL case 3: // CODE_LAUNCH_FAIL
if (FormMessage.Error("Video Playback Error", "Error launching video player, this may be caused by missing Windows Media Player. Do you want to open the video in a browser?", FormMessage.Yes, FormMessage.No)){ if (FormMessage.Error("Video Playback Error", "Error launching video player, this may be caused by missing Windows Media Player. Do you want to open the video in your browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl); BrowserUtils.OpenExternalBrowser(lastUrl);
} }
break; break;
case 4: // CODE_MEDIA_ERROR case 4: // CODE_MEDIA_ERROR
if (FormMessage.Error("Video Playback Error", "The video could not be loaded, most likely due to unknown format. Do you want to open the video in a browser?", FormMessage.Yes, FormMessage.No)){ if (FormMessage.Error("Video Playback Error", "The video could not be loaded, most likely due to unknown format. Do you want to open the video in your browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl); BrowserUtils.OpenExternalBrowser(lastUrl);
} }

View File

@@ -35,7 +35,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
} }
private void btnOpenWiki_Click(object sender, EventArgs e){ private void btnOpenWiki_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/wiki"); BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki");
} }
private void btnApply_Click(object sender, EventArgs e){ private void btnApply_Click(object sender, EventArgs e){

View File

@@ -19,7 +19,7 @@ namespace TweetDuck.Core.Other.Settings.Dialogs{
} }
private void btnHelp_Click(object sender, EventArgs e){ private void btnHelp_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("http://peter.sh/experiments/chromium-command-line-switches/"); BrowserUtils.OpenExternalBrowser("http://peter.sh/experiments/chromium-command-line-switches/");
} }
private void btnApply_Click(object sender, EventArgs e){ private void btnApply_Click(object sender, EventArgs e){

View File

@@ -16,7 +16,7 @@ namespace TweetDuck.Core.Other.Settings{
} }
private void btnSendFeedback_Click(object sender, EventArgs e){ private void btnSendFeedback_Click(object sender, EventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/issues/new"); BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/issues/new");
} }
private void checkDataCollection_CheckedChanged(object sender, EventArgs e){ private void checkDataCollection_CheckedChanged(object sender, EventArgs e){
@@ -24,7 +24,7 @@ namespace TweetDuck.Core.Other.Settings{
} }
private void labelDataCollectionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e){ private void labelDataCollectionLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e){
BrowserUtils.OpenExternalBrowserUnsafe("https://github.com/chylex/TweetDuck/wiki/Send-anonymous-data"); BrowserUtils.OpenExternalBrowser("https://github.com/chylex/TweetDuck/wiki/Send-anonymous-data");
} }
} }
} }

View File

@@ -8,7 +8,7 @@ namespace TweetDuck.Core.Utils{
static class BrowserCache{ static class BrowserCache{
private static bool ClearOnExit { get; set; } private static bool ClearOnExit { get; set; }
private static readonly string CacheFolder = Path.Combine(Program.StoragePath, "Cache"); public static readonly string CacheFolder = Path.Combine(Program.StoragePath, "Cache");
private static IEnumerable<string> CacheFiles{ private static IEnumerable<string> CacheFiles{
get{ get{

View File

@@ -70,12 +70,12 @@ namespace TweetDuck.Core.Utils{
switch(CheckUrl(url)){ switch(CheckUrl(url)){
case UrlCheckResult.Fine: case UrlCheckResult.Fine:
OpenExternalBrowserUnsafe(url); WindowsUtils.OpenAssociatedProgram(url);
break; break;
case UrlCheckResult.Tracking: 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)){ 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); WindowsUtils.OpenAssociatedProgram(url);
} }
break; break;
@@ -86,10 +86,6 @@ namespace TweetDuck.Core.Utils{
} }
} }
public static void OpenExternalBrowserUnsafe(string url){
using(Process.Start(url)){}
}
public static string GetFileNameFromUrl(string url){ public static string GetFileNameFromUrl(string url){
string file = Path.GetFileName(new Uri(url).AbsolutePath); string file = Path.GetFileName(new Uri(url).AbsolutePath);
return string.IsNullOrEmpty(file) ? null : file; return string.IsNullOrEmpty(file) ? null : file;

View File

@@ -21,6 +21,10 @@ namespace TweetDuck.Core.Utils{
"tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD" "tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD"
}; };
public static readonly string[] ValidImageExtensions = {
".jpg", ".jpeg", ".png", ".gif"
};
public enum ImageQuality{ public enum ImageQuality{
Default, Orig Default, Orig
} }
@@ -53,6 +57,10 @@ namespace TweetDuck.Core.Utils{
} }
} }
public static string GetImageFileName(string url){
return BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(url));
}
public static void DownloadImage(string url, string username, ImageQuality quality){ public static void DownloadImage(string url, string username, ImageQuality quality){
DownloadImages(new string[]{ url }, username, quality); DownloadImages(new string[]{ url }, username, quality);
} }
@@ -65,7 +73,7 @@ namespace TweetDuck.Core.Utils{
string firstImageLink = GetMediaLink(urls[0], quality); string firstImageLink = GetMediaLink(urls[0], quality);
int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/')); int qualityIndex = firstImageLink.IndexOf(':', firstImageLink.LastIndexOf('/'));
string file = BrowserUtils.GetFileNameFromUrl(ExtractMediaBaseLink(firstImageLink)); string file = GetImageFileName(firstImageLink);
string ext = Path.GetExtension(file); // includes dot string ext = Path.GetExtension(file); // includes dot
string[] fileNameParts = qualityIndex == -1 ? new string[]{ string[] fileNameParts = qualityIndex == -1 ? new string[]{
@@ -103,7 +111,7 @@ namespace TweetDuck.Core.Utils{
} }
} }
public static void DownloadVideo(string url){ public static void DownloadVideo(string url, string username){
string filename = BrowserUtils.GetFileNameFromUrl(url); string filename = BrowserUtils.GetFileNameFromUrl(url);
string ext = Path.GetExtension(filename); string ext = Path.GetExtension(filename);
@@ -111,7 +119,7 @@ namespace TweetDuck.Core.Utils{
AutoUpgradeEnabled = true, AutoUpgradeEnabled = true,
OverwritePrompt = true, OverwritePrompt = true,
Title = "Save video", Title = "Save video",
FileName = filename, FileName = string.IsNullOrEmpty(username) ? filename : $"{username} {filename}",
Filter = "Video"+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}") Filter = "Video"+(string.IsNullOrEmpty(ext) ? " (unknown)|*.*" : $" (*{ext})|*{ext}")
}){ }){
if (dialog.ShowDialog() == DialogResult.OK){ if (dialog.ShowDialog() == DialogResult.OK){

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Management; using System.Management;
@@ -49,17 +50,22 @@ namespace TweetDuck.Core.Utils{
} }
} }
public static Process StartProcess(string file, string arguments, bool runElevated){ public static bool OpenAssociatedProgram(string file, string arguments = "", bool runElevated = false){
ProcessStartInfo processInfo = new ProcessStartInfo{ try{
FileName = file, using(Process.Start(new ProcessStartInfo{
Arguments = arguments FileName = file,
}; Arguments = arguments,
Verb = runElevated ? "runas" : string.Empty,
if (runElevated){ ErrorDialog = true
processInfo.Verb = "runas"; })){
return true;
}
}catch(Win32Exception e) when (e.NativeErrorCode == 0x000004C7){ // operation canceled by the user
return false;
}catch(Exception e){
Program.Reporter.HandleException("Error opening file", e.Message, true, e);
return false;
} }
return Process.Start(processInfo);
} }
public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){ public static bool TrySleepUntil(Func<bool> test, int timeoutMillis, int timeStepMillis){
@@ -104,7 +110,7 @@ namespace TweetDuck.Core.Utils{
} }
public static void ClipboardStripHtmlStyles(){ public static void ClipboardStripHtmlStyles(){
if (!Clipboard.ContainsText(TextDataFormat.Html)){ if (!Clipboard.ContainsText(TextDataFormat.Html) || !Clipboard.ContainsText(TextDataFormat.UnicodeText)){
return; return;
} }

View File

@@ -21,7 +21,7 @@ namespace TweetDuck{
public const string BrandName = "TweetDuck"; public const string BrandName = "TweetDuck";
public const string Website = "https://tweetduck.chylex.com"; public const string Website = "https://tweetduck.chylex.com";
public const string VersionTag = "1.10.1"; public const string VersionTag = "1.10.2";
public static readonly bool IsPortable = File.Exists("makeportable"); public static readonly bool IsPortable = File.Exists("makeportable");
@@ -166,8 +166,12 @@ namespace TweetDuck{
string updaterArgs = "/SP- /SILENT /CLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : ""); string updaterArgs = "/SP- /SILENT /CLOSEAPPLICATIONS /UPDATEPATH=\""+ProgramPath+"\" /RUNARGS=\""+Arguments.GetCurrentForInstallerCmd()+"\""+(IsPortable ? " /PORTABLE=1" : "");
bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath); bool runElevated = !IsPortable || !WindowsUtils.CheckFolderWritePermission(ProgramPath);
WindowsUtils.StartProcess(mainForm.UpdateInstallerPath, updaterArgs, runElevated); if (WindowsUtils.OpenAssociatedProgram(mainForm.UpdateInstallerPath, updaterArgs, runElevated)){
Application.Exit(); Application.Exit();
}
else{
RestartWithArgsInternal(Arguments.GetCurrentClean());
}
} }
} }
@@ -211,11 +215,14 @@ namespace TweetDuck{
FormBrowser browserForm = Application.OpenForms.OfType<FormBrowser>().FirstOrDefault(); FormBrowser browserForm = Application.OpenForms.OfType<FormBrowser>().FirstOrDefault();
if (browserForm == null)return; if (browserForm == null)return;
args.AddFlag(Arguments.ArgRestart);
browserForm.ForceClose(); browserForm.ForceClose();
ExitCleanup();
ExitCleanup();
RestartWithArgsInternal(args);
}
private static void RestartWithArgsInternal(CommandLineArgs args){
args.AddFlag(Arguments.ArgRestart);
Process.Start(Application.ExecutablePath, args.ToString()); Process.Start(Application.ExecutablePath, args.ToString());
Application.Exit(); Application.Exit();
} }

View File

@@ -2,10 +2,6 @@
[Follow TweetDuck on Twitter](https://twitter.com/TryTweetDuck) &nbsp;|&nbsp; [Support via PayPal](https://paypal.me/chylex) &nbsp;|&nbsp; [Support via Patreon](https://www.patreon.com/chylex) [Follow TweetDuck on Twitter](https://twitter.com/TryTweetDuck) &nbsp;|&nbsp; [Support via PayPal](https://paypal.me/chylex) &nbsp;|&nbsp; [Support via Patreon](https://www.patreon.com/chylex)
<a target='_blank' rel='nofollow' href='https://app.codesponsor.io/link/2W76v7hRmUe7NfaeUKq9K8Wq/chylex/TweetDuck'>
<img alt='Sponsor' width='888' height='68' src='https://app.codesponsor.io/embed/2W76v7hRmUe7NfaeUKq9K8Wq/chylex/TweetDuck.svg' />
</a>
# Build Instructions # Build Instructions
### Setup ### Setup
@@ -33,21 +29,34 @@ To do that, open **TweetDuck Properties**, click the **Debug** tab, make sure yo
### Build ### Build
To make a release build of TweetDuck, open **Batch Build**, tick all `Release` configurations except for the `UnitTest` project (otherwise the build will fail), and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, open the **Output** view and see which part of the build failed. To make a release build of TweetDuck, open **Batch Build**, tick all `Release` configurations except for the `UnitTest` project (otherwise the build will fail), and click **Rebuild**. Check the status bar to make sure it says **Rebuild All succeeded**; if not, see the [Troubleshooting](#troubleshooting) section.
After the build succeeds, the **bin/x86/Release** folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#Installers) section for automated installer generation. After the build succeeds, the `bin/x86/Release` folder will contain files intended for distribution (no debug symbols or other unnecessary files). You may package these files yourself, or see the [Installers](#installers) section for automated installer generation.
If you decide to release a custom version publicly, please make it clear that it is not an official release of TweetDuck. If you decide to release a custom version publicly, please make it clear that it is not an official release of TweetDuck.
### Troubleshooting
There are a few quirks in the build process that may catch you off guard:
- **Plugin files are not updated automatically**
- Since official plugins (`Resources/Plugins`) are not included in the project, Visual Studio will not automatically detect changes in the files
- To ensure plugins are updated when testing the app, click **Rebuild Solution** before clicking **Start**
- **Error: The command (...) exited with code 1**
- If the post-build event fails, open the **Output** tab and look for the cause
- Determine if there was an IO error while copying files or modifying folders, or whether the final .ps1 script failed (`Encountered an error while running PostBuild.ps1 on line xyz`)
- Some files are checked for invalid characters:
- `Resources/Plugins/emoji-keyboard/emoji-ordering.txt` line endings must be LF (line feed); any CR (carriage return) in the file will cause a failed build, and you will need to ensure correct line endings in your text editor
### Installers ### Installers
TweetDuck uses **Inno Setup** to automate the creation of installers. First, download and install [InnoSetup QuickStart Pack](http://www.jrsoftware.org/isdl.php) (non-unicode; editor and encryption support not required) and the [Inno Download Plugin](https://code.google.com/archive/p/inno-download-plugin). TweetDuck uses **Inno Setup** to automate the creation of installers. First, download and install [InnoSetup QuickStart Pack](http://www.jrsoftware.org/isdl.php) (non-unicode; editor and encryption support not required) and the [Inno Download Plugin](https://code.google.com/archive/p/inno-download-plugin).
Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer for the change to take place. Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer for the change to take place.
Now you can generate installers after a build by running **bld/RUN BUILD.bat**. Note that despite the name, this will only package the files, you still need to run the [build](#Build) in Visual Studio! Now you can generate installers after a build by running `bld/RUN BUILD.bat`. Note that despite the name, this will only package the files, you still need to run the [build](#build) in Visual Studio!
After the window closes, three installers will be generated inside the **bld/Output** folder: After the window closes, three installers will be generated inside the `bld/Output` folder:
* **TweetDuck.exe** * **TweetDuck.exe**
* This is the main installer that creates entries in the Start Menu & Programs and Features, and an optional desktop icon * This is the main installer that creates entries in the Start Menu & Programs and Features, and an optional desktop icon
* **TweetDuck.Update.exe** * **TweetDuck.Update.exe**

View File

@@ -8,7 +8,7 @@ Edit layout & design
chylex chylex
[version] [version]
1.1.4 1.1.5
[website] [website]
https://tweetduck.chylex.com https://tweetduck.chylex.com

View File

@@ -12,6 +12,7 @@ enabled(){
moveTweetActionsToRight: true, moveTweetActionsToRight: true,
themeColorTweaks: true, themeColorTweaks: true,
revertIcons: true, revertIcons: true,
increaseQuoteTextSize: false,
smallComposeTextSize: false, smallComposeTextSize: false,
optimizeAnimations: true, optimizeAnimations: true,
avatarRadius: 2 avatarRadius: 2
@@ -366,6 +367,10 @@ enabled(){
this.css.insert(".tweet-actions > li:nth-child(4) { margin-right: 2px !important }"); this.css.insert(".tweet-actions > li:nth-child(4) { margin-right: 2px !important }");
} }
if (this.config.increaseQuoteTextSize){
this.css.insert(".quoted-tweet { font-size: 1em !important }");
}
if (this.config.smallComposeTextSize){ if (this.config.smallComposeTextSize){
this.css.insert(".compose-text { font-size: 12px !important; height: 120px !important }"); this.css.insert(".compose-text { font-size: 12px !important; height: 120px !important }");
} }
@@ -510,6 +515,10 @@ enabled(){
html[data-td-font] { font-size: ${this.config.fontSize} !important } html[data-td-font] { font-size: ${this.config.fontSize} !important }
.avatar { border-radius: ${this.config.avatarRadius}% !important } .avatar { border-radius: ${this.config.avatarRadius}% !important }
${this.config.increaseQuoteTextSize ? `
.quoted-tweet { font-size: 1em !important }
` : ``}
${this.config.revertIcons ? ` ${this.config.revertIcons ? `
@font-face { font-family: 'tweetdeckold'; src: url(\"https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff\") format(\"woff\"); font-weight: normal; font-style: normal } @font-face { font-family: 'tweetdeckold'; src: url(\"https://ton.twimg.com/tweetdeck-web/web/assets/fonts/tweetdeck-regular-webfont.5f4ea87976.woff\") format(\"woff\"); font-weight: normal; font-style: normal }
.icon-reply:before{content:"\\f006";font-family:tweetdeckold} .icon-reply:before{content:"\\f006";font-family:tweetdeckold}

View File

@@ -61,15 +61,13 @@
<option value="16px">Largest (16px)</option> <option value="16px">Largest (16px)</option>
<option value="custom">Custom</option> <option value="custom">Custom</option>
</select> </select>
<label class="checkbox">
<!-- ADVANCED --> <input data-td-key="increaseQuoteTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox">
Increase quoted tweet font size
<label class="txt-uppercase touch-larger-label">
<b>Advanced</b>
</label> </label>
<label class="checkbox"> <label class="checkbox">
<input data-td-key="optimizeAnimations" class="js-theme-checkbox touch-larger-label" type="checkbox"> <input data-td-key="smallComposeTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox">
Use more memory for smoother animations Small tweet input font size
</label> </label>
</div> </div>
@@ -102,9 +100,15 @@
<input data-td-key="revertIcons" class="js-theme-checkbox touch-larger-label" type="checkbox"> <input data-td-key="revertIcons" class="js-theme-checkbox touch-larger-label" type="checkbox">
Revert icon design Revert icon design
</label> </label>
<!-- ADVANCED -->
<label class="txt-uppercase touch-larger-label">
<b>Advanced</b>
</label>
<label class="checkbox"> <label class="checkbox">
<input data-td-key="smallComposeTextSize" class="js-theme-checkbox touch-larger-label" type="checkbox"> <input data-td-key="optimizeAnimations" class="js-theme-checkbox touch-larger-label" type="checkbox">
Small compose tweet font size Use more memory for smoother animations
</label> </label>
</div> </div>
@@ -163,11 +167,12 @@
.td-modal-inner-cols .l-column { .td-modal-inner-cols .l-column {
padding: 15px 9px; padding: 15px 9px;
box-sizing: border-box; box-sizing: border-box;
width: 225px;
font-size: 0; /* fix custom font size breaking the modal layout */ font-size: 0; /* fix custom font size breaking the modal layout */
} }
.td-modal-inner-cols .l-column:nth-child(2) { .td-modal-inner-cols .l-column:nth-child(3) {
width: 250px; width: 200px;
} }
.td-modal-inner-full { .td-modal-inner-full {
@@ -199,6 +204,14 @@
cursor: pointer; cursor: pointer;
} }
.td-modal-content select + label.checkbox {
margin-top: 9px;
}
.td-modal-content input.js-theme-checkbox, .td-modal-content input.js-theme-radio {
margin-top: 1px;
}
/* Avatar shape */ /* Avatar shape */
.td-avatar-shape-container { .td-avatar-shape-container {

View File

@@ -9,7 +9,7 @@ Emoji keyboard
chylex chylex
[version] [version]
1.4 1.4.1
[website] [website]
https://tweetduck.chylex.com https://tweetduck.chylex.com

View File

@@ -303,23 +303,26 @@ enabled(){
let firstColon = val.lastIndexOf(':', ele[0].selectionStart); let firstColon = val.lastIndexOf(':', ele[0].selectionStart);
return if firstColon === -1; return if firstColon === -1;
let search = val.substring(firstColon+1, ele[0].selectionStart); let search = val.substring(firstColon+1, ele[0].selectionStart).toLowerCase();
return if !/^[a-z_]+$/i.test(search); return if !/^[a-z_]+$/.test(search);
let keywords = search.split("_").filter(kw => kw.length > 0).map(kw => kw.toLowerCase()); let keywords = search.split("_").filter(kw => kw.length > 0).map(kw => kw.toLowerCase());
return if keywords.length === 0; return if keywords.length === 0;
let foundName = me.emojiNames.filter(name => keywords.every(kw => name.includes(kw))); let foundNames = me.emojiNames.filter(name => keywords.every(kw => name.includes(kw)));
if (foundName.length === 0){ if (foundNames.length === 0){
return; return;
} }
else if (foundNames.length > 1 && foundNames.includes(search)){
foundNames = [ search ];
}
lastEmojiKeyword = `:${search}:`; lastEmojiKeyword = `:${search}:`;
lastEmojiPosition = lastEmojiLength = 0; lastEmojiPosition = lastEmojiLength = 0;
if (foundName.length === 1){ if (foundNames.length === 1){
let foundIndex = me.emojiNames.indexOf(foundName[0]); let foundIndex = me.emojiNames.indexOf(foundNames[0]);
let foundEmoji; let foundEmoji;
for(let array of [ me.emojiData1, me.emojiData2[me.selectedSkinTone], me.emojiData3 ]){ for(let array of [ me.emojiData1, me.emojiData2[me.selectedSkinTone], me.emojiData3 ]){
@@ -346,7 +349,7 @@ enabled(){
lastEmojiLength = foundEmoji.length; lastEmojiLength = foundEmoji.length;
} }
} }
else if (foundName.length > 1){ else if (foundNames.length > 1){
e.preventDefault(); e.preventDefault();
ele.val(val.substring(0, firstColon)+val.substring(ele[0].selectionStart)); ele.val(val.substring(0, firstColon)+val.substring(ele[0].selectionStart));
ele[0].selectionEnd = ele[0].selectionStart = firstColon; ele[0].selectionEnd = ele[0].selectionStart = firstColon;

View File

@@ -8,7 +8,7 @@ Custom reply account
chylex chylex
[version] [version]
1.2.3 1.2.4
[website] [website]
https://tweetduck.chylex.com https://tweetduck.chylex.com
@@ -20,4 +20,4 @@ configuration.js
configuration.default.js configuration.default.js
[requires] [requires]
1.8 1.10.3

View File

@@ -6,7 +6,7 @@ enabled(){
this.lastSelectedAccount = null; this.lastSelectedAccount = null;
this.uiComposeTweetEvent = (e, data) => { this.uiComposeTweetEvent = (e, data) => {
return if data.type !== "reply" || data.popFromInline || !("element" in data); return if !(data.type === "reply" || (data.type === "tweet" && "quotedTweet" in data)) || data.popFromInline || !("element" in data);
var query; var query;

View File

@@ -3,6 +3,18 @@ $ErrorActionPreference = "Stop"
Set-Location $dir Set-Location $dir
function Check-Carriage-Return{
Param([Parameter(Mandatory = $True, Position = 1)] $fname)
$file = @(Get-ChildItem -Include $fname -Recurse)[0]
if ((Get-Content -Path $file.FullName -Raw).Contains("`r")){
Throw "$fname cannot contain carriage return"
}
Write-Host "Verified" $file.FullName.Substring($dir.Length)
}
function Rewrite-File{ function Rewrite-File{
[CmdletBinding()] [CmdletBinding()]
Param([Parameter(Mandatory = $True, ValueFromPipeline = $True)][array] $lines, [Parameter(Mandatory = $True, Position = 1)] $file) Param([Parameter(Mandatory = $True, ValueFromPipeline = $True)][array] $lines, [Parameter(Mandatory = $True, Position = 1)] $file)
@@ -11,23 +23,31 @@ function Rewrite-File{
Write-Host "Processed" $file.FullName.Substring($dir.Length) Write-Host "Processed" $file.FullName.Substring($dir.Length)
} }
ForEach($file in Get-ChildItem -Include *.js -Exclude configuration.default.js -Recurse){ try{
$lines = Get-Content -Path $file.FullName Check-Carriage-Return("emoji-ordering.txt")
$lines = $lines | % { $_.TrimStart() }
$lines = $lines -Replace '^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$', '$1'
$lines = $lines -Replace '(?<!\w)return(\s.*?)? if (.*?);', 'if ($2)return$1;'
,$lines | Rewrite-File $file
}
ForEach($file in Get-ChildItem -Include *.css -Recurse){ ForEach($file in Get-ChildItem -Filter *.js -Exclude configuration.default.js -Recurse){
$lines = Get-Content -Path $file.FullName $lines = Get-Content -Path $file.FullName
$lines = $lines -Replace '\s*/\*.*?\*/', '' $lines = $lines | % { $_.TrimStart() }
$lines = $lines -Replace '^\s+(.+):\s?(.+?)(?:\s?(!important))?;$', '$1:$2$3;' $lines = $lines -Replace '^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$', '$1'
$lines = $lines -Replace '^(\S.*?) {$', '$1{' $lines = $lines -Replace '(?<!\w)return(\s.*?)? if (.*?);', 'if ($2)return$1;'
@(($lines | Where { $_ -ne '' }) -Join ' ') | Rewrite-File $file ,$lines | Rewrite-File $file
} }
ForEach($file in Get-ChildItem -Include *.html -Recurse){ ForEach($file in Get-ChildItem -Filter *.css -Recurse){
$lines = Get-Content -Path $file.FullName $lines = Get-Content -Path $file.FullName
,($lines | % { $_.TrimStart() }) | Rewrite-File $file $lines = $lines -Replace '\s*/\*.*?\*/', ''
$lines = $lines -Replace '^\s+(.+):\s?(.+?)(?:\s?(!important))?;$', '$1:$2$3;'
$lines = $lines -Replace '^(\S.*?) {$', '$1{'
@(($lines | Where { $_ -ne '' }) -Join ' ') | Rewrite-File $file
}
ForEach($file in Get-ChildItem -Filter *.html -Recurse){
$lines = Get-Content -Path $file.FullName
,($lines | % { $_.TrimStart() }) | Rewrite-File $file
}
}catch{
Write-Host "Encountered an error while running PostBuild.ps1 on line" $_.InvocationInfo.ScriptLineNumber
Write-Host $_
Exit 1
} }

View File

@@ -814,25 +814,63 @@
}); });
// //
// Block: Make middle click on tweet reply icon open the compose drawer. Only works for non-temporary columns. // Block: Make middle click on tweet reply icon open the compose drawer, retweet icon trigger a quote, and favorite icon open a 'Like from accounts...' modal.
// //
app.delegate(".js-reply-action", "auxclick", function(e){ app.delegate(".tweet-action,.tweet-detail-action", "auxclick", function(e){
if (e.which === 2){ return if e.which !== 2;
let column = $(this).closest(".js-column");
if (column && column.hasClass("column") && $("[data-drawer='compose']").hasClass("is-hidden")){ let column = TD.controller.columnManager.get($(this).closest("section.js-column").attr("data-column"));
$(document).trigger("uiDrawerShowDrawer", { return if !column;
drawer: "compose",
withAnimation: true let ele = $(this).closest("article");
let tweet = column.findChirp(ele.attr("data-tweet-id")) || column.findChirp(ele.attr("data-key"));
return if !tweet;
switch($(this).attr("rel")){
case "reply":
let main = tweet.getMainTweet();
$(document).trigger("uiDockedComposeTweet", {
type: "reply",
from: [ tweet.account.getKey() ],
inReplyTo: {
id: tweet.id,
htmlText: main.htmlText,
user: {
screenName: main.user.screenName,
name: main.user.name,
profileImageURL: main.user.profileImageURL
}
},
mentions: tweet.getReplyUsers(),
element: ele
}); });
window.setTimeout(() => $(this).trigger("click"), 1); break;
}
e.preventDefault(); case "favorite":
e.stopPropagation(); $(document).trigger("uiShowFavoriteFromOptions", { tweet });
e.stopImmediatePropagation(); break;
case "retweet":
TD.controller.stats.quoteTweet();
$(document).trigger("uiComposeTweet", {
type: "tweet",
from: [ tweet.account.getKey() ],
quotedTweet: tweet.getMainTweet(),
element: ele // triggers reply-account plugin
});
break;
default:
return;
} }
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}); });
// //
@@ -890,12 +928,12 @@
// Block: Setup video player hooks. // Block: Setup video player hooks.
// //
(function(){ (function(){
var playVideo = function(url){ window.TDGF_playVideo = function(url, username){
$('<div id="td-video-player-overlay" class="ovl" style="display:block"></div>').on("click contextmenu", function(){ $('<div id="td-video-player-overlay" class="ovl" style="display:block"></div>').on("click contextmenu", function(){
$TD.playVideo(null); $TD.playVideo(null, null);
}).appendTo(app); }).appendTo(app);
$TD.playVideo(url); $TD.playVideo(url, username || null);
}; };
var getVideoTweetLink = function(obj){ var getVideoTweetLink = function(obj){
@@ -904,12 +942,16 @@
return link.attr("href"); return link.attr("href");
} }
var getUsername = function(tweet){
return tweet && (tweet.quotedTweet || tweet).getMainUser().screenName;
}
app.delegate(".js-gif-play", { app.delegate(".js-gif-play", {
click: function(e){ click: function(e){
let src = !e.ctrlKey && $(this).closest(".js-media-gif-container").find("video").attr("src"); let src = !e.ctrlKey && $(this).closest(".js-media-gif-container").find("video").attr("src");
if (src){ if (src){
playVideo(src); window.TDGF_playVideo(src, getUsername(highlightedTweetObj));
} }
else{ else{
$TD.openBrowser(getVideoTweetLink($(this))); $TD.openBrowser(getVideoTweetLink($(this)));
@@ -945,7 +987,7 @@
let media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId); let media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service === "twitter"){ if (media && media.isVideo && media.service === "twitter"){
playVideo(media.chooseVideoVariant().url); window.TDGF_playVideo(media.chooseVideoVariant().url, getUsername(this.chirp));
cancelModal = true; cancelModal = true;
} }
}); });

View File

@@ -87,14 +87,14 @@
}; };
// //
// Block: Setup global function to change plugin state. // Block: Setup a function to change plugin state.
// //
window.TDPF_setPluginState = function(identifier, enable){ window.TDPF_setPluginState = function(identifier, enable){
window.TD_PLUGINS.setState(window.TD_PLUGINS.findObject(identifier), enable); window.TD_PLUGINS.setState(window.TD_PLUGINS.findObject(identifier), enable);
}; };
// //
// Block: Setup global function to reload the page. // Block: Setup a function to reload the page.
// //
window.TDPF_requestReload = function(){ window.TDPF_requestReload = function(){
if (!isReloading){ if (!isReloading){
@@ -102,4 +102,9 @@
isReloading = true; isReloading = true;
} }
}; };
//
// Block: Setup a function to allow plugins to play video.
//
window.TDPF_playVideo = window.TDGF_playVideo;
})(); })();

View File

@@ -46,7 +46,7 @@
border-radius: 1px !important; border-radius: 1px !important;
} }
.compose-text-container, .compose-reply-tweet, .compose-message-recipient-input-container, .compose-message-recipient, .compose-media-bar-holder, .media-grid-container { .compose-text-container, .compose-reply-tweet, .compose-message-recipient-input-container, .compose-message-recipient, .compose-media-bar-holder, .media-grid-container, .js-quote-tweet-holder {
border-radius: 0 !important; border-radius: 0 !important;
} }

View File

@@ -31,7 +31,7 @@ namespace TweetDuck.Updates{
timerDownloadCheck.Stop(); timerDownloadCheck.Stop();
if (FormMessage.Error("Update Has Failed", "Could not download the update: "+(updateInfo.DownloadError?.Message ?? "unknown error")+"\n\nDo you want to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){ if (FormMessage.Error("Update Has Failed", "Could not download the update: "+(updateInfo.DownloadError?.Message ?? "unknown error")+"\n\nDo you want to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowserUnsafe(Program.Website); BrowserUtils.OpenExternalBrowser(Program.Website);
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
} }