mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-09-14 10:32:10 +02:00
Compare commits
36 Commits
1.22.1
...
extensions
Author | SHA1 | Date | |
---|---|---|---|
c2b5cf23cf
|
|||
095c23b472
|
|||
c5a42e74d9
|
|||
23ebd58da0
|
|||
9a6394d079
|
|||
88a55c8795
|
|||
d406866a02
|
|||
c2094bd2c0
|
|||
065b5a751b
|
|||
0dc454e61f
|
|||
416a43f0b1
|
|||
7a338076db
|
|||
54bf1c2012
|
|||
32681259f6
|
|||
1c1aa5ea44
|
|||
da54af221c
|
|||
6c8d518e0d
|
|||
697f4f1569
|
|||
15d4ec3228
|
|||
c303346bc3
|
|||
b9af966849
|
|||
0a7459b72e
|
|||
9953f06ab1
|
|||
0c8159aa79
|
|||
c785a7ed8c
|
|||
b1328e5b1f
|
|||
cb94f0c81e
|
|||
8de2989f12
|
|||
1cf7d13873
|
|||
35c2ee3673
|
|||
a1b4c31450
|
|||
ea95e5cbac
|
|||
2927097e8e
|
|||
b5bffdb95b
|
|||
bee894bfbb
|
|||
96d2e7cc7c
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ bld/*
|
|||||||
# Rider
|
# Rider
|
||||||
**/.idea/dictionaries
|
**/.idea/dictionaries
|
||||||
**/.idea/misc.xml
|
**/.idea/misc.xml
|
||||||
|
**/.idea/riderMarkupCache.xml
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.suo
|
*.suo
|
||||||
|
@@ -11,8 +11,8 @@
|
|||||||
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
<option name="PROJECT_KIND" value="Console" />
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
<option name="PROJECT_TFM" value=".NETFramework,Version=v4.7.2" />
|
<option name="PROJECT_TFM" value="net7.0-windows" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Build" />
|
<option name="Build" />
|
||||||
</method>
|
</method>
|
||||||
|
32
README.md
32
README.md
@@ -43,14 +43,14 @@ Download links and system requirements are on the [official website](https://twe
|
|||||||
|
|
||||||
Building TweetDuck for Windows requires at minimum [Visual Studio 2019](https://visualstudio.microsoft.com/downloads) and Windows 7. Before opening the solution, open Visual Studio Installer and make sure you have the following Visual Studio workloads and components installed:
|
Building TweetDuck for Windows requires at minimum [Visual Studio 2019](https://visualstudio.microsoft.com/downloads) and Windows 7. Before opening the solution, open Visual Studio Installer and make sure you have the following Visual Studio workloads and components installed:
|
||||||
* **.NET desktop development**
|
* **.NET desktop development**
|
||||||
* .NET Framework 4.7.2 targeting pack
|
* .NET SDK
|
||||||
* F# desktop language support
|
* F# desktop language support
|
||||||
* **Desktop development with C++**
|
* **Desktop development with C++**
|
||||||
* MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.20 / Latest)
|
* MSVC v142 - VS 2019 C++ x64/x86 build tools (v14.20 / Latest)
|
||||||
|
|
||||||
In the **Installation details** panel, you can expand the workloads you selected, and uncheck any components that are not listed above to save space.
|
In the **Installation details** panel, you can expand the workloads you selected, and uncheck any components that are not listed above to save space. You may uncheck the .NET SDK component if you installed the [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) directly.
|
||||||
|
|
||||||
Building TweetDuck for Linux requires [.NET 5](https://docs.microsoft.com/en-us/dotnet/core/install/linux). The Linux project has its own solution file in the `linux/` folder.
|
Building TweetDuck for Linux requires [.NET 6 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux). The Linux project has its own solution file in the `linux/` folder.
|
||||||
|
|
||||||
### Editors
|
### Editors
|
||||||
|
|
||||||
@@ -66,15 +66,15 @@ Icons and logos were designed in [Affinity Designer](https://affinity.serif.com/
|
|||||||
> If you don't want to build installers using the existing foundations, you can skip this section.
|
> If you don't want to build installers using the existing foundations, you can skip this section.
|
||||||
|
|
||||||
Official Windows installers are built using [InnoSetup](https://jrsoftware.org/isinfo.php) and [Inno Download Plugin](https://mitrichsoftware.wordpress.com/inno-setup-tools/inno-download-plugin/), specifically:
|
Official Windows installers are built using [InnoSetup](https://jrsoftware.org/isinfo.php) and [Inno Download Plugin](https://mitrichsoftware.wordpress.com/inno-setup-tools/inno-download-plugin/), specifically:
|
||||||
* [InnoSetup 5.6.1](https://files.jrsoftware.org/is/5/innosetup-5.6.1.exe) with Preprocessor support
|
* [InnoSetup 6.2.1](https://files.jrsoftware.org/is/6/innosetup-6.2.1.exe)
|
||||||
* [Inno Download Plugin 1.5.0](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list)
|
* [Inno Download Plugin 1.5.1](https://drive.google.com/folderview?id=0Bzw1xBVt0mokSXZrUEFIanV4azA&usp=sharing#list)
|
||||||
|
|
||||||
When installing InnoSetup, you can choose to include Inno Script Studio which I recommend for editing and testing installer configuration files in the `bld` folder (`.iss` extension).
|
During installation, the download plugin will ask whether to add its include path to `ISPPBuiltins.iss`. Note that this option does not work with InnoSetup 6, so TweetDuck installers don't need it.
|
||||||
|
|
||||||
Scripts for building installers require the `PATH` environment variable to include the InnoSetup installation folder. You can either edit `PATH` manually, or use a program like [Rapid Environment Editor](https://www.rapidee.com/en/about) to simplify the process. For example, this is the installation folder I added to `PATH` under **User variables**:
|
Scripts for building installers require the `PATH` environment variable to include the InnoSetup installation folder. You can either edit `PATH` manually, or use a program like [Rapid Environment Editor](https://www.rapidee.com/en/about) to simplify the process. For example, this is the installation folder I added to `PATH` under **User variables**:
|
||||||
* `C:\Program Files (x86)\Inno Setup 5`
|
* `C:\Program Files (x86)\Inno Setup 6`
|
||||||
|
|
||||||
You may need to restart Visual Studio after changing `PATH` for the change to take place.
|
You may need to restart Visual Studio or Rider after changing `PATH` for the change to take place.
|
||||||
|
|
||||||
## Solution Overview
|
## Solution Overview
|
||||||
|
|
||||||
@@ -84,13 +84,13 @@ On Windows, TweetDuck uses the [CefSharp](https://github.com/cefsharp/CefSharp/)
|
|||||||
|
|
||||||
On Linux, TweetDuck uses the [ChromiumGtk](https://github.com/lunixo/ChromiumGtk) library, which combines [CefGlue](https://gitlab.com/xiliumhq/chromiumembedded/cefglue) for the browser component and [GtkSharp](https://github.com/GtkSharp/GtkSharp) for the GUI.
|
On Linux, TweetDuck uses the [ChromiumGtk](https://github.com/lunixo/ChromiumGtk) library, which combines [CefGlue](https://gitlab.com/xiliumhq/chromiumembedded/cefglue) for the browser component and [GtkSharp](https://github.com/GtkSharp/GtkSharp) for the GUI.
|
||||||
|
|
||||||
The solution contains several C# projects for executables and libraries, and F# projects for automated tests.
|
The solution contains several C# projects for executables and libraries, and F# projects for automated tests. All projects target `.NET 6` and either `C# 10` or `F#`.
|
||||||
|
|
||||||
Projects are organized into folders:
|
Projects are organized into folders:
|
||||||
* Windows projects are in the `windows/` folder, and target `.NET Framework 4.7.2` + `C# 8.0`
|
* Windows projects are in the `windows/` folder
|
||||||
* Linux projects are in the `linux/` folder, and target `.NET 5` + `C#`
|
* Linux projects are in the `linux/` folder
|
||||||
* Libraries (`TweetLib.*`) are in the `lib/` folder, and target `.NET Standard 2.0` + `C# 9.0`
|
* Libraries (`TweetLib.*`) are in the `lib/` folder
|
||||||
* Tests (`TweetTest.*`) are also in the `lib/` folder, and target `.NET Framework 4.7.2` + `F#`
|
* Tests (`TweetTest.*`) are also in the `lib/` folder
|
||||||
|
|
||||||
Here are a few things to keep in mind:
|
Here are a few things to keep in mind:
|
||||||
* Executable projects have their entry points in `Program.cs`
|
* Executable projects have their entry points in `Program.cs`
|
||||||
@@ -130,7 +130,7 @@ Main Windows executable. It has a dependency on [CefSharp](https://github.com/ce
|
|||||||
|
|
||||||
#### TweetDuck.Browser
|
#### TweetDuck.Browser
|
||||||
|
|
||||||
Windows executable that hosts various Chromium processes. It depends on two specific DLLs from the [CefSharp](https://github.com/cefsharp/CefSharp/) package. After updating [CefSharp](https://github.com/cefsharp/CefSharp/), run the `windows/TweetDuck/Resources/PostCefUpdate.ps1` PowerShell script to update these dependencies to the new version.
|
Windows executable that hosts various Chromium processes. It has a dependency on [CefSharp](https://github.com/cefsharp/CefSharp/).
|
||||||
|
|
||||||
#### TweetDuck.Video
|
#### TweetDuck.Video
|
||||||
|
|
||||||
@@ -142,6 +142,10 @@ By default, [CefSharp](https://github.com/cefsharp/CefSharp/) is not built with
|
|||||||
|
|
||||||
Windows library that implements `TweetLib.Browser.CEF` using the [CefSharp](https://github.com/cefsharp/CefSharp/) library and Windows Forms.
|
Windows library that implements `TweetLib.Browser.CEF` using the [CefSharp](https://github.com/cefsharp/CefSharp/) library and Windows Forms.
|
||||||
|
|
||||||
|
#### TweetLib.WinForms.Legacy
|
||||||
|
|
||||||
|
Windows library that re-adds some legacy Windows Forms components that were removed in .NET Core 3.1. The sources were taken from the [.NET Core 3.0 sources of Windows Forms](https://github.com/dotnet/winforms/tree/v3.0.2), and edited to remove unnecessary features.
|
||||||
|
|
||||||
### Linux Projects
|
### Linux Projects
|
||||||
|
|
||||||
#### TweetDuck
|
#### TweetDuck
|
||||||
|
@@ -10,6 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetDuck.Video", "windows\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetImpl.CefSharp", "windows\TweetImpl.CefSharp\TweetImpl.CefSharp.csproj", "{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetImpl.CefSharp", "windows\TweetImpl.CefSharp\TweetImpl.CefSharp.csproj", "{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.WinForms.Legacy", "windows\TweetLib.WinForms.Legacy\TweetLib.WinForms.Legacy.csproj", "{B54E732A-4090-4DAA-9ABD-311368C17B68}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Api", "lib\TweetLib.Api\TweetLib.Api.csproj", "{85596C10-F76E-4619-9CC6-6C1593880F83}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
|
||||||
@@ -48,6 +52,14 @@ Global
|
|||||||
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Debug|x86.Build.0 = Debug|x86
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Debug|x86.Build.0 = Debug|x86
|
||||||
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.ActiveCfg = Release|x86
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.ActiveCfg = Release|x86
|
||||||
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.Build.0 = Release|x86
|
{44DF3E2E-F465-4A31-8B43-F40FFFB018BA}.Release|x86.Build.0 = Release|x86
|
||||||
|
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
|
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.Build.0 = Debug|x86
|
||||||
|
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.Build.0 = Release|x86
|
||||||
|
{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
|
{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.Build.0 = Debug|x86
|
||||||
|
{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.Build.0 = Release|x86
|
||||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
|
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
|
{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
|
||||||
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
|
{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
@@ -6,6 +6,6 @@ using TweetDuck;
|
|||||||
|
|
||||||
namespace TweetDuck {
|
namespace TweetDuck {
|
||||||
internal static class Version {
|
internal static class Version {
|
||||||
public const string Tag = "1.22.1";
|
public const string Tag = "1.25.4";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
bld/Redist/API-MS-Win-core-xstate-l2-1-0.dll
Normal file
BIN
bld/Redist/API-MS-Win-core-xstate-l2-1-0.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
bld/Redist/api-ms-win-core-console-l1-2-0.dll
Normal file
BIN
bld/Redist/api-ms-win-core-console-l1-2-0.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bld/Redist/api-ms-win-core-fibers-l1-1-0.dll
Normal file
BIN
bld/Redist/api-ms-win-core-fibers-l1-1-0.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,16 +1,21 @@
|
|||||||
; Script generated by the Inno Script Studio Wizard.
|
; Script generated by the Inno Script Studio Wizard.
|
||||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
|
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
||||||
|
|
||||||
#define MyAppName "TweetDuck"
|
#define MyAppName "TweetDuck"
|
||||||
#define MyAppPublisher "chylex"
|
#define MyAppPublisher "chylex"
|
||||||
#define MyAppURL "https://tweetduck.chylex.com"
|
#define MyAppURL "https://tweetduck.chylex.com"
|
||||||
#define MyAppShortURL "https://td.chylex.com"
|
#define MyAppShortURL "https://td.chylex.com"
|
||||||
#define MyAppExeName "TweetDuck.exe"
|
#define MyAppExeName "TweetDuck.exe"
|
||||||
|
|
||||||
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
#define MyAppArchitecture "x86"
|
||||||
|
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\TweetDuck.exe")
|
||||||
|
|
||||||
|
#include ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") + "\idp.iss"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
AppId={{{#MyAppID}}
|
||||||
AppName={#MyAppName}
|
AppName={#MyAppName}
|
||||||
AppVersion={#MyAppVersion}
|
AppVersion={#MyAppVersion}
|
||||||
AppVerName={#MyAppName} {#MyAppVersion}
|
AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
@@ -29,13 +34,11 @@ Uninstallable=TDIsUninstallable
|
|||||||
UninstallDisplayName={#MyAppName}
|
UninstallDisplayName={#MyAppName}
|
||||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||||
Compression=lzma2/ultra
|
Compression=lzma2/ultra
|
||||||
LZMADictionarySize=15360
|
LZMADictionarySize=32768
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
InternalCompressLevel=normal
|
InternalCompressLevel=normal
|
||||||
MinVersion=0,6.1
|
MinVersion=0,6.1
|
||||||
|
|
||||||
#include <idp.iss>
|
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
@@ -43,8 +46,8 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
|||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalTasks}"; Flags: unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||||
@@ -68,20 +71,11 @@ AdditionalTasks=Additional shortcuts and components:
|
|||||||
var UpdatePath: String;
|
var UpdatePath: String;
|
||||||
var VisitedTasksPage: Boolean;
|
var VisitedTasksPage: Boolean;
|
||||||
|
|
||||||
function TDGetNetFrameworkVersion: Cardinal; forward;
|
{ Prepare installation variables. }
|
||||||
|
|
||||||
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
|
|
||||||
function InitializeSetup: Boolean;
|
function InitializeSetup: Boolean;
|
||||||
begin
|
begin
|
||||||
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
|
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
|
||||||
VisitedTasksPage := False
|
VisitedTasksPage := False
|
||||||
|
|
||||||
if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
|
|
||||||
begin
|
|
||||||
Result := False
|
|
||||||
Exit
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := True
|
Result := True
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -140,17 +134,3 @@ function TDIsUninstallable: Boolean;
|
|||||||
begin
|
begin
|
||||||
Result := (UpdatePath = '')
|
Result := (UpdatePath = '')
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Return DWORD value containing the build version of .NET Framework. }
|
|
||||||
function TDGetNetFrameworkVersion: Cardinal;
|
|
||||||
var FrameworkVersion: Cardinal;
|
|
||||||
|
|
||||||
begin
|
|
||||||
if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', FrameworkVersion) then
|
|
||||||
begin
|
|
||||||
Result := FrameworkVersion
|
|
||||||
Exit
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := 0
|
|
||||||
end;
|
|
||||||
|
@@ -1,16 +1,21 @@
|
|||||||
; Script generated by the Inno Script Studio Wizard.
|
; Script generated by the Inno Script Studio Wizard.
|
||||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
|
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
||||||
|
|
||||||
#define MyAppName "TweetDuck"
|
#define MyAppName "TweetDuck"
|
||||||
#define MyAppPublisher "chylex"
|
#define MyAppPublisher "chylex"
|
||||||
#define MyAppURL "https://tweetduck.chylex.com"
|
#define MyAppURL "https://tweetduck.chylex.com"
|
||||||
#define MyAppShortURL "https://td.chylex.com"
|
#define MyAppShortURL "https://td.chylex.com"
|
||||||
#define MyAppExeName "TweetDuck.exe"
|
#define MyAppExeName "TweetDuck.exe"
|
||||||
|
|
||||||
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
#define MyAppArchitecture "x86"
|
||||||
|
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\TweetDuck.exe")
|
||||||
|
|
||||||
|
#include ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") + "\idp.iss"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{8C25A716-7E11-4AAD-9992-8B5D0C78AE06}
|
AppId={{{#MyAppID}}
|
||||||
AppName={#MyAppName} Portable
|
AppName={#MyAppName} Portable
|
||||||
AppVersion={#MyAppVersion}
|
AppVersion={#MyAppVersion}
|
||||||
AppVerName={#MyAppName} {#MyAppVersion}
|
AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
@@ -29,19 +34,17 @@ Uninstallable=no
|
|||||||
UsePreviousAppDir=no
|
UsePreviousAppDir=no
|
||||||
PrivilegesRequired=lowest
|
PrivilegesRequired=lowest
|
||||||
Compression=lzma2/ultra
|
Compression=lzma2/ultra
|
||||||
LZMADictionarySize=15360
|
LZMADictionarySize=32768
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
InternalCompressLevel=normal
|
InternalCompressLevel=normal
|
||||||
MinVersion=0,6.1
|
MinVersion=0,6.1
|
||||||
|
|
||||||
#include <idp.iss>
|
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall shellexec skipifsilent
|
||||||
@@ -52,19 +55,10 @@ AdditionalTasks=Additional components:
|
|||||||
[Code]
|
[Code]
|
||||||
var UpdatePath: String;
|
var UpdatePath: String;
|
||||||
|
|
||||||
function TDGetNetFrameworkVersion: Cardinal; forward;
|
{ Prepare installation variables. }
|
||||||
|
|
||||||
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. }
|
|
||||||
function InitializeSetup: Boolean;
|
function InitializeSetup: Boolean;
|
||||||
begin
|
begin
|
||||||
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
|
UpdatePath := ExpandConstant('{param:UPDATEPATH}')
|
||||||
|
|
||||||
if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
|
|
||||||
begin
|
|
||||||
Result := False
|
|
||||||
Exit
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := True
|
Result := True
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -102,17 +96,3 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Return DWORD value containing the build version of .NET Framework. }
|
|
||||||
function TDGetNetFrameworkVersion: Cardinal;
|
|
||||||
var FrameworkVersion: Cardinal;
|
|
||||||
|
|
||||||
begin
|
|
||||||
if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', FrameworkVersion) then
|
|
||||||
begin
|
|
||||||
Result := FrameworkVersion
|
|
||||||
Exit
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := 0
|
|
||||||
end;
|
|
||||||
|
@@ -1,15 +1,19 @@
|
|||||||
; Script generated by the Inno Script Studio Wizard.
|
; Script generated by the Inno Script Studio Wizard.
|
||||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||||
|
|
||||||
|
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
||||||
|
|
||||||
#define MyAppName "TweetDuck"
|
#define MyAppName "TweetDuck"
|
||||||
#define MyAppPublisher "chylex"
|
#define MyAppPublisher "chylex"
|
||||||
#define MyAppURL "https://tweetduck.chylex.com"
|
#define MyAppURL "https://tweetduck.chylex.com"
|
||||||
#define MyAppShortURL "https://td.chylex.com"
|
#define MyAppShortURL "https://td.chylex.com"
|
||||||
#define MyAppExeName "TweetDuck.exe"
|
#define MyAppExeName "TweetDuck.exe"
|
||||||
|
|
||||||
#define MyAppID "8C25A716-7E11-4AAD-9992-8B5D0C78AE06"
|
#define MyAppArchitecture "x86"
|
||||||
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\TweetDuck.exe")
|
#define MyAppVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\TweetDuck.exe")
|
||||||
#define CefVersion GetFileVersion("..\windows\TweetDuck\bin\x86\Release\libcef.dll")
|
#define CefVersion GetFileVersion("..\windows\TweetDuck\bin\" + MyAppArchitecture + "\Release\libcef.dll")
|
||||||
|
|
||||||
|
#include ReadReg(HKLM, "Software\Mitrich Software\Inno Download Plugin", "InstallDir") + "\idp.iss"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={{{#MyAppID}}
|
AppId={{{#MyAppID}}
|
||||||
@@ -37,19 +41,17 @@ SolidCompression=True
|
|||||||
InternalCompressLevel=normal
|
InternalCompressLevel=normal
|
||||||
MinVersion=0,6.1
|
MinVersion=0,6.1
|
||||||
|
|
||||||
#include <idp.iss>
|
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\TweetDuck.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\TweetImpl.*"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\TweetImpl.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\TweetLib.*"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\guide\*.*"; DestDir: "{app}\guide"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\resources\*.*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
Source: "..\windows\TweetDuck\bin\x86\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\windows\TweetDuck\bin\{#MyAppArchitecture}\Release\plugins\*.*"; DestDir: "{app}\plugins"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: TDIsUninstallable
|
||||||
@@ -69,22 +71,24 @@ Type: filesandordirs; Name: "{localappdata}\{#MyAppName}\GPUCache"
|
|||||||
Type: files; Name: "{app}\CEFSHARP-LICENSE.txt"
|
Type: files; Name: "{app}\CEFSHARP-LICENSE.txt"
|
||||||
Type: files; Name: "{app}\LICENSE.txt"
|
Type: files; Name: "{app}\LICENSE.txt"
|
||||||
Type: files; Name: "{app}\README.txt"
|
Type: files; Name: "{app}\README.txt"
|
||||||
Type: files; Name: "{app}\natives_blob.bin"
|
|
||||||
Type: files; Name: "{app}\cef.pak"
|
Type: files; Name: "{app}\cef.pak"
|
||||||
Type: files; Name: "{app}\cef_100_percent.pak"
|
Type: files; Name: "{app}\cef_100_percent.pak"
|
||||||
Type: files; Name: "{app}\cef_200_percent.pak"
|
Type: files; Name: "{app}\cef_200_percent.pak"
|
||||||
Type: files; Name: "{app}\cef_extensions.pak"
|
Type: files; Name: "{app}\cef_extensions.pak"
|
||||||
Type: files; Name: "{app}\devtools_resources.pak"
|
Type: files; Name: "{app}\devtools_resources.pak"
|
||||||
|
Type: files; Name: "{app}\natives_blob.bin"
|
||||||
|
Type: files; Name: "{app}\dbgshim.dll"
|
||||||
|
Type: files; Name: "{app}\mscordaccore_x86_x86_6.*.dll"
|
||||||
Type: filesandordirs; Name: "{app}\guide"
|
Type: filesandordirs; Name: "{app}\guide"
|
||||||
Type: filesandordirs; Name: "{app}\plugins\official"
|
Type: filesandordirs; Name: "{app}\plugins\official"
|
||||||
Type: filesandordirs; Name: "{app}\resources"
|
Type: filesandordirs; Name: "{app}\resources"
|
||||||
Type: filesandordirs; Name: "{app}\scripts"
|
Type: filesandordirs; Name: "{app}\scripts"
|
||||||
|
Type: filesandordirs; Name: "{app}\swiftshader"
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
function TDIsUninstallable: Boolean; forward;
|
function TDIsUninstallable: Boolean; forward;
|
||||||
function TDGetRunArgs(Param: String): String; forward;
|
function TDGetRunArgs(Param: String): String; forward;
|
||||||
function TDFindUpdatePath: String; forward;
|
function TDFindUpdatePath: String; forward;
|
||||||
function TDGetNetFrameworkVersion: Cardinal; forward;
|
|
||||||
function TDGetAppVersionClean: String; forward;
|
function TDGetAppVersionClean: String; forward;
|
||||||
function TDGetFullDownloadFileName: String; forward;
|
function TDGetFullDownloadFileName: String; forward;
|
||||||
function TDIsMatchingCEFVersion: Boolean; forward;
|
function TDIsMatchingCEFVersion: Boolean; forward;
|
||||||
@@ -93,7 +97,7 @@ procedure TDExecuteFullDownload; forward;
|
|||||||
var IsPortable: Boolean;
|
var IsPortable: Boolean;
|
||||||
var UpdatePath: String;
|
var UpdatePath: String;
|
||||||
|
|
||||||
{ Check .NET Framework version on startup, ask user if they want to proceed if older than 4.7.2. Prepare full download package if required. }
|
{ Prepare update installation, and the full download package if required. }
|
||||||
function InitializeSetup: Boolean;
|
function InitializeSetup: Boolean;
|
||||||
begin
|
begin
|
||||||
IsPortable := ExpandConstant('{param:PORTABLE}') = '1'
|
IsPortable := ExpandConstant('{param:PORTABLE}') = '1'
|
||||||
@@ -111,12 +115,6 @@ begin
|
|||||||
idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe'))
|
idpAddFile('https://github.com/{#MyAppPublisher}/{#MyAppName}/releases/download/'+TDGetAppVersionClean()+'/'+TDGetFullDownloadFileName(), ExpandConstant('{tmp}\{#MyAppName}.Full.exe'))
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if (TDGetNetFrameworkVersion() < 461808) and (MsgBox('{#MyAppName} requires .NET Framework 4.7.2 or newer,'+#13+#10+'please visit {#MyAppShortURL} for a download link.'+#13+#10+#13+#10'Do you want to proceed with the setup anyway?', mbCriticalError, MB_YESNO or MB_DEFBUTTON2) = IDNO) then
|
|
||||||
begin
|
|
||||||
Result := False
|
|
||||||
Exit
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := True
|
Result := True
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -211,20 +209,6 @@ begin
|
|||||||
Result := Path
|
Result := Path
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ Return DWORD value containing the build version of .NET Framework. }
|
|
||||||
function TDGetNetFrameworkVersion: Cardinal;
|
|
||||||
var FrameworkVersion: Cardinal;
|
|
||||||
|
|
||||||
begin
|
|
||||||
if RegQueryDWordValue(HKEY_LOCAL_MACHINE, 'Software\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', FrameworkVersion) then
|
|
||||||
begin
|
|
||||||
Result := FrameworkVersion
|
|
||||||
Exit
|
|
||||||
end;
|
|
||||||
|
|
||||||
Result := 0
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ Return the name of the full installer file to download from GitHub. }
|
{ Return the name of the full installer file to download from GitHub. }
|
||||||
function TDGetFullDownloadFileName: String;
|
function TDGetFullDownloadFileName: String;
|
||||||
begin
|
begin
|
||||||
|
7
global.json
Normal file
7
global.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"rollForward": "latestMinor",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
37
lib/TweetLib.Api/Data/NamespacedResource.cs
Normal file
37
lib/TweetLib.Api/Data/NamespacedResource.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace TweetLib.Api.Data {
|
||||||
|
public readonly struct NamespacedResource {
|
||||||
|
public Resource Namespace { get; }
|
||||||
|
public Resource Path { get; }
|
||||||
|
|
||||||
|
public NamespacedResource(Resource ns, Resource path) {
|
||||||
|
Namespace = ns;
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(NamespacedResource other) {
|
||||||
|
return Namespace.Equals(other.Namespace) && Path.Equals(other.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) {
|
||||||
|
return obj is NamespacedResource other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(NamespacedResource left, NamespacedResource right) {
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(NamespacedResource left, NamespacedResource right) {
|
||||||
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
unchecked {
|
||||||
|
return (Namespace.GetHashCode() * 397) ^ Path.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return $"{Namespace}:{Path}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
namespace TweetLib.Api.Data.Notification {
|
||||||
|
/// <summary>
|
||||||
|
/// Allows extensions to decide which screen to use for notifications.
|
||||||
|
///
|
||||||
|
/// Every registered provider becomes available in the Options dialog and has to be explicitly selected by the user. Only one provider
|
||||||
|
/// can be active at any given time.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDesktopNotificationScreenProvider {
|
||||||
|
/// <summary>
|
||||||
|
/// A unique identifier of this provider. Only needs to be unique within the scope of this plugin.
|
||||||
|
/// </summary>
|
||||||
|
Resource Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text displayed in the user interface.
|
||||||
|
/// </summary>
|
||||||
|
string DisplayName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a screen that will be used to display the next desktop notification.
|
||||||
|
///
|
||||||
|
/// If the return value is <c>null</c> or a screen that is not present in <see cref="IScreenLayout.AllScreens" />, desktop
|
||||||
|
/// notifications will be temporarily paused and this method will be called again after an unspecified amount of time (but
|
||||||
|
/// not sooner than 1 second since the last call).
|
||||||
|
/// </summary>
|
||||||
|
IScreen? PickScreen(IScreenLayout layout);
|
||||||
|
}
|
||||||
|
}
|
7
lib/TweetLib.Api/Data/Notification/IScreen.cs
Normal file
7
lib/TweetLib.Api/Data/Notification/IScreen.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace TweetLib.Api.Data.Notification {
|
||||||
|
public interface IScreen {
|
||||||
|
ScreenBounds Bounds { get; }
|
||||||
|
string Name { get; }
|
||||||
|
bool IsPrimary { get; }
|
||||||
|
}
|
||||||
|
}
|
9
lib/TweetLib.Api/Data/Notification/IScreenLayout.cs
Normal file
9
lib/TweetLib.Api/Data/Notification/IScreenLayout.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace TweetLib.Api.Data.Notification {
|
||||||
|
public interface IScreenLayout {
|
||||||
|
IScreen PrimaryScreen { get; }
|
||||||
|
IScreen TweetDuckScreen { get; }
|
||||||
|
List<IScreen> AllScreens { get; }
|
||||||
|
}
|
||||||
|
}
|
18
lib/TweetLib.Api/Data/Notification/ScreenBounds.cs
Normal file
18
lib/TweetLib.Api/Data/Notification/ScreenBounds.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace TweetLib.Api.Data.Notification {
|
||||||
|
public readonly struct ScreenBounds {
|
||||||
|
public int X1 { get; }
|
||||||
|
public int Y1 { get; }
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
|
||||||
|
public int X2 => X1 + Width;
|
||||||
|
public int Y2 => Y1 + Height;
|
||||||
|
|
||||||
|
public ScreenBounds(int x1, int y1, int width, int height) {
|
||||||
|
X1 = x1;
|
||||||
|
Y1 = y1;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
lib/TweetLib.Api/Data/Resource.cs
Normal file
43
lib/TweetLib.Api/Data/Resource.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace TweetLib.Api.Data {
|
||||||
|
public readonly struct Resource {
|
||||||
|
private const string ValidCharacterPattern = "^[a-z0-9_]+$";
|
||||||
|
private static readonly Regex ValidCharacterRegex = new Regex(ValidCharacterPattern, RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public Resource(string name) {
|
||||||
|
if (!ValidCharacterRegex.IsMatch(name)) {
|
||||||
|
throw new ArgumentException("Resource name must match the regex: " + ValidCharacterPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(Resource other) {
|
||||||
|
return Name == other.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) {
|
||||||
|
return obj is Resource other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(Resource left, Resource right) {
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(Resource left, Resource right) {
|
||||||
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() {
|
||||||
|
return Name.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
lib/TweetLib.Api/ITweetDuckApi.cs
Normal file
5
lib/TweetLib.Api/ITweetDuckApi.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace TweetLib.Api {
|
||||||
|
public interface ITweetDuckApi {
|
||||||
|
T? FindService<T>() where T : class, ITweetDuckService;
|
||||||
|
}
|
||||||
|
}
|
3
lib/TweetLib.Api/ITweetDuckService.cs
Normal file
3
lib/TweetLib.Api/ITweetDuckService.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace TweetLib.Api {
|
||||||
|
public interface ITweetDuckService {}
|
||||||
|
}
|
7
lib/TweetLib.Api/Service/INotificationService.cs
Normal file
7
lib/TweetLib.Api/Service/INotificationService.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
using TweetLib.Api.Data.Notification;
|
||||||
|
|
||||||
|
namespace TweetLib.Api.Service {
|
||||||
|
public interface INotificationService : ITweetDuckService {
|
||||||
|
void RegisterDesktopNotificationScreenProvider(IDesktopNotificationScreenProvider provider);
|
||||||
|
}
|
||||||
|
}
|
15
lib/TweetLib.Api/TweetDuckExtension.cs
Normal file
15
lib/TweetLib.Api/TweetDuckExtension.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using TweetLib.Api.Data;
|
||||||
|
|
||||||
|
namespace TweetLib.Api {
|
||||||
|
public abstract class TweetDuckExtension {
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier of the extension.
|
||||||
|
/// </summary>
|
||||||
|
public abstract Resource Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the extension is loaded on startup, or enabled at runtime.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Enable(ITweetDuckApi api);
|
||||||
|
}
|
||||||
|
}
|
16
lib/TweetLib.Api/TweetLib.Api.csproj
Normal file
16
lib/TweetLib.Api/TweetLib.Api.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
|
<Platforms>x86;x64</Platforms>
|
||||||
|
<LangVersion>10</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="..\..\Version.cs" Link="Version.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@@ -2,7 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace TweetLib.Browser.CEF.Data {
|
namespace TweetLib.Browser.CEF.Data {
|
||||||
abstract class ContextMenuActionRegistry<T> {
|
abstract class ContextMenuActionRegistry<T> where T : notnull {
|
||||||
private readonly Dictionary<T, Action> actions = new ();
|
private readonly Dictionary<T, Action> actions = new ();
|
||||||
|
|
||||||
protected abstract T NextId(int n);
|
protected abstract T NextId(int n);
|
||||||
|
@@ -21,7 +21,7 @@ namespace TweetLib.Browser.CEF.Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void Register(string url, Func<TResourceHandler> factory) {
|
private void Register(string url, Func<TResourceHandler> factory) {
|
||||||
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) {
|
||||||
throw new ArgumentException("Resource handler URL must be absolute!");
|
throw new ArgumentException("Resource handler URL must be absolute!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,7 +29,7 @@ namespace TweetLib.Browser.CEF.Logic {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fileDialogOpener.OpenFile("Open Files", multiple, filters, files => {
|
fileDialogOpener.OpenFile("Open Files", multiple, filters, files => {
|
||||||
string ext = Path.GetExtension(files[0])!.ToLower();
|
string ext = Path.GetExtension(files[0]).ToLower();
|
||||||
callbackAdapter.Continue(callback, Array.FindIndex(supportedExtensions, filter => ParseFileType(filter).Contains(ext)), files);
|
callbackAdapter.Continue(callback, Array.FindIndex(supportedExtensions, filter => ParseFileType(filter).Contains(ext)), files);
|
||||||
callbackAdapter.Dispose(callback);
|
callbackAdapter.Dispose(callback);
|
||||||
}, () => {
|
}, () => {
|
||||||
|
@@ -9,7 +9,7 @@ namespace TweetLib.Browser.CEF.Logic {
|
|||||||
|
|
||||||
int pipe = text.IndexOf('|');
|
int pipe = text.IndexOf('|');
|
||||||
if (pipe != -1) {
|
if (pipe != -1) {
|
||||||
type = text.Substring(0, pipe) switch {
|
type = text[..pipe] switch {
|
||||||
"error" => MessageDialogType.Error,
|
"error" => MessageDialogType.Error,
|
||||||
"warning" => MessageDialogType.Warning,
|
"warning" => MessageDialogType.Warning,
|
||||||
"info" => MessageDialogType.Information,
|
"info" => MessageDialogType.Information,
|
||||||
@@ -18,7 +18,7 @@ namespace TweetLib.Browser.CEF.Logic {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (type != MessageDialogType.None) {
|
if (type != MessageDialogType.None) {
|
||||||
text = text.Substring(pipe + 1);
|
text = text[(pipe + 1)..];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Configurations>Debug;Release</Configurations>
|
<Configurations>Debug;Release</Configurations>
|
||||||
<Platforms>x86;x64</Platforms>
|
<Platforms>x86;x64</Platforms>
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>11</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@@ -8,7 +8,7 @@ namespace TweetLib.Browser.CEF.Utils {
|
|||||||
return Path.Combine(storagePath, "Cache");
|
return Path.Combine(storagePath, "Cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommandLineArgs ParseCommandLineArguments(string argumentString) {
|
public static CommandLineArgs ParseCommandLineArguments(string? argumentString) {
|
||||||
CommandLineArgs args = new CommandLineArgs();
|
CommandLineArgs args = new CommandLineArgs();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(argumentString)) {
|
if (string.IsNullOrWhiteSpace(argumentString)) {
|
||||||
@@ -26,8 +26,8 @@ namespace TweetLib.Browser.CEF.Utils {
|
|||||||
value = "1";
|
value = "1";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
key = matchValue.Substring(0, indexEquals).TrimStart('-');
|
key = matchValue[..indexEquals].TrimStart('-');
|
||||||
value = matchValue.Substring(indexEquals + 1).Trim('"');
|
value = matchValue[(indexEquals + 1)..].Trim('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.Length != 0) {
|
if (key.Length != 0) {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
namespace TweetLib.Browser.Contexts {
|
namespace TweetLib.Browser.Contexts {
|
||||||
public struct Notification {
|
public struct Notification {
|
||||||
public string TweetUrl { get; }
|
public string? TweetUrl { get; }
|
||||||
public string? QuoteUrl { get; }
|
public string? QuoteUrl { get; }
|
||||||
|
|
||||||
public Notification(string tweetUrl, string? quoteUrl) {
|
public Notification(string? tweetUrl, string? quoteUrl) {
|
||||||
TweetUrl = tweetUrl;
|
TweetUrl = tweetUrl;
|
||||||
QuoteUrl = quoteUrl;
|
QuoteUrl = quoteUrl;
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Configurations>Debug;Release</Configurations>
|
<Configurations>Debug;Release</Configurations>
|
||||||
<Platforms>x86;x64</Platforms>
|
<Platforms>x86;x64</Platforms>
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>11</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@@ -13,13 +13,11 @@ namespace TweetLib.Communication.Pipe {
|
|||||||
|
|
||||||
public static Client CreateClient(string token) {
|
public static Client CreateClient(string token) {
|
||||||
int space = token.IndexOf(' ');
|
int space = token.IndexOf(' ');
|
||||||
return new Client(token.Substring(0, space), token.Substring(space + 1));
|
return new Client(token[..space], token[(space + 1)..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly PipeStream pipeIn;
|
private readonly PipeStream pipeIn;
|
||||||
private readonly PipeStream pipeOut;
|
private readonly PipeStream pipeOut;
|
||||||
|
|
||||||
private readonly Thread readerThread;
|
|
||||||
private readonly StreamWriter writerStream;
|
private readonly StreamWriter writerStream;
|
||||||
|
|
||||||
public event EventHandler<PipeReadEventArgs>? DataIn;
|
public event EventHandler<PipeReadEventArgs>? DataIn;
|
||||||
@@ -27,21 +25,20 @@ namespace TweetLib.Communication.Pipe {
|
|||||||
private DuplexPipe(PipeStream pipeIn, PipeStream pipeOut) {
|
private DuplexPipe(PipeStream pipeIn, PipeStream pipeOut) {
|
||||||
this.pipeIn = pipeIn;
|
this.pipeIn = pipeIn;
|
||||||
this.pipeOut = pipeOut;
|
this.pipeOut = pipeOut;
|
||||||
|
|
||||||
this.readerThread = new Thread(ReaderThread) {
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
|
|
||||||
this.readerThread.Start();
|
|
||||||
this.writerStream = new StreamWriter(this.pipeOut);
|
this.writerStream = new StreamWriter(this.pipeOut);
|
||||||
|
|
||||||
|
new Thread(ReaderThread) { IsBackground = true }.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReaderThread() {
|
private void ReaderThread() {
|
||||||
using StreamReader read = new StreamReader(pipeIn);
|
using StreamReader read = new StreamReader(pipeIn);
|
||||||
string? data;
|
|
||||||
|
|
||||||
while ((data = read.ReadLine()) != null) {
|
try {
|
||||||
DataIn?.Invoke(this, new PipeReadEventArgs(data));
|
while (read.ReadLine() is {} data) {
|
||||||
|
DataIn?.Invoke(this, new PipeReadEventArgs(data));
|
||||||
|
}
|
||||||
|
} catch (ObjectDisposedException) {
|
||||||
|
// expected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,12 +53,6 @@ namespace TweetLib.Communication.Pipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
try {
|
|
||||||
readerThread.Abort();
|
|
||||||
} catch {
|
|
||||||
// /shrug
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeIn.Dispose();
|
pipeIn.Dispose();
|
||||||
writerStream.Dispose();
|
writerStream.Dispose();
|
||||||
}
|
}
|
||||||
@@ -98,8 +89,8 @@ namespace TweetLib.Communication.Pipe {
|
|||||||
Data = string.Empty;
|
Data = string.Empty;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Key = line.Substring(0, separatorIndex);
|
Key = line[..separatorIndex];
|
||||||
Data = line.Substring(separatorIndex + 1);
|
Data = line[(separatorIndex + 1)..];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Platforms>x86</Platforms>
|
<Platforms>x86</Platforms>
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>11</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
@@ -5,6 +5,8 @@ using TweetLib.Browser.Request;
|
|||||||
using TweetLib.Core.Application;
|
using TweetLib.Core.Application;
|
||||||
using TweetLib.Core.Features;
|
using TweetLib.Core.Features;
|
||||||
using TweetLib.Core.Features.Plugins;
|
using TweetLib.Core.Features.Plugins;
|
||||||
|
using TweetLib.Core.Resources;
|
||||||
|
using TweetLib.Core.Systems.Api;
|
||||||
using TweetLib.Core.Systems.Configuration;
|
using TweetLib.Core.Systems.Configuration;
|
||||||
using TweetLib.Core.Systems.Logging;
|
using TweetLib.Core.Systems.Logging;
|
||||||
using TweetLib.Utils.Static;
|
using TweetLib.Utils.Static;
|
||||||
@@ -21,8 +23,11 @@ namespace TweetLib.Core {
|
|||||||
internal static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
internal static readonly string PluginPath = Path.Combine(ProgramPath, "plugins");
|
||||||
internal static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
|
internal static readonly string GuidePath = Path.Combine(ProgramPath, "guide");
|
||||||
|
|
||||||
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
|
public static readonly string ExtensionPath = Path.Combine(ProgramPath, "extensions");
|
||||||
public static readonly string LogoPath = Path.Combine(ResourcesPath, "images/logo.png");
|
public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
|
||||||
|
public static readonly string LogoPath = Path.Combine(ResourcesPath, "images/logo.png");
|
||||||
|
|
||||||
|
public static ApiImplementation Api { get; } = new ();
|
||||||
|
|
||||||
public static Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
|
public static Logger Logger { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
|
||||||
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
|
public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
|
||||||
@@ -110,11 +115,11 @@ namespace TweetLib.Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AppBuilder {
|
public sealed class AppBuilder {
|
||||||
public IAppSetup? Setup { get; set; }
|
public IAppSetup? Setup { get; init; }
|
||||||
public IAppErrorHandler? ErrorHandler { get; set; }
|
public IAppErrorHandler? ErrorHandler { get; init; }
|
||||||
public IAppSystemHandler? SystemHandler { get; set; }
|
public IAppSystemHandler? SystemHandler { get; init; }
|
||||||
public IAppMessageDialogs? MessageDialogs { get; set; }
|
public IAppMessageDialogs? MessageDialogs { get; init; }
|
||||||
public IAppFileDialogs? FileDialogs { get; set; }
|
public IAppFileDialogs? FileDialogs { get; init; }
|
||||||
|
|
||||||
internal static AppBuilder? Instance { get; private set; }
|
internal static AppBuilder? Instance { get; private set; }
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ namespace TweetLib.Core.Features {
|
|||||||
if (resourceType is ResourceType.Script or ResourceType.Stylesheet && TweetDeckHashes.Count > 0) {
|
if (resourceType is ResourceType.Script or ResourceType.Stylesheet && TweetDeckHashes.Count > 0) {
|
||||||
Match match = TweetDeckResourceUrl.Match(url);
|
Match match = TweetDeckResourceUrl.Match(url);
|
||||||
|
|
||||||
if (match.Success && TweetDeckHashes.TryGetValue($"{match.Groups[1]}.{match.Groups[3]}", out string hash)) {
|
if (match.Success && TweetDeckHashes.TryGetValue($"{match.Groups[1]}.{match.Groups[3]}", out var hash)) {
|
||||||
if (match.Groups[2].Value == hash) {
|
if (match.Groups[2].Value == hash) {
|
||||||
App.Logger.Debug("[RequestHandlerBase] Accepting " + url);
|
App.Logger.Debug("[RequestHandlerBase] Accepting " + url);
|
||||||
}
|
}
|
||||||
|
41
lib/TweetLib.Core/Features/Extensions/ExtensionLoader.cs
Normal file
41
lib/TweetLib.Core/Features/Extensions/ExtensionLoader.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using TweetLib.Api;
|
||||||
|
using TweetLib.Core.Systems.Api;
|
||||||
|
|
||||||
|
namespace TweetLib.Core.Features.Extensions {
|
||||||
|
public static class ExtensionLoader {
|
||||||
|
public static void LoadAllInFolder(string extensionFolder) {
|
||||||
|
if (!Directory.Exists(extensionFolder)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach (string file in Directory.EnumerateFiles(extensionFolder, "*.dll", SearchOption.TopDirectoryOnly)) {
|
||||||
|
try {
|
||||||
|
Assembly assembly = Assembly.LoadFile(file);
|
||||||
|
foreach (Type type in assembly.GetTypes()) {
|
||||||
|
if (typeof(TweetDuckExtension).IsAssignableFrom(type) && Activator.CreateInstance(type) is TweetDuckExtension extension) {
|
||||||
|
EnableExtension(extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
App.ErrorHandler.HandleException("Extension Error", "Could not load extension: " + Path.GetFileNameWithoutExtension(file), true, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (DirectoryNotFoundException) {
|
||||||
|
// ignore
|
||||||
|
} catch (Exception e) {
|
||||||
|
App.ErrorHandler.HandleException("Extension Error", "Could not load extensions.", true, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnableExtension(TweetDuckExtension extension) {
|
||||||
|
ApiImplementation apiImplementation = App.Api;
|
||||||
|
apiImplementation.CurrentExtension = extension;
|
||||||
|
extension.Enable(apiImplementation);
|
||||||
|
apiImplementation.CurrentExtension = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -84,7 +84,7 @@ namespace TweetLib.Core.Features {
|
|||||||
var settings = new SaveFileDialogSettings {
|
var settings = new SaveFileDialogSettings {
|
||||||
DialogTitle = oneImage ? "Save Image" : "Save Images",
|
DialogTitle = oneImage ? "Save Image" : "Save Images",
|
||||||
OverwritePrompt = oneImage,
|
OverwritePrompt = oneImage,
|
||||||
FileName = qualityIndex == -1 ? filename : $"{author} {Path.ChangeExtension(filename, null)} {firstImageLink.Substring(qualityIndex + 1)}".Trim() + ext,
|
FileName = qualityIndex == -1 ? filename : $"{author} {Path.ChangeExtension(filename, null)} {firstImageLink[(qualityIndex + 1)..]}".Trim() + ext,
|
||||||
Filters = new [] { new FileDialogFilter(oneImage ? "Image" : "Images", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) }
|
Filters = new [] { new FileDialogFilter(oneImage ? "Image" : "Images", string.IsNullOrEmpty(ext) ? Array.Empty<string>() : new [] { ext }) }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -56,7 +56,9 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
menu.AddSeparator();
|
menu.AddSeparator();
|
||||||
|
|
||||||
if (context.Notification is {} notification) {
|
if (context.Notification is {} notification) {
|
||||||
AddCopyAction(menu, "Copy tweet address", notification.TweetUrl);
|
if (!string.IsNullOrEmpty(notification.TweetUrl)) {
|
||||||
|
AddCopyAction(menu, "Copy tweet address", notification.TweetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(notification.QuoteUrl)) {
|
if (!string.IsNullOrEmpty(notification.QuoteUrl)) {
|
||||||
AddCopyAction(menu, "Copy quoted tweet address", notification.QuoteUrl!);
|
AddCopyAction(menu, "Copy quoted tweet address", notification.QuoteUrl!);
|
||||||
@@ -72,7 +74,7 @@ namespace TweetLib.Core.Features.Notifications {
|
|||||||
this.browserComponent.PageLoadEnd -= BrowserComponentOnPageLoadEnd;
|
this.browserComponent.PageLoadEnd -= BrowserComponentOnPageLoadEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BrowserComponentOnPageLoadEnd(object sender, PageLoadEventArgs e) {
|
private void BrowserComponentOnPageLoadEnd(object? sender, PageLoadEventArgs e) {
|
||||||
string url = e.Url;
|
string url = e.Url;
|
||||||
|
|
||||||
if (TwitterUrls.IsTweetDeck(url) && url != BlankURL) {
|
if (TwitterUrls.IsTweetDeck(url) && url != BlankURL) {
|
||||||
|
@@ -93,7 +93,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
return Identifier.GetHashCode();
|
return Identifier.GetHashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object obj) {
|
public override bool Equals(object? obj) {
|
||||||
return obj is Plugin plugin && plugin.Identifier.Equals(Identifier);
|
return obj is Plugin plugin && plugin.Identifier.Equals(Identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -48,17 +48,17 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal Plugin? GetPluginFromToken(int token) {
|
internal Plugin? GetPluginFromToken(int token) {
|
||||||
return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
|
return tokens.TryGetValue(token, out var plugin) ? plugin : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
|
|
||||||
private void manager_Reloaded(object sender, PluginErrorEventArgs e) {
|
private void manager_Reloaded(object? sender, PluginErrorEventArgs e) {
|
||||||
tokens.Clear();
|
tokens.Clear();
|
||||||
fileCache.Clear();
|
fileCache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e) {
|
private void Config_PluginChangedState(object? sender, PluginChangedStateEventArgs e) {
|
||||||
if (!e.IsEnabled) {
|
if (!e.IsEnabled) {
|
||||||
int token = GetTokenFromPlugin(e.Plugin);
|
int token = GetTokenFromPlugin(e.Plugin);
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
private string ReadFileUnsafe(int token, PluginFolder folder, string path, bool readCached) {
|
private string ReadFileUnsafe(int token, PluginFolder folder, string path, bool readCached) {
|
||||||
string fullPath = GetFullPathOrThrow(token, folder, path);
|
string fullPath = GetFullPathOrThrow(token, folder, path);
|
||||||
|
|
||||||
if (readCached && fileCache.TryGetValue(token, folder, path, out string cachedContents)) {
|
if (readCached && fileCache.TryGetValue(token, folder, path, out var cachedContents)) {
|
||||||
return cachedContents;
|
return cachedContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
cache.Clear();
|
cache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetValue(int token, PluginFolder folder, string path, out string contents) {
|
public bool TryGetValue(int token, PluginFolder folder, string path, [MaybeNullWhen(false)] out string contents) {
|
||||||
return cache.TryGetValue(token, Key(folder, path), out contents);
|
return cache.TryGetValue(token, Key(folder, path), out contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
private static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group) {
|
private static Plugin FromFolder(string name, string pathRoot, string pathData, PluginGroup group) {
|
||||||
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
Plugin.Builder builder = new Plugin.Builder(group, name, pathRoot, pathData);
|
||||||
|
|
||||||
foreach (var environment in Directory.EnumerateFiles(pathRoot, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).Select(EnvironmentFromFileName)) {
|
foreach (var environment in Directory.EnumerateFiles(pathRoot, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName).Select(EnvironmentFromFileName!)) {
|
||||||
builder.AddEnvironment(environment);
|
builder.AddEnvironment(environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
string currentContents = string.Empty;
|
string currentContents = string.Empty;
|
||||||
|
|
||||||
foreach (string line in File.ReadAllLines(metaFile, Encoding.UTF8).Concat(EndTag).Select(static line => line.TrimEnd()).Where(static line => line.Length > 0)) {
|
foreach (string line in File.ReadAllLines(metaFile, Encoding.UTF8).Concat(EndTag).Select(static line => line.TrimEnd()).Where(static line => line.Length > 0)) {
|
||||||
if (line[0] == '[' && line[line.Length - 1] == ']') {
|
if (line[0] == '[' && line[^1] == ']') {
|
||||||
if (currentTag != null) {
|
if (currentTag != null) {
|
||||||
SetProperty(builder, currentTag, currentContents);
|
SetProperty(builder, currentTag, currentContents);
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
builder.ConfigDefault = value;
|
builder.ConfigDefault = value;
|
||||||
break;
|
break;
|
||||||
case "REQUIRES":
|
case "REQUIRES":
|
||||||
builder.RequiredVersion = Version.TryParse(value, out Version version) ? version : throw new FormatException($"Invalid required minimum version: {value}");
|
builder.RequiredVersion = Version.TryParse(value, out var version) ? version : throw new FormatException($"Invalid required minimum version: {value}");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new FormatException($"Invalid metadata tag: {tag}");
|
throw new FormatException($"Invalid metadata tag: {tag}");
|
||||||
|
@@ -102,7 +102,7 @@ namespace TweetLib.Core.Features.Plugins {
|
|||||||
Executed?.Invoke(this, new PluginErrorEventArgs(errors));
|
Executed?.Invoke(this, new PluginErrorEventArgs(errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Config_PluginChangedState(object sender, PluginChangedStateEventArgs e) {
|
private void Config_PluginChangedState(object? sender, PluginChangedStateEventArgs e) {
|
||||||
browserExecutor?.RunFunction("TDPF_setPluginState", e.Plugin, e.IsEnabled);
|
browserExecutor?.RunFunction("TDPF_setPluginState", e.Plugin, e.IsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -71,12 +71,12 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
App.UserConfiguration.SoundNotificationChanged -= UserConfiguration_SoundNotificationChanged;
|
App.UserConfiguration.SoundNotificationChanged -= UserConfiguration_SoundNotificationChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void browserComponent_BrowserLoaded(object sender, BrowserLoadedEventArgs e) {
|
private void browserComponent_BrowserLoaded(object? sender, BrowserLoadedEventArgs e) {
|
||||||
e.AddDictionaryWords("tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD");
|
e.AddDictionaryWords("tweetdeck", "TweetDeck", "tweetduck", "TweetDuck", "TD");
|
||||||
isBrowserReady = true;
|
isBrowserReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void browserComponent_PageLoadStart(object sender, PageLoadEventArgs e) {
|
private void browserComponent_PageLoadStart(object? sender, PageLoadEventArgs e) {
|
||||||
string url = e.Url;
|
string url = e.Url;
|
||||||
|
|
||||||
if (TwitterUrls.IsTweetDeck(url) || (TwitterUrls.IsTwitter(url) && !TwitterUrls.IsTwitterLogin2Factor(url))) {
|
if (TwitterUrls.IsTweetDeck(url) || (TwitterUrls.IsTwitter(url) && !TwitterUrls.IsTwitterLogin2Factor(url))) {
|
||||||
@@ -84,7 +84,7 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void browserComponent_PageLoadEnd(object sender, PageLoadEventArgs e) {
|
private void browserComponent_PageLoadEnd(object? sender, PageLoadEventArgs e) {
|
||||||
string url = e.Url;
|
string url = e.Url;
|
||||||
|
|
||||||
if (TwitterUrls.IsTweetDeck(url)) {
|
if (TwitterUrls.IsTweetDeck(url)) {
|
||||||
@@ -105,7 +105,7 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
browserComponent.RunBootstrap("update");
|
browserComponent.RunBootstrap("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pluginManager_Reloaded(object sender, PluginErrorEventArgs e) {
|
private void pluginManager_Reloaded(object? sender, PluginErrorEventArgs e) {
|
||||||
if (e.HasErrors) {
|
if (e.HasErrors) {
|
||||||
App.MessageDialogs.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors));
|
App.MessageDialogs.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors));
|
||||||
}
|
}
|
||||||
@@ -115,14 +115,14 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pluginManager_Executed(object sender, PluginErrorEventArgs e) {
|
private void pluginManager_Executed(object? sender, PluginErrorEventArgs e) {
|
||||||
if (e.HasErrors) {
|
if (e.HasErrors) {
|
||||||
App.MessageDialogs.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors));
|
App.MessageDialogs.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateChecker_CheckFinished(object sender, UpdateCheckEventArgs e) {
|
private void updateChecker_CheckFinished(object? sender, UpdateCheckEventArgs e) {
|
||||||
var updateChecker = (UpdateChecker) sender;
|
var updateChecker = (UpdateChecker) sender!;
|
||||||
|
|
||||||
e.Result.Handle(update => {
|
e.Result.Handle(update => {
|
||||||
string tag = update.VersionTag;
|
string tag = update.VersionTag;
|
||||||
@@ -144,7 +144,7 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
ignoreUpdateCheckError = true;
|
ignoreUpdateCheckError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UserConfiguration_GeneralEventHandler(object sender, EventArgs e) {
|
private void UserConfiguration_GeneralEventHandler(object? sender, EventArgs e) {
|
||||||
UpdatePropertyObject();
|
UpdatePropertyObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +236,11 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
private const string UrlVersionCheck = "/web/dist/version.json";
|
private const string UrlVersionCheck = "/web/dist/version.json";
|
||||||
|
|
||||||
public override RequestHandleResult? Handle(string url, ResourceType resourceType) {
|
public override RequestHandleResult? Handle(string url, ResourceType resourceType) {
|
||||||
|
var result = base.Handle(url, resourceType);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
switch (resourceType) {
|
switch (resourceType) {
|
||||||
case ResourceType.MainFrame when url.EndsWithOrdinal("://twitter.com/"):
|
case ResourceType.MainFrame when url.EndsWithOrdinal("://twitter.com/"):
|
||||||
return new RequestHandleResult.Redirect(TwitterUrls.TweetDeck); // redirect plain twitter.com requests, fixes bugs with login 2FA
|
return new RequestHandleResult.Redirect(TwitterUrls.TweetDeck); // redirect plain twitter.com requests, fixes bugs with login 2FA
|
||||||
@@ -256,7 +261,7 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
return new RequestHandleResult.Redirect(url.Replace("include_entities=1", "include_entities=1&include_ext_has_nft_avatar=1"));
|
return new RequestHandleResult.Redirect(url.Replace("include_entities=1", "include_entities=1&include_ext_has_nft_avatar=1"));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return base.Handle(url, resourceType);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,15 +24,15 @@ namespace TweetLib.Core.Features.TweetDeck {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "link":
|
case "link":
|
||||||
Link = new Link(url!, url!);
|
Link = new Link(url, url);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "image":
|
case "image":
|
||||||
Media = new Media(Type.Image, TwitterUrls.GetMediaLink(url!, App.UserConfiguration.TwitterImageQuality));
|
Media = new Media(Type.Image, TwitterUrls.GetMediaLink(url, App.UserConfiguration.TwitterImageQuality));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "video":
|
case "video":
|
||||||
Media = new Media(Type.Video, url!);
|
Media = new Media(Type.Video, url);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -57,7 +57,7 @@ namespace TweetLib.Core.Features.Twitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
string originalUrl = url.Substring(0, question);
|
string originalUrl = url[..question];
|
||||||
|
|
||||||
obj = new ImageUrl(Path.HasExtension(originalUrl) ? originalUrl : originalUrl + imageExtension, imageQuality);
|
obj = new ImageUrl(Path.HasExtension(originalUrl) ? originalUrl : originalUrl + imageExtension, imageQuality);
|
||||||
return true;
|
return true;
|
||||||
|
@@ -49,11 +49,11 @@ namespace TweetLib.Core.Features.Twitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static UrlType Check(string url) {
|
public static UrlType Check(string url) {
|
||||||
if (url.Contains("\"")) {
|
if (url.Contains('"')) {
|
||||||
return UrlType.Invalid;
|
return UrlType.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
|
if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) {
|
||||||
string scheme = uri.Scheme;
|
string scheme = uri.Scheme;
|
||||||
|
|
||||||
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto) {
|
if (scheme == Uri.UriSchemeHttps || scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeFtp || scheme == Uri.UriSchemeMailto) {
|
||||||
|
25
lib/TweetLib.Core/Systems/Api/ApiImplementation.cs
Normal file
25
lib/TweetLib.Core/Systems/Api/ApiImplementation.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using TweetLib.Api;
|
||||||
|
|
||||||
|
namespace TweetLib.Core.Systems.Api {
|
||||||
|
public class ApiImplementation : ITweetDuckApi {
|
||||||
|
public TweetDuckExtension? CurrentExtension { get; internal set; }
|
||||||
|
|
||||||
|
private readonly Dictionary<Type, ITweetDuckService> services = new Dictionary<Type, ITweetDuckService>();
|
||||||
|
|
||||||
|
internal ApiImplementation() {}
|
||||||
|
|
||||||
|
public void RegisterService<T>(T service) where T : class, ITweetDuckService {
|
||||||
|
if (!typeof(T).IsInterface) {
|
||||||
|
throw new ArgumentException("Api service implementation must be registered with its interface type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
services.Add(typeof(T), service);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? FindService<T>() where T : class, ITweetDuckService {
|
||||||
|
return services.TryGetValue(typeof(T), out ITweetDuckService? service) ? service as T : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -10,13 +10,13 @@ using TweetLib.Utils.Static;
|
|||||||
|
|
||||||
namespace TweetLib.Core.Systems.Configuration {
|
namespace TweetLib.Core.Systems.Configuration {
|
||||||
public abstract class ConfigManager {
|
public abstract class ConfigManager {
|
||||||
protected static TypeConverterRegistry ConverterRegistry { get; } = new ();
|
public static TypeConverterRegistry ConverterRegistry { get; } = new ();
|
||||||
|
|
||||||
static ConfigManager() {
|
static ConfigManager() {
|
||||||
ConverterRegistry.Register(typeof(WindowState), new BasicTypeConverter<WindowState> {
|
ConverterRegistry.Register(typeof(WindowState), new BasicTypeConverter<WindowState> {
|
||||||
ConvertToString = static value => $"{(value.IsMaximized ? 'M' : '_')}{value.Bounds.X} {value.Bounds.Y} {value.Bounds.Width} {value.Bounds.Height}",
|
ConvertToString = static value => $"{(value.IsMaximized ? 'M' : '_')}{value.Bounds.X} {value.Bounds.Y} {value.Bounds.Width} {value.Bounds.Height}",
|
||||||
ConvertToObject = static value => {
|
ConvertToObject = static value => {
|
||||||
int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
|
int[] elements = StringUtils.ParseInts(value[1..], ' ');
|
||||||
|
|
||||||
return new WindowState {
|
return new WindowState {
|
||||||
Bounds = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
|
Bounds = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
|
||||||
|
@@ -43,7 +43,7 @@ namespace TweetLib.Core.Systems.Logging {
|
|||||||
build.Append("Please, report all issues to: ").Append(Lib.IssueTrackerUrl).Append("\r\n\r\n");
|
build.Append("Please, report all issues to: ").Append(Lib.IssueTrackerUrl).Append("\r\n\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
build.Append("[").Append(DateTime.Now.ToString("G", Lib.Culture)).Append("] ").Append(level).Append("\r\n");
|
build.Append('[').Append(DateTime.Now.ToString("G", Lib.Culture)).Append("] ").Append(level).Append("\r\n");
|
||||||
build.Append(message).Append("\r\n\r\n");
|
build.Append(message).Append("\r\n\r\n");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -37,7 +37,7 @@ namespace TweetLib.Core.Systems.Updates {
|
|||||||
InteractionManager.Dispose();
|
InteractionManager.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void timer_Elapsed(object sender, ElapsedEventArgs e) {
|
private void timer_Elapsed(object? sender, ElapsedEventArgs e) {
|
||||||
Check(false);
|
Check(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ namespace TweetLib.Core.Systems.Updates {
|
|||||||
nextUpdate = null;
|
nextUpdate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e) {
|
private void updates_CheckFinished(object? sender, UpdateCheckEventArgs e) {
|
||||||
UpdateInfo? foundUpdate = e.Result.HasValue ? e.Result.Value : null;
|
UpdateInfo? foundUpdate = e.Result.HasValue ? e.Result.Value : null;
|
||||||
|
|
||||||
if (nextUpdate != null && !nextUpdate.Equals(foundUpdate)) {
|
if (nextUpdate != null && !nextUpdate.Equals(foundUpdate)) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user