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

Compare commits

...

521 Commits

Author SHA1 Message Date
29fee155d7 Release 1.21.0.1 2021-12-25 19:22:34 +01:00
32728fc20a Fix JS error when hiding update notification 2021-12-25 19:22:34 +01:00
394cb80022 Fix custom CSS and sound notification settings not applying on launch 2021-12-25 19:09:22 +01:00
21e49505d1 Release 1.21 2021-12-25 08:06:21 +01:00
e0025e02d1 Add option to use system proxy for all connections
Closes #319
2021-12-25 08:06:21 +01:00
4a9590c1d9 Disable color profile detection by default & add option to re-enable it 2021-12-25 08:06:20 +01:00
7de0c50387 Apply default background color only to Twitter and TweetDeck websites 2021-12-25 05:52:59 +01:00
ccb87351c1 Fix notifications having outdated theme-related attributes on the <html> element 2021-12-24 15:50:01 +01:00
273e7266eb Fix bootstrap script issues (wrong ID and stylesheets appearing in notifications) 2021-12-24 15:31:10 +01:00
427975e5ce Automatically update relative time shown in notifications 2021-12-24 14:54:58 +01:00
adb304b6a2 Remove section on Dev Tools in the guide 2021-12-24 12:38:15 +01:00
3a89a28f8b Move the official guide directly into the app 2021-12-24 11:30:42 +01:00
cff93dcc97 Remove scripts folder from pre-build script 2021-12-24 11:00:12 +01:00
dfde38ea3b Fix screenshotting wrong tweet if cursor moves away from right-clicked tweet too quickly 2021-12-24 10:14:12 +01:00
a8e7f065cf Rewrite screenshot functionality using new DevTools API 2021-12-24 09:12:46 +01:00
5ebfc67e48 Replace ScriptLoader & reimplement resource hot swap 2021-12-24 06:41:34 +01:00
57b03baad9 Add in-memory caching to td:// and tdp:// schemes 2021-12-23 18:26:42 +01:00
c91f1d0e5e Remove "scripts" folder and JS/CSS minification 2021-12-23 18:22:21 +01:00
6a421292b3 Refactor notification bootstrapping 2021-12-23 16:00:35 +01:00
ceae748503 Fix the possibility of plugin functions running in wrong order if main module loading gets delayed 2021-12-23 05:23:02 +01:00
008de87e55 Refactor plugin bootstrapping 2021-12-23 05:02:53 +01:00
e7479ef9e3 Fix missing module import 2021-12-22 13:25:03 +01:00
13f8f12ac8 Refactor error page 2021-12-22 10:05:01 +01:00
fd634379d7 Improve resistance to errors in bootstrapping script and modules 2021-12-22 08:49:00 +01:00
e854315a81 Reorganize resources folder 2021-12-22 08:37:45 +01:00
86136d7692 Refactor update checker as a bootstrapped module 2021-12-22 07:53:52 +01:00
db251bfdfd Add new resources folder to installers & remove old CEF files on update 2021-12-22 07:25:39 +01:00
9d39f26d54 Refactor introduction dialog as a bootstrapped module 2021-12-22 06:40:14 +01:00
9cd813c02c Refactor login page scripts & styles into modules and remove stuff broken by Twitter updates 2021-12-22 06:35:47 +01:00
8e1f87e062 Expand module bootstrapping mechanism 2021-12-22 06:35:47 +01:00
dda954285c Refactor main browser CSS injections 2021-12-22 05:08:04 +01:00
ed4f7b6b72 Refactor main browser code into JS modules 2021-12-22 03:00:14 +01:00
7239dcf4d2 Add td:// scheme for modularized resources 2021-12-21 12:48:45 +01:00
901cca268e Minor refactoring of custom URL schemes 2021-12-21 12:48:45 +01:00
b909341988 Delete devtools_resources.pak when updating 2021-12-21 12:48:45 +01:00
3e607ae0fe Fix JS script execution after updating CefSharp 2021-12-21 12:27:29 +01:00
587060f73c Update Rider project settings 2021-12-21 12:27:28 +01:00
3ea6f6ac18 Make opening dev tools via context menu immediately focus right-clicked element 2021-12-18 03:13:18 +01:00
c162761464 Reduce minification of resource files 2021-12-18 01:49:01 +01:00
994f9635ef Fix post build scripts to support moved F# executables 2021-12-18 01:29:31 +01:00
ab8752845d Fix importing/exporting login session after CEF updates 2021-12-17 23:48:02 +01:00
a94ee2fe4b Update CefSharp to 96 & add VC++ 2019 and UCRT DLLs to the repository 2021-12-17 23:48:02 +01:00
763c999b09 Remove analytics 2021-12-17 20:27:48 +01:00
b18cd2658c Fix source code indentation problems 2021-12-17 01:46:08 +01:00
d9782f554f Move TweetLib.Communication library to .NET Standard 2.0 2021-12-17 00:38:40 +01:00
f1d9663709 Update CefSharp to 92 2021-12-16 23:31:40 +01:00
bcf77052a5 Reformat solution & setup Rider project 2021-08-07 09:41:00 +02:00
85d15b32e9 Release 1.20 2021-02-03 04:27:46 +01:00
3d3b695c85 Fix broken login page after Twitter changed it
Closes #313
2021-02-03 03:52:00 +01:00
88b7cac298 Add Rider module 2021-02-03 03:51:02 +01:00
c7efa631e1 Enforce LF line endings 2021-02-01 00:39:45 +01:00
aeb90cbb1a Update CefSharp to 86 2021-02-01 00:00:34 +01:00
ae1c59847f Refactor locking mechanism & improve error reporting for failed locks 2020-06-16 00:28:35 +02:00
651d9be57c Release 1.19.0.2 2020-06-14 15:54:19 +02:00
eeb32db6fb Work around CEF tooltip showing for links 2020-06-14 15:22:58 +02:00
daa0780644 Release 1.19.0.1 2020-06-09 20:27:29 +02:00
8502f9105f Add sound notification file size warning as it's now loaded into memory 2020-06-08 23:33:43 +02:00
16ced3d827 Fix resource handlers reuse & broken notification sound 2020-06-08 23:33:43 +02:00
1c16187346 Release 1.19 2020-06-08 16:58:58 +02:00
2fe058d9cb Fix crash if reloading plugins reports errors before the main window appears 2020-06-08 09:03:22 +02:00
cefdadd53a Update installer to remove native_blob.bin 2020-06-07 18:01:40 +02:00
c21c10df63 Add a way to exclude <link> tags from being auto-added to notifications 2020-06-07 15:45:55 +02:00
b4d359d30c Add TDPF_createStorage as a plugin replacement for localStorage 2020-06-07 11:33:58 +02:00
1e8c62ac25 Add plugin object validation to TDPF functions 2020-06-07 10:28:55 +02:00
c578f36644 Block CSP reports 2020-06-06 10:00:25 +02:00
c973d0cff4 Fix missing resources from csproj & reorganize it 2020-06-06 09:10:54 +02:00
0c3d9ae46a Refactor main JS code & split code.js into multiple files 2020-06-06 09:01:50 +02:00
a834e8b6a2 Fix broken post-build script import handling 2020-06-06 07:36:03 +02:00
9f5580d983 Eliminate a few post-launch frames of misaligned TweetDeck load icon 2020-06-06 07:36:03 +02:00
e94e3cecf8 Let JS continue even if jQuery or bridge objects are missing 2020-06-06 07:36:02 +02:00
1991f7f50f Bypass 'tdp://' CORS without AddCrossOriginWhitelistEntry 2020-06-06 07:36:02 +02:00
9eb4e623e7 Work around broken smooth scrolling in notifications 2020-06-06 07:36:02 +02:00
ad28d2279f Fix runtime errors & minor tweaks after updating CefSharp 2020-06-05 06:20:19 +02:00
1e3de31fc3 Fix compile errors after updating CefSharp 2020-06-05 06:20:19 +02:00
f85bd41b96 Update CefSharp to 81 2020-06-05 06:20:19 +02:00
563124b68c Fix ResourceHandlerNotification buffer being unnecessarily large 2020-06-05 06:19:50 +02:00
63de08c635 Fix plugin code running in blank notifications from previous commit 2020-06-04 04:33:50 +02:00
8a0a215443 Reduce notification code hackery 2020-06-03 00:57:24 +02:00
f1b7cd633e Refactor WindowsUtils.OpenAssociatedProgram & update installer code 2020-06-02 23:56:03 +02:00
458eeeccda Add tdp:// scheme for plugins (with 'root/' to access root files) 2020-06-02 12:31:34 +02:00
464e758b94 Ensure window.jQuery is available alongside window.$ 2020-06-02 06:45:51 +02:00
4c61047e9b Fix large notification HTML overflowing CEF buffer and silently crashing 2020-06-02 04:05:29 +02:00
1bf9e7fb56 Release 1.18.6 2020-05-16 19:15:07 +02:00
ca69554f37 Fix docked composer not re-focusing after image upload 2020-05-16 18:08:51 +02:00
cf7029037e Fix twitter login page after it was migrated and broke detection 2020-05-16 18:02:37 +02:00
418388b0ab Merge branch 'master' of https://github.com/chylex/TweetDuck 2020-05-16 17:54:00 +02:00
a0f8689d4f Fix TweetDeck bug where docked composer loses focus after Alt+Tab
Closes #297
2020-05-16 17:53:50 +02:00
95007bdd26 Update FUNDING.yml 2020-05-13 23:23:34 +02:00
ab91540deb Move some TweetLib.Core files into different namespaces 2020-05-04 17:13:20 +02:00
b2ebb984f8 Release 1.18.5 2020-05-04 16:20:44 +02:00
f7e9ad74d1 Fix stuck processes after closing the app
Closes #294
2020-05-04 13:45:56 +02:00
d48da3d51c Add option for notification window opacity 2020-04-27 10:20:07 +02:00
76d22554c5 Make trackbars in settings wider 2020-04-27 10:03:51 +02:00
6eaafd883b Release 1.18.4 2020-04-25 06:35:43 +02:00
5961a80b23 Fix blank notifications on certain hardware configurations w/ disabled acceleration
Closes #274
2020-04-25 05:47:17 +02:00
f41c6fe533 Unify all exe & dll versions 2020-04-25 05:05:05 +02:00
65b8efe13c Fix non-quoted tweet links opening in browser despite also opening in the column
Closes #273
2020-04-25 03:48:20 +02:00
89529f9c96 Add $TD.makeGetRequest and fix template plugin AJAX
Closes #272
2020-04-25 03:15:52 +02:00
e90f6ebc63 Add 'Copy image' to context menu
Closes #287
2020-04-25 02:43:12 +02:00
5888d540a6 Move clipboard utils into ClipboardManager & add SetImage 2020-04-25 02:33:39 +02:00
ae8b740600 Reorganize namespaces in main project 2020-04-25 02:16:57 +02:00
ab4e2f5bda Add error message to template plugin when AJAX request fails 2020-04-25 00:19:29 +02:00
1091b6d232 Fix IDE warnings (dispose, lang features) & nullability settings 2020-04-24 23:14:22 +02:00
fc89744238 Apparently video duration minus 0.05 causes complete hangs in short videos... 2020-03-07 01:40:07 +01:00
34e049a002 Work around video player black screen when looping & reduce player polling 2020-03-05 13:26:08 +01:00
980bf2c307 Release 1.18.3 2020-02-16 18:04:21 +01:00
762aea1e20 Add option to set first day of week in date picker
Closes #276
2020-02-16 15:47:26 +01:00
c1aefc7163 Add a way to register callbacks when $TDX property object gets updated 2020-02-16 14:39:15 +01:00
9480ba26e0 Move system tray options to a separate tab & reorganize General tab 2020-02-16 14:39:15 +01:00
c2c9160ed9 Allow dragging Twitter account links onto the app to view their profile
Closes #288
2020-02-16 11:35:20 +01:00
175b067a17 Add missing tooltip on custom video player selection 2020-02-16 11:27:46 +01:00
9d8656ca20 Add option for whether dev tools window should stay on top 2020-02-16 11:15:25 +01:00
0863001c80 Add option for custom video player executable & arguments 2020-02-15 21:17:44 +01:00
0ee22a30ad Add option for custom browser args w/ new dialog & disallow quotes in URLs 2020-02-15 20:26:07 +01:00
447697ba45 Add StringUtils.NullIfEmpty & update existing code to use it 2020-02-15 17:25:00 +01:00
aea77ff909 Make dev tools dialog a proper window that appears in taskbar 2020-02-15 15:18:16 +01:00
af5da76f72 Make video player open tweet URL instead of video URL if playback fails 2020-02-15 14:34:19 +01:00
a369c65451 Release 1.18.2 2019-10-23 02:26:53 +02:00
318f65f187 Merge branch 'master' of https://github.com/chylex/TweetDuck 2019-10-17 13:04:19 +02:00
1cd60e831c Add a few missing translation languages 2019-10-12 18:30:12 +02:00
b988959eaa Only activate mouse hook while cursor is over the notification window 2019-10-07 04:49:39 +02:00
1eb1f9848a Prepare login/logout page scripts and styles for Twitter redesign & minor fixes 2019-10-07 03:01:17 +02:00
7f6cc0da01 Fix mouse back/forward button triggering navigation if history wasn't empty
Closes #286
2019-10-06 14:47:49 +02:00
19fcb69525 Fix prebuild event not killing hung browser processes reliably 2019-09-05 01:27:36 +02:00
22cef0a44c Fix C# version in secondary projects 2019-09-05 00:48:18 +02:00
2459d31bff Remove RegexOptions.Compiled where not needed 2019-09-05 00:16:25 +02:00
19f104239a Fix missing spaces in C#/F# code 2019-08-23 01:56:31 +02:00
bd0be65038 Minor refactoring & removal of unnecessary code 2019-08-23 01:56:18 +02:00
bbb7907e54 Move LockManager to TweetLib.Core & remove WindowsUtils.CurrentProcessID 2019-08-22 06:29:32 +02:00
a6963a18d4 Move debug resource hot swap into a separate class 2019-08-21 10:31:30 +02:00
92716ea3e0 Move URL-related code from UrlUtils & TwitterUtils to TwitterUrls 2019-08-21 10:12:19 +02:00
aec4c1feea Move TweetNotification to TweetLib.Core as DesktopNotification 2019-08-21 10:12:19 +02:00
d505b3305b Initial refactoring of ScriptLoader & making it accessible in TweetLib.Core 2019-08-21 10:12:19 +02:00
a34a02e14d Generalize PluginListFlowLayout and move it 2019-07-15 00:49:28 +02:00
26d2d7a51e Move PluginManager to Core lib & refactor plugin enums 2019-07-14 20:44:25 +02:00
c2f7e52d13 Add IAppSystemHandler w/ OpenFileExplorer and update existing code to use it 2019-07-14 20:44:25 +02:00
de68d8934d Add IScriptExecutor w/ implementation for CefSharp browser 2019-07-14 17:15:14 +02:00
4fdf7fc958 Release 1.18.1 2019-07-13 19:50:49 +02:00
42a5e72f19 Revert README change & lock Inno Setup to version 5.6.1 2019-07-13 19:39:08 +02:00
f7359ebc8a Update README with instructions for fixing Inno Download Plugin 2019-07-13 18:44:02 +02:00
f395ac53dc Fix wrong colors in dropdown menus w/ black theme 2019-07-13 18:22:38 +02:00
1113e0b559 Fix new image url parser not checking if an extension already exists 2019-07-13 18:16:11 +02:00
5e3bd31862 Delete corrupted downloads after an error 2019-07-13 18:10:16 +02:00
11d978dad1 Fix GIF thumbnails not loading after Twitter changed image urls
Closes #271
2019-07-13 17:51:16 +02:00
f7961024d7 Enable popup for linking another account
Closes #269
2019-07-13 06:17:30 +02:00
72973a8707 Restore smooth scrolling in columns
Fixes #251
2019-07-13 06:07:01 +02:00
68254f48d5 Fix TweetDeck bug with broken DM image previews
References #271
2019-07-13 00:48:53 +02:00
eac4f30c50 Support new image urls & fix missing filename features w/o Best Image Quality
Fixes #270
2019-07-13 00:40:27 +02:00
25680fa980 Add StringUtils.SplitInTwo & use it in RequestHandlerBase 2019-07-12 22:17:29 +02:00
ff5e1da14d Fix wrong 'X columns on screen' width calculation after a TweetDeck update 2019-06-03 11:30:14 +02:00
95afff7879 Update F# compiler location 2019-06-03 10:33:57 +02:00
50bd526025 Continue refactoring and moving plugin code 2019-05-27 19:46:39 +02:00
108a0fefc3 Fix PluginManager crashing after error(s) during plugin execution 2019-05-27 19:38:53 +02:00
dd8c5d27be Update code to use C# 8 switch expression 2019-05-27 16:04:08 +02:00
b2937bc776 Fix broken image upload dialog in new composer 2019-05-27 12:37:30 +02:00
4d8e764211 Release 1.18 2019-05-26 21:29:46 +02:00
544b8664fd Add edit-design plugin option to set composer/drawer size 2019-05-26 18:41:23 +02:00
d0610865bd Fix wrong background color in screenshots 2019-05-26 18:12:40 +02:00
ebc0b51590 Merge branch 'master' of https://github.com/chylex/TweetDuck 2019-05-26 18:03:17 +02:00
4487f1169e Fix composer input refocus & emoji keyboard broken after switching composers 2019-05-26 18:02:11 +02:00
85559b6083 Fix and refactor 'Stay open' pin, that was broken after composer update 2019-05-26 18:01:47 +02:00
1056273c57 Add a custom JS event when the old composer is activated 2019-05-26 17:58:58 +02:00
61af2ebc8b Fix template panel not hiding when switching to different drawer/new composer 2019-05-26 17:57:23 +02:00
9121c86656 Update README (drop VS 2017, update support section) 2019-05-26 15:03:16 +02:00
1ccefe853a Update .NET & begin refactoring code into a core lib (#264)
* Switch to .NET Framework 4.7.2 & C# 8.0, update libraries

* Add TweetLib.Core project targeting .NET Standard 2.0

* Enable reference nullability checks for TweetLib.Core

* Move a bunch of utility classes into TweetLib.Core & refactor

* Partially move TweetDuck plugin & update system to TweetLib.Core

* Move some constants and CultureInfo setup to TweetLib.Core

* Move some configuration classes to TweetLib.Core

* Minor refactoring and warning suppression

* Add App to TweetLib.Core

* Add IAppErrorHandler w/ implementation

* Continue moving config, plugin, and update classes to TweetLib.Core

* Fix a few nullability checks

* Update installers to check for .NET Framework 4.7.2
2019-05-26 14:55:12 +02:00
aca438b837 Create FUNDING.yml 2019-05-25 10:17:51 +02:00
7210c29cd8 Update readme (VS 2019, CefSharp version, remove MyGet reference) 2019-05-08 13:13:09 +02:00
26d90c0c9b Work around missing culture codes on Wine 2019-05-08 12:44:16 +02:00
a03b222a95 Fix emoji keyboard button not working after re-enabling w/ compose drawer open
Closes #256
2019-04-04 20:05:52 +02:00
7944a24d3c Release 1.17.4 2019-03-08 19:29:26 +01:00
cc8459c759 Fix clear-columns plugin nav button to match new TweetDeck style 2019-03-08 19:25:39 +01:00
10074ff92c Fix various alignment issues with the verified badge 2019-03-08 18:57:00 +01:00
173f25bebc Add option to disable automatic DM input focus
Closes #253
2019-03-08 18:16:37 +01:00
31680fc4ae Fix colors in retweet dialog w/ black theme 2019-03-07 19:12:35 +01:00
e937d43614 Fix broken compose drawer hooks after a recent TweetDeck update 2019-03-07 19:03:30 +01:00
20e29a7975 Release 1.17.3 2019-01-28 23:59:32 +01:00
ef815dabce Add verbose logging controlled by command line flag & update existing calls 2019-01-28 23:43:52 +01:00
1fb133e6b8 Make TweetDeck resource freezing a command line argument 2019-01-28 23:17:33 +01:00
50b58cd6a6 Add keyboard shortcut to open dev tools (Ctrl+Shift+I) 2019-01-28 18:43:55 +01:00
01485d7ef9 Add a base class for browser keyboard handling 2019-01-28 18:42:27 +01:00
b17c6a5ac7 Safeguard video player to avoid showing overlay if the URL is null 2019-01-28 17:43:53 +01:00
d2ed2b4a00 Force video player UI layout update to work around an edge case 2019-01-28 17:18:05 +01:00
710a7524a1 Kill subprocess if it doesn't exit after the app is closed 2019-01-23 15:28:05 +01:00
2be46464d6 Release 1.17.2 2018-11-23 06:19:43 +01:00
8d536a6734 Fix video player seek bar resizing when clipped & adjust min window size 2018-11-23 04:13:39 +01:00
250d502238 Add a compact layout for video player controls if the window is small
Closes #245
2018-11-23 03:03:58 +01:00
e8de7266d0 Fix cursor staying on 'resize' when moved over minimum size video player 2018-11-23 01:31:01 +01:00
9414f372d7 Fix video player size with a small window on high DPI 2018-11-23 00:45:02 +01:00
b0f9de67cf Release 1.7.1 2018-11-21 04:41:18 +01:00
9b082e114e Redirect plain twitter.com requests to TD to fix 2FA bug 2018-11-20 20:58:04 +01:00
816a5334ac Make the fix to visible scrollbar when loading TweetDeck more reliable 2018-11-20 20:56:52 +01:00
15a4e10da9 Hide the Manage Options dialog when cancelling profile import from login page 2018-11-20 20:44:44 +01:00
01b9302b0c Fix colors in introduction dialog 2018-11-20 20:13:18 +01:00
442126a11a Rewrite login/logout page CSS handling to fix broken 2FA styles
Closes #218
2018-11-20 20:07:42 +01:00
a9c140c0fc Fix video player seek bar tooltip not disappearing when cursor moves outside 2018-11-20 18:30:06 +01:00
97ad7a3e68 Fix video player bug where playback freezes for ~3s on non-primary screen 2018-11-20 18:04:34 +01:00
7d737eefb6 Fix video player's minimum size 2018-11-20 17:33:09 +01:00
4ac05b38d3 Fix column loading spinner color when using black theme 2018-11-20 15:25:26 +01:00
651bbbb672 Fix crash when showing a browser error message
Closes #244
2018-11-20 14:55:40 +01:00
52da4d8687 Release 1.17 2018-11-16 22:06:47 +01:00
36063ae76a Fix Visual Studio being stupid 2018-11-15 10:16:27 +01:00
2fcec2d2cd Update CefSharp to 67 (release) 2018-11-14 22:53:55 +01:00
762a7fdfb7 Disable compression on vendor.js as the x-ton-expected-size header isn't sometimes sent
Closes #241
2018-11-14 19:22:49 +01:00
cd7aeaeed2 Create and use a custom resource handler factory 2018-11-14 18:47:19 +01:00
6f414d312c Clear cache after each update 2018-11-10 06:20:22 +01:00
1b5304efb7 Release 1.16.3 2018-11-07 11:55:37 +01:00
d59375308f Work around clear-columns plugin reappearing after being disabled
Closes #240
2018-11-07 11:16:44 +01:00
8c9509a906 Fix broken colors of plugin elements with dark theme 2018-11-07 10:27:08 +01:00
fb86d8f3a8 Fix broken black theme colors 2018-11-07 10:18:24 +01:00
50e909cb3d Move debug TweetDeck resource freezing and update it to support CSS 2018-11-07 05:45:20 +01:00
2f54edf7e7 Fix missing <body> margin reset 2018-11-07 00:26:43 +01:00
c251603e1e Fix broken Arial font override 2018-11-07 00:26:22 +01:00
4476edb6c3 Release 1.16.2 2018-10-18 22:31:28 +02:00
28fc67660f Fix border color of large timeline cards 2018-10-18 22:30:11 +02:00
6e8b5a5ce5 Bypass t.co in new timeline cards 2018-10-18 21:26:03 +02:00
e53681416f Fix TweetDeck update breaking theme setting in edit-design plugin 2018-10-18 20:44:09 +02:00
acb5e184e8 Update styles for new timeline cards (rounded borders, black theme colors) 2018-10-18 20:36:42 +02:00
bdbafb3e5c Fix rounded borders in media badges and sensitive media overlay 2018-10-02 01:18:43 +02:00
ac70cf87c6 Fix slight misalignments in tweet composer elements when using old icons 2018-09-27 22:03:00 +02:00
93de835ab4 Fix rounded borders in composer (media elements & media description dialog) 2018-09-27 22:01:34 +02:00
2ea38b88ce Release 1.16.1 2018-08-28 21:11:31 +02:00
2c4f2be57d Support system font in notifications, but revert to Arial everywhere by default 2018-08-28 21:06:53 +02:00
fa4beea425 Reset script cache when holding Shift during browser reload 2018-08-24 16:34:55 +02:00
7a976edc82 Ignore cached files for updates and viewed images if their contents are empty 2018-08-23 20:08:34 +02:00
bb22c35221 Fix broken context menu options for images in DMs
Closes #238
2018-08-23 19:51:43 +02:00
ff3dc59016 Move 'View image in photo viewer' handling to TwitterUtils 2018-08-23 19:32:20 +02:00
2e4dd3df3e Fix column icon alignment & clear icon not being hidden for certain columns 2018-08-18 14:55:08 +02:00
b82e5d33f9 Add .column to data-td-icon attribute selectors (very minor optimization) 2018-08-18 14:41:30 +02:00
65d56b336b Release 1.16 2018-08-17 20:09:15 +02:00
7836d61173 Rewrite hovered column/tweet detection again to improve consistency & performance 2018-08-17 16:13:09 +02:00
898437720b Refactor and optimize the plugin configuration file 2018-08-17 11:09:07 +02:00
d9a80d1085 Make it easier to modify custom values for column width and font size 2018-08-17 08:09:28 +02:00
ab3d8b0ae2 Load edit-design plugin configuration dialog lazily 2018-08-17 07:53:43 +02:00
b865074c32 Mark DMs as read when replying to them 2018-08-17 07:02:22 +02:00
2b8ca77c0d Add a safeguard and a TODO to DM notification fix 2018-08-16 20:36:12 +02:00
fa8b4e1e7f Remove broken feature that showed missed notifications after reload 2018-08-16 20:33:31 +02:00
a310c5bcc1 Fix a broken workaround for DM notifications not showing if the convo is open 2018-08-16 19:53:48 +02:00
b5dccd6b91 Merge branch 'master' of https://github.com/chylex/TweetDuck 2018-08-15 15:55:23 +02:00
c2bcb39b38 Fix broken hover color on column filter icons 2018-08-15 15:55:02 +02:00
4515add0a0 Update README.md 2018-08-12 16:06:38 +02:00
5b6aaec48c Reorganize installer batch files 2018-08-12 15:56:14 +02:00
810e56ca31 Update CefSharp to 67 (pre01) 2018-08-12 15:36:48 +02:00
5bcc8ac2e0 Remove ITweetDeckBrowser 2018-08-10 03:18:26 +02:00
f5bfb35867 Add a notification script to the debug plugin for testing 2018-08-10 03:14:39 +02:00
9088b8cd07 Finish refactoring context menu structures & fix bugs from previous commits 2018-08-10 00:58:44 +02:00
a7d90dc708 Allow RT & Add to List/Collection dialogs to be smaller
Closes #236
2018-08-09 20:20:08 +02:00
5968b57a01 Add account context menu items to follow notifications 2018-08-09 20:07:51 +02:00
94946a9ed6 Refactor context menu link handling 2018-08-09 19:55:53 +02:00
561aec5ef0 Refactor highlighted column/tweet variables 2018-08-09 18:13:45 +02:00
e31696d843 Update README.md 2018-07-31 14:30:05 +02:00
8b33fd2002 Update README.md 2018-07-31 14:23:25 +02:00
064e43750f Rewrite PostBuild into F# with optional compilation for performance 2018-07-31 12:48:07 +02:00
29d2f4f681 Why the fuck is Copy-Item file exclusion not working 2018-07-31 03:01:15 +02:00
c11b40b94a Tweak appearance of list-related notifications 2018-07-31 02:33:51 +02:00
e874e1d798 Refactor StringBuilder usage 2018-07-30 01:58:23 +02:00
01244ec632 Replace td-example-notification attribute with .td-example body class 2018-07-30 01:49:38 +02:00
671657e2b0 Remove user & system config properties from Program class & fix field visibility 2018-07-29 10:16:29 +02:00
dff7278e2e Refactor zoom config events & fix notification zoom not updating when outside TweetDeck 2018-07-29 09:41:52 +02:00
0881328636 Release 1.15.2 2018-07-27 22:13:16 +02:00
a951c3a7c9 Tweak Options visuals (labels, spacing, colors) & fix off-by-pixel bug on high DPI 2018-07-27 19:29:21 +02:00
b25fae54fe Update PostCefUpdate script to remove x64/AnyCPU from .targets file 2018-07-27 05:59:27 +02:00
a87bc4609e Add a cue text to Sounds tab in Options 2018-07-27 05:32:17 +02:00
3e68026949 Redesign Options to reduce amount of tabs and organize code 2018-07-27 05:12:17 +02:00
78d6d285cd Remove GDI charset from fonts in designer files 2018-07-26 20:17:53 +02:00
72e3596a3c Tweak heading font in Options and fix minor issues (tab order, overflowing text) 2018-07-26 19:37:07 +02:00
643ebcaab4 Prompt restart after profile import/reset only if an updated option requires it 2018-07-25 22:01:37 +02:00
52ef6cd95a Unify configs (backups, behavior, error handling) & make future development easier 2018-07-25 17:25:20 +02:00
4aec2f3260 Refactor FileSerializer to attempt error recovery & tweak StreamWriter usage 2018-07-25 03:38:44 +02:00
73549515eb Remove old user configuration fix 2018-07-24 06:16:52 +02:00
d83d2660cf Initial Options dialog refactoring to use an event for restart requests 2018-07-24 04:04:44 +02:00
8de913172c Minor visual fixes (old column icon alignment, TweetDeck reply triangle bug) 2018-07-23 20:14:54 +02:00
4acfd64cff Update xUnit 2018-07-23 20:13:04 +02:00
b56f90ed52 Add README instructions to fix broken MSBuild task 2018-07-23 14:20:07 +02:00
1375630727 Safeguard ResponseFilterBase in case input stream is null
Closes #234 (hopefully)
2018-07-23 12:37:57 +02:00
668825bc01 Add option to toggle touch adjustment & disable it by default
Closes #230
2018-07-23 01:36:49 +02:00
90414ae579 Release 1.15.1 2018-07-13 08:28:44 +02:00
41c8caa2a4 Make avatars in DM recipient list square 2018-07-12 15:23:01 +02:00
15305ecabe Revert TweetDeck style changes to the 'Add column' dialog
Closes #233
2018-07-12 08:37:44 +02:00
50090effd4 Refactor image/video download code and fix video download error message 2018-07-10 18:31:39 +02:00
6dffdcd1ed Add update notifications outside TweetDeck, tweak changelog styling 2018-07-10 08:50:09 +02:00
db9daf2714 Refactor update notification into a bridge object & tweak installer pre-downloading 2018-07-10 08:17:42 +02:00
502ac4ebc1 Enable high DPI awareness for video player process
Closes #232
2018-07-09 18:14:58 +02:00
3f44f3bab4 Fix notification scrolling issue by disabling threaded scrolling & latching
Closes #221
2018-07-09 17:47:20 +02:00
746eba185b Release 1.15 2018-07-09 14:06:13 +02:00
82dec92510 Fix wrong screenshot width on high DPI 2018-07-09 14:06:07 +02:00
2edc93df47 Update CefSharp to the newest build 2018-07-09 13:23:13 +02:00
c5a6738199 Fix a missing 'Clear columns' button after re-enabling the clear-columns plugin 2018-07-09 13:20:58 +02:00
dfd578165b Revert custom throttling, it is not ready due to issues w/ notifications 2018-07-09 13:04:55 +02:00
285d400f69 Refactor ScriptLoader to cache files on its own & change method signatures 2018-07-09 12:22:29 +02:00
47a3444ace Remove unnecessary styles and tweak formatting in black theme CSS 2018-07-09 10:21:44 +02:00
0ec227da52 Redesign Edit CSS dialog, add 'Open Dev Tools' button and better editor 2018-07-08 12:57:22 +02:00
f9cf582306 ... 2018-07-08 10:09:44 +02:00
b0883229bf Move HasDevTools to BrowserUtils 2018-07-08 10:06:40 +02:00
31b5d9a4c0 Protect code.js from uncaught errors & add custom 'throw if' statement 2018-07-08 09:53:42 +02:00
6841a78556 Rewrite clear-columns plugin to work w/o reload & fix broken Clear icon 2018-07-08 08:09:29 +02:00
9d2089a8ce Extract markup from introduction.js into an imported file & fix import regex 2018-07-08 06:14:33 +02:00
6d6bb79199 Massive JS formatting refactoring 2018-07-08 06:09:51 +02:00
937c8e22c4 Force UTF-8 in VS Code workspace settings 2018-07-08 05:16:31 +02:00
34c8d44dfe Project tweaks (make update installer on build, stopwatch resource swap, formatting) 2018-07-08 05:03:30 +02:00
2dbf778e56 Change the new DM limit behavior b/c older DMs don't load when scrolled to bottom 2018-07-06 22:09:09 +02:00
873fe2b393 Remove legacy HW acceleration support check & update tooltip 2018-07-06 09:37:25 +02:00
0c53bc6f32 Move icons and images to a separate folder 2018-07-06 07:26:22 +02:00
fa407e150b Add a quick way to import profile from the login page
Closes #228
2018-07-06 07:03:39 +02:00
4e52102c5c Add option for browser throttling w/ default when covered by larger window 2018-07-06 06:26:50 +02:00
0355a5c646 Add VS Code workspace file & fix some warnings 2018-07-06 06:04:10 +02:00
377d9c3554 Make an interface for dialogs to avoid hard-coding them in FormManager 2018-07-06 05:30:12 +02:00
20b1b3c895 Add more safety checks to code.js 2018-07-05 09:05:20 +02:00
41bbe7c51b Decrease amount of loaded DMs to lower performance hit from re-opening DMs 2018-07-05 07:39:26 +02:00
27fa6aefd3 Improve CSS minification 2018-07-05 06:32:45 +02:00
a63c3232da Update CefSharp to 67 (early) 2018-07-05 06:30:41 +02:00
146908a115 Include files for importing in the project to let VS detect changes 2018-07-04 17:27:49 +02:00
48b0f35fee Move template plugin HTML and CSS into a separate file 2018-07-04 17:24:32 +02:00
6c435ebe26 Extract styles from update.js into an imported CSS file 2018-07-04 07:03:48 +02:00
b8d0b721a2 Extract styles from introduction.js into an imported CSS file 2018-07-04 07:00:47 +02:00
a6d5957f46 Extract styles from twitter.js into imported CSS files 2018-07-04 06:55:10 +02:00
9e5f676e23 Move imported files into structured folder for easier handling & tweak syntax 2018-07-04 06:53:31 +02:00
414f3a1f9d Import shared plugin setup code into individual environment files 2018-07-04 06:26:41 +02:00
fedf9c60ee Hack up a simple import system in the build process 2018-07-04 06:25:22 +02:00
b6385d9622 Change unit test namespaces 2018-07-04 03:07:45 +02:00
76d25a712d Move old IO test project in preparation for future refactoring 2018-07-04 03:07:15 +02:00
2c6d935273 Rewrite unit tests for CommandLineArgs into xUnit 2018-07-04 02:45:11 +02:00
830d98a101 Rewrite unit tests for TwoKeyDictionary into xUnit 2018-07-03 21:23:34 +02:00
d7378bd75a Add leading slashes to URL strings in RequestHandlerBrowser 2018-07-02 20:32:23 +02:00
0f41cb9dbc Add response filter to restore global jQuery & unfreeze TweetDeck resources 2018-07-02 20:27:54 +02:00
77bc922d93 Add base class for response filters to safely modify intercepted files 2018-07-02 19:57:24 +02:00
c91b635132 Fix TDPF_createCustomStyle's remove function depending on jQuery 2018-07-01 16:51:57 +02:00
e5521de34a Split icon design files & update 256x256 icon 2018-07-01 15:45:33 +02:00
7c0f8d0f24 Remove temporarily forced update checking 2018-07-01 03:10:47 +02:00
f60d5f650f Add new program and tray icons to show when notifications are muted 2018-06-30 21:28:41 +02:00
ca67f2fe0a Redo app icon background as vector & update all icons 2018-06-30 19:39:16 +02:00
16cce8be1d Remove unnecessary code for disabling TweetDeck update notification 2018-06-30 15:02:46 +02:00
770619d948 Make update notification work without jQuery & optimize slide animation 2018-06-30 14:30:45 +02:00
cfedb7d6b1 Improve logged error message when update checking fails 2018-06-30 13:22:37 +02:00
738557b3a2 Add exception type that adds (potentially sensitive) details to error log 2018-06-30 13:20:25 +02:00
38b01deec1 Make it easier to freeze TweetDeck resources in the future 2018-06-30 11:49:46 +02:00
1a31e69ec9 Release 1.14.4.1 2018-06-29 19:56:00 +02:00
e065983c95 Delay the apocalypse (freeze TweetDeck JS resources, force update checking) 2018-06-29 19:41:02 +02:00
30a169171a Apparently deserializing @TryMyAwesomeApp sometimes causes a hidden crash 2018-06-29 17:10:11 +02:00
8d1900362e Update PostCefUpdate script to remove invalid build configurations (x64, AnyCPU) 2018-06-29 17:05:53 +02:00
e154189de1 Cleanup TweetDuck.csproj and fix names in TestResult unit test 2018-06-28 14:06:03 +02:00
b0f147de24 Fix misaligned 'Add column' icons when using old icon design 2018-06-28 08:10:48 +02:00
979b3548db Release 1.14.4 2018-06-28 07:54:38 +02:00
05d6c578b3 Move InjectedHTML unit tests to xUnit and rename Inject method 2018-06-26 11:19:44 +02:00
a117559063 Minor formatting tweaks 2018-06-26 10:05:53 +02:00
f87c649b09 Fix Twitter experiment causing crash in notifications and subsequent render corruption 2018-06-26 09:49:37 +02:00
6504dc9184 Add unit tests for Result and a few utility methods & fix edge case in StringUtils 2018-06-24 21:41:02 +02:00
25a8ddffd4 Rewrite and tweak existing Core namespace unit tests into xUnit 2018-06-24 19:29:24 +02:00
fa0f9b89cf Remove 'Release' configuration from UnitTests project 2018-06-24 16:16:11 +02:00
4d00a67891 Add a new F# xUnit test project 2018-06-24 16:09:21 +02:00
bd2c43e1f4 Fix analytics not counting applying ROT13 on non-editable text 2018-06-19 21:35:35 +02:00
c7279eaa34 Fix bug with falsely detecting symlinks in plugins if a file/folder doesn't exist 2018-06-19 21:32:21 +02:00
fd523e552c Symlinks/junctions in plugin folders can go to hell 2018-06-13 22:20:10 +02:00
cb877b8b23 Fix broken desktop notifications for retweets with sensitive media 2018-06-10 23:53:01 +02:00
ed1bee8b89 Release 1.14.3 2018-06-10 20:13:05 +02:00
a8e1492056 Push pin icon to the repository 2018-06-08 10:16:58 +02:00
5587216c01 Fix one more case of breaking overlays (account dialog) 2018-06-07 17:48:52 +02:00
86569261ad Add a visual response when hovering filter icons under column header 2018-06-07 14:52:39 +02:00
4a9049c7aa Fix CSS change in dialog overlays breaking some cases 2018-06-07 14:09:35 +02:00
75d60a8182 Work around browser redirection when dragging links into a scrolling column 2018-06-07 13:23:19 +02:00
14d4dc2ed9 Fix more instances of cut off badges 2018-06-06 16:04:15 +02:00
fd0e1740a5 Rename SetLastRightClickInfo bridge method and make it browser-only 2018-06-06 15:11:07 +02:00
70ca890bef Fix not stripping t.co in notifications when dragging & sometimes when copying 2018-06-06 15:10:52 +02:00
b9318dfd8e Minor visual fixes (Edit List modal, set minimum column width) 2018-06-05 04:25:05 +02:00
995642a719 Add support for 1 or 2 columns on screen in edit-design plugin 2018-06-05 03:48:40 +02:00
d14de4ac9e Lower minimum width of browser window & fix modals breaking in small windows 2018-06-05 02:51:21 +02:00
b7f325a241 Minor RequestHandlerBase refactoring 2018-06-05 02:05:04 +02:00
27c55718c2 Release 1.14.2.1 2018-06-04 13:01:44 +02:00
421ff0654b Temporarily work around buggy notification scrolling 2018-06-04 12:35:34 +02:00
ed947458f9 Update fallback HTML in desktop notifications 2018-06-04 12:09:02 +02:00
9cdb20ba84 Fix several broken column types in 'Add column' modal 2018-06-04 11:02:52 +02:00
d8774b735f Release 1.14.2 2018-06-04 08:59:32 +02:00
adcb42695f Update CEF so I don't have to workaround wrong dev tools version
Closes #220
2018-06-01 22:32:44 +02:00
dd77b5bcbb Revert smooth scrolling fix and rewrite horizontal scrolling fix to avoid column jumping 2018-06-01 21:48:26 +02:00
d2445be155 Fix missing GIF previews in tweet screenshots 2018-05-31 03:07:51 +02:00
10254c8af7 Fix tweet screenshots with Aero disabled by making the window visible
Closes #223
2018-05-31 02:58:25 +02:00
d7e830badf Slightly increase default notification height for larger font sizes 2018-05-31 01:11:17 +02:00
b445a3a9b8 Fix broken reply-account advanced selector arguments after a TweetDeck update 2018-05-31 00:52:08 +02:00
97f42ead66 Make it easier to debug screenshots 2018-05-30 19:50:11 +02:00
03730fafb9 ...because I can't be bothered 2018-05-29 15:34:02 +02:00
0be9465dca Fix column icons being hidden by title with clear-columns or edit-design features on 2018-05-29 15:23:41 +02:00
d7f1df4995 Release 1.14.1 2018-05-16 13:07:47 +02:00
3cb0f90706 Prevent an unlikely crash when showing an error message in context menu 2018-05-16 13:05:59 +02:00
a3e9b15a8a Add 'Apply ROT13' to non-editable selections to allow decoding tweets 2018-05-16 12:40:49 +02:00
00bfa68a57 Fix UI issues after recent TweetDeck updates 2018-05-16 07:48:47 +02:00
c311e24f08 Make it easier to update devtools file after updating CEF 2018-05-15 12:42:57 +02:00
1cdd4e4455 Update CefSharp to 66 (early) to fix memory leak 2018-05-15 10:10:03 +02:00
8078c0081a Add a script to fix browser project references after updating CEF 2018-05-15 10:04:08 +02:00
a867e1fc40 Optimize speed and memory usage of build process for app & installers 2018-05-08 20:20:02 +02:00
61da36ac1c Update README.md 2018-05-08 19:37:06 +02:00
720ca2a018 Update README.md 2018-05-08 19:36:08 +02:00
b39c593552 Update README.md 2018-05-08 06:28:07 +02:00
c808952a45 Update README.md 2018-05-08 00:03:08 +02:00
b468d7a766 Update README.md 2018-05-07 20:03:50 +02:00
28578f60be Include plugin files in the project & update README 2018-05-07 19:35:08 +02:00
92a39e2527 Push debug configuration start arguments (-datafolder TweetDuckDebug) 2018-05-07 18:11:56 +02:00
1bce5e4342 Release 1.14 2018-05-07 17:28:34 +02:00
68f586e104 Fix wrong info in the analytics dialog 2018-05-07 15:42:41 +02:00
d27a66202e Remove "Show this thread" in quoted tweets from notifications and screenshots 2018-05-07 03:01:24 +02:00
07de2f450c Fix broken notification scrolling in CEF 65 properly 2018-05-07 02:43:40 +02:00
3c03726634 Remove old and no longer necessary code from update installer 2018-05-06 22:47:58 +02:00
6fb2643063 Slightly increase installer compression level 2018-05-06 22:24:55 +02:00
5eef6c8196 Kill stubborn app processes when installing updates 2018-05-06 21:18:23 +02:00
829c332e13 Add a global function for mustache injection & fix broken clear-columns mustache 2018-05-05 11:47:19 +02:00
47eec14bca Fix freshly broken GIF previews in notifications 2018-05-04 21:07:33 +02:00
e7ee1d6be7 Revert "Fix broken notification scrolling in CEF 65"
This reverts commit 1029ea5840.
2018-05-04 14:00:41 +02:00
e41b5e5ff7 Replace generated license files with just one and include CEF license 2018-05-04 13:30:54 +02:00
ba1bacd08c Fix minor formatting and method modifier issues 2018-05-04 13:04:23 +02:00
1029ea5840 Fix broken notification scrolling in CEF 65 2018-05-04 11:41:20 +02:00
339eaf0195 Fix button appearance in introduction dialog and some plugins 2018-05-03 16:02:44 +02:00
aa1e1549d8 Fix Twitter's broken Cancel button when logging out 2018-05-03 15:04:09 +02:00
1f8ae9ef80 Update CefSharp to 65 (pre01) and fix blank example notification
* Update CefSharp to 65 (pre01)

* Fix blank example notification on first load
2018-05-03 14:05:56 +02:00
65165de060 Release 1.13.6 2018-05-02 16:27:23 +02:00
485836d2ce Replace about:blank in FormGuide with a dummy page 2018-05-02 15:21:23 +02:00
64c07c14d9 Revert "Update to CefSharp 65 (early) (#215)"
This reverts commit b6a599f8a6.
2018-05-02 15:19:01 +02:00
b6313c2b72 Update CefSharp to 65 (pre01) 2018-05-02 00:05:08 +02:00
58124b5821 Force Chrome UA on TweetDeck and remove -chromeagent argument 2018-05-01 19:31:39 +02:00
b6a599f8a6 Update to CefSharp 65 (early) (#215) 2018-05-01 19:20:13 +02:00
19a6bc0dbd Improve performance of PostBuild.ps1 2018-05-01 16:37:06 +02:00
8cb81d44ee Fix update installer changing uninstaller name and remove /MERGETASKS parameter 2018-05-01 14:36:51 +02:00
22d0a372d8 Add dev tools to the installer as an optional component 2018-05-01 14:30:56 +02:00
988fae75c3 Add a command line argument to use Chrome user agent 2018-04-30 21:02:15 +02:00
a82b0e3622 Release 1.13.5 2018-04-29 20:55:33 +02:00
bc6cacacf9 Fix portable install not recognizing itself after importing login session 2018-04-29 19:00:17 +02:00
03ad1b3cbc Update instructions for reply-account plugin and TDPF_getColumnName 2018-04-29 14:29:33 +02:00
eac300627f Fix broken column names again and make getColumnName accessible to plugins 2018-04-29 13:17:00 +02:00
12525ac386 Fix screenshots with zoom & try to fix rendering issues 2018-04-28 20:36:49 +02:00
7558551859 Add a debug flag to generate individual screenshot frames 2018-04-28 18:15:25 +02:00
a9cce13eef Fix visual inconsistencies with new icons 2018-04-28 15:34:15 +02:00
5bb2c43dd0 Fix edit-design plugin not loading when enabled after a restart 2018-04-28 15:22:55 +02:00
5b1dcc88cc Make Plugins form always show Configure button when configurable 2018-04-28 15:19:58 +02:00
5c8fc1d136 Fix clear-columns plugin button disappearing when adding/removing columns 2018-04-27 21:13:38 +02:00
82c2ab3448 Fix broken smooth and horizontal scrolling after a TweetDeck update 2018-04-27 19:33:32 +02:00
b05c8d180f Remove UpdaterSettings and fix not restarting the timer after a dismissed update 2018-04-27 19:21:42 +02:00
87109e5d01 Fix a few visual issues with high DPI 2018-04-27 18:29:02 +02:00
be1a809098 Update all forms and dialogs to use the 'Segoe UI' font 2018-04-27 18:06:45 +02:00
ba0e3f1bd4 Continue redesign of Plugins form (tweak visuals, position, and size limits) 2018-04-27 14:59:36 +02:00
27d41e6164 Begin redesign of the Plugins form (layout reorganization, fixes, optimization) 2018-04-27 13:47:29 +02:00
1ce5ddfd98 Rewrite names and descriptions of plugins & update debug plugin 2018-04-26 21:54:40 +02:00
0096a1a4ef Move debug configuration build events to PostBuild script 2018-04-26 15:37:02 +02:00
d2a6560a90 Measure PostBuild script duration and fix formatting 2018-04-25 19:35:57 +02:00
4d7c048139 Remove versions from official plugins and make them only work on one app version 2018-04-25 19:35:04 +02:00
1d78bd2655 Release 1.13.4.1 2018-04-24 18:16:13 +02:00
9250f1907c Quick semi-temporary fix for removed column.isOfType 2018-04-24 18:10:22 +02:00
a63e210b88 Release 1.13.4 2018-04-15 19:59:35 +02:00
06bd65b7f8 Fix wrong behavior when an update is canceled during download & multiple check errors in some cases 2018-04-15 19:01:39 +02:00
b6c17eb05e Remove unused selectors and classes from styles & add a related TODO note 2018-04-15 18:08:37 +02:00
a3d40fdc2b Push a quick utility to detect unused selectors and classes 2018-04-15 18:08:03 +02:00
c064ef7a30 Improve screenshot reliability 2018-04-15 16:24:26 +02:00
762717da1e Move clear-columns plugin nav button next to 'Add column' button & add isClearable safeguard 2018-04-15 14:39:59 +02:00
b7d3758bea Add error handling when checking updates, and remove unnecessary TODO 2018-04-15 14:03:11 +02:00
d20541fd24 Fix clear-columns plugin to hide the Clear button on scheduled & collection columns 2018-04-14 20:28:48 +02:00
2c2f860f26 Fix issues caused by recent TweetDeck update (notifications, column styles, reply account)
Closes #211
2018-04-14 19:40:51 +02:00
d1db3aa673 Remove command line argument for pre-releases & reorganize restart dialog 2018-04-11 10:39:17 +02:00
cedc52cdf5 Move update notification trigger code to TweetDeckBrowser 2018-04-11 10:01:55 +02:00
33f8eafbcf Remove unused VC120 NuGet package 2018-04-11 09:59:10 +02:00
ad45cf8c72 Begin rewriting update checker to run within C# 2018-04-11 09:59:00 +02:00
f99d035621 Add a Result class that acts as an Either monad for a value or exception 2018-04-10 19:45:41 +02:00
f3072caea8 Fix broken element resizing in the Edit CSS dialog 2018-04-07 13:42:36 +02:00
1410974292 Release 1.13.3 2018-04-07 11:56:36 +02:00
44413fa96c Swap order of 'Search in' items in selection context menu 2018-04-07 11:15:14 +02:00
342a4b4067 Minor code formatting tweaks 2018-04-07 03:46:10 +02:00
4356dde92d Fix wrong c# language version setting for Release builds 2018-04-06 16:36:27 +02:00
21e64a18d8 Fix screenshots to work properly with combinations of DPI and zoom settings 2018-04-06 07:28:57 +02:00
5a305a6740 Fix wrong screenshot size when browser zoom is not 100% 2018-04-06 03:05:06 +02:00
44595bad40 Refactor plugin loading and validation 2018-04-05 21:34:35 +02:00
7fc9edc9cb Fix wrong namespace in update event classes 2018-04-05 09:58:52 +02:00
93e191f522 Reorganize hot swap code & add support for hot swapping plugins 2018-04-05 03:34:11 +02:00
8d8355e792 Rewrite PluginManager setup scripts to use a custom array-based dictionary 2018-04-05 02:09:51 +02:00
a5379d290c Add resource hotswap for easier debugging 2018-04-04 23:13:44 +02:00
caea8d4315 Move most of post build event (copying, cleanup) to PostBuild.ps1 2018-04-04 20:05:59 +02:00
24224ab4c6 Increase height of Options form to avoid scrollbars in General tab 2018-04-04 07:43:00 +02:00
4dbc02360c Add context menu options to search selected text in a column or browser
Closes #209
2018-04-04 05:22:41 +02:00
aa7a29af0c Fix combo box issues (closing while opening, minor browser selection bug) 2018-04-04 03:33:14 +02:00
296d0c6199 Fix ScriptLoader showing multiple errors at once sometimes & change error title 2018-04-03 23:46:00 +02:00
812a034e8d Include version header in ScriptLoader files to detect failed installs 2018-04-03 23:44:43 +02:00
e9de789b79 Minor refactoring, including removal of unnecessary enableCustomCSS parameter 2018-04-03 20:49:21 +02:00
cfbc1b9575 Enable custom CSS in screenshots and move styles from code.js to notification.css 2018-04-03 20:48:33 +02:00
e39e85e4dd Add 'td-notification' body class to desktop notifications & update notification.css 2018-04-03 20:26:54 +02:00
3f0b161cd0 Move screenshot height calculation to the screenshot window 2018-04-03 18:26:33 +02:00
ebe3868720 Fix ScriptLoader crash when showing error message from another thread 2018-04-03 18:19:39 +02:00
ffd0f5e986 Rewrite screenshot rendering to fix current and future visual issues 2018-04-03 02:05:22 +02:00
217535a3ba Make td-notification-padded styles available in screenshots 2018-04-03 02:02:15 +02:00
7abfbea2da Fix "Replying to" user link not using black theme in screenshots
Closes #208
2018-04-03 01:10:53 +02:00
86ffeaac9a Remove no longer supported keycap emoji from the emoji keyboard
Closes #207
2018-04-03 00:04:18 +02:00
ab915b7115 Move accounts above hashtags in search results 2018-04-02 23:26:51 +02:00
705b5d38cf Add design files for logo and video player buttons 2018-04-01 19:35:12 +02:00
fc2acb00b3 Add a batch file to build update installer only 2018-03-16 18:50:37 +01:00
5add8a1d0e Move ITweetDeckBrowser and refactor some things 2018-03-16 18:48:41 +01:00
063d3a2637 Remove unnecessary null fallback in SetClipboardText 2018-03-09 14:43:59 +01:00
f1f90a2ee3 Refactor code to avoid nulls (#206)
* Ensure potential nulls have a fallback value & add/remove null checks

* Refactor update check code to avoid nulls

* Refactor ProfileManager exception handling to avoid nulls

* Refactor a few more various classes and fix nulls in ContextInfo

* Force c#7 everywhere and revert usage of newer features from cherry-picked commits

* Remove unused #pragma declaration
2018-03-07 14:37:03 +01:00
ed317a4e46 Refactor VideoPlayer 2018-03-06 21:17:22 +01:00
cca16f3bb1 Release 1.13.2 2018-03-06 18:38:24 +01:00
aba156cb3b Fix typo from refactoring breaking context menu for some links 2018-03-02 06:00:53 +01:00
cd4e4d7095 Fix hashtags and search links being recognized as account links 2018-03-02 05:59:20 +01:00
8fbb639430 Refactor & optimize context menu, send last tweet info only on right-click 2018-03-02 05:24:45 +01:00
d5bf8ec558 Fix missing image/video context menu for tweets that have both media and a quote 2018-03-02 00:15:28 +01:00
b6cff40f1e Warn when checking updates outside TweetDeck & fix visual and unlikely issues 2018-02-28 11:03:55 +01:00
833e42f455 Add a IsTweetDeckWebsite bool to ITweetDeckBrowser 2018-02-28 07:24:40 +01:00
8134843dad Fix background color & twitter.com hooks not applying quickly enough sometimes 2018-02-28 02:34:29 +01:00
1f92d5e633 Remove 'Shift Selects Multiple Accounts' option & fix refocusing after switching account 2018-02-28 01:43:44 +01:00
dc51c0ae85 Remove unnecessary console logging in debug builds 2018-02-26 17:45:56 +01:00
45c79643d6 Why the fuck is TLS 1.2 disabled by default in .NET on some computers 2018-02-25 23:15:00 +01:00
9041bfc627 Tweak search input font size and icon position 2018-02-21 22:17:51 +01:00
0b3b3dd0be Fix a crash when downloading tweet images with no username
Happens when someone accidentally or through dev tools gets to
twitter.com and tries downloading an image.
2018-02-21 19:48:00 +01:00
89e92dab59 Fix middle-clicking on links in desktop notifications not skipping them or stripping t.co 2018-02-19 17:31:28 +01:00
8c168c9ad7 Fix emoji keyboard button size & tweak composer button layout 2018-02-17 12:21:26 +01:00
9f63357a92 Pre-check desktop icon option in installer when not updating 2018-02-17 10:52:19 +01:00
d91b4bd1f3 Release 1.13.1 2018-02-14 18:43:02 +01:00
c0c64f6d62 Remove old TweetDeck installation check from the installer & tweak formatting 2018-02-14 17:45:19 +01:00
1a5d2af779 Decrease post-update analytics report to 8h and increase startup delay to 2m 2018-02-14 17:10:24 +01:00
f40a33192b Revert and fix various changes from recent TweetDeck update 2018-02-14 15:39:27 +01:00
ca4900aff0 Fix 'Manage templates' button after a recent TweetDeck update 2018-02-14 15:17:58 +01:00
56fc9e2d40 Fix black theme issues (mismatched detection & rare bug with wrong notification background color) 2018-02-13 16:51:40 +01:00
d2174c0b69 Fix misaligned avatars in activity columns 2018-02-13 15:59:21 +01:00
9f76754ad3 Force full install from 1.13 to 1.13.0.1 2018-02-13 13:37:30 +01:00
118ceaec35 Release 1.13.0.1 2018-02-13 13:27:52 +01:00
5a57d28a7d Fix crash by checking and downloading for VC++ 2015 in the installer
Closes #205
2018-02-13 13:17:40 +01:00
07af99f862 Fix wrong background color in tweet screenshots when using black theme 2018-02-13 12:42:25 +01:00
59fba7fba0 Fix a hidden crash that prevented desktop notifications from showing 2018-02-13 12:37:11 +01:00
dd4edc4249 Update CefSharp to latest 64 and remove VC++ 2012 2018-02-13 11:28:48 +01:00
856226473a Update README.md 2018-02-13 05:52:03 +01:00
535 changed files with 24141 additions and 16520 deletions

4
.gitattributes vendored
View File

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

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
github: chylex
patreon: chylex
ko_fi: chylex

7
.gitignore vendored
View File

@@ -5,8 +5,14 @@
bld/*
!bld/*.iss
!bld/*.bat
!bld/*.ps1
!bld/Redist
!bld/Resources
# Rider
.idea/.idea.TweetDuck/.idea/dictionaries
.idea/.idea.TweetDuck/.idea/misc.xml
# User-specific files
*.suo
*.user
@@ -140,4 +146,3 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

8
.idea/.idea.TweetDuck/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/projectSettingsUpdater.xml
/modules.xml
/contentModel.xml
/.idea.TweetDuck.iml

View File

@@ -0,0 +1,450 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="AUTODETECT_INDENTS" value="false" />
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</value>
</option>
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="RIGHT_MARGIN" value="999" />
<option name="FORMATTER_TAGS_ENABLED" value="true" />
<CssCodeStyleSettings>
<option name="HEX_COLOR_LOWER_CASE" value="true" />
</CssCodeStyleSettings>
<DB2CodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</DB2CodeStyleSettings>
<DerbyCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</DerbyCodeStyleSettings>
<GoCodeStyleSettings>
<option name="MOVE_ALL_STDLIB_IMPORTS_IN_ONE_GROUP" value="true" />
<option name="GROUP_STDLIB_IMPORTS" value="true" />
<option name="WRAP_COMP_LIT" value="5" />
<option name="WRAP_FUNC_PARAMS" value="5" />
<option name="WRAP_FUNC_RESULT" value="5" />
</GoCodeStyleSettings>
<H2CodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</H2CodeStyleSettings>
<H2CodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</H2CodeStyleSettings>
<HSQLCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</HSQLCodeStyleSettings>
<HTMLCodeStyleSettings>
<option name="HTML_ALIGN_TEXT" value="true" />
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_ELEMENTS_TO_REMOVE_NEW_LINE_BEFORE" value="" />
<option name="HTML_DO_NOT_INDENT_CHILDREN_OF" value="" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACKETS" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="USE_EXPLICIT_JS_EXTENSION" value="TRUE" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
<option name="USE_CHAINED_CALLS_GROUP_INDENTS" value="true" />
<option name="SPACE_BEFORE_ASYNC_ARROW_LPAREN" value="false" />
<option name="IMPORT_SORT_MODULE_NAME" value="true" />
</JSCodeStyleSettings>
<JSON>
<option name="OBJECT_WRAPPING" value="5" />
<option name="ARRAY_WRAPPING" value="5" />
</JSON>
<JavaCodeStyleSettings>
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" />
<package name="javax" withSubpackages="true" static="false" />
<package name="java" withSubpackages="true" static="false" />
<package name="" withSubpackages="true" static="true" />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="ALIGN_IN_COLUMNS_CASE_BRANCH" value="true" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="IMPORT_NESTED_CLASSES" value="true" />
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<LessCodeStyleSettings>
<option name="HEX_COLOR_LOWER_CASE" value="true" />
</LessCodeStyleSettings>
<MSSQLCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</MSSQLCodeStyleSettings>
<MySQLCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</MySQLCodeStyleSettings>
<Objective-C>
<option name="INDENT_DIRECTIVE_AS_CODE" value="true" />
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
<option name="KEEP_CASE_EXPRESSIONS_IN_ONE_LINE" value="true" />
<option name="SPACE_BEFORE_INIT_LIST" value="true" />
<option name="SPACE_AFTER_DICTIONARY_LITERAL_COLON" value="false" />
</Objective-C>
<OracleCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</OracleCodeStyleSettings>
<PHPCodeStyleSettings>
<option name="ALIGN_KEY_VALUE_PAIRS" value="true" />
<option name="CONCAT_SPACES" value="false" />
<option name="COMMA_AFTER_LAST_ARRAY_ELEMENT" value="true" />
<option name="PHPDOC_BLANK_LINE_BEFORE_TAGS" value="true" />
<option name="LOWER_CASE_BOOLEAN_CONST" value="true" />
<option name="LOWER_CASE_NULL_CONST" value="true" />
<option name="ELSE_IF_STYLE" value="COMBINE" />
<option name="VARIABLE_NAMING_STYLE" value="SNAKE_CASE" />
<option name="KEEP_BLANK_LINES_AFTER_LBRACE" value="0" />
<option name="SPACE_BEFORE_CLOSURE_LEFT_PARENTHESIS" value="false" />
<option name="FORCE_SHORT_DECLARATION_ARRAY_STYLE" value="true" />
<option name="NEW_LINE_AFTER_PHP_OPENING_TAG" value="true" />
<option name="SPACE_AROUND_ASSIGNMENT_IN_DECLARE" value="true" />
</PHPCodeStyleSettings>
<PostgresCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</PostgresCodeStyleSettings>
<Properties>
<option name="KEEP_BLANK_LINES" value="true" />
</Properties>
<Python>
<option name="SPACE_AROUND_EQ_IN_NAMED_PARAMETER" value="true" />
<option name="SPACE_AROUND_EQ_IN_KEYWORD_ARGUMENT" value="true" />
<option name="NEW_LINE_AFTER_COLON" value="true" />
<option name="DICT_WRAPPING" value="5" />
<option name="DICT_NEW_LINE_AFTER_LEFT_BRACE" value="true" />
<option name="DICT_NEW_LINE_BEFORE_RIGHT_BRACE" value="true" />
</Python>
<RsCodeStyleSettings>
<option name="ALIGN_RET_TYPE" value="false" />
<option name="ALIGN_TYPE_PARAMS" value="true" />
<option name="ALLOW_ONE_LINE_MATCH" value="true" />
<option name="SPACE_AROUND_ASSOC_TYPE_BINDING" value="true" />
</RsCodeStyleSettings>
<Ruby>
<option name="INDENT_PRIVATE_METHODS" value="true" />
<option name="INDENT_PROTECTED_METHODS" value="true" />
<option name="INDENT_PUBLIC_METHODS" value="true" />
<option name="INDENT_WHEN_CASES" value="true" />
<option name="CHAIN_CALLS_ALIGNMENT" value="2" />
</Ruby>
<SQLiteCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</SQLiteCodeStyleSettings>
<ScssCodeStyleSettings>
<option name="HEX_COLOR_LOWER_CASE" value="true" />
</ScssCodeStyleSettings>
<SqlCodeStyleSettings version="6">
<option name="KEYWORD_CASE" value="2" />
<option name="TYPE_CASE" value="2" />
<option name="CUSTOM_TYPE_CASE" value="2" />
<option name="SUBQUERY_CONTENT" value="1" />
<option name="SUBQUERY_CLOSING" value="1" />
<option name="INSERT_TABLE_EL_LINE" value="0" />
<option name="INSERT_EL_WRAP" value="2" />
<option name="SET_EL_WRAP" value="2" />
<option name="SET_ALIGN_EQUAL_SIGN" value="false" />
<option name="FROM_EL_WRAP" value="2" />
<option name="FROM_ALIGN_JOIN_TABLES" value="true" />
<option name="FROM_INDENT_JOIN" value="false" />
<option name="FROM_ONLY_JOIN_INDENT" value="2" />
<option name="WHERE_EL_WRAP" value="2" />
<option name="TABLE_OPENING" value="1" />
<option name="TABLE_CONTENT" value="2" />
<option name="TABLE_CLOSING" value="3" />
<option name="TABLE_DEFAULTS_ALIGN" value="false" />
<option name="TABLE_NULLABILITIES_ALIGN" value="false" />
<option name="CONSTRAINT_WRAP_1" value="false" />
<option name="CONSTRAINT_WRAP_3" value="true" />
<option name="CONSTRAINT_WRAP_4" value="true" />
<option name="VIEW_INDENT_QUERY" value="true" />
<option name="EXPR_CASE_WHEN_WRAP" value="false" />
</SqlCodeStyleSettings>
<SybaseCodeStyleSettings version="6">
<option name="USE_GENERIC_STYLE" value="true" />
</SybaseCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
<option name="USE_CHAINED_CALLS_GROUP_INDENTS" value="true" />
<option name="SPACE_BEFORE_ASYNC_ARROW_LPAREN" value="false" />
</TypeScriptCodeStyleSettings>
<XML>
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</XML>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="CSS">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Groovy">
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="HTML">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="999" />
<option name="BLANK_LINES_AFTER_PACKAGE" value="0" />
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="SPACE_AFTER_TYPE_CAST" value="false" />
<option name="SPACE_BEFORE_SYNCHRONIZED_PARENTHESES" value="false" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="SPACE_WITHIN_BRACKETS" value="true" />
<option name="SPACE_WITHIN_BRACES" value="true" />
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="LESS">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Lua">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="PHP">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="KEEP_FIRST_COLUMN_COMMENT" value="false" />
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="CLASS_BRACE_STYLE" value="1" />
<option name="METHOD_BRACE_STYLE" value="1" />
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="SPECIAL_ELSE_IF_TREATMENT" value="true" />
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Puppet">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Python">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="RHTML">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Rust">
<option name="RIGHT_MARGIN" value="140" />
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SASS">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SCSS">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="SQL">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Shell Script">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TOML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="go">
<option name="CALL_PARAMETERS_WRAP" value="5" />
<indentOptions>
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="METHOD_ANNOTATION_WRAP" value="0" />
<option name="CLASS_ANNOTATION_WRAP" value="0" />
<option name="FIELD_ANNOTATION_WRAP" value="0" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="liquid">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ruby">
<option name="SPACE_WITHIN_BRACES" value="true" />
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="yaml">
<indentOptions>
<option name="KEEP_INDENTS_ON_EMPTY_LINES" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -0,0 +1,770 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project" />
<inspection_tool class="AccessToNonThreadSafeStaticFieldFromInstance" enabled="true" level="WARNING" enabled_by_default="true">
<option name="nonThreadSafeClasses">
<value />
</option>
<option name="nonThreadSafeTypes" value="" />
</inspection_tool>
<inspection_tool class="AccessToStaticFieldLockedOnInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AddOperatorModifier" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="AmbiguousFieldAccess" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AmbiguousMethodCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AnonymousInnerClassMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ArrayEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssertEqualsCalledOnArray" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssertsWithoutMessagesTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentOrReturnOfFieldWithMutableType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToCatchBlockParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToLambdaParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentToMethodParameter" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreTransformationOfOriginalParameter" value="true" />
</inspection_tool>
<inspection_tool class="AssignmentToStaticFieldFromInstanceMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AssignmentUsedAsCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AutoBoxing" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreAddedToCollection" value="false" />
</inspection_tool>
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredTypes" value="java.util.stream.Stream,java.util.stream.IntStream,java.util.stream.LongStream,java.util.stream.DoubleStream,net.minecraft.client.Minecraft,net.minecraft.client.MainWindow" />
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,net.minecraft.client.MinecraftClient,getInstance|getWindow" />
</inspection_tool>
<inspection_tool class="AutoUnboxing" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AwaitNotInLoop" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AwaitWithoutCorrespondingSignal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="BadOddness" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="BigDecimalEquals" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="BigDecimalLegacyMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="BooleanExpressionMayBeConditional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CallToNativeMethodWhileLocked" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CallableParameterUseCaseInTypeContextInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CascadeStringReplacementInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="USE_SHORT_ARRAYS_SYNTAX" value="true" />
</inspection_tool>
<inspection_tool class="CastConflictsWithInstanceof" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CastToIncompatibleInterface" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ChainedEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ChannelResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="ClassIndependentOfModule" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassLoaderInstantiation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassMayBeInterface" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="reportClassesWithNonAbstractMethods" value="true" />
</inspection_tool>
<inspection_tool class="ClassMethodNameMatchesFieldNameInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ClassNameDiffersFromFileName" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassNewInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassOnlyUsedInOneModule" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassOnlyUsedInOnePackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassOverridesFieldOfSuperClassInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_PRIVATE_REDEFINITION" value="false" />
</inspection_tool>
<inspection_tool class="ClassUnconnectedToPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ClassWithOnlyPrivateConstructors" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CloneableClassInSecureContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CollectionContainsUrl" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CollectionsFieldAccessReplaceableByMethodCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ComparableImplementedButEqualsNotOverridden" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ComparatorNotSerializable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CompareToUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ComposeMissingKeys" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ConditionSignal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConditionalExpression" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="ignoreSimpleAssignmentsAndReturns" value="true" />
</inspection_tool>
<inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConfusingElse" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<option name="reportWhenNoStatementFollow" value="true" />
</inspection_tool>
<inspection_tool class="ConfusingMainMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConfusingOctalEscape" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConfusingPlusesOrMinusesJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConstantJUnitAssertArgument" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConstantMathCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConstantTestNGAssertArgument" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ConvertJavadoc" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ConvertLambdaToReference" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ConvertOldAnnotations" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CssConvertColorToHexInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="CssConvertColorToRgbInspection" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="CssMissingSemicolon" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CssReplaceWithShorthandUnsafely" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="CustomClassloader" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CustomSecurityManager" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CyclicClassDependency" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CyclicPackageDependency" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DateToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DefaultNotLastCaseInSwitch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DisallowWritingIntoStaticPropertiesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="DisconnectedForeachInstructionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="DisjointPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DivideByZeroJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DocumentWriteJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DoubleBraceInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DoubleCheckedLocking" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreOnVolatileVariables" value="false" />
</inspection_tool>
<inspection_tool class="DriverManagerGetConnection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateBooleanBranch" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DuplicateConditionJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="DynamicallyGeneratedCodeJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ES6ConvertIndexedForToForOf" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ES6ConvertLetToConst" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ES6ConvertToForOf" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ES6ShorthandObjectProperty" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ES6TopLevelAwaitExpression" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="EmptyDirectory" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EmptyStatementBody" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_reportEmptyBlocks" value="true" />
<option name="commentsAreContent" value="true" />
</inspection_tool>
<inspection_tool class="EmptySynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EnumSwitchStatementWhichMissesCases" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoreSwitchStatementsWithDefault" value="true" />
</inspection_tool>
<inspection_tool class="EqualsCalledOnEnumConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EqualsHashCodeCalledOnUrl" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="EqualsUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ErrorRethrown" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExceptionNameDoesntEndWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExceptionPackage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExpectedExceptionNeverThrownTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExplicitArgumentCanBeLambda" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ExtendsThread" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExtendsThrowable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ExternalizableWithSerializationMethods" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FallthruInSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FieldAccessedSynchronizedAndUnsynchronized" enabled="true" level="WARNING" enabled_by_default="true">
<option name="countGettersAndSetters" value="false" />
</inspection_tool>
<inspection_tool class="FieldDeclarationSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="FieldHidesSuperclassField" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="false" />
<option name="ignoreStaticFields" value="false" />
</inspection_tool>
<inspection_tool class="FieldMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Finalize" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreTrivialFinalizers" value="true" />
</inspection_tool>
<inspection_tool class="FinalizeNotProtected" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FixedTimeStartWithInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FloatingPointEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="FoldInitializerAndIfToElvis" enabled="false" level="INFO" enabled_by_default="false" />
<inspection_tool class="ForLoopThatDoesntUseLoopVariableJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ForgottenDebugOutputInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="configuration">
<list>
<option value="\Codeception\Util\Debug::debug" />
<option value="\Codeception\Util\Debug::pause" />
<option value="\Doctrine\Common\Util\Debug::dump" />
<option value="\Doctrine\Common\Util\Debug::export" />
<option value="\Illuminate\Support\Debug\Dumper::dump" />
<option value="\Symfony\Component\Debug\Debug::enable" />
<option value="\Symfony\Component\Debug\DebugClassLoader::enable" />
<option value="\Symfony\Component\Debug\ErrorHandler::register" />
<option value="\Symfony\Component\Debug\ExceptionHandler::register" />
<option value="\TYPO3\CMS\Core\Utility\DebugUtility::debug" />
<option value="\Zend\Debug\Debug::dump" />
<option value="\Zend\Di\Display\Console::export" />
<option value="dd" />
<option value="debug_print_backtrace" />
<option value="debug_zval_dump" />
<option value="dpm" />
<option value="dpq" />
<option value="dsm" />
<option value="dump" />
<option value="dvm" />
<option value="error_log" />
<option value="kpr" />
<option value="phpinfo" />
<option value="print_r" />
<option value="var_dump" />
<option value="var_export" />
<option value="wp_die" />
<option value="xdebug_break" />
<option value="xdebug_call_class" />
<option value="xdebug_call_file" />
<option value="xdebug_call_function" />
<option value="xdebug_call_line" />
<option value="xdebug_code_coverage_started" />
<option value="xdebug_debug_zval" />
<option value="xdebug_debug_zval_stdout" />
<option value="xdebug_dump_superglobals" />
<option value="xdebug_enable" />
<option value="xdebug_get_code_coverage" />
<option value="xdebug_get_collected_errors" />
<option value="xdebug_get_declared_vars" />
<option value="xdebug_get_function_stack" />
<option value="xdebug_get_headers" />
<option value="xdebug_get_monitored_functions" />
<option value="xdebug_get_profiler_filename" />
<option value="xdebug_get_stack_depth" />
<option value="xdebug_get_tracefile_name" />
<option value="xdebug_is_enabled" />
<option value="xdebug_memory_usage" />
<option value="xdebug_peak_memory_usage" />
<option value="xdebug_print_function_stack" />
<option value="xdebug_start_code_coverage" />
<option value="xdebug_start_error_collection" />
<option value="xdebug_start_function_monitor" />
<option value="xdebug_start_trace" />
<option value="xdebug_stop_code_coverage" />
<option value="xdebug_stop_error_collection" />
<option value="xdebug_stop_function_monitor" />
<option value="xdebug_stop_trace" />
<option value="xdebug_time_index" />
<option value="xdebug_var_dump" />
</list>
</option>
<option name="migratedIntoUserSpace" value="true" />
</inspection_tool>
<inspection_tool class="FunctionNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_regex" value="[a-z][A-Za-z]*" />
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="99" />
</inspection_tool>
<inspection_tool class="FunctionWithInconsistentReturnsJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HashCodeUsesNonFinalVariable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HibernateResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="HtmlMissingClosingTag" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlPresentationalElement" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlRequiredTitleAttribute" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="IOResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredTypesString" value="java.io.ByteArrayOutputStream,java.io.ByteArrayInputStream,java.io.StringBufferInputStream,java.io.CharArrayWriter,java.io.CharArrayReader,java.io.StringWriter,java.io.StringReader" />
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="IfStatementWithIdenticalBranchesJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ImplicitDefaultCharsetUsage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InconsistentLanguageLevel" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InconsistentLineSeparators" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IncrementDecrementOperationEquivalentInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="InnerClassOnInterface" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInnerInterfaces" value="true" />
</inspection_tool>
<inspection_tool class="InnerClassReferencedViaSubclass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InnerClassVariableHidesOuterClassVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="false" />
</inspection_tool>
<inspection_tool class="InsertLiteralUnderscores" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="InstanceofCatchParameter" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InstanceofIncompatibleInterface" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InstanceofThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="InterfaceMayBeAnnotatedFunctional" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IsEmptyFunctionUsageInspection" enabled="false" level="WARNING" enabled_by_default="false">
<option name="SUGGEST_TO_USE_COUNT_CHECK" value="true" />
</inspection_tool>
<inspection_tool class="IsNullFunctionUsageInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="IteratorNextDoesNotThrowNoSuchElementException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JDBCExecuteWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JDBCPrepareStatementWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JDBCResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="JNDIResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="JSClassNamingConvention" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="99" />
</inspection_tool>
<inspection_tool class="JSConstructorReturnsPrimitive" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JSEqualityComparisonWithCoercion.TS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="mySeverity" value="Always" />
</inspection_tool>
<inspection_tool class="JSJoinVariableDeclarationAndAssignment" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="JSMissingSwitchBranches" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="JSNonASCIINames" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myAllowOnlyAscii" value="true" />
</inspection_tool>
<inspection_tool class="JSNonStrictModeUsed" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JSOctalInteger" enabled="true" level="ERROR" enabled_by_default="true">
<option name="myReportNonStrictEs5" value="true" />
</inspection_tool>
<inspection_tool class="JSUndeclaredVariable" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="myCheckGlobalDefinitions" value="true" />
</inspection_tool>
<inspection_tool class="JSUnusedGlobalSymbols" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myReportUnusedDefinitions" value="true" />
<option name="myReportUnusedProperties" value="true" />
</inspection_tool>
<inspection_tool class="JUnitDatapoint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JUnitRule" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JUnitTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JavadocHtmlLint" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="JoinDeclarationAndAssignmentJava" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="LambdaCanBeMethodCall" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="LengthOneStringInIndexOf" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LengthOneStringsInConcatenation" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="LoadLibraryWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="LocalCanBeFinal" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_VARIABLES" value="true" />
<option name="REPORT_PARAMETERS" value="true" />
</inspection_tool>
<inspection_tool class="LocalVariableDeclarationSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="LocalVariableNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_regex" value="[a-z][A-Za-z]*" />
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="99" />
</inspection_tool>
<inspection_tool class="MalformedSetUpTearDown" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapReplaceableByEnumMap" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodCallSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MethodMayBeStatic" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_onlyPrivateOrFinal" value="false" />
<option name="m_ignoreEmptyMethods" value="true" />
</inspection_tool>
<inspection_tool class="MethodMayBeSynchronized" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodOverloadsParentMethod" enabled="true" level="WARNING" enabled_by_default="true">
<option name="reportIncompatibleParameters" value="true" />
</inspection_tool>
<inspection_tool class="MethodOverridesInaccessibleMethodOfSuper" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodOverridesStaticMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MethodSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="MisorderedAssertEqualsArgumentsTestNG" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MisorderedAssertEqualsParameters" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MisorderedModifiersInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="MissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreObjectMethods" value="false" />
<option name="ignoreAnonymousClassMethods" value="false" />
</inspection_tool>
<inspection_tool class="MissortedModifiers" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_requireAnnotationsFirst" value="true" />
</inspection_tool>
<inspection_tool class="MultipleTopLevelClassesInFile" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NakedNotify" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NegatedConditional" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreNegatedNullComparison" value="true" />
</inspection_tool>
<inspection_tool class="NestedAssignment" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedClassSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NestedConditionalExpressionJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedSynchronizedStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedTernaryOperatorInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NewExpressionSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="NonBlockStatementBodyJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonExceptionNameEndsWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalClone" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalFieldInEnum" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalFieldOfException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonFinalUtilityClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonReproducibleMathCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonSerializableFieldInSerializableClass" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignorableAnnotations">
<value />
</option>
<option name="ignoreAnonymousInnerClasses" value="false" />
<option name="superClassString" value="java.awt.Component" />
</inspection_tool>
<inspection_tool class="NonSerializableObjectBoundToHttpSession" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonSerializableObjectPassedToObjectStream" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonSerializableWithSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonSerializableWithSerializationMethods" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonShortCircuitBoolean" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonSynchronizedMethodOverridesSynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NonThreadSafeLazyInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NotOptimalIfConditionsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="REPORT_DUPLICATE_CONDITIONS" value="false" />
<option name="SUGGEST_OPTIMIZING_CONDITIONS" value="false" />
</inspection_tool>
<inspection_tool class="NotifyCalledOnCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NotifyWithoutCorrespondingWait" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NullThrown" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NumericToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="OCInconsistentNaming" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ObjectAllocationIgnoredJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObjectInstantiationInEqualsHashCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObjectNotify" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObjectToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ObsoleteCollection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreRequiredObsoleteCollectionTypes" value="true" />
</inspection_tool>
<inspection_tool class="OctalAndDecimalIntegersMixed" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="OffsetOperationsInspection" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="OneTimeUseVariablesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ALLOW_LONG_STATEMENTS" value="true" />
</inspection_tool>
<inspection_tool class="OverloadedVarargsMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PackageInMultipleModules" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ParameterHidingMemberVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreInvisibleFields" value="true" />
<option name="m_ignoreStaticMethodParametersHidingInstanceFields" value="true" />
<option name="m_ignoreForConstructors" value="true" />
<option name="m_ignoreForPropertySetters" value="true" />
<option name="m_ignoreForAbstractMethods" value="false" />
</inspection_tool>
<inspection_tool class="ParameterNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_regex" value="[a-z][A-Za-z]*" />
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="99" />
</inspection_tool>
<inspection_tool class="ParameterizedParametersStaticCollection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PhpAssignmentInConditionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpAssignmentReplaceableWithOperatorAssignmentInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpAssignmentReplaceableWithPrefixExpressionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpCSValidationInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<option name="EXTENSIONS" value="php,js,css,inc" />
</inspection_tool>
<inspection_tool class="PhpClassNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="PhpClosureCanBeConvertedToShortArrowFunctionInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpCompoundNamespaceDepthInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpConstantNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="PhpConstantReassignmentInspection" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="PhpDivisionByZeroInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="SKIP_ON_EMPTY_PHPDOC" value="false" />
</inspection_tool>
<inspection_tool class="PhpFunctionNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="PhpInconsistentReturnPointsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ALLOW_RETURN_NULL_IN_VOID" value="false" />
</inspection_tool>
<inspection_tool class="PhpLongTypeFormInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpLoopCanBeConvertedToArrayFillInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpLoopCanBeConvertedToArrayFilterInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpLoopCanBeConvertedToArrayMapInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpMethodNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="PhpMethodOrClassCallIsNotCaseSensitiveInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PhpMissingParentCallMagicInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PhpMissingReturnTypeInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpMissingStrictTypesDeclarationInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PhpMissingVisibilityInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpNewClassMissingParameterListInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpNonCanonicalElementsOrderInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpOverridingMethodVisibilityInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpPropertyNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_regex" value="[a-z][_a-z\d]*" />
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="PhpRedundantClosingTagInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PhpSeparateElseIfInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpShortOpenTagInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpSingleStatementWithBracesInspection" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="PhpStatementHasEmptyBodyInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myCommentsCountAsContent" value="true" />
</inspection_tool>
<inspection_tool class="PhpStatementWithoutBracesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpStaticAsDynamicMethodCallInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="SHOW_FOR_MAGIC" value="false" />
</inspection_tool>
<inspection_tool class="PhpTraditionalSyntaxArrayLiteralInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpTraitsUseListInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpUndefinedCallbackInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PhpUndefinedClassConstantInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="DOWNGRADE_SEVERITY" value="true" />
</inspection_tool>
<inspection_tool class="PhpUndefinedMethodInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="DOWNGRADE_SEVERITY" value="true" />
</inspection_tool>
<inspection_tool class="PhpUnused" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="SHOW_UNUSED_BY_ENTRIES" value="false" />
</inspection_tool>
<inspection_tool class="PhpUnusedParameterInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="DONT_REPORT_ANONYMOUS" value="true" />
</inspection_tool>
<inspection_tool class="PhpVarUsageInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PhpVariableNamingConventionInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_minLength" value="0" />
<option name="m_maxLength" value="0" />
</inspection_tool>
<inspection_tool class="PhpVariableVariableInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PointlessBitwiseExpressionJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreExpressionsContainingConstants" value="false" />
</inspection_tool>
<inspection_tool class="ProblematicVarargsMethodOverride" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ProblematicWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PropertyCanBeStaticInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ProtectedMemberInFinalClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PublicFieldAccessedInSynchronizedContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PublicStaticArrayField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PublicStaticCollectionField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyAugmentAssignmentInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="PyClassicStyleClassInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="PyMissingTypeHintsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="m_onlyWhenTypesAreKnown" value="false" />
</inspection_tool>
<inspection_tool class="RawTypeCanBeGeneric" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReadObjectAndWriteObjectPrivate" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReadObjectInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReadResolveAndWriteReplaceProtected" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantElseClauseInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RedundantImplements" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSerializable" value="true" />
<option name="ignoreCloneable" value="true" />
</inspection_tool>
<inspection_tool class="RedundantObjectTypeCheck" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="RedundantSuppression" enabled="true" level="WARNING" enabled_by_default="true">
<option name="IGNORE_ALL" value="true" />
</inspection_tool>
<inspection_tool class="ReferencingObjectsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpOctalEscape" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceAssignmentWithOperatorAssignment" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreLazyOperators" value="true" />
<option name="ignoreObscureOperators" value="true" />
</inspection_tool>
<inspection_tool class="ReplaceAssignmentWithOperatorAssignmentJS" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceCollectionCountWithSize" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceGuardClauseWithFunctionCall" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceStringFormatWithLiteral" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceSubstringWithDropLast" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceSubstringWithIndexingOperation" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceSubstringWithSubstringAfter" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceSubstringWithSubstringBefore" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ReplaceSubstringWithTake" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="ResultOfObjectAllocationIgnored" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ResultSetIndexZero" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReturnOfInnerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RsSimplifyBooleanExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RuntimeExec" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="RuntimeExecWithNonConstantString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SafeLock" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SecurityAdvisoriesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="optionConfiguration">
<list>
<option value="barryvdh/laravel-debugbar" />
<option value="behat/behat" />
<option value="brianium/paratest" />
<option value="codeception/codeception" />
<option value="codedungeon/phpunit-result-printer" />
<option value="composer/composer" />
<option value="doctrine/coding-standard" />
<option value="filp/whoops" />
<option value="friendsofphp/php-cs-fixer" />
<option value="humbug/humbug" />
<option value="infection/infection" />
<option value="jakub-onderka/php-parallel-lint" />
<option value="johnkary/phpunit-speedtrap" />
<option value="kalessil/production-dependencies-guard" />
<option value="mikey179/vfsStream" />
<option value="mockery/mockery" />
<option value="mybuilder/phpunit-accelerator" />
<option value="orchestra/testbench" />
<option value="pdepend/pdepend" />
<option value="phan/phan" />
<option value="phing/phing" />
<option value="phpcompatibility/php-compatibility" />
<option value="phpmd/phpmd" />
<option value="phpro/grumphp" />
<option value="phpspec/phpspec" />
<option value="phpspec/prophecy" />
<option value="phpstan/phpstan" />
<option value="phpunit/phpunit" />
<option value="povils/phpmnd" />
<option value="roave/security-advisories" />
<option value="satooshi/php-coveralls" />
<option value="sebastian/phpcpd" />
<option value="slevomat/coding-standard" />
<option value="spatie/phpunit-watcher" />
<option value="squizlabs/php_codesniffer" />
<option value="sstalle/php7cc" />
<option value="symfony/debug" />
<option value="symfony/maker-bundle" />
<option value="symfony/phpunit-bridge" />
<option value="symfony/var-dumper" />
<option value="vimeo/psalm" />
<option value="wimg/php-compatibility" />
<option value="wp-coding-standards/wpcs" />
<option value="yiisoft/yii2-coding-standards" />
<option value="yiisoft/yii2-debug" />
<option value="yiisoft/yii2-gii" />
<option value="zendframework/zend-coding-standard" />
<option value="zendframework/zend-debug" />
<option value="zendframework/zend-test" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SerialPersistentFieldsWithWrongSignature" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SerialVersionUIDNotStaticFinal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SerializableHasSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreAnonymousInnerClasses" value="false" />
<option name="superClassString" value="java.awt.Component" />
</inspection_tool>
<inspection_tool class="SerializableHasSerializationMethods" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreAnonymousInnerClasses" value="false" />
<option name="superClassString" value="java.awt.Component" />
</inspection_tool>
<inspection_tool class="SerializableInnerClassHasSerialVersionUIDField" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreAnonymousInnerClasses" value="false" />
<option name="superClassString" value="java.awt.Component" />
</inspection_tool>
<inspection_tool class="SerializableInnerClassWithNonSerializableOuterClass" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreAnonymousInnerClasses" value="false" />
<option name="superClassString" value="java.awt.Component" />
</inspection_tool>
<inspection_tool class="SerializableStoresNonSerializable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SerializableWithUnconstructableAncestor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SetReplaceableByEnumSet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SeveralTargetsMessage" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SharedThreadLocalRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ShortEchoTagCanBeUsedInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="SignalWithoutCorrespondingAwait" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SimpleDateFormatWithoutLocale" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SimplifiableAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SimplifiableIfStatement" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="SingleStatementInBlock" enabled="false" level="INFORMATION" enabled_by_default="false" />
<inspection_tool class="SizeReplaceableByIsEmpty" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SleepWhileHoldingLock" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SocketResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="insideTryAllowed" value="false" />
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="SqlGotoInspection" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SqlRedundantOrderingDirectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlWithoutWhereInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myDontWarnForLimit" value="false" />
</inspection_tool>
<inspection_tool class="StrTrUsageAsStrReplaceInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="StringBufferToStringInConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringConcatenationInFormatCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringConcatenationInMessageFormatCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringConcatenationMissingWhitespace" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="StringEqualsEmptyString" enabled="true" level="WARNING" enabled_by_default="true">
<option name="SUPPRESS_FOR_VALUES_WHICH_COULD_BE_NULL" value="true" />
</inspection_tool>
<inspection_tool class="StringReplaceableByStringBuffer" enabled="true" level="WARNING" enabled_by_default="true">
<option name="onlyWarnOnLoop" value="true" />
</inspection_tool>
<inspection_tool class="StringToUpperWithoutLocale" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SubStrUsedAsStrPosInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SubtractionInCompareTo" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SuspiciousArrayCast" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SuspiciousIndentAfterControlStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SuspiciousLiteralUnderscore" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SwitchStatementWithConfusingDeclaration" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizationOnStaticField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizeOnLock" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizeOnThis" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SynchronizedMethod" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_includeNativeMethods" value="true" />
<option name="ignoreSynchronizedSuperMethods" value="true" />
</inspection_tool>
<inspection_tool class="SynchronizedOnLiteralObject" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SystemGC" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SystemGetenv" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SystemProperties" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SystemRunFinalizersOnExit" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SystemSetSecurityManager" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TestCaseInProductCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TestCaseWithConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TestCaseWithNoTestMethods" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSupers" value="true" />
</inspection_tool>
<inspection_tool class="TestMethodInProductCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TestMethodWithoutAssertion" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TextLabelInSwitchStatementJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ThreadLocalNotStaticFinal" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ThreadPriority" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ThreadStartInConstruction" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ThreadStopSuspendResume" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ThreadYield" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ThrowCaughtLocally" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreRethrownExceptions" value="true" />
</inspection_tool>
<inspection_tool class="ThrowRawExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="TimeToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TooBroadScope" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="m_allowConstructorAsInitializer" value="false" />
<option name="m_onlyLookAtBlocks" value="true" />
</inspection_tool>
<inspection_tool class="TransientFieldInNonSerializableClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TransientFieldNotInitialized" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TrivialStringConcatenation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnNecessaryDoubleQuotesInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="UnaryPlus" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnclearBinaryExpression" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="UnconditionalWait" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnconstrainedVariableType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UndeclaredTests" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnknownInspectionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="UnnecessarilyQualifiedStaticUsage" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreStaticFieldAccesses" value="false" />
<option name="m_ignoreStaticMethodCalls" value="false" />
<option name="m_ignoreStaticAccessFromStaticContext" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessarilyQualifiedStaticallyImportedElement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryBoxing" enabled="true" level="WARNING" enabled_by_default="true">
<option name="onlyReportSuperfluouslyBoxed" value="true" />
</inspection_tool>
<inspection_tool class="UnnecessaryConstantArrayCreationExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryConstructor" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreAnnotations" value="true" />
</inspection_tool>
<inspection_tool class="UnnecessaryLocalVariable" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreImmediatelyReturnedVariables" value="true" />
<option name="m_ignoreAnnotatedVariables" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryLocalVariableJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreImmediatelyReturnedVariables" value="true" />
<option name="m_ignoreAnnotatedVariables" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryParentheses" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="ignoreClarifyingParentheses" value="true" />
<option name="ignoreParenthesesOnConditionals" value="false" />
<option name="ignoreParenthesesOnLambdaParameter" value="false" />
</inspection_tool>
<inspection_tool class="UnnecessaryToStringCall" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryUnaryMinus" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnnecessaryUnboxing" enabled="true" level="WARNING" enabled_by_default="true">
<option name="onlyReportSuperfluouslyUnboxed" value="true" />
</inspection_tool>
<inspection_tool class="UnsetConstructsCanBeMergedInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="UnterminatedStatementJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreSemicolonAtEndOfBlock" value="false" />
</inspection_tool>
<inspection_tool class="UnusedCatchParameterJS" enabled="true" level="WARNING" enabled_by_default="true">
<option name="m_ignoreCatchBlocksWithComments" value="true" />
</inspection_tool>
<inspection_tool class="UnusedProperty" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="UseOfAWTPeerClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfJDBCDriverClass" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfObsoleteAssert" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfObsoleteDateTimeApi" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfProcessBuilder" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfPropertiesAsHashtable" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UseOfSunClasses" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UsingInclusionReturnValueInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="UtilityClassWithPublicConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UtilityClassWithoutPrivateConstructor" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignorableAnnotations">
<value />
</option>
<option name="ignoreClassesWithOnlyMain" value="true" />
</inspection_tool>
<inspection_tool class="VariableNotUsedInsideIf" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="VariableUseSideOnly" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="VoidExpressionJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="VolatileArrayField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WaitCalledOnCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WaitNotInLoop" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WaitNotifyNotInSynchronizedContext" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WaitOrAwaitWithoutTimeout" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WaitWhileHoldingTwoLocks" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WaitWithoutCorrespondingNotify" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="WhileLoopSpinsOnField" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoreNonEmtpyLoops" value="false" />
</inspection_tool>
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Project" />
<version value="1.0" />
</settings>
</component>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{@types/jquery}" />
</component>
</project>

View File

@@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="TweetDuck" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/bin/x86/Debug/TweetDuck.exe" />
<option name="PROGRAM_PARAMETERS" value="-datafolder TweetDuckDebug -nogdpr" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/bin/x86/Debug" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/TweetDuck.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="Console" />
<option name="PROJECT_TFM" value=".NETFramework,Version=v4.7.2" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

11
.idea/.idea.TweetDuck/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list />
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,28 @@
using System;
using System.Diagnostics;
using System.IO;
using TweetLib.Core.Application;
namespace TweetDuck.Application {
class SystemHandler : IAppSystemHandler {
void IAppSystemHandler.OpenAssociatedProgram(string path) {
try {
using (Process.Start(new ProcessStartInfo {
FileName = path,
ErrorDialog = true
})) {}
} catch (Exception e) {
Program.Reporter.HandleException("Error Opening Program", "Could not open the associated program for " + path, true, e);
}
}
void IAppSystemHandler.OpenFileExplorer(string path) {
if (File.Exists(path)) {
using (Process.Start("explorer.exe", "/select,\"" + path.Replace('/', '\\') + "\"")) {}
}
else if (Directory.Exists(path)) {
using (Process.Start("explorer.exe", '"' + path.Replace('/', '\\') + '"')) {}
}
}
}
}

View File

@@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.IO;
using CefSharp;
using TweetDuck.Utils;
using TweetLib.Core.Browser;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Adapters {
sealed class CefScriptExecutor : IScriptExecutor {
private readonly IWebBrowser browser;
public CefScriptExecutor(IWebBrowser browser) {
this.browser = browser;
}
public void RunFunction(string name, params object[] args) {
browser.ExecuteJsAsync(name, args);
}
public void RunScript(string identifier, string script) {
using IFrame frame = browser.GetMainFrame();
RunScript(frame, script, identifier);
}
public void RunBootstrap(string moduleNamespace) {
using IFrame frame = browser.GetMainFrame();
RunBootstrap(frame, moduleNamespace);
}
// Helpers
public static void RunScript(IFrame frame, string script, string identifier) {
if (script != null) {
frame.ExecuteJavaScriptAsync(script, identifier, 1);
}
}
public static void RunBootstrap(IFrame frame, string moduleNamespace) {
string script = GetBootstrapScript(moduleNamespace, includeStylesheets: true);
if (script != null) {
RunScript(frame, script, "bootstrap");
}
}
public static string GetBootstrapScript(string moduleNamespace, bool includeStylesheets) {
string script = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "bootstrap.js"));
if (script == null) {
return null;
}
string path = Path.Combine(Program.ResourcesPath, moduleNamespace);
var files = new DirectoryInfo(path).GetFiles();
var moduleNames = new List<string>();
var stylesheetNames = new List<string>();
foreach (var file in files) {
var ext = Path.GetExtension(file.Name);
var targetList = ext switch {
".js" => moduleNames,
".css" => includeStylesheets ? stylesheetNames : null,
_ => null
};
targetList?.Add(Path.GetFileNameWithoutExtension(file.Name));
}
script = script.Replace("{{namespace}}", moduleNamespace);
script = script.Replace("{{modules}}", string.Join("|", moduleNames));
script = script.Replace("{{stylesheets}}", string.Join("|", stylesheetNames));
return script;
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Text;
using TweetDuck.Configuration;
using TweetLib.Core;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Bridge {
static class PropertyBridge {
public enum Environment {
Browser,
Notification
}
public static string GenerateScript(Environment environment) {
static string Bool(bool value) => value ? "true;" : "false;";
static string Str(string value) => $"\"{value}\";";
UserConfig config = Program.Config.User;
StringBuilder build = new StringBuilder(384).Append("(function(x){");
build.Append("x.expandLinksOnHover=").Append(Bool(config.ExpandLinksOnHover));
if (environment == Environment.Browser) {
build.Append("x.focusDmInput=").Append(Bool(config.FocusDmInput));
build.Append("x.openSearchInFirstColumn=").Append(Bool(config.OpenSearchInFirstColumn));
build.Append("x.keepLikeFollowDialogsOpen=").Append(Bool(config.KeepLikeFollowDialogsOpen));
build.Append("x.muteNotifications=").Append(Bool(config.MuteNotifications));
build.Append("x.notificationMediaPreviews=").Append(Bool(config.NotificationMediaPreviews));
build.Append("x.translationTarget=").Append(Str(config.TranslationTarget));
build.Append("x.firstDayOfWeek=").Append(config.CalendarFirstDay == -1 ? LocaleUtils.GetJQueryDayOfWeek(Lib.Culture.DateTimeFormat.FirstDayOfWeek) : config.CalendarFirstDay);
}
if (environment == Environment.Notification) {
build.Append("x.skipOnLinkClick=").Append(Bool(config.NotificationSkipOnLinkClick));
}
return build.Append("})(window.$TDX=window.$TDX||{});if(window.TDGF_onPropertiesUpdated)window.TDGF_onPropertiesUpdated()").ToString();
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Handling;
using TweetDuck.Browser.Notification;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetDuck.Management;
using TweetDuck.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Bridge {
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class TweetDeckBridge {
public static void ResetStaticProperties() {
FormNotificationBase.FontSize = null;
FormNotificationBase.HeadLayout = null;
}
private readonly FormBrowser form;
private readonly FormNotificationMain notification;
private TweetDeckBridge(FormBrowser form, FormNotificationMain notification) {
this.form = form;
this.notification = notification;
}
// Browser only
public sealed class Browser : TweetDeckBridge {
public Browser(FormBrowser form, FormNotificationMain notification) : base(form, notification) {}
public void OnModulesLoaded(string moduleNamespace) {
form.InvokeAsyncSafe(() => form.OnModulesLoaded(moduleNamespace));
}
public void OpenContextMenu() {
form.InvokeAsyncSafe(form.OpenContextMenu);
}
public void OpenProfileImport() {
form.InvokeAsyncSafe(form.OpenProfileImport);
}
public void OnIntroductionClosed(bool showGuide) {
form.InvokeAsyncSafe(() => form.OnIntroductionClosed(showGuide));
}
public void LoadNotificationLayout(string fontSize, string headLayout) {
form.InvokeAsyncSafe(() => {
FormNotificationBase.FontSize = fontSize;
FormNotificationBase.HeadLayout = headLayout;
});
}
public void SetRightClickedLink(string type, string url) {
ContextMenuBase.CurrentInfo.SetLink(type, url);
}
public void SetRightClickedChirp(string columnId, string chirpId, string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages) {
ContextMenuBase.CurrentInfo.SetChirp(columnId, chirpId, tweetUrl, quoteUrl, chirpAuthors, chirpImages);
}
public void DisplayTooltip(string text) {
form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
}
}
// Notification only
public sealed class Notification : TweetDeckBridge {
public Notification(FormBrowser form, FormNotificationMain notification) : base(form, notification) {}
public void DisplayTooltip(string text) {
notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
}
public void LoadNextNotification() {
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
}
public void ShowTweetDetail() {
notification.InvokeAsyncSafe(notification.ShowTweetDetail);
}
}
// Global
public void OnTweetPopup(string columnId, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl) {
notification.InvokeAsyncSafe(() => {
form.OnTweetNotification();
notification.ShowNotification(new DesktopNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
});
}
public void OnTweetSound() {
form.InvokeAsyncSafe(() => {
form.OnTweetNotification();
form.OnTweetSound();
});
}
public void ScreenshotTweet(string html, int width) {
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width));
}
public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
form.InvokeAsyncSafe(() => form.PlayVideo(videoUrl, tweetUrl, username, callShowOverlay));
}
public void StopVideo() {
form.InvokeAsyncSafe(form.StopVideo);
}
public void FixClipboard() {
form.InvokeAsyncSafe(ClipboardManager.StripHtmlStyles);
}
public void OpenBrowser(string url) {
form.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
}
public void MakeGetRequest(string url, IJavascriptCallback onSuccess, IJavascriptCallback onError) {
Task.Run(async () => {
var client = WebUtils.NewClient(BrowserUtils.UserAgentVanilla);
try {
var result = await client.DownloadStringTaskAsync(url);
await onSuccess.ExecuteAsync(result);
} catch (Exception e) {
await onError.ExecuteAsync(e.Message);
} finally {
onSuccess.Dispose();
onError.Dispose();
client.Dispose();
}
});
}
public int GetIdleSeconds() {
return NativeMethods.GetIdleSeconds();
}
public void Alert(string type, string contents) {
MessageBoxIcon icon = type switch {
"error" => MessageBoxIcon.Error,
"warning" => MessageBoxIcon.Warning,
"info" => MessageBoxIcon.Information,
_ => MessageBoxIcon.None
};
FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
}
public void CrashDebug(string message) {
#if DEBUG
System.Diagnostics.Debug.WriteLine(message);
System.Diagnostics.Debugger.Break();
#endif
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using TweetDuck.Controls;
using TweetLib.Core.Systems.Updates;
namespace TweetDuck.Browser.Bridge {
[SuppressMessage("ReSharper", "UnusedMember.Global")]
class UpdateBridge {
private readonly UpdateHandler updates;
private readonly Control sync;
private UpdateInfo nextUpdate = null;
public event EventHandler<UpdateInfo> UpdateAccepted;
public event EventHandler<UpdateInfo> UpdateDismissed;
public UpdateBridge(UpdateHandler updates, Control sync) {
this.sync = sync;
this.updates = updates;
this.updates.CheckFinished += updates_CheckFinished;
}
internal void Cleanup() {
updates.CheckFinished -= updates_CheckFinished;
nextUpdate?.DeleteInstaller();
}
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e) {
UpdateInfo foundUpdate = e.Result.HasValue ? e.Result.Value : null;
if (nextUpdate != null && !nextUpdate.Equals(foundUpdate)) {
nextUpdate.DeleteInstaller();
}
nextUpdate = foundUpdate;
}
private void HandleInteractionEvent(EventHandler<UpdateInfo> eventHandler) {
UpdateInfo tmpInfo = nextUpdate;
if (tmpInfo != null) {
sync.InvokeAsyncSafe(() => eventHandler?.Invoke(this, tmpInfo));
}
}
// Bridge methods
public void TriggerUpdateCheck() {
updates.Check(false);
}
public void OnUpdateAccepted() {
HandleInteractionEvent(UpdateAccepted);
}
public void OnUpdateDismissed() {
HandleInteractionEvent(UpdateDismissed);
nextUpdate?.DeleteInstaller();
nextUpdate = null;
}
}
}

166
Browser/Data/ContextInfo.cs Normal file
View File

@@ -0,0 +1,166 @@
using System;
using CefSharp;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Data {
sealed class ContextInfo {
private LinkInfo link;
private ChirpInfo? chirp;
public ContextInfo() {
Reset();
}
public void SetLink(string type, string url) {
link = string.IsNullOrEmpty(url) ? null : new LinkInfo(type, url);
}
public void SetChirp(string columnId, string chirpId, string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages) {
chirp = string.IsNullOrEmpty(tweetUrl) ? (ChirpInfo?) null : new ChirpInfo(columnId, chirpId, tweetUrl, quoteUrl, chirpAuthors, chirpImages);
}
public ContextData Reset() {
link = null;
chirp = null;
return ContextData.Empty;
}
public ContextData Create(IContextMenuParams parameters) {
ContextData.Builder builder = new ContextData.Builder();
builder.AddContext(parameters);
if (link != null) {
builder.AddOverride(link.Type, link.Url);
}
if (chirp.HasValue) {
builder.AddChirp(chirp.Value);
}
return builder.Build();
}
// Data structures
private sealed class LinkInfo {
public string Type { get; }
public string Url { get; }
public LinkInfo(string type, string url) {
this.Type = type;
this.Url = url;
}
}
public readonly struct ChirpInfo {
public string ColumnId { get; }
public string ChirpId { get; }
public string TweetUrl { get; }
public string QuoteUrl { get; }
public string[] Authors => chirpAuthors?.Split(';') ?? StringUtils.EmptyArray;
public string[] Images => chirpImages?.Split(';') ?? StringUtils.EmptyArray;
private readonly string chirpAuthors;
private readonly string chirpImages;
public ChirpInfo(string columnId, string chirpId, string tweetUrl, string quoteUrl, string chirpAuthors, string chirpImages) {
this.ColumnId = columnId;
this.ChirpId = chirpId;
this.TweetUrl = tweetUrl;
this.QuoteUrl = quoteUrl;
this.chirpAuthors = chirpAuthors;
this.chirpImages = chirpImages;
}
}
// Constructed context
[Flags]
public enum ContextType {
Unknown = 0,
Link = 0b0001,
Image = 0b0010,
Video = 0b0100,
Chirp = 0b1000
}
public sealed class ContextData {
public static readonly ContextData Empty = new Builder().Build();
public ContextType Types { get; }
public string LinkUrl { get; }
public string UnsafeLinkUrl { get; }
public string MediaUrl { get; }
public ChirpInfo Chirp { get; }
private ContextData(ContextType types, string linkUrl, string unsafeLinkUrl, string mediaUrl, ChirpInfo chirp) {
Types = types;
LinkUrl = linkUrl;
UnsafeLinkUrl = unsafeLinkUrl;
MediaUrl = mediaUrl;
Chirp = chirp;
}
public sealed class Builder {
private ContextType types = ContextType.Unknown;
private string linkUrl = string.Empty;
private string unsafeLinkUrl = string.Empty;
private string mediaUrl = string.Empty;
private ChirpInfo chirp = default;
public void AddContext(IContextMenuParams parameters) {
ContextMenuType flags = parameters.TypeFlags;
if (flags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) {
types |= ContextType.Image;
types &= ~ContextType.Video;
mediaUrl = parameters.SourceUrl;
}
if (flags.HasFlag(ContextMenuType.Link)) {
types |= ContextType.Link;
linkUrl = parameters.LinkUrl;
unsafeLinkUrl = parameters.UnfilteredLinkUrl;
}
}
public void AddOverride(string type, string url) {
switch (type) {
case "link":
types |= ContextType.Link;
linkUrl = url;
unsafeLinkUrl = url;
break;
case "image":
types |= ContextType.Image;
types &= ~(ContextType.Video | ContextType.Link);
mediaUrl = url;
break;
case "video":
types |= ContextType.Video;
types &= ~(ContextType.Image | ContextType.Link);
mediaUrl = url;
break;
}
}
public void AddChirp(ChirpInfo chirp) {
this.types |= ContextType.Chirp;
this.chirp = chirp;
}
public ContextData Build() {
return new ContextData(types, linkUrl, unsafeLinkUrl, mediaUrl, chirp);
}
}
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Concurrent;
using CefSharp;
namespace TweetDuck.Browser.Data {
sealed class ResourceHandlers {
private readonly ConcurrentDictionary<string, Func<IResourceHandler>> handlers = new ConcurrentDictionary<string, Func<IResourceHandler>>(StringComparer.OrdinalIgnoreCase);
public bool HasHandler(IRequest request) {
return handlers.ContainsKey(request.Url);
}
public IResourceHandler GetHandler(IRequest request) {
return handlers.TryGetValue(request.Url, out var factory) ? factory() : null;
}
public bool Register(string url, Func<IResourceHandler> factory) {
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
handlers.AddOrUpdate(uri.AbsoluteUri, factory, (key, prev) => factory);
return true;
}
return false;
}
public bool Register(ResourceLink link) {
return Register(link.Url, link.Factory);
}
public bool Unregister(string url) {
return handlers.TryRemove(url, out _);
}
public bool Unregister(ResourceLink link) {
return Unregister(link.Url);
}
public static Func<IResourceHandler> ForString(string str) {
return () => ResourceHandler.FromString(str);
}
public static Func<IResourceHandler> ForString(string str, string mimeType) {
return () => ResourceHandler.FromString(str, mimeType: mimeType);
}
public static Func<IResourceHandler> ForBytes(byte[] bytes, string mimeType) {
return () => ResourceHandler.FromByteArray(bytes, mimeType);
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using CefSharp;
namespace TweetDuck.Browser.Data {
sealed class ResourceLink {
public string Url { get; }
public Func<IResourceHandler> Factory { get; }
public ResourceLink(string url, Func<IResourceHandler> factory) {
this.Url = url;
this.Factory = factory;
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Controls;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Data {
sealed class WindowState {
private Rectangle rect;
private bool isMaximized;
public void Save(Form form) {
rect = form.WindowState == FormWindowState.Normal ? form.DesktopBounds : form.RestoreBounds;
isMaximized = form.WindowState == FormWindowState.Maximized;
}
public void Restore(Form form, bool firstTimeFullscreen) {
if (rect != Rectangle.Empty) {
form.DesktopBounds = rect;
form.WindowState = isMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
}
if ((rect == Rectangle.Empty && firstTimeFullscreen) || form.IsFullyOutsideView()) {
form.DesktopBounds = Screen.PrimaryScreen.WorkingArea;
form.WindowState = FormWindowState.Maximized;
Save(form);
}
}
public static readonly SingleTypeConverter<WindowState> Converter = new SingleTypeConverter<WindowState> {
ConvertToString = value => $"{(value.isMaximized ? 'M' : '_')}{value.rect.X} {value.rect.Y} {value.rect.Width} {value.rect.Height}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value.Substring(1), ' ');
return new WindowState {
rect = new Rectangle(elements[0], elements[1], elements[2], elements[3]),
isMaximized = value[0] == 'M'
};
}
};
}
}

View File

@@ -1,21 +1,10 @@
namespace TweetDuck.Core {
namespace TweetDuck.Browser {
sealed partial class FormBrowser {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
@@ -24,7 +13,7 @@
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
this.trayIcon = new TweetDuck.Core.Other.TrayIcon(this.components);
this.trayIcon = new TrayIcon(this.components);
this.toolTip = new System.Windows.Forms.ToolTip(this.components);
this.timerResize = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout();
@@ -38,18 +27,18 @@
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = TweetDuck.Core.Utils.TwitterUtils.BackgroundColor;
this.ClientSize = new System.Drawing.Size(400, 386);
this.BackColor = TweetDuck.Utils.TwitterUtils.BackgroundColor;
this.ClientSize = new System.Drawing.Size(1008, 730);
this.Icon = Properties.Resources.icon;
this.Location = TweetDuck.Core.Controls.ControlExtensions.InvisibleLocation;
this.MinimumSize = new System.Drawing.Size(416, 424);
this.Location = TweetDuck.Controls.ControlExtensions.InvisibleLocation;
this.MinimumSize = new System.Drawing.Size(348, 424);
this.Name = "FormBrowser";
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.Activated += new System.EventHandler(this.FormBrowser_Activated);
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FormBrowser_FormClosing);
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.FormBrowser_FormClosed);
this.LocationChanged += new System.EventHandler(this.FormBrowser_LocationChanged);
this.ResizeEnd += new System.EventHandler(this.FormBrowser_ResizeEnd);
this.LocationChanged += new System.EventHandler(this.FormBrowser_LocationChanged);
this.Resize += new System.EventHandler(this.FormBrowser_Resize);
this.ResumeLayout(false);
@@ -57,7 +46,7 @@
#endregion
private TweetDuck.Core.Other.TrayIcon trayIcon;
private TrayIcon trayIcon;
private System.Windows.Forms.ToolTip toolTip;
private System.Windows.Forms.Timer timerResize;
}

589
Browser/FormBrowser.cs Normal file
View File

@@ -0,0 +1,589 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Bridge;
using TweetDuck.Browser.Handling;
using TweetDuck.Browser.Handling.General;
using TweetDuck.Browser.Notification;
using TweetDuck.Browser.Notification.Screenshot;
using TweetDuck.Configuration;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetDuck.Dialogs.Settings;
using TweetDuck.Management;
using TweetDuck.Plugins;
using TweetDuck.Updates;
using TweetDuck.Utils;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Systems.Updates;
#if DEBUG
using TweetDuck.Resources;
#endif
namespace TweetDuck.Browser {
sealed partial class FormBrowser : Form {
private static UserConfig Config => Program.Config.User;
public bool IsWaiting {
set {
if (value) {
browser.Enabled = false;
Cursor = Cursors.WaitCursor;
}
else {
browser.Enabled = true;
Cursor = Cursors.Default;
if (Focused) { // re-focus browser only if the window or a child is activated
browser.Focus();
}
}
}
}
public UpdateInstaller UpdateInstaller { get; private set; }
private bool ignoreUpdateCheckError;
#pragma warning disable IDE0069 // Disposable fields should be disposed
private readonly TweetDeckBrowser browser;
private readonly FormNotificationTweet notification;
#pragma warning restore IDE0069 // Disposable fields should be disposed
private readonly ResourceProvider resourceProvider;
private readonly PluginManager plugins;
private readonly UpdateHandler updates;
private readonly ContextMenu contextMenu;
private readonly UpdateBridge updateBridge;
private bool isLoaded;
private FormWindowState prevState;
private TweetScreenshotManager notificationScreenshotManager;
private VideoPlayer videoPlayer;
public FormBrowser(ResourceProvider resourceProvider, PluginSchemeFactory pluginScheme) {
InitializeComponent();
Text = Program.BrandName;
this.resourceProvider = resourceProvider;
this.plugins = new PluginManager(Program.Config.Plugins, Program.PluginPath, Program.PluginDataPath);
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
this.plugins.Reload();
pluginScheme.Setup(plugins);
this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show();
this.updates = new UpdateHandler(new UpdateCheckClient(Program.InstallerPath), TaskScheduler.FromCurrentSynchronizationContext());
this.updates.CheckFinished += updates_CheckFinished;
this.updateBridge = new UpdateBridge(updates, this);
this.updateBridge.UpdateAccepted += updateBridge_UpdateAccepted;
this.updateBridge.UpdateDismissed += updateBridge_UpdateDismissed;
this.browser = new TweetDeckBrowser(this, plugins, new TweetDeckBridge.Browser(this, notification), updateBridge);
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
Controls.Add(new MenuStrip { Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
Disposed += (sender, args) => {
Config.MuteToggled -= Config_MuteToggled;
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
browser.Dispose();
};
Config.MuteToggled += Config_MuteToggled;
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
this.trayIcon.ClickClose += trayIcon_ClickClose;
Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
UpdateTray();
if (Config.MuteNotifications) {
UpdateFormIcon();
}
RestoreWindow();
}
protected override void Dispose(bool disposing) {
if (disposing) {
components?.Dispose();
updates.Dispose();
contextMenu.Dispose();
notificationScreenshotManager?.Dispose();
videoPlayer?.Dispose();
}
base.Dispose(disposing);
}
private void ShowChildForm(Form form) {
form.VisibleChanged += (sender, args) => form.MoveToCenter(this);
form.Show(this);
}
public void ForceClose() {
trayIcon.Visible = false; // checked in FormClosing event
Close();
}
// window setup
private void RestoreWindow() {
Config.BrowserWindow.Restore(this, true);
browser.PrepareSize(ClientSize);
prevState = WindowState;
isLoaded = true;
}
private void UpdateFormIcon() { // TODO fix to show icon in taskbar too
Icon = Config.MuteNotifications ? Properties.Resources.icon_muted : Properties.Resources.icon;
}
private void UpdateTray() {
trayIcon.Visible = Config.TrayBehavior.ShouldDisplayIcon();
}
// event handlers
private void timerResize_Tick(object sender, EventArgs e) {
FormBrowser_ResizeEnd(this, e); // also stops timer
}
private void FormBrowser_Activated(object sender, EventArgs e) {
if (!isLoaded) {
return;
}
trayIcon.HasNotifications = false;
if (!browser.Enabled) { // when taking a screenshot, the window is unfocused and
browser.Enabled = true; // the browser is disabled; if the user clicks back into
} // the window, enable the browser again
}
private void FormBrowser_LocationChanged(object sender, EventArgs e) {
if (!isLoaded) {
return;
}
timerResize.Stop();
timerResize.Start();
}
private void FormBrowser_Resize(object sender, EventArgs e) {
if (!isLoaded) {
return;
}
if (WindowState != prevState) {
prevState = WindowState;
if (WindowState == FormWindowState.Minimized) {
if (Config.TrayBehavior.ShouldHideOnMinimize()) {
Hide(); // hides taskbar too?! welp that works I guess
}
}
else {
FormBrowser_ResizeEnd(sender, e);
}
}
else {
timerResize.Stop();
timerResize.Start();
}
}
private void FormBrowser_ResizeEnd(object sender, EventArgs e) { // also triggers when the window moves
if (!isLoaded) {
return;
}
timerResize.Stop();
browser.PrepareSize(ClientSize); // needed to pre-size browser control when launched in maximized state
if (Location != ControlExtensions.InvisibleLocation) {
Config.BrowserWindow.Save(this);
Config.Save();
}
}
private void FormBrowser_FormClosing(object sender, FormClosingEventArgs e) {
if (!isLoaded) {
return;
}
if (Config.TrayBehavior.ShouldHideOnClose() && trayIcon.Visible && e.CloseReason == CloseReason.UserClosing) {
Hide(); // hides taskbar too?! welp that works I guess
e.Cancel = true;
}
}
private void FormBrowser_FormClosed(object sender, FormClosedEventArgs e) {
if (isLoaded && UpdateInstaller == null) {
updateBridge.Cleanup();
}
}
private void Config_MuteToggled(object sender, EventArgs e) {
UpdateFormIcon();
}
private void Config_TrayBehaviorChanged(object sender, EventArgs e) {
UpdateTray();
}
private void trayIcon_ClickRestore(object sender, EventArgs e) {
Show();
RestoreWindow();
Activate();
UpdateTray();
}
private void trayIcon_ClickClose(object sender, EventArgs e) {
ForceClose();
}
private void plugins_Reloaded(object sender, PluginErrorEventArgs e) {
if (e.HasErrors) {
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK);
}
if (isLoaded) {
browser.ReloadToTweetDeck();
}
}
private void plugins_Executed(object sender, PluginErrorEventArgs e) {
if (e.HasErrors) {
this.InvokeAsyncSafe(() => { FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n" + string.Join("\n\n", e.Errors), FormMessage.OK); });
}
}
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e) {
e.Result.Handle(update => {
string tag = update.VersionTag;
if (tag != Program.VersionTag && tag != Config.DismissedUpdate) {
update.BeginSilentDownload();
browser.ShowUpdateNotification(tag, update.ReleaseNotes);
}
else {
updates.StartTimer();
}
}, ex => {
if (!ignoreUpdateCheckError) {
Program.Reporter.HandleException("Update Check Error", "An error occurred while checking for updates.", true, ex);
updates.StartTimer();
}
});
ignoreUpdateCheckError = true;
}
private void updateBridge_UpdateAccepted(object sender, UpdateInfo update) {
FormManager.CloseAllDialogs();
if (!string.IsNullOrEmpty(Config.DismissedUpdate)) {
Config.DismissedUpdate = null;
Config.Save();
}
void OnFinished() {
UpdateDownloadStatus status = update.DownloadStatus;
if (status == UpdateDownloadStatus.Done) {
UpdateInstaller = new UpdateInstaller(update.InstallerPath);
ForceClose();
}
else if (status != UpdateDownloadStatus.Canceled && FormMessage.Error("Update Has Failed", "Could not automatically download the update: " + (update.DownloadError?.Message ?? "unknown error") + "\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)) {
BrowserUtils.OpenExternalBrowser(Program.Website);
ForceClose();
}
else {
Show();
}
}
if (update.DownloadStatus.IsFinished(true)) {
OnFinished();
}
else {
FormUpdateDownload downloadForm = new FormUpdateDownload(update);
downloadForm.VisibleChanged += (sender2, args2) => {
downloadForm.MoveToCenter(this);
Hide();
};
downloadForm.FormClosed += (sender2, args2) => {
if (downloadForm.DialogResult != DialogResult.OK) {
update.CancelDownload();
}
downloadForm.Dispose();
OnFinished();
};
downloadForm.Show();
}
}
private void updateBridge_UpdateDismissed(object sender, UpdateInfo update) {
Config.DismissedUpdate = update.VersionTag;
Config.Save();
}
protected override void WndProc(ref Message m) {
if (isLoaded && m.Msg == Program.WindowRestoreMessage) {
using Process me = Process.GetCurrentProcess();
if (me.Id == m.WParam.ToInt32()) {
trayIcon_ClickRestore(trayIcon, EventArgs.Empty);
}
return;
}
if (browser.Ready && m.Msg == NativeMethods.WM_PARENTNOTIFY && (m.WParam.ToInt32() & 0xFFFF) == NativeMethods.WM_XBUTTONDOWN) {
if (videoPlayer != null && videoPlayer.Running) {
videoPlayer.Close();
}
else {
browser.OnMouseClickExtra(m.WParam);
}
return;
}
base.WndProc(ref m);
}
// bridge methods
public void OnModulesLoaded(string moduleNamespace) {
browser.OnModulesLoaded(moduleNamespace);
}
public void PauseNotification() {
notification.PauseNotification();
}
public void ResumeNotification() {
notification.ResumeNotification();
}
public void ReinjectCustomCSS(string css) {
browser.ReinjectCustomCSS(css);
}
public void ReloadToTweetDeck() {
#if DEBUG
ResourceHotSwap.Run();
resourceProvider.ClearCache();
#else
if (ModifierKeys.HasFlag(Keys.Shift)) {
resourceProvider.ClearCache();
}
#endif
ignoreUpdateCheckError = false;
browser.ReloadToTweetDeck();
}
public void AddSearchColumn(string query) {
browser.AddSearchColumn(query);
}
public void TriggerTweetScreenshot(string columnId, string chirpId) {
browser.TriggerTweetScreenshot(columnId, chirpId);
}
public void ReloadColumns() {
browser.ReloadColumns();
}
public void PlaySoundNotification() {
browser.PlaySoundNotification();
}
public void ApplyROT13() {
browser.ApplyROT13();
}
public void OpenDevTools() {
browser.OpenDevTools();
}
// callback handlers
public void OnIntroductionClosed(bool showGuide) {
if (Config.FirstRun) {
Config.FirstRun = false;
Config.Save();
}
if (showGuide) {
FormGuide.Show();
}
}
public void OpenContextMenu() {
contextMenu.Show(this, PointToClient(Cursor.Position));
}
public void OpenSettings() {
OpenSettings(null);
}
public void OpenSettings(Type startTab) {
if (!FormManager.TryBringToFront<FormSettings>()) {
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
FormSettings form = new FormSettings(this, plugins, updates, startTab);
form.FormClosed += (sender, args) => {
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck) {
Config.DismissedUpdate = null;
Config.Save();
updates.Check(true);
}
if (!Config.EnableTrayHighlight) {
trayIcon.HasNotifications = false;
}
BrowserCache.RefreshTimer();
if (form.ShouldReloadBrowser) {
FormManager.TryFind<FormPlugins>()?.Close();
plugins.Reload(); // also reloads the browser
}
else {
browser.UpdateProperties();
}
notification.RequiresResize = true;
form.Dispose();
};
ShowChildForm(form);
}
}
public void OpenAbout() {
if (!FormManager.TryBringToFront<FormAbout>()) {
ShowChildForm(new FormAbout());
}
}
public void OpenPlugins() {
if (!FormManager.TryBringToFront<FormPlugins>()) {
ShowChildForm(new FormPlugins(plugins));
}
}
public void OpenProfileImport() {
FormManager.TryFind<FormSettings>()?.Close();
using DialogSettingsManage dialog = new DialogSettingsManage(plugins, true);
if (!dialog.IsDisposed && dialog.ShowDialog() == DialogResult.OK && !dialog.IsRestarting) { // needs disposal check because the dialog may be closed in constructor
BrowserProcessHandler.UpdatePrefs();
FormManager.TryFind<FormPlugins>()?.Close();
plugins.Reload(); // also reloads the browser
}
}
public void OnTweetNotification() { // may be called multiple times, once for each type of notification
if (Config.EnableTrayHighlight && !ContainsFocus) {
trayIcon.HasNotifications = true;
}
}
public void OnTweetSound() {}
public void PlayVideo(string videoUrl, string tweetUrl, string username, IJavascriptCallback callShowOverlay) {
string playerPath = Config.VideoPlayerPath;
if (playerPath == null || !File.Exists(playerPath)) {
if (videoPlayer == null) {
videoPlayer = new VideoPlayer(this);
videoPlayer.ProcessExited += (sender, args) => browser.HideVideoOverlay(true);
}
callShowOverlay.ExecuteAsync();
callShowOverlay.Dispose();
videoPlayer.Launch(videoUrl, tweetUrl, username);
}
else {
callShowOverlay.Dispose();
string quotedUrl = '"' + videoUrl + '"';
string playerArgs = Config.VideoPlayerPathArgs == null ? quotedUrl : Config.VideoPlayerPathArgs + ' ' + quotedUrl;
try {
using (Process.Start(playerPath, playerArgs)) {}
} catch (Exception e) {
Program.Reporter.HandleException("Error Opening Video Player", "Could not open the video player.", true, e);
}
}
}
public void StopVideo() {
videoPlayer?.Close();
}
public bool ProcessBrowserKey(Keys key) {
if (videoPlayer != null && videoPlayer.Running) {
videoPlayer.SendKeyEvent(key);
return true;
}
return false;
}
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl) {
Activate();
if (!browser.IsTweetDeckWebsite) {
FormMessage.Error("View Tweet Detail", "TweetDeck is not currently loaded.", FormMessage.OK);
return;
}
notification.FinishCurrentNotification();
browser.ShowTweetDetail(columnId, chirpId, fallbackUrl);
}
public void OnTweetScreenshotReady(string html, int width) {
notificationScreenshotManager ??= new TweetScreenshotManager(this, plugins);
notificationScreenshotManager.Trigger(html, width);
}
public void DisplayTooltip(string text) {
if (string.IsNullOrEmpty(text)) {
toolTip.Hide(this);
}
else {
Point position = PointToClient(Cursor.Position);
position.Offset(20, 10);
toolTip.Show(text, this, position);
}
}
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Adapters;
using TweetDuck.Browser.Data;
using TweetDuck.Browser.Notification;
using TweetDuck.Configuration;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetDuck.Management;
using TweetDuck.Utils;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Handling {
abstract class ContextMenuBase : IContextMenuHandler {
public static ContextInfo CurrentInfo { get; } = new ContextInfo();
protected static UserConfig Config => Program.Config.User;
private static ImageQuality ImageQuality => Config.TwitterImageQuality;
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand) 26500;
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand) 26501;
private const CefMenuCommand MenuCopyUsername = (CefMenuCommand) 26502;
private const CefMenuCommand MenuViewImage = (CefMenuCommand) 26503;
private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand) 26504;
private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand) 26505;
private const CefMenuCommand MenuCopyImage = (CefMenuCommand) 26506;
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand) 26507;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand) 26508;
private const CefMenuCommand MenuSearchInBrowser = (CefMenuCommand) 26509;
private const CefMenuCommand MenuReadApplyROT13 = (CefMenuCommand) 26510;
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26599;
protected ContextInfo.ContextData Context { get; private set; }
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
if (!TwitterUrls.IsTweetDeck(frame.Url) || browser.IsLoading) {
Context = CurrentInfo.Reset();
}
else {
Context = CurrentInfo.Create(parameters);
}
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection) && !parameters.TypeFlags.HasFlag(ContextMenuType.Editable)) {
model.AddItem(MenuSearchInBrowser, "Search in browser");
model.AddSeparator();
model.AddItem(MenuReadApplyROT13, "Apply ROT13");
model.AddSeparator();
}
static string TextOpen(string name) => "Open " + name + " in browser";
static string TextCopy(string name) => "Copy " + name + " address";
static string TextSave(string name) => "Save " + name + " as...";
if (Context.Types.HasFlag(ContextInfo.ContextType.Link) && !Context.UnsafeLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal)) {
if (TwitterUrls.RegexAccount.IsMatch(Context.UnsafeLinkUrl)) {
model.AddItem(MenuOpenLinkUrl, TextOpen("account"));
model.AddItem(MenuCopyLinkUrl, TextCopy("account"));
model.AddItem(MenuCopyUsername, "Copy account username");
}
else {
model.AddItem(MenuOpenLinkUrl, TextOpen("link"));
model.AddItem(MenuCopyLinkUrl, TextCopy("link"));
}
model.AddSeparator();
}
if (Context.Types.HasFlag(ContextInfo.ContextType.Video)) {
model.AddItem(MenuOpenMediaUrl, TextOpen("video"));
model.AddItem(MenuCopyMediaUrl, TextCopy("video"));
model.AddItem(MenuSaveMedia, TextSave("video"));
model.AddSeparator();
}
else if (Context.Types.HasFlag(ContextInfo.ContextType.Image) && Context.MediaUrl != FormNotificationBase.AppLogo.Url) {
model.AddItem(MenuViewImage, "View image in photo viewer");
model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
model.AddItem(MenuCopyImage, "Copy image");
model.AddItem(MenuSaveMedia, TextSave("image"));
if (Context.Chirp.Images.Length > 1) {
model.AddItem(MenuSaveTweetImages, TextSave("all images"));
}
model.AddSeparator();
}
}
public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
Control control = browserControl.AsControl();
switch (commandId) {
case MenuOpenLinkUrl:
OpenBrowser(control, Context.LinkUrl);
break;
case MenuCopyLinkUrl:
SetClipboardText(control, Context.UnsafeLinkUrl);
break;
case MenuCopyUsername: {
string url = Context.UnsafeLinkUrl;
Match match = TwitterUrls.RegexAccount.Match(url);
SetClipboardText(control, match.Success ? match.Groups[1].Value : url);
break;
}
case MenuOpenMediaUrl:
OpenBrowser(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
break;
case MenuCopyMediaUrl:
SetClipboardText(control, TwitterUrls.GetMediaLink(Context.MediaUrl, ImageQuality));
break;
case MenuCopyImage: {
string url = Context.MediaUrl;
control.InvokeAsyncSafe(() => { TwitterUtils.CopyImage(url, ImageQuality); });
break;
}
case MenuViewImage: {
string url = Context.MediaUrl;
control.InvokeAsyncSafe(() => {
TwitterUtils.ViewImage(url, ImageQuality);
});
break;
}
case MenuSaveMedia: {
bool isVideo = Context.Types.HasFlag(ContextInfo.ContextType.Video);
string url = Context.MediaUrl;
string username = Context.Chirp.Authors.LastOrDefault();
control.InvokeAsyncSafe(() => {
if (isVideo) {
TwitterUtils.DownloadVideo(url, username);
}
else {
TwitterUtils.DownloadImage(url, username, ImageQuality);
}
});
break;
}
case MenuSaveTweetImages: {
string[] urls = Context.Chirp.Images;
string username = Context.Chirp.Authors.LastOrDefault();
control.InvokeAsyncSafe(() => {
TwitterUtils.DownloadImages(urls, username, ImageQuality);
});
break;
}
case MenuReadApplyROT13:
string selection = parameters.SelectionText;
control.InvokeAsyncSafe(() => FormMessage.Information("ROT13", StringUtils.ConvertRot13(selection), FormMessage.OK));
return true;
case MenuSearchInBrowser:
string query = parameters.SelectionText;
control.InvokeAsyncSafe(() => BrowserUtils.OpenExternalSearch(query));
DeselectAll(frame);
break;
case MenuOpenDevTools:
browserControl.OpenDevToolsCustom(new Point(parameters.XCoord, parameters.YCoord));
break;
}
return false;
}
public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
Context = CurrentInfo.Reset();
}
public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) {
return false;
}
protected static void DeselectAll(IFrame frame) {
CefScriptExecutor.RunScript(frame, "window.getSelection().removeAllRanges()", "gen:deselect");
}
protected static void OpenBrowser(Control control, string url) {
control.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
}
protected static void SetClipboardText(Control control, string text) {
control.InvokeAsyncSafe(() => ClipboardManager.SetText(text, TextDataFormat.UnicodeText));
}
protected static void InsertSelectionSearchItem(IMenuModel model, CefMenuCommand insertCommand, string insertLabel) {
model.InsertItemAt(model.GetIndexOf(MenuSearchInBrowser) + 1, insertCommand, insertLabel);
}
protected static void AddDebugMenuItems(IMenuModel model) {
if (Config.DevToolsInContextMenu) {
AddSeparator(model);
model.AddItem(MenuOpenDevTools, "Open dev tools");
}
}
protected static void RemoveSeparatorIfLast(IMenuModel model) {
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) {
model.RemoveAt(model.Count - 1);
}
}
protected static void AddSeparator(IMenuModel model) {
if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate
model.AddSeparator();
}
}
}
}

View File

@@ -0,0 +1,179 @@
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Data;
using TweetDuck.Controls;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Handling {
sealed class ContextMenuBrowser : ContextMenuBase {
private const CefMenuCommand MenuGlobal = (CefMenuCommand) 26600;
private const CefMenuCommand MenuMute = (CefMenuCommand) 26601;
private const CefMenuCommand MenuSettings = (CefMenuCommand) 26602;
private const CefMenuCommand MenuPlugins = (CefMenuCommand) 26003;
private const CefMenuCommand MenuAbout = (CefMenuCommand) 26604;
private const CefMenuCommand MenuOpenTweetUrl = (CefMenuCommand) 26610;
private const CefMenuCommand MenuCopyTweetUrl = (CefMenuCommand) 26611;
private const CefMenuCommand MenuOpenQuotedTweetUrl = (CefMenuCommand) 26612;
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand) 26613;
private const CefMenuCommand MenuScreenshotTweet = (CefMenuCommand) 26614;
private const CefMenuCommand MenuWriteApplyROT13 = (CefMenuCommand) 26615;
private const CefMenuCommand MenuSearchInColumn = (CefMenuCommand) 26616;
private const string TitleReloadBrowser = "Reload browser";
private const string TitleMuteNotifications = "Mute notifications";
private const string TitleSettings = "Options";
private const string TitlePlugins = "Plugins";
private const string TitleAboutProgram = "About " + Program.BrandName;
private readonly FormBrowser form;
public ContextMenuBrowser(FormBrowser form) {
this.form = form;
}
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
bool isSelecting = parameters.TypeFlags.HasFlag(ContextMenuType.Selection);
bool isEditing = parameters.TypeFlags.HasFlag(ContextMenuType.Editable);
model.Remove(CefMenuCommand.Back);
model.Remove(CefMenuCommand.Forward);
model.Remove(CefMenuCommand.Print);
model.Remove(CefMenuCommand.ViewSource);
RemoveSeparatorIfLast(model);
if (isSelecting) {
if (isEditing) {
model.AddSeparator();
model.AddItem(MenuWriteApplyROT13, "Apply ROT13");
}
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (isSelecting && !isEditing && TwitterUrls.IsTweetDeck(frame.Url)) {
InsertSelectionSearchItem(model, MenuSearchInColumn, "Search in a column");
}
if (Context.Types.HasFlag(ContextInfo.ContextType.Chirp) && !isSelecting && !isEditing) {
model.AddItem(MenuOpenTweetUrl, "Open tweet in browser");
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
model.AddItem(MenuScreenshotTweet, "Screenshot tweet to clipboard");
if (!string.IsNullOrEmpty(Context.Chirp.QuoteUrl)) {
model.AddSeparator();
model.AddItem(MenuOpenQuotedTweetUrl, "Open quoted tweet in browser");
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
}
model.AddSeparator();
}
if (!isSelecting && !isEditing) {
AddSeparator(model);
IMenuModel globalMenu = model.Count == 0 ? model : model.AddSubMenu(MenuGlobal, Program.BrandName);
globalMenu.AddItem(CefMenuCommand.Reload, TitleReloadBrowser);
globalMenu.AddCheckItem(MenuMute, TitleMuteNotifications);
globalMenu.SetChecked(MenuMute, Config.MuteNotifications);
globalMenu.AddSeparator();
globalMenu.AddItem(MenuSettings, TitleSettings);
globalMenu.AddItem(MenuPlugins, TitlePlugins);
globalMenu.AddItem(MenuAbout, TitleAboutProgram);
AddDebugMenuItems(globalMenu);
}
RemoveSeparatorIfLast(model);
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)) {
return true;
}
switch (commandId) {
case CefMenuCommand.Reload:
form.InvokeAsyncSafe(form.ReloadToTweetDeck);
return true;
case MenuSettings:
form.InvokeAsyncSafe(form.OpenSettings);
return true;
case MenuAbout:
form.InvokeAsyncSafe(form.OpenAbout);
return true;
case MenuPlugins:
form.InvokeAsyncSafe(form.OpenPlugins);
return true;
case MenuMute:
form.InvokeAsyncSafe(ToggleMuteNotifications);
return true;
case MenuOpenTweetUrl:
OpenBrowser(form, Context.Chirp.TweetUrl);
return true;
case MenuCopyTweetUrl:
SetClipboardText(form, Context.Chirp.TweetUrl);
return true;
case MenuScreenshotTweet:
var chirp = Context.Chirp;
form.InvokeAsyncSafe(() => form.TriggerTweetScreenshot(chirp.ColumnId, chirp.ChirpId));
return true;
case MenuOpenQuotedTweetUrl:
OpenBrowser(form, Context.Chirp.QuoteUrl);
return true;
case MenuCopyQuotedTweetUrl:
SetClipboardText(form, Context.Chirp.QuoteUrl);
return true;
case MenuWriteApplyROT13:
form.InvokeAsyncSafe(form.ApplyROT13);
return true;
case MenuSearchInColumn:
string query = parameters.SelectionText;
form.InvokeAsyncSafe(() => form.AddSearchColumn(query));
DeselectAll(frame);
break;
}
return false;
}
public static ContextMenu CreateMenu(FormBrowser form) {
ContextMenu menu = new ContextMenu();
menu.MenuItems.Add(TitleReloadBrowser, (sender, args) => form.ReloadToTweetDeck());
menu.MenuItems.Add(TitleMuteNotifications, (sender, args) => ToggleMuteNotifications());
menu.MenuItems.Add("-");
menu.MenuItems.Add(TitleSettings, (sender, args) => form.OpenSettings());
menu.MenuItems.Add(TitlePlugins, (sender, args) => form.OpenPlugins());
menu.MenuItems.Add(TitleAboutProgram, (sender, args) => form.OpenAbout());
menu.Popup += (sender, args) => {
menu.MenuItems[1].Checked = Config.MuteNotifications;
};
return menu;
}
private static void ToggleMuteNotifications() {
Config.MuteNotifications = !Config.MuteNotifications;
Config.Save();
}
}
}

View File

@@ -0,0 +1,11 @@
using CefSharp;
namespace TweetDuck.Browser.Handling {
sealed class ContextMenuGuide : ContextMenuBase {
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
model.Clear();
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
AddDebugMenuItems(model);
}
}
}

View File

@@ -0,0 +1,93 @@
using CefSharp;
using TweetDuck.Browser.Notification;
using TweetDuck.Controls;
namespace TweetDuck.Browser.Handling {
sealed class ContextMenuNotification : ContextMenuBase {
private const CefMenuCommand MenuViewDetail = (CefMenuCommand) 26600;
private const CefMenuCommand MenuSkipTweet = (CefMenuCommand) 26601;
private const CefMenuCommand MenuFreeze = (CefMenuCommand) 26602;
private const CefMenuCommand MenuCopyTweetUrl = (CefMenuCommand) 26603;
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand) 26604;
private readonly FormNotificationBase form;
private readonly bool enableCustomMenu;
public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu) {
this.form = form;
this.enableCustomMenu = enableCustomMenu;
}
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) {
model.Clear();
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)) {
model.AddItem(CefMenuCommand.Copy, "Copy");
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (enableCustomMenu) {
if (form.CanViewDetail) {
model.AddItem(MenuViewDetail, "View detail");
}
model.AddItem(MenuSkipTweet, "Skip tweet");
model.AddCheckItem(MenuFreeze, "Freeze");
model.SetChecked(MenuFreeze, form.FreezeTimer);
if (!string.IsNullOrEmpty(form.CurrentTweetUrl)) {
model.AddSeparator();
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
if (!string.IsNullOrEmpty(form.CurrentQuoteUrl)) {
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
}
}
}
AddDebugMenuItems(model);
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(() => {
form.ContextMenuOpen = true;
});
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) {
if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)) {
return true;
}
switch (commandId) {
case MenuSkipTweet:
form.InvokeAsyncSafe(form.FinishCurrentNotification);
return true;
case MenuFreeze:
form.InvokeAsyncSafe(() => form.FreezeTimer = !form.FreezeTimer);
return true;
case MenuViewDetail:
form.InvokeSafe(form.ShowTweetDetail);
return true;
case MenuCopyTweetUrl:
SetClipboardText(form, form.CurrentTweetUrl);
return true;
case MenuCopyQuotedTweetUrl:
SetClipboardText(form, form.CurrentQuoteUrl);
return true;
}
return false;
}
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) {
base.OnContextMenuDismissed(browserControl, browser, frame);
form.InvokeAsyncSafe(() => form.ContextMenuOpen = false);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using CefSharp;
using CefSharp.Enums;
using TweetDuck.Utils;
namespace TweetDuck.Browser.Handling {
sealed class DragHandlerBrowser : IDragHandler {
private readonly RequestHandlerBrowser requestHandler;
public DragHandlerBrowser(RequestHandlerBrowser requestHandler) {
this.requestHandler = requestHandler;
}
public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask) {
void TriggerDragStart(string type, string data = null) {
browserControl.ExecuteJsAsync("window.TDGF_onGlobalDragStart", type, data);
}
requestHandler.BlockNextUserNavUrl = dragData.LinkUrl; // empty if not a link
if (dragData.IsLink) {
TriggerDragStart("link", dragData.LinkUrl);
}
else if (dragData.IsFragment) {
TriggerDragStart("text", dragData.FragmentText.Trim());
}
else {
TriggerDragStart("unknown");
}
return false;
}
public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IFrame frame, IList<DraggableRegion> regions) {}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.IO;
using System.Text;
using CefSharp;
namespace TweetDuck.Browser.Handling.Filters {
abstract class ResponseFilterBase : IResponseFilter {
private enum State {
Reading,
Writing,
Done
}
private readonly Encoding encoding;
private byte[] responseData;
private State state;
private int offset;
protected ResponseFilterBase(int totalBytes, Encoding encoding) {
this.responseData = new byte[totalBytes];
this.encoding = encoding;
this.state = State.Reading;
}
FilterStatus IResponseFilter.Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten) {
int responseLength = responseData.Length;
if (state == State.Reading) {
int bytesToRead = Math.Min(responseLength - offset, (int) Math.Min(dataIn?.Length ?? 0, int.MaxValue));
dataIn?.Read(responseData, offset, bytesToRead);
offset += bytesToRead;
dataInRead = bytesToRead;
dataOutWritten = 0;
if (offset >= responseLength) {
responseData = encoding.GetBytes(ProcessResponse(encoding.GetString(responseData)));
state = State.Writing;
offset = 0;
}
return FilterStatus.NeedMoreData;
}
else if (state == State.Writing) {
int bytesToWrite = Math.Min(responseLength - offset, (int) Math.Min(dataOut.Length, int.MaxValue));
if (bytesToWrite > 0) {
dataOut.Write(responseData, offset, bytesToWrite);
offset += bytesToWrite;
}
dataOutWritten = bytesToWrite;
dataInRead = 0;
if (offset < responseLength) {
return FilterStatus.NeedMoreData;
}
else {
state = State.Done;
return FilterStatus.Done;
}
}
else {
throw new InvalidOperationException("This resource filter cannot be reused.");
}
}
public abstract bool InitFilter();
protected abstract string ProcessResponse(string text);
public abstract void Dispose();
}
}

View File

@@ -0,0 +1,20 @@
using System.Text;
using System.Text.RegularExpressions;
namespace TweetDuck.Browser.Handling.Filters {
sealed class ResponseFilterVendor : ResponseFilterBase {
private static readonly Regex RegexRestoreJQuery = new Regex(@"(\w+)\.fn=\1\.prototype", RegexOptions.Compiled);
public ResponseFilterVendor(int totalBytes) : base(totalBytes, Encoding.UTF8) {}
public override bool InitFilter() {
return true;
}
protected override string ProcessResponse(string text) {
return RegexRestoreJQuery.Replace(text, "window.$$=$1;$&", 1);
}
public override void Dispose() {}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using CefSharp;
using TweetDuck.Configuration;
namespace TweetDuck.Browser.Handling.General {
sealed class BrowserProcessHandler : IBrowserProcessHandler {
public static Task UpdatePrefs() {
return Cef.UIThreadTaskFactory.StartNew(UpdatePrefsInternal);
}
private static void UpdatePrefsInternal() {
UserConfig config = Program.Config.User;
using IRequestContext ctx = Cef.GetGlobalRequestContext();
ctx.SetPreference("browser.enable_spellchecking", config.EnableSpellCheck, out string _);
ctx.SetPreference("spellcheck.dictionary", config.SpellCheckLanguage, out string _);
ctx.SetPreference("settings.a11y.animation_policy", config.EnableAnimatedImages ? "allowed" : "none", out string _);
}
void IBrowserProcessHandler.OnContextInitialized() {
UpdatePrefsInternal();
}
void IBrowserProcessHandler.OnScheduleMessagePumpWork(long delay) {}
void IDisposable.Dispose() {}
}
}

View File

@@ -0,0 +1,35 @@
using CefSharp;
using CefSharp.Handler;
using TweetDuck.Controls;
using TweetDuck.Utils;
namespace TweetDuck.Browser.Handling.General {
sealed class CustomLifeSpanHandler : LifeSpanHandler {
private static bool IsPopupAllowed(string url) {
return url.StartsWith("https://twitter.com/teams/authorize?");
}
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl) {
switch (targetDisposition) {
case WindowOpenDisposition.NewBackgroundTab:
case WindowOpenDisposition.NewForegroundTab:
case WindowOpenDisposition.NewPopup when !IsPopupAllowed(targetUrl):
case WindowOpenDisposition.NewWindow:
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
return true;
default:
return false;
}
}
protected override bool OnBeforePopup(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser) {
newBrowser = null;
return HandleLinkClick(browserControl, targetDisposition, targetUrl);
}
protected override bool DoClose(IWebBrowser browserControl, IBrowser browser) {
return false;
}
}
}

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using CefSharp;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Handling.General {
sealed class FileDialogHandler : IDialogHandler {
public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback) {
if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple) {
string allFilters = string.Join(";", acceptFilters.SelectMany(ParseFileType).Where(filter => !string.IsNullOrEmpty(filter)).Select(filter => "*" + filter));
using OpenFileDialog dialog = new OpenFileDialog {
AutoUpgradeEnabled = true,
DereferenceLinks = true,
Multiselect = mode == CefFileDialogMode.OpenMultiple,
Title = "Open Files",
Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*"
};
if (dialog.ShowDialog() == DialogResult.OK) {
string ext = Path.GetExtension(dialog.FileName)?.ToLower();
callback.Continue(acceptFilters.FindIndex(filter => ParseFileType(filter).Contains(ext)), dialog.FileNames.ToList());
}
else {
callback.Cancel();
}
callback.Dispose();
return true;
}
else {
callback.Dispose();
return false;
}
}
private static IEnumerable<string> ParseFileType(string type) {
if (string.IsNullOrEmpty(type)) {
return StringUtils.EmptyArray;
}
if (type[0] == '.') {
return new string[] { type };
}
switch (type) {
case "image/jpeg": return new string[] { ".jpg", ".jpeg" };
case "image/png": return new string[] { ".png" };
case "image/gif": return new string[] { ".gif" };
case "image/webp": return new string[] { ".webp" };
case "video/mp4": return new string[] { ".mp4" };
case "video/quicktime": return new string[] { ".mov", ".qt" };
}
System.Diagnostics.Debugger.Break();
return StringUtils.EmptyArray;
}
}
}

View File

@@ -0,0 +1,93 @@
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetDuck.Utils;
namespace TweetDuck.Browser.Handling.General {
sealed class JavaScriptDialogHandler : IJsDialogHandler {
private static FormMessage CreateMessageForm(string caption, string text) {
MessageBoxIcon icon = MessageBoxIcon.None;
int pipe = text.IndexOf('|');
if (pipe != -1) {
icon = text.Substring(0, pipe) switch {
"error" => MessageBoxIcon.Error,
"warning" => MessageBoxIcon.Warning,
"info" => MessageBoxIcon.Information,
"question" => MessageBoxIcon.Question,
_ => MessageBoxIcon.None
};
if (icon != MessageBoxIcon.None) {
text = text.Substring(pipe + 1);
}
}
return new FormMessage(caption, text, icon);
}
bool IJsDialogHandler.OnJSDialog(IWebBrowser browserControl, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage) {
browserControl.AsControl().InvokeSafe(() => {
FormMessage form;
TextBox input = null;
if (dialogType == CefJsDialogType.Alert) {
form = CreateMessageForm("Browser Message", messageText);
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
}
else if (dialogType == CefJsDialogType.Confirm) {
form = CreateMessageForm("Browser Confirmation", messageText);
form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel);
form.AddButton(FormMessage.Yes, ControlType.Focused);
}
else if (dialogType == CefJsDialogType.Prompt) {
form = CreateMessageForm("Browser Prompt", messageText);
form.AddButton(FormMessage.Cancel, DialogResult.Cancel, ControlType.Cancel);
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
float dpiScale = form.GetDPIScale();
int inputPad = form.HasIcon ? 43 : 0;
input = new TextBox {
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
Font = SystemFonts.MessageBoxFont,
Location = new Point(BrowserUtils.Scale(22 + inputPad, dpiScale), form.ActionPanelY - BrowserUtils.Scale(46, dpiScale)),
Size = new Size(form.ClientSize.Width - BrowserUtils.Scale(44 + inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale))
};
form.Controls.Add(input);
form.ActiveControl = input;
form.Height += input.Size.Height + input.Margin.Vertical;
}
else {
callback.Continue(false);
return;
}
bool success = form.ShowDialog() == DialogResult.OK;
if (input == null) {
callback.Continue(success);
}
else {
callback.Continue(success, input.Text);
input.Dispose();
}
form.Dispose();
});
return true;
}
bool IJsDialogHandler.OnBeforeUnloadDialog(IWebBrowser browserControl, IBrowser browser, string messageText, bool isReload, IJsDialogCallback callback) {
callback.Dispose();
return false;
}
void IJsDialogHandler.OnResetDialogState(IWebBrowser browserControl, IBrowser browser) {}
void IJsDialogHandler.OnDialogClosed(IWebBrowser browserControl, IBrowser browser) {}
}
}

View File

@@ -0,0 +1,28 @@
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Utils;
namespace TweetDuck.Browser.Handling {
class KeyboardHandlerBase : IKeyboardHandler {
protected virtual bool HandleRawKey(IWebBrowser browserControl, Keys key, CefEventFlags modifiers) {
if (modifiers == (CefEventFlags.ControlDown | CefEventFlags.ShiftDown) && key == Keys.I) {
browserControl.OpenDevToolsCustom();
return true;
}
return false;
}
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut) {
if (type == KeyType.RawKeyDown && !browser.FocusedFrame.Url.StartsWith("devtools://")) {
return HandleRawKey(browserControl, (Keys) windowsKeyCode, modifiers);
}
return false;
}
bool IKeyboardHandler.OnKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey) {
return false;
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Windows.Forms;
using CefSharp;
namespace TweetDuck.Browser.Handling {
sealed class KeyboardHandlerBrowser : KeyboardHandlerBase {
private readonly FormBrowser form;
public KeyboardHandlerBrowser(FormBrowser form) {
this.form = form;
}
protected override bool HandleRawKey(IWebBrowser browserControl, Keys key, CefEventFlags modifiers) {
return base.HandleRawKey(browserControl, key, modifiers) || form.ProcessBrowserKey(key);
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Notification;
using TweetDuck.Controls;
namespace TweetDuck.Browser.Handling {
sealed class KeyboardHandlerNotification : KeyboardHandlerBase {
private readonly FormNotificationBase notification;
public KeyboardHandlerNotification(FormNotificationBase notification) {
this.notification = notification;
}
protected override bool HandleRawKey(IWebBrowser browserControl, Keys key, CefEventFlags modifiers) {
if (base.HandleRawKey(browserControl, key, modifiers)) {
return true;
}
switch (key) {
case Keys.Enter:
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
return true;
case Keys.Escape:
notification.InvokeAsyncSafe(notification.HideNotification);
return true;
case Keys.Space:
notification.InvokeAsyncSafe(() => notification.FreezeTimer = !notification.FreezeTimer);
return true;
default:
return false;
}
}
}
}

View File

@@ -0,0 +1,23 @@
using CefSharp;
using CefSharp.Handler;
using TweetDuck.Browser.Handling.General;
namespace TweetDuck.Browser.Handling {
class RequestHandlerBase : RequestHandler {
private readonly bool autoReload;
public RequestHandlerBase(bool autoReload) {
this.autoReload = autoReload;
}
protected override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) {
return CustomLifeSpanHandler.HandleLinkClick(browserControl, targetDisposition, targetUrl);
}
protected override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status) {
if (autoReload) {
browser.Reload();
}
}
}
}

View File

@@ -0,0 +1,23 @@
using CefSharp;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Handling {
sealed class RequestHandlerBrowser : RequestHandlerBase {
public string BlockNextUserNavUrl { get; set; }
public RequestHandlerBrowser() : base(true) {}
protected override bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) {
if (userGesture && request.TransitionType == TransitionType.LinkClicked) {
bool block = request.Url == BlockNextUserNavUrl;
BlockNextUserNavUrl = string.Empty;
return block;
}
else if (request.TransitionType.HasFlag(TransitionType.ForwardBack) && TwitterUrls.IsTweetDeck(frame.Url)) {
return true;
}
return base.OnBeforeBrowse(browserControl, browser, frame, request, userGesture, isRedirect);
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Text;
using CefSharp;
using CefSharp.Callback;
namespace TweetDuck.Browser.Handling {
sealed class ResourceHandlerNotification : IResourceHandler {
private readonly NameValueCollection headers = new NameValueCollection(0);
private MemoryStream dataIn;
public void SetHTML(string html) {
dataIn?.Dispose();
dataIn = ResourceHandler.GetMemoryStream(html, Encoding.UTF8);
}
public void Dispose() {
if (dataIn != null) {
dataIn.Dispose();
dataIn = null;
}
}
bool IResourceHandler.Open(IRequest request, out bool handleRequest, ICallback callback) {
callback.Dispose();
handleRequest = true;
if (dataIn != null) {
dataIn.Position = 0;
}
return true;
}
void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl) {
redirectUrl = null;
response.MimeType = "text/html";
response.StatusCode = 200;
response.StatusText = "OK";
response.Headers = headers;
responseLength = dataIn?.Length ?? 0;
}
bool IResourceHandler.Read(Stream dataOut, out int bytesRead, IResourceReadCallback callback) {
callback?.Dispose(); // TODO unnecessary null check once ReadResponse is removed
try {
byte[] buffer = new byte[Math.Min(dataIn.Length - dataIn.Position, dataOut.Length)];
int length = buffer.Length;
dataIn.Read(buffer, 0, length);
dataOut.Write(buffer, 0, length);
bytesRead = length;
} catch { // catch IOException, possibly NullReferenceException if dataIn is null
bytesRead = 0;
}
return bytesRead > 0;
}
bool IResourceHandler.Skip(long bytesToSkip, out long bytesSkipped, IResourceSkipCallback callback) {
bytesSkipped = -2; // ERR_FAILED
callback.Dispose();
return false;
}
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback) {
return ((IResourceHandler) this).Open(request, out bool _, callback);
}
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback) {
return ((IResourceHandler) this).Read(dataOut, out bytesRead, null);
}
void IResourceHandler.Cancel() {}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using CefSharp;
using TweetLib.Core.Browser;
using IOFile = System.IO.File;
namespace TweetDuck.Browser.Handling {
internal sealed class ResourceProvider : IResourceProvider<IResourceHandler> {
private readonly Dictionary<string, ICachedResource> cache = new Dictionary<string, ICachedResource>();
public IResourceHandler Status(HttpStatusCode code, string message) {
return CreateStatusHandler(code, message);
}
public IResourceHandler File(string path) {
string key = new Uri(path).LocalPath;
if (cache.TryGetValue(key, out var cachedResource)) {
return cachedResource.GetResource();
}
cachedResource = FileWithCaching(path);
cache[key] = cachedResource;
return cachedResource.GetResource();
}
private ICachedResource FileWithCaching(string path) {
try {
return new CachedFile(System.IO.File.ReadAllBytes(path), Path.GetExtension(path));
} catch (FileNotFoundException) {
return new CachedStatus(HttpStatusCode.NotFound, "File not found.");
} catch (DirectoryNotFoundException) {
return new CachedStatus(HttpStatusCode.NotFound, "Directory not found.");
} catch (Exception e) {
return new CachedStatus(HttpStatusCode.InternalServerError, e.Message);
}
}
public void ClearCache() {
cache.Clear();
}
private static ResourceHandler CreateHandler(byte[] bytes) {
var handler = ResourceHandler.FromStream(new MemoryStream(bytes), autoDisposeStream: true);
handler.Headers.Set("Access-Control-Allow-Origin", "*");
return handler;
}
private static IResourceHandler CreateFileContentsHandler(byte[] bytes, string extension) {
if (bytes.Length == 0) {
return CreateStatusHandler(HttpStatusCode.NoContent, "File is empty."); // FromByteArray crashes CEF internals with no contents
}
else {
var handler = CreateHandler(bytes);
handler.MimeType = Cef.GetMimeType(extension);
return handler;
}
}
private static IResourceHandler CreateStatusHandler(HttpStatusCode code, string message) {
var handler = CreateHandler(Encoding.UTF8.GetBytes(message));
handler.StatusCode = (int) code;
return handler;
}
private interface ICachedResource {
IResourceHandler GetResource();
}
private sealed class CachedFile : ICachedResource {
private readonly byte[] bytes;
private readonly string extension;
public CachedFile(byte[] bytes, string extension) {
this.bytes = bytes;
this.extension = extension;
}
public IResourceHandler GetResource() {
return CreateFileContentsHandler(bytes, extension);
}
}
private sealed class CachedStatus : ICachedResource {
private readonly HttpStatusCode code;
private readonly string message;
public CachedStatus(HttpStatusCode code, string message) {
this.code = code;
this.message = message;
}
public IResourceHandler GetResource() {
return CreateStatusHandler(code, message);
}
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
using CefSharp;
using TweetDuck.Browser.Data;
namespace TweetDuck.Browser.Handling {
abstract class ResourceRequestHandler : CefSharp.Handler.ResourceRequestHandler {
private class SelfFactoryImpl : IResourceRequestHandlerFactory {
private readonly ResourceRequestHandler me;
public SelfFactoryImpl(ResourceRequestHandler me) {
this.me = me;
}
bool IResourceRequestHandlerFactory.HasHandlers => true;
[SuppressMessage("ReSharper", "RedundantAssignment")]
IResourceRequestHandler IResourceRequestHandlerFactory.GetResourceRequestHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) {
disableDefaultHandling = me.ResourceHandlers.HasHandler(request);
return me;
}
}
public IResourceRequestHandlerFactory SelfFactory { get; }
public ResourceHandlers ResourceHandlers { get; }
protected ResourceRequestHandler() {
this.SelfFactory = new SelfFactoryImpl(this);
this.ResourceHandlers = new ResourceHandlers();
}
protected override IResourceHandler GetResourceHandler(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request) {
return ResourceHandlers.GetHandler(request);
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using CefSharp;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Handling {
class ResourceRequestHandlerBase : ResourceRequestHandler {
private static readonly Regex TweetDeckResourceUrl = new Regex(@"/dist/(.*?)\.(.*?)\.(css|js)$");
private static readonly SortedList<string, string> TweetDeckHashes = new SortedList<string, string>(4);
public static void LoadResourceRewriteRules(string rules) {
if (string.IsNullOrEmpty(rules)) {
return;
}
TweetDeckHashes.Clear();
foreach (string rule in rules.Replace(" ", "").ToLower().Split(',')) {
var (key, hash) = StringUtils.SplitInTwo(rule, '=') ?? throw new ArgumentException("A rule must have one '=' character: " + rule);
if (hash.All(chr => char.IsDigit(chr) || (chr >= 'a' && chr <= 'f'))) {
TweetDeckHashes.Add(key, hash);
}
else {
throw new ArgumentException("Invalid hash characters: " + rule);
}
}
}
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
if (request.ResourceType == ResourceType.CspReport) {
callback.Dispose();
return CefReturnValue.Cancel;
}
NameValueCollection headers = request.Headers;
headers.Remove("x-devtools-emulate-network-conditions-client-id");
request.Headers = headers;
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
protected override bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
if ((request.ResourceType == ResourceType.Script || request.ResourceType == ResourceType.Stylesheet) && TweetDeckHashes.Count > 0) {
string url = request.Url;
Match match = TweetDeckResourceUrl.Match(url);
if (match.Success && TweetDeckHashes.TryGetValue($"{match.Groups[1]}.{match.Groups[3]}", out string hash)) {
if (match.Groups[2].Value == hash) {
Program.Reporter.LogVerbose("[RequestHandlerBase] Accepting " + url);
}
else {
Program.Reporter.LogVerbose("[RequestHandlerBase] Replacing " + url + " hash with " + hash);
request.Url = TweetDeckResourceUrl.Replace(url, $"/dist/$1.{hash}.$3");
return true;
}
}
}
return base.OnResourceResponse(browserControl, browser, frame, request, response);
}
}
}

View File

@@ -0,0 +1,52 @@
using CefSharp;
using TweetDuck.Browser.Handling.Filters;
using TweetDuck.Utils;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Handling {
class ResourceRequestHandlerBrowser : ResourceRequestHandlerBase {
private const string UrlVendorResource = "/dist/vendor";
private const string UrlLoadingSpinner = "/backgrounds/spinner_blue";
private const string UrlVersionCheck = "/web/dist/version.json";
protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) {
if (request.ResourceType == ResourceType.MainFrame) {
if (request.Url.EndsWith("//twitter.com/")) {
request.Url = TwitterUrls.TweetDeck; // redirect plain twitter.com requests, fixes bugs with login 2FA
}
}
else if (request.ResourceType == ResourceType.Image) {
if (request.Url.Contains(UrlLoadingSpinner)) {
request.Url = TwitterUtils.LoadingSpinner.Url;
}
}
else if (request.ResourceType == ResourceType.Script) {
string url = request.Url;
if (url.Contains("analytics.")) {
callback.Dispose();
return CefReturnValue.Cancel;
}
else if (url.Contains(UrlVendorResource)) {
request.SetHeaderByName("Accept-Encoding", "identity", overwrite: true);
}
}
else if (request.ResourceType == ResourceType.Xhr) {
if (request.Url.Contains(UrlVersionCheck)) {
callback.Dispose();
return CefReturnValue.Cancel;
}
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) {
if (request.ResourceType == ResourceType.Script && request.Url.Contains(UrlVendorResource) && int.TryParse(response.Headers["Content-Length"], out int totalBytes)) {
return new ResponseFilterVendor(totalBytes);
}
return base.GetResourceResponseFilter(browserControl, browser, frame, request, response);
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.IO;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Controls;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Notification.Example {
sealed class FormNotificationExample : FormNotificationMain {
public override bool RequiresResize => true;
protected override bool CanDragWindow => Config.NotificationPosition == DesktopNotification.Position.Custom;
protected override FormBorderStyle NotificationBorderStyle {
get {
if (Config.NotificationSize == DesktopNotification.Size.Custom) {
switch (base.NotificationBorderStyle) {
case FormBorderStyle.FixedSingle: return FormBorderStyle.Sizable;
case FormBorderStyle.FixedToolWindow: return FormBorderStyle.SizableToolWindow;
}
}
return base.NotificationBorderStyle;
}
}
protected override string BodyClasses => base.BodyClasses + " td-example";
public event EventHandler Ready;
private readonly DesktopNotification exampleNotification;
public FormNotificationExample(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, false) {
browser.LoadingStateChanged += browser_LoadingStateChanged;
string exampleTweetHTML = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "notification/example/example.html"))?.Replace("{avatar}", AppLogo.Url) ?? string.Empty;
#if DEBUG
exampleTweetHTML = exampleTweetHTML.Replace("</p>", @"</p><div style='margin-top:256px'>Scrollbar test padding...</div>");
#endif
exampleNotification = new DesktopNotification(string.Empty, string.Empty, "Home", exampleTweetHTML, 176, string.Empty, string.Empty);
}
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
if (!e.IsLoading) {
Ready?.Invoke(this, EventArgs.Empty);
browser.LoadingStateChanged -= browser_LoadingStateChanged;
}
}
public override void HideNotification() {
Location = ControlExtensions.InvisibleLocation;
}
public override void FinishCurrentNotification() {}
public void ShowExampleNotification(bool reset) {
if (reset) {
LoadTweet(exampleNotification);
}
else {
PrepareAndDisplayWindow();
}
UpdateTitle();
}
}
}

View File

@@ -1,21 +1,10 @@
namespace TweetDuck.Core.Notification {
namespace TweetDuck.Browser.Notification {
partial class FormNotificationBase {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
@@ -34,7 +23,7 @@
this.BackColor = System.Drawing.SystemColors.Control;
this.ClientSize = new System.Drawing.Size(284, 122);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.Location = TweetDuck.Core.Controls.ControlExtensions.InvisibleLocation;
this.Location = TweetDuck.Controls.ControlExtensions.InvisibleLocation;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "FormNotification";

View File

@@ -0,0 +1,253 @@
using System.Drawing;
using System.Windows.Forms;
using CefSharp.WinForms;
using TweetDuck.Browser.Data;
using TweetDuck.Browser.Handling;
using TweetDuck.Browser.Handling.General;
using TweetDuck.Configuration;
using TweetDuck.Controls;
using TweetDuck.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Notification {
abstract partial class FormNotificationBase : Form {
public static readonly ResourceLink AppLogo = new ResourceLink("https://ton.twimg.com/tduck/avatar", ResourceHandlers.ForBytes(Properties.Resources.avatar, "image/png"));
protected const string BlankURL = TwitterUrls.TweetDeck + "/?blank";
public static string FontSize = null;
public static string HeadLayout = null;
protected static UserConfig Config => Program.Config.User;
protected static int FontSizeLevel {
get => FontSize switch {
"largest" => 4,
"large" => 3,
"small" => 1,
"smallest" => 0,
_ => 2
};
}
protected virtual Point PrimaryLocation {
get {
Screen screen;
if (Config.NotificationDisplay > 0 && Config.NotificationDisplay <= Screen.AllScreens.Length) {
screen = Screen.AllScreens[Config.NotificationDisplay - 1];
}
else {
screen = Screen.FromControl(owner);
}
int edgeDist = Config.NotificationEdgeDistance;
switch (Config.NotificationPosition) {
case DesktopNotification.Position.TopLeft:
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + edgeDist);
case DesktopNotification.Position.TopRight:
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
case DesktopNotification.Position.BottomLeft:
return new Point(screen.WorkingArea.X + edgeDist, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
case DesktopNotification.Position.BottomRight:
return new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + screen.WorkingArea.Height - edgeDist - Height);
case DesktopNotification.Position.Custom:
if (!Config.IsCustomNotificationPositionSet) {
Config.CustomNotificationPosition = new Point(screen.WorkingArea.X + screen.WorkingArea.Width - edgeDist - Width, screen.WorkingArea.Y + edgeDist);
Config.Save();
}
return Config.CustomNotificationPosition;
}
return Location;
}
}
protected bool IsNotificationVisible => Location != ControlExtensions.InvisibleLocation;
protected virtual bool CanDragWindow => true;
public new Point Location {
get { return base.Location; }
set {
Visible = (base.Location = value) != ControlExtensions.InvisibleLocation;
FormBorderStyle = NotificationBorderStyle;
}
}
protected virtual FormBorderStyle NotificationBorderStyle {
get {
if (WindowsUtils.ShouldAvoidToolWindow && Visible) { // Visible = workaround for alt+tab
return FormBorderStyle.FixedSingle;
}
else {
return FormBorderStyle.FixedToolWindow;
}
}
}
protected override bool ShowWithoutActivation => true;
protected float DpiScale { get; }
protected double SizeScale => DpiScale * Config.ZoomLevel / 100.0;
private readonly FormBrowser owner;
#pragma warning disable IDE0069 // Disposable fields should be disposed
protected readonly ChromiumWebBrowser browser;
#pragma warning restore IDE0069 // Disposable fields should be disposed
private readonly ResourceHandlerNotification resourceHandler = new ResourceHandlerNotification();
private DesktopNotification currentNotification;
private int pauseCounter;
public string CurrentTweetUrl => currentNotification?.TweetUrl;
public string CurrentQuoteUrl => currentNotification?.QuoteUrl;
public bool CanViewDetail => currentNotification != null && !string.IsNullOrEmpty(currentNotification.ColumnId) && !string.IsNullOrEmpty(currentNotification.ChirpId);
protected bool IsPaused => pauseCounter > 0;
protected bool IsCursorOverBrowser => browser.Bounds.Contains(PointToClient(Cursor.Position));
public bool FreezeTimer { get; set; }
public bool ContextMenuOpen { get; set; }
protected FormNotificationBase(FormBrowser owner, bool enableContextMenu) {
InitializeComponent();
this.owner = owner;
this.owner.FormClosed += owner_FormClosed;
var resourceRequestHandler = new ResourceRequestHandlerBase();
var resourceHandlers = resourceRequestHandler.ResourceHandlers;
resourceHandlers.Register(BlankURL, ResourceHandlers.ForString(string.Empty));
resourceHandlers.Register(TwitterUrls.TweetDeck, () => this.resourceHandler);
resourceHandlers.Register(AppLogo);
this.browser = new ChromiumWebBrowser(BlankURL) {
MenuHandler = new ContextMenuNotification(this, enableContextMenu),
JsDialogHandler = new JavaScriptDialogHandler(),
LifeSpanHandler = new CustomLifeSpanHandler(),
RequestHandler = new RequestHandlerBase(false),
ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
};
this.browser.Dock = DockStyle.None;
this.browser.ClientSize = ClientSize;
this.browser.SetupZoomEvents();
Controls.Add(browser);
Disposed += (sender, args) => {
this.owner.FormClosed -= owner_FormClosed;
this.browser.Dispose();
};
DpiScale = this.GetDPIScale();
// ReSharper disable once VirtualMemberCallInContructor
UpdateTitle();
}
protected override void Dispose(bool disposing) {
if (disposing) {
components?.Dispose();
resourceHandler.Dispose();
}
base.Dispose(disposing);
}
protected override void WndProc(ref Message m) {
if (m.Msg == 0x0112 && (m.WParam.ToInt32() & 0xFFF0) == 0xF010 && !CanDragWindow) { // WM_SYSCOMMAND, SC_MOVE
return;
}
base.WndProc(ref m);
}
// event handlers
private void owner_FormClosed(object sender, FormClosedEventArgs e) {
Close();
}
// notification methods
public virtual void HideNotification() {
browser.Load(BlankURL);
DisplayTooltip(null);
Location = ControlExtensions.InvisibleLocation;
currentNotification = null;
}
public virtual void FinishCurrentNotification() {}
public virtual void PauseNotification() {
if (pauseCounter++ == 0 && IsNotificationVisible) {
Location = ControlExtensions.InvisibleLocation;
}
}
public virtual void ResumeNotification() {
if (pauseCounter > 0) {
--pauseCounter;
}
}
protected abstract string GetTweetHTML(DesktopNotification tweet);
protected virtual void LoadTweet(DesktopNotification tweet) {
currentNotification = tweet;
resourceHandler.SetHTML(GetTweetHTML(tweet));
browser.Load(TwitterUrls.TweetDeck);
DisplayTooltip(null);
}
protected virtual void SetNotificationSize(int width, int height) {
browser.ClientSize = ClientSize = new Size(BrowserUtils.Scale(width, SizeScale), BrowserUtils.Scale(height, SizeScale));
}
protected virtual void UpdateTitle() {
string title = currentNotification?.ColumnTitle;
Text = string.IsNullOrEmpty(title) || !Config.DisplayNotificationColumn ? Program.BrandName : $"{Program.BrandName} - {title}";
}
public void ShowTweetDetail() {
if (currentNotification != null) {
owner.ShowTweetDetail(currentNotification.ColumnId, currentNotification.ChirpId, currentNotification.TweetUrl);
}
}
public void MoveToVisibleLocation() {
bool needsReactivating = Location == ControlExtensions.InvisibleLocation;
Location = PrimaryLocation;
if (needsReactivating) {
NativeMethods.SetFormPos(this, NativeMethods.HWND_TOPMOST, NativeMethods.SWP_NOACTIVATE);
}
}
public void DisplayTooltip(string text) {
if (string.IsNullOrEmpty(text)) {
toolTip.Hide(this);
}
else {
Point position = PointToClient(Cursor.Position);
position.Offset(20, 5);
toolTip.Show(text, this, position);
}
}
}
}

View File

@@ -1,4 +1,4 @@
namespace TweetDuck.Core.Notification {
namespace TweetDuck.Browser.Notification {
partial class FormNotificationMain {
/// <summary>
/// Required designer variable.
@@ -26,7 +26,7 @@
this.components = new System.ComponentModel.Container();
this.timerDisplayDelay = new System.Windows.Forms.Timer(this.components);
this.timerProgress = new System.Windows.Forms.Timer(this.components);
this.progressBarTimer = new TweetDuck.Core.Controls.FlatProgressBar();
this.progressBarTimer = new TweetDuck.Controls.FlatProgressBar();
this.SuspendLayout();
//
// timerDisplayDelay

View File

@@ -0,0 +1,282 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Adapters;
using TweetDuck.Browser.Bridge;
using TweetDuck.Browser.Handling;
using TweetDuck.Controls;
using TweetDuck.Plugins;
using TweetDuck.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Twitter;
namespace TweetDuck.Browser.Notification {
abstract partial class FormNotificationMain : FormNotificationBase {
private readonly PluginManager plugins;
private readonly int timerBarHeight;
protected int timeLeft, totalTime;
protected bool pausedDuringNotification;
private readonly NativeMethods.HookProc mouseHookDelegate;
private IntPtr mouseHook;
private bool blockXButtonUp;
private int currentOpacity;
private bool? prevDisplayTimer;
private int? prevFontSize;
public virtual bool RequiresResize {
get { return !prevDisplayTimer.HasValue || !prevFontSize.HasValue || prevDisplayTimer != Config.DisplayNotificationTimer || prevFontSize != FontSizeLevel; }
set {
if (value) {
prevDisplayTimer = null;
prevFontSize = null;
}
else {
prevDisplayTimer = Config.DisplayNotificationTimer;
prevFontSize = FontSizeLevel;
}
}
}
private int BaseClientWidth {
get => Config.NotificationSize switch {
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Width,
_ => BrowserUtils.Scale(284, SizeScale * (1.0 + 0.05 * FontSizeLevel))
};
}
private int BaseClientHeight {
get => Config.NotificationSize switch {
DesktopNotification.Size.Custom => Config.CustomNotificationSize.Height,
_ => BrowserUtils.Scale(122, SizeScale * (1.0 + 0.08 * FontSizeLevel))
};
}
protected virtual string BodyClasses => IsCursorOverBrowser ? "td-notification td-hover" : "td-notification";
public Size BrowserSize => Config.DisplayNotificationTimer ? new Size(ClientSize.Width, ClientSize.Height - timerBarHeight) : ClientSize;
protected FormNotificationMain(FormBrowser owner, PluginManager pluginManager, bool enableContextMenu) : base(owner, enableContextMenu) {
InitializeComponent();
this.plugins = pluginManager;
this.timerBarHeight = BrowserUtils.Scale(4, DpiScale);
browser.KeyboardHandler = new KeyboardHandlerNotification(this);
browser.RegisterJsBridge("$TD", new TweetDeckBridge.Notification(owner, this));
browser.LoadingStateChanged += Browser_LoadingStateChanged;
plugins.Register(PluginEnvironment.Notification, new PluginDispatcher(browser, url => TwitterUrls.IsTweetDeck(url) && url != BlankURL));
mouseHookDelegate = MouseHookProc;
Disposed += (sender, args) => StopMouseHook(true);
}
// helpers
private void SetOpacity(int opacity) {
if (currentOpacity != opacity) {
currentOpacity = opacity;
Opacity = opacity / 100.0;
}
}
// mouse wheel hook
private void StartMouseHook() {
if (mouseHook == IntPtr.Zero) {
mouseHook = NativeMethods.SetWindowsHookEx(NativeMethods.WM_MOUSE_LL, mouseHookDelegate, IntPtr.Zero, 0);
}
}
private void StopMouseHook(bool force) {
if (mouseHook != IntPtr.Zero && (force || !blockXButtonUp)) {
NativeMethods.UnhookWindowsHookEx(mouseHook);
mouseHook = IntPtr.Zero;
blockXButtonUp = false;
}
}
private IntPtr MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam) {
if (nCode == 0) {
int eventType = wParam.ToInt32();
if (eventType == NativeMethods.WM_MOUSEWHEEL && IsCursorOverBrowser) {
int delta = BrowserUtils.Scale(NativeMethods.GetMouseHookData(lParam), Config.NotificationScrollSpeed * 0.01);
if (Config.EnableSmoothScrolling) {
browser.ExecuteJsAsync("window.TDGF_scrollSmoothly", (int) Math.Round(-delta / 0.6));
}
else {
browser.SendMouseWheelEvent(0, 0, 0, delta, CefEventFlags.None);
}
return NativeMethods.HOOK_HANDLED;
}
else if (eventType == NativeMethods.WM_XBUTTONDOWN && DesktopBounds.Contains(Cursor.Position)) {
int extraButton = NativeMethods.GetMouseHookData(lParam);
if (extraButton == 2) { // forward button
this.InvokeAsyncSafe(FinishCurrentNotification);
}
else if (extraButton == 1) { // back button
this.InvokeAsyncSafe(Close);
}
blockXButtonUp = true;
return NativeMethods.HOOK_HANDLED;
}
else if (eventType == NativeMethods.WM_XBUTTONUP && blockXButtonUp) {
blockXButtonUp = false;
if (!Visible) {
StopMouseHook(false);
}
return NativeMethods.HOOK_HANDLED;
}
}
return NativeMethods.CallNextHookEx(mouseHook, nCode, wParam, lParam);
}
// event handlers
private void FormNotification_FormClosing(object sender, FormClosingEventArgs e) {
if (e.CloseReason == CloseReason.UserClosing) {
HideNotification();
e.Cancel = true;
}
}
private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
if (!e.IsLoading && browser.Address != BlankURL) {
this.InvokeSafe(() => {
Visible = true; // ensures repaint before moving the window to a visible location
timerDisplayDelay.Start();
});
}
}
private void timerDisplayDelay_Tick(object sender, EventArgs e) {
OnNotificationReady();
timerDisplayDelay.Stop();
}
private void timerHideProgress_Tick(object sender, EventArgs e) {
bool isCursorInside = Bounds.Contains(Cursor.Position);
if (isCursorInside) {
StartMouseHook();
SetOpacity(100);
}
else {
StopMouseHook(false);
SetOpacity(Config.NotificationWindowOpacity);
}
if (isCursorInside || FreezeTimer || ContextMenuOpen) {
return;
}
timeLeft -= timerProgress.Interval;
int value = BrowserUtils.Scale(progressBarTimer.Maximum + 25, (totalTime - timeLeft) / (double) totalTime);
progressBarTimer.SetValueInstant(Config.NotificationTimerCountDown ? progressBarTimer.Maximum - value : value);
if (timeLeft <= 0) {
FinishCurrentNotification();
}
}
// notification methods
public virtual void ShowNotification(DesktopNotification notification) {
LoadTweet(notification);
}
public override void HideNotification() {
base.HideNotification();
progressBarTimer.Value = Config.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum;
timerProgress.Stop();
totalTime = 0;
StopMouseHook(false);
}
public override void FinishCurrentNotification() {
timerProgress.Stop();
}
public override void PauseNotification() {
if (!IsPaused) {
pausedDuringNotification = IsNotificationVisible;
timerProgress.Stop();
StopMouseHook(true);
}
base.PauseNotification();
}
public override void ResumeNotification() {
bool wasPaused = IsPaused;
base.ResumeNotification();
if (wasPaused && !IsPaused && pausedDuringNotification) {
OnNotificationReady();
}
}
protected override string GetTweetHTML(DesktopNotification tweet) {
return tweet.GenerateHtml(BodyClasses, HeadLayout, Config.CustomNotificationCSS, plugins.NotificationInjections, new string[] {
PropertyBridge.GenerateScript(PropertyBridge.Environment.Notification),
CefScriptExecutor.GetBootstrapScript("notification", includeStylesheets: false)
});
}
protected override void LoadTweet(DesktopNotification tweet) {
timerProgress.Stop();
totalTime = timeLeft = tweet.GetDisplayDuration(Config.NotificationDurationValue);
progressBarTimer.Value = Config.NotificationTimerCountDown ? progressBarTimer.Maximum : progressBarTimer.Minimum;
base.LoadTweet(tweet);
}
protected override void SetNotificationSize(int width, int height) {
if (Config.DisplayNotificationTimer) {
ClientSize = new Size(width, height + timerBarHeight);
progressBarTimer.Visible = true;
}
else {
ClientSize = new Size(width, height);
progressBarTimer.Visible = false;
}
browser.ClientSize = new Size(width, height);
}
protected void PrepareAndDisplayWindow() {
if (RequiresResize) {
RequiresResize = false;
SetNotificationSize(BaseClientWidth, BaseClientHeight);
}
SetOpacity(IsCursorOverBrowser ? 100 : Config.NotificationWindowOpacity);
MoveToVisibleLocation();
}
protected virtual void OnNotificationReady() {
PrepareAndDisplayWindow();
timerProgress.Start();
}
}
}

View File

@@ -1,4 +1,4 @@
namespace TweetDuck.Core.Notification {
namespace TweetDuck.Browser.Notification {
partial class FormNotificationTweet {
/// <summary>
/// Required designer variable.

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
namespace TweetDuck.Browser.Notification {
sealed partial class FormNotificationTweet : FormNotificationMain {
private const int NonIntrusiveIdleLimit = 30;
private const int TrimMinimum = 32;
protected override Point PrimaryLocation => hasTemporarilyMoved && IsNotificationVisible ? Location : base.PrimaryLocation;
private bool IsCursorOverNotificationArea => new Rectangle(PrimaryLocation, Size).Contains(Cursor.Position);
protected override bool CanDragWindow {
get {
if (ModifierKeys.HasFlag(Keys.Alt)) {
hasTemporarilyMoved = true;
return true;
}
else {
return false;
}
}
}
private readonly Queue<DesktopNotification> tweetQueue = new Queue<DesktopNotification>(4);
private bool needsTrim;
private bool hasTemporarilyMoved;
public FormNotificationTweet(FormBrowser owner, PluginManager pluginManager) : base(owner, pluginManager, true) {
InitializeComponent();
Config.MuteToggled += Config_MuteToggled;
Disposed += (sender, args) => Config.MuteToggled -= Config_MuteToggled;
if (Config.MuteNotifications) {
PauseNotification();
}
}
protected override void WndProc(ref Message m) {
if (m.Msg == 0x00A7) { // WM_NCMBUTTONDOWN
int hitTest = m.WParam.ToInt32();
if (hitTest == 2 || hitTest == 20) { // HTCAPTION, HTCLOSE
hasTemporarilyMoved = false;
MoveToVisibleLocation();
return;
}
}
base.WndProc(ref m);
}
// event handlers
private void Config_MuteToggled(object sender, EventArgs e) {
if (Config.MuteNotifications) {
PauseNotification();
}
else {
ResumeNotification();
}
}
private void timerCursorCheck_Tick(object sender, EventArgs e) {
if (!IsCursorOverNotificationArea) {
ResumeNotification();
timerCursorCheck.Stop();
}
}
private void timerIdlePauseCheck_Tick(object sender, EventArgs e) {
if (NativeMethods.GetIdleSeconds() < Config.NotificationIdlePauseSeconds) {
ResumeNotification();
timerIdlePauseCheck.Stop();
}
}
// notification methods
public override void ShowNotification(DesktopNotification notification) {
tweetQueue.Enqueue(notification);
if (!IsPaused) {
UpdateTitle();
if (totalTime == 0) {
LoadNextNotification();
}
}
needsTrim |= tweetQueue.Count >= TrimMinimum;
}
public override void HideNotification() {
base.HideNotification();
tweetQueue.Clear();
if (needsTrim) {
tweetQueue.TrimExcess();
needsTrim = false;
}
hasTemporarilyMoved = false;
}
public override void FinishCurrentNotification() {
if (tweetQueue.Count > 0) {
LoadNextNotification();
}
else {
HideNotification();
}
}
public override void ResumeNotification() {
bool wasPaused = IsPaused;
base.ResumeNotification();
if (wasPaused && !IsPaused && !pausedDuringNotification && tweetQueue.Count > 0) {
LoadNextNotification();
}
}
private void LoadNextNotification() {
if (!IsNotificationVisible) {
if (Config.NotificationNonIntrusiveMode && IsCursorOverNotificationArea && NativeMethods.GetIdleSeconds() < NonIntrusiveIdleLimit) {
if (!timerCursorCheck.Enabled) {
PauseNotification();
timerCursorCheck.Start();
}
return;
}
else if (Config.NotificationIdlePauseSeconds > 0 && NativeMethods.GetIdleSeconds() >= Config.NotificationIdlePauseSeconds) {
if (!timerIdlePauseCheck.Enabled) {
PauseNotification();
timerIdlePauseCheck.Start();
}
return;
}
}
LoadTweet(tweetQueue.Dequeue());
}
protected override void UpdateTitle() {
base.UpdateTitle();
if (tweetQueue.Count > 0) {
Text = Text + " (" + tweetQueue.Count + " more left)";
}
}
protected override void OnNotificationReady() {
UpdateTitle();
base.OnNotificationReady();
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using CefSharp;
using CefSharp.DevTools.Page;
using TweetDuck.Browser.Adapters;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetDuck.Utils;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser.Notification.Screenshot {
sealed class FormNotificationScreenshotable : FormNotificationBase {
protected override bool CanDragWindow => false;
private readonly PluginManager plugins;
private int height;
public FormNotificationScreenshotable(Action callback, FormBrowser owner, PluginManager pluginManager, string html, int width) : base(owner, false) {
this.plugins = pluginManager;
int realWidth = BrowserUtils.Scale(width, DpiScale);
browser.RegisterJsBridge("$TD_NotificationScreenshot", new ScreenshotBridge(this, SetScreenshotHeight, callback));
browser.LoadingStateChanged += (sender, args) => {
if (args.IsLoading) {
return;
}
string script = FileUtils.ReadFileOrNull(Path.Combine(Program.ResourcesPath, "notification/screenshot/screenshot.js"));
if (script == null) {
this.InvokeAsyncSafe(callback);
return;
}
using IFrame frame = args.Browser.MainFrame;
CefScriptExecutor.RunScript(frame, script.Replace("{width}", realWidth.ToString()).Replace("1/*FRAMES*/", TweetScreenshotManager.WaitFrames.ToString()), "gen:screenshot");
};
SetNotificationSize(realWidth, 1024);
LoadTweet(new DesktopNotification(string.Empty, string.Empty, string.Empty, html, 0, string.Empty, string.Empty));
}
protected override string GetTweetHTML(DesktopNotification tweet) {
return tweet.GenerateHtml("td-screenshot", HeadLayout, Config.CustomNotificationCSS, plugins.NotificationInjections, Array.Empty<string>());
}
private void SetScreenshotHeight(int browserHeight) {
this.height = BrowserUtils.Scale(browserHeight, SizeScale);
}
public Task<Image> TakeScreenshot(bool ignoreHeightError = false) {
if (!ignoreHeightError) {
if (height == 0) {
FormMessage.Error("Screenshot Failed", "Could not detect screenshot size.", FormMessage.OK);
return null;
}
else if (height > ClientSize.Height) {
FormMessage.Error("Screenshot Failed", $"Screenshot is too large: {height}px > {ClientSize.Height}px", FormMessage.OK);
return null;
}
}
return Task.Run(TakeScreenshotImpl);
}
private async Task<Image> TakeScreenshotImpl() {
if (this.height == 0) {
return null;
}
Viewport viewport = new Viewport {
Width = this.ClientSize.Width,
Height = this.height,
Scale = 1
};
byte[] data;
using (var devToolsClient = browser.GetDevToolsClient()) {
data = (await devToolsClient.Page.CaptureScreenshotAsync(CaptureScreenshotFormat.Png, clip: viewport)).Data;
}
return Image.FromStream(new MemoryStream(data));
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using TweetDuck.Controls;
namespace TweetDuck.Browser.Notification.Screenshot {
[SuppressMessage("ReSharper", "UnusedMember.Global")]
sealed class ScreenshotBridge {
private readonly Control owner;
private readonly Action<int> safeSetHeight;
private readonly Action safeTriggerScreenshot;
public ScreenshotBridge(Control owner, Action<int> safeSetHeight, Action safeTriggerScreenshot) {
this.owner = owner;
this.safeSetHeight = safeSetHeight;
this.safeTriggerScreenshot = safeTriggerScreenshot;
}
public void SetHeight(int tweetHeight) {
owner.InvokeSafe(() => safeSetHeight(tweetHeight));
}
public void TriggerScreenshot() {
owner.InvokeSafe(safeTriggerScreenshot);
}
}
}

View File

@@ -0,0 +1,163 @@
#if DEBUG
// Uncomment to keep screenshot windows visible for debugging
// #define NO_HIDE_SCREENSHOTS
// Uncomment to generate screenshots of individual frames for at most 1 second
// #define GEN_SCREENSHOT_FRAMES
#endif
using System;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using TweetDuck.Controls;
using TweetLib.Core.Features.Plugins;
#if GEN_SCREENSHOT_FRAMES
using System.Drawing.Imaging;
using System.IO;
using TweetDuck.Utils;
#endif
namespace TweetDuck.Browser.Notification.Screenshot {
sealed class TweetScreenshotManager : IDisposable {
private readonly FormBrowser owner;
private readonly PluginManager plugins;
private readonly Timer timeout;
private readonly Timer disposer;
#if GEN_SCREENSHOT_FRAMES
private readonly Timer debugger;
private int frameCounter;
public const int WaitFrames = 60;
#else
public const int WaitFrames = 5;
#endif
private FormNotificationScreenshotable screenshot;
public TweetScreenshotManager(FormBrowser owner, PluginManager pluginManager) {
this.owner = owner;
this.plugins = pluginManager;
this.timeout = new Timer { Interval = 8000 };
this.timeout.Tick += timeout_Tick;
this.disposer = new Timer { Interval = 1 };
this.disposer.Tick += disposer_Tick;
#if GEN_SCREENSHOT_FRAMES
this.debugger = new Timer { Interval = 16 };
this.debugger.Tick += debugger_Tick;
#endif
}
private void timeout_Tick(object sender, EventArgs e) {
timeout.Stop();
OnFinished();
}
private void disposer_Tick(object sender, EventArgs e) {
disposer.Stop();
screenshot.Dispose();
screenshot = null;
}
public void Trigger(string html, int width) {
if (screenshot != null) {
return;
}
screenshot = new FormNotificationScreenshotable(Callback, owner, plugins, html, width);
screenshot.Show();
timeout.Start();
#if GEN_SCREENSHOT_FRAMES
StartDebugger();
#endif
#if !NO_HIDE_SCREENSHOTS
owner.IsWaiting = true;
#endif
}
private void Callback() {
if (!timeout.Enabled) {
return;
}
timeout.Stop();
screenshot.TakeScreenshot().ContinueWith(HandleResult, TaskScheduler.FromCurrentSynchronizationContext());
}
private void HandleResult(Task<Image> task) {
if (task.IsFaulted) {
Program.Reporter.HandleException("Screenshot Failed", "An error occurred while taking a screenshot.", true, task.Exception!.InnerException);
}
else if (task.IsCompleted) {
Clipboard.SetImage(task.Result);
#if !NO_HIDE_SCREENSHOTS
OnFinished();
#else
screenshot.MoveToVisibleLocation();
screenshot.FormClosed += (sender, args) => disposer.Start();
#endif
}
}
private void OnFinished() {
#if GEN_SCREENSHOT_FRAMES
debugger.Stop();
#endif
screenshot.Location = ControlExtensions.InvisibleLocation;
owner.IsWaiting = false;
disposer.Start();
}
public void Dispose() {
#if GEN_SCREENSHOT_FRAMES
debugger.Dispose();
#endif
timeout.Dispose();
disposer.Dispose();
screenshot?.Dispose();
}
#if GEN_SCREENSHOT_FRAMES
private static readonly string DebugScreenshotPath = Path.Combine(Program.StoragePath, "TD_Screenshots");
private void StartDebugger() {
frameCounter = 0;
try {
Directory.Delete(DebugScreenshotPath, true);
WindowsUtils.TrySleepUntil(() => !Directory.Exists(DebugScreenshotPath), 1000, 10);
} catch (DirectoryNotFoundException) {}
Directory.CreateDirectory(DebugScreenshotPath);
debugger.Start();
}
private void debugger_Tick(object sender, EventArgs e) {
if (frameCounter < 63) {
int frame = ++frameCounter;
screenshot.TakeScreenshot(true).ContinueWith(task => SaveDebugFrame(task, frame), TaskScheduler.FromCurrentSynchronizationContext());
}
else {
debugger.Stop();
}
}
private static void SaveDebugFrame(Task<Image> task, int frame) {
if (task.IsFaulted) {
System.Diagnostics.Debug.WriteLine("Failed generating frame " + frame + ": " + task.Exception!.InnerException);
}
else if (task.IsCompleted) {
task.Result?.Save(Path.Combine(DebugScreenshotPath, "frame_" + (++frame) + ".png"), ImageFormat.Png);
}
}
#endif
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Browser.Data;
using TweetDuck.Controls;
using TweetDuck.Dialogs;
using TweetDuck.Dialogs.Settings;
using TweetDuck.Management;
namespace TweetDuck.Browser.Notification {
static class SoundNotification {
public const string SupportedFormats = "*.wav;*.ogg;*.mp3;*.flac;*.opus;*.weba;*.webm";
public static Func<IResourceHandler> CreateFileHandler(string path) {
string mimeType = Path.GetExtension(path) switch {
".weba" => "audio/webm",
".webm" => "audio/webm",
".wav" => "audio/wav",
".ogg" => "audio/ogg",
".mp3" => "audio/mp3",
".flac" => "audio/flac",
".opus" => "audio/ogg; codecs=opus",
_ => null
};
try {
return ResourceHandlers.ForBytes(File.ReadAllBytes(path), mimeType);
} catch {
FormBrowser browser = FormManager.TryFind<FormBrowser>();
browser?.InvokeAsyncSafe(() => {
using FormMessage form = new FormMessage("Sound Notification Error", "Could not find custom notification sound file:\n" + path, MessageBoxIcon.Error);
form.AddButton(FormMessage.Ignore, ControlType.Cancel | ControlType.Focused);
Button btnViewOptions = form.AddButton("View Options");
btnViewOptions.Width += 16;
btnViewOptions.Location = new Point(btnViewOptions.Location.X - 16, btnViewOptions.Location.Y);
if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnViewOptions) {
browser.OpenSettings(typeof(TabSettingsSounds));
}
});
return null;
}
}
}
}

View File

@@ -1,21 +1,10 @@
namespace TweetDuck.Core.Other {
namespace TweetDuck.Browser {
partial class TrayIcon {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>

124
Browser/TrayIcon.cs Normal file
View File

@@ -0,0 +1,124 @@
using System;
using System.ComponentModel;
using System.Windows.Forms;
using TweetDuck.Configuration;
using Res = TweetDuck.Properties.Resources;
namespace TweetDuck.Browser {
sealed partial class TrayIcon : Component {
public enum Behavior { // keep order
Disabled,
DisplayOnly,
MinimizeToTray,
CloseToTray,
Combined
}
private static UserConfig Config => Program.Config.User;
public event EventHandler ClickRestore;
public event EventHandler ClickClose;
public bool Visible {
get { return notifyIcon.Visible; }
set {
notifyIcon.Visible = value;
hasNotifications = false;
UpdateIcon();
}
}
public bool HasNotifications {
get { return hasNotifications; }
set {
if (hasNotifications != value) {
hasNotifications = value;
UpdateIcon();
}
}
}
private readonly ContextMenu contextMenu;
private bool hasNotifications;
public TrayIcon() {
InitializeComponent();
this.contextMenu = new ContextMenu();
this.contextMenu.MenuItems.Add("Restore", menuItemRestore_Click);
this.contextMenu.MenuItems.Add("Mute notifications", menuItemMuteNotifications_Click);
this.contextMenu.MenuItems.Add("Close", menuItemClose_Click);
this.contextMenu.Popup += contextMenu_Popup;
this.notifyIcon.ContextMenu = contextMenu;
this.notifyIcon.Text = Program.BrandName;
Config.MuteToggled += Config_MuteToggled;
Disposed += (sender, args) => Config.MuteToggled -= Config_MuteToggled;
}
public TrayIcon(IContainer container) : this() {
container.Add(this);
}
protected override void Dispose(bool disposing) {
if (disposing) {
components?.Dispose();
contextMenu.Dispose();
}
base.Dispose(disposing);
}
private void UpdateIcon() {
if (Visible) {
notifyIcon.Icon = hasNotifications ? Res.icon_tray_new : Config.MuteNotifications ? Res.icon_tray_muted : Res.icon_tray;
}
}
// event handlers
private void Config_MuteToggled(object sender, EventArgs e) {
UpdateIcon();
}
private void trayIcon_MouseClick(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
menuItemRestore_Click(sender, e);
}
}
private void contextMenu_Popup(object sender, EventArgs e) {
contextMenu.MenuItems[1].Checked = Config.MuteNotifications;
}
private void menuItemRestore_Click(object sender, EventArgs e) {
ClickRestore?.Invoke(this, e);
}
private void menuItemMuteNotifications_Click(object sender, EventArgs e) {
Config.MuteNotifications = !contextMenu.MenuItems[1].Checked;
Config.Save();
}
private void menuItemClose_Click(object sender, EventArgs e) {
ClickClose?.Invoke(this, e);
}
}
static class BehaviorExtensions {
public static bool ShouldDisplayIcon(this TrayIcon.Behavior behavior) {
return behavior != TrayIcon.Behavior.Disabled;
}
public static bool ShouldHideOnMinimize(this TrayIcon.Behavior behavior) {
return behavior == TrayIcon.Behavior.MinimizeToTray || behavior == TrayIcon.Behavior.Combined;
}
public static bool ShouldHideOnClose(this TrayIcon.Behavior behavior) {
return behavior == TrayIcon.Behavior.CloseToTray || behavior == TrayIcon.Behavior.Combined;
}
}
}

272
Browser/TweetDeckBrowser.cs Normal file
View File

@@ -0,0 +1,272 @@
using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Browser.Adapters;
using TweetDuck.Browser.Bridge;
using TweetDuck.Browser.Data;
using TweetDuck.Browser.Handling;
using TweetDuck.Browser.Handling.General;
using TweetDuck.Browser.Notification;
using TweetDuck.Configuration;
using TweetDuck.Controls;
using TweetDuck.Plugins;
using TweetDuck.Utils;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Enums;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Utils;
namespace TweetDuck.Browser {
sealed class TweetDeckBrowser : IDisposable {
private static UserConfig Config => Program.Config.User;
private const string NamespaceTweetDeck = "tweetdeck";
public bool Ready { get; private set; }
public bool Enabled {
get => browser.Enabled;
set => browser.Enabled = value;
}
public bool IsTweetDeckWebsite {
get {
if (!Ready) {
return false;
}
using IFrame frame = browser.GetBrowser().MainFrame;
return TwitterUrls.IsTweetDeck(frame.Url);
}
}
private readonly ChromiumWebBrowser browser;
private readonly ResourceHandlers resourceHandlers;
private string prevSoundNotificationPath = null;
public TweetDeckBrowser(FormBrowser owner, PluginManager plugins, TweetDeckBridge tdBridge, UpdateBridge updateBridge) {
var resourceRequestHandler = new ResourceRequestHandlerBrowser();
resourceHandlers = resourceRequestHandler.ResourceHandlers;
resourceHandlers.Register(FormNotificationBase.AppLogo);
resourceHandlers.Register(TwitterUtils.LoadingSpinner);
RequestHandlerBrowser requestHandler = new RequestHandlerBrowser();
this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck) {
DialogHandler = new FileDialogHandler(),
DragHandler = new DragHandlerBrowser(requestHandler),
MenuHandler = new ContextMenuBrowser(owner),
JsDialogHandler = new JavaScriptDialogHandler(),
KeyboardHandler = new KeyboardHandlerBrowser(owner),
LifeSpanHandler = new CustomLifeSpanHandler(),
RequestHandler = requestHandler,
ResourceRequestHandlerFactory = resourceRequestHandler.SelfFactory
};
this.browser.LoadingStateChanged += browser_LoadingStateChanged;
this.browser.FrameLoadStart += browser_FrameLoadStart;
this.browser.FrameLoadEnd += browser_FrameLoadEnd;
this.browser.LoadError += browser_LoadError;
this.browser.RegisterJsBridge("$TD", tdBridge);
this.browser.RegisterJsBridge("$TDU", updateBridge);
this.browser.Dock = DockStyle.None;
this.browser.Location = ControlExtensions.InvisibleLocation;
this.browser.SetupZoomEvents();
owner.Controls.Add(browser);
plugins.Register(PluginEnvironment.Browser, new PluginDispatcher(browser, TwitterUrls.IsTweetDeck));
Config.MuteToggled += Config_MuteToggled;
Config.SoundNotificationChanged += Config_SoundNotificationInfoChanged;
}
// setup and management
public void PrepareSize(Size size) {
if (!Ready) {
browser.Size = size;
}
}
private void OnBrowserReady() {
if (!Ready) {
browser.Location = Point.Empty;
browser.Dock = DockStyle.Fill;
Ready = true;
}
}
public void Focus() {
browser.Focus();
}
public void Dispose() {
Config.MuteToggled -= Config_MuteToggled;
Config.SoundNotificationChanged -= Config_SoundNotificationInfoChanged;
browser.Dispose();
}
// event handlers
private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) {
if (!e.IsLoading) {
foreach (string word in TwitterUtils.DictionaryWords) {
browser.AddWordToDictionary(word);
}
browser.BeginInvoke(new Action(OnBrowserReady));
browser.LoadingStateChanged -= browser_LoadingStateChanged;
}
}
private void browser_FrameLoadStart(object sender, FrameLoadStartEventArgs e) {
IFrame frame = e.Frame;
if (frame.IsMain) {
string url = frame.Url;
if (TwitterUrls.IsTweetDeck(url) || (TwitterUrls.IsTwitter(url) && !TwitterUrls.IsTwitterLogin2Factor(url))) {
frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorOverride);
}
}
}
private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
IFrame frame = e.Frame;
string url = frame.Url;
if (frame.IsMain) {
if (TwitterUrls.IsTweetDeck(url)) {
UpdateProperties();
CefScriptExecutor.RunBootstrap(frame, NamespaceTweetDeck);
TweetDeckBridge.ResetStaticProperties();
if (Arguments.HasFlag(Arguments.ArgIgnoreGDPR)) {
CefScriptExecutor.RunScript(frame, "TD.storage.Account.prototype.requiresConsent = function(){ return false; }", "gen:gdpr");
}
if (Config.FirstRun) {
CefScriptExecutor.RunBootstrap(frame, "introduction");
}
}
else if (TwitterUrls.IsTwitter(url)) {
CefScriptExecutor.RunBootstrap(frame, "login");
}
CefScriptExecutor.RunBootstrap(frame, "update");
}
}
private void browser_LoadError(object sender, LoadErrorEventArgs e) {
if (e.ErrorCode == CefErrorCode.Aborted) {
return;
}
if (!e.FailedUrl.StartsWith("td://", StringComparison.Ordinal)) {
string errorName = Enum.GetName(typeof(CefErrorCode), e.ErrorCode);
string errorTitle = StringUtils.ConvertPascalCaseToScreamingSnakeCase(errorName ?? string.Empty);
browser.Load("td://resources/error/error.html#" + Uri.EscapeDataString(errorTitle));
}
}
private void Config_MuteToggled(object sender, EventArgs e) {
UpdateProperties();
}
private void Config_SoundNotificationInfoChanged(object sender, EventArgs e) {
const string soundUrl = "https://ton.twimg.com/tduck/updatesnd";
bool hasCustomSound = Config.IsCustomSoundNotificationSet;
string newNotificationPath = Config.NotificationSoundPath;
if (prevSoundNotificationPath != newNotificationPath) {
prevSoundNotificationPath = newNotificationPath;
if (hasCustomSound) {
resourceHandlers.Register(soundUrl, SoundNotification.CreateFileHandler(newNotificationPath));
}
else {
resourceHandlers.Unregister(soundUrl);
}
}
browser.ExecuteJsAsync("TDGF_setSoundNotificationData", hasCustomSound, Config.NotificationSoundVolume);
}
// external handling
public void HideVideoOverlay(bool focus) {
if (focus) {
browser.GetBrowser().GetHost().SendFocusEvent(true);
}
browser.ExecuteJsAsync("$('#td-video-player-overlay').remove()");
}
// javascript calls
public void ReloadToTweetDeck() {
browser.ExecuteJsAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUrls.TweetDeck}'");
}
public void OnModulesLoaded(string moduleNamespace) {
if (moduleNamespace == NamespaceTweetDeck) {
ReinjectCustomCSS(Config.CustomBrowserCSS);
Config_SoundNotificationInfoChanged(null, EventArgs.Empty);
}
}
public void UpdateProperties() {
browser.ExecuteJsAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Browser));
}
public void ReinjectCustomCSS(string css) {
browser.ExecuteJsAsync("TDGF_reinjectCustomCSS", css?.Replace(Environment.NewLine, " ") ?? string.Empty);
}
public void OnMouseClickExtra(IntPtr param) {
browser.ExecuteJsAsync("TDGF_onMouseClickExtra", (param.ToInt32() >> 16) & 0xFFFF);
}
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl) {
browser.ExecuteJsAsync("TDGF_showTweetDetail", columnId, chirpId, fallbackUrl);
}
public void AddSearchColumn(string query) {
browser.ExecuteJsAsync("TDGF_performSearch", query);
}
public void TriggerTweetScreenshot(string columnId, string chirpId) {
browser.ExecuteJsAsync("TDGF_triggerScreenshot", columnId, chirpId);
}
public void ReloadColumns() {
browser.ExecuteJsAsync("TDGF_reloadColumns()");
}
public void PlaySoundNotification() {
browser.ExecuteJsAsync("TDGF_playSoundNotification()");
}
public void ApplyROT13() {
browser.ExecuteJsAsync("TDGF_applyROT13()");
}
public void ShowUpdateNotification(string versionTag, string releaseNotes) {
browser.ExecuteJsAsync("TDUF_displayNotification", versionTag, Convert.ToBase64String(Encoding.GetEncoding("iso-8859-1").GetBytes(releaseNotes)));
}
public void OpenDevTools() {
browser.OpenDevToolsCustom();
}
}
}

View File

@@ -1,47 +1,48 @@
using System;
using TweetDuck.Data;
using TweetLib.Core.Collections;
namespace TweetDuck.Configuration{
static class Arguments{
// public args
public const string ArgDataFolder = "-datafolder";
public const string ArgLogging = "-log";
public const string ArgDebugUpdates = "-debugupdates";
namespace TweetDuck.Configuration {
static class Arguments {
// public args
public const string ArgDataFolder = "-datafolder";
public const string ArgLogging = "-log";
public const string ArgIgnoreGDPR = "-nogdpr";
public const string ArgFreeze = "-freeze";
// internal args
public const string ArgRestart = "-restart";
public const string ArgImportCookies = "-importcookies";
public const string ArgDeleteCookies = "-deletecookies";
public const string ArgUpdated = "-updated";
// internal args
public const string ArgRestart = "-restart";
public const string ArgImportCookies = "-importcookies";
public const string ArgDeleteCookies = "-deletecookies";
public const string ArgUpdated = "-updated";
// class data and methods
private static readonly CommandLineArgs Current = CommandLineArgs.FromStringArray('-', Environment.GetCommandLineArgs());
// class data and methods
private static readonly CommandLineArgs Current = CommandLineArgs.FromStringArray('-', Environment.GetCommandLineArgs());
public static bool HasFlag(string flag){
return Current.HasFlag(flag);
}
public static bool HasFlag(string flag) {
return Current.HasFlag(flag);
}
public static string GetValue(string key, string defaultValue){
return Current.GetValue(key, defaultValue);
}
public static string GetValue(string key) {
return Current.GetValue(key);
}
public static CommandLineArgs GetCurrentClean(){
CommandLineArgs args = Current.Clone();
args.RemoveFlag(ArgRestart);
args.RemoveFlag(ArgImportCookies);
args.RemoveFlag(ArgDeleteCookies);
args.RemoveFlag(ArgUpdated);
return args;
}
public static CommandLineArgs GetCurrentClean() {
CommandLineArgs args = Current.Clone();
args.RemoveFlag(ArgRestart);
args.RemoveFlag(ArgImportCookies);
args.RemoveFlag(ArgDeleteCookies);
args.RemoveFlag(ArgUpdated);
return args;
}
public static CommandLineArgs GetCurrentForInstaller(){
CommandLineArgs args = GetCurrentClean();
args.AddFlag(ArgUpdated);
return args;
}
public static CommandLineArgs GetCurrentForInstaller() {
CommandLineArgs args = GetCurrentClean();
args.AddFlag(ArgUpdated);
return args;
}
public static string GetCurrentForInstallerCmd(){
return GetCurrentForInstaller().ToString().Replace("\"", "::");
}
}
public static string GetCurrentForInstallerCmd() {
return GetCurrentForInstaller().ToString().Replace("\"", "::");
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Drawing;
using TweetDuck.Browser.Data;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Serialization.Converters;
using TweetLib.Core.Systems.Configuration;
using TweetLib.Core.Utils;
namespace TweetDuck.Configuration {
sealed class ConfigManager : IConfigManager {
public UserConfig User { get; }
public SystemConfig System { get; }
public PluginConfig Plugins { get; }
public event EventHandler ProgramRestartRequested;
private readonly FileConfigInstance<UserConfig> infoUser;
private readonly FileConfigInstance<SystemConfig> infoSystem;
private readonly PluginConfigInstance<PluginConfig> infoPlugins;
private readonly IConfigInstance<BaseConfig>[] infoList;
public ConfigManager() {
User = new UserConfig(this);
System = new SystemConfig(this);
Plugins = new PluginConfig(this);
infoList = new IConfigInstance<BaseConfig>[] {
infoUser = new FileConfigInstance<UserConfig>(Program.UserConfigFilePath, User, "program options"),
infoSystem = new FileConfigInstance<SystemConfig>(Program.SystemConfigFilePath, System, "system options"),
infoPlugins = new PluginConfigInstance<PluginConfig>(Program.PluginConfigFilePath, Plugins)
};
// TODO refactor further
infoUser.Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
infoUser.Serializer.RegisterTypeConverter(typeof(Point), new SingleTypeConverter<Point> {
ConvertToString = value => $"{value.X} {value.Y}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Point(elements[0], elements[1]);
}
});
infoUser.Serializer.RegisterTypeConverter(typeof(Size), new SingleTypeConverter<Size> {
ConvertToString = value => $"{value.Width} {value.Height}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Size(elements[0], elements[1]);
}
});
}
public void LoadAll() {
infoUser.Load();
infoSystem.Load();
infoPlugins.Load();
}
public void SaveAll() {
infoUser.Save();
infoSystem.Save();
infoPlugins.Save();
}
public void ReloadAll() {
infoUser.Reload();
infoSystem.Reload();
infoPlugins.Reload();
}
void IConfigManager.TriggerProgramRestartRequested() {
ProgramRestartRequested?.Invoke(this, EventArgs.Empty);
}
IConfigInstance<BaseConfig> IConfigManager.GetInstanceInfo(BaseConfig instance) {
Type instanceType = instance.GetType();
return Array.Find(infoList, info => info.Instance.GetType() == instanceType); // TODO handle null
}
}
}

View File

@@ -1,183 +0,0 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading;
using TweetDuck.Core.Utils;
namespace TweetDuck.Configuration{
sealed class LockManager{
private const int RetryDelay = 250;
public enum Result{
Success, HasProcess, Fail
}
private readonly string file;
private FileStream lockStream;
private Process lockingProcess;
public LockManager(string file){
this.file = file;
}
// Lock file
private bool ReleaseLockFileStream(){
if (lockStream != null){
lockStream.Dispose();
lockStream = null;
return true;
}
else{
return false;
}
}
private Result TryCreateLockFile(){
void CreateLockFileStream(){
lockStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read);
lockStream.Write(BitConverter.GetBytes(WindowsUtils.CurrentProcessID), 0, sizeof(int));
lockStream.Flush(true);
}
try{
CreateLockFileStream();
return Result.Success;
}catch(DirectoryNotFoundException){
try{
CreateLockFileStream();
return Result.Success;
}catch{
ReleaseLockFileStream();
return Result.Fail;
}
}catch(IOException){
return Result.HasProcess;
}catch{
ReleaseLockFileStream();
return Result.Fail;
}
}
// Lock management
public Result Lock(){
if (lockStream != null){
return Result.Success;
}
Result initialResult = TryCreateLockFile();
if (initialResult == Result.HasProcess){
try{
int pid;
using(FileStream fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)){
byte[] bytes = new byte[sizeof(int)];
fileStream.Read(bytes, 0, bytes.Length);
pid = BitConverter.ToInt32(bytes, 0);
}
try{
Process foundProcess = Process.GetProcessById(pid);
using(Process currentProcess = Process.GetCurrentProcess()){
if (foundProcess.MainModule.FileVersionInfo.InternalName == currentProcess.MainModule.FileVersionInfo.InternalName){
lockingProcess = foundProcess;
}
else{
foundProcess.Close();
}
}
}catch{
// GetProcessById throws ArgumentException if the process is missing
// Process.MainModule can throw exceptions in some cases
}
return lockingProcess == null ? Result.Fail : Result.HasProcess;
}catch{
return Result.Fail;
}
}
return initialResult;
}
public Result LockWait(int timeout){
for(int elapsed = 0; elapsed < timeout; elapsed += RetryDelay){
Result result = Lock();
if (result == Result.HasProcess){
Thread.Sleep(RetryDelay);
}
else{
return result;
}
}
return Lock();
}
public bool Unlock(){
if (ReleaseLockFileStream()){
try{
File.Delete(file);
}catch(Exception e){
Program.Reporter.Log(e.ToString());
return false;
}
}
return true;
}
// Locking process
public bool RestoreLockingProcess(int failTimeout){
if (lockingProcess != null && lockingProcess.MainWindowHandle == IntPtr.Zero){ // restore if the original process is in tray
NativeMethods.BroadcastMessage(Program.WindowRestoreMessage, (uint)lockingProcess.Id, 0);
if (WindowsUtils.TrySleepUntil(() => CheckLockingProcessExited() || (lockingProcess.MainWindowHandle != IntPtr.Zero && lockingProcess.Responding), failTimeout, RetryDelay)){
return true;
}
}
return false;
}
public bool CloseLockingProcess(int closeTimeout, int killTimeout){
if (lockingProcess != null){
try{
if (lockingProcess.CloseMainWindow()){
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, closeTimeout, RetryDelay);
}
if (!lockingProcess.HasExited){
lockingProcess.Kill();
WindowsUtils.TrySleepUntil(CheckLockingProcessExited, killTimeout, RetryDelay);
}
if (lockingProcess.HasExited){
lockingProcess.Dispose();
lockingProcess = null;
return true;
}
}catch(Exception ex) when (ex is InvalidOperationException || ex is Win32Exception){
if (lockingProcess != null){
bool hasExited = CheckLockingProcessExited();
lockingProcess.Dispose();
return hasExited;
}
}
}
return false;
}
private bool CheckLockingProcessExited(){
lockingProcess.Refresh();
return lockingProcess.HasExited;
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using TweetLib.Core.Features.Plugins;
using TweetLib.Core.Features.Plugins.Config;
using TweetLib.Core.Features.Plugins.Events;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Configuration {
sealed class PluginConfig : BaseConfig, IPluginConfig {
private static readonly string[] DefaultDisabled = {
"official/clear-columns",
"official/reply-account"
};
// CONFIGURATION DATA
private readonly HashSet<string> disabled = new HashSet<string>(DefaultDisabled);
// EVENTS
public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;
// END OF CONFIG
public PluginConfig(IConfigManager configManager) : base(configManager) {}
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
return new PluginConfig(configManager);
}
// INTERFACE IMPLEMENTATION
IEnumerable<string> IPluginConfig.DisabledPlugins => disabled;
void IPluginConfig.Reset(IEnumerable<string> newDisabledPlugins) {
disabled.Clear();
disabled.UnionWith(newDisabledPlugins);
}
public void SetEnabled(Plugin plugin, bool enabled) {
if ((enabled && disabled.Remove(plugin.Identifier)) || (!enabled && disabled.Add(plugin.Identifier))) {
PluginChangedState?.Invoke(this, new PluginChangedStateEventArgs(plugin, enabled));
Save();
}
}
public bool IsEnabled(Plugin plugin) {
return !disabled.Contains(plugin.Identifier);
}
}
}

View File

@@ -1,54 +1,27 @@
using System;
using System.IO;
using TweetDuck.Data.Serialization;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Configuration{
sealed class SystemConfig{
private static readonly FileSerializer<SystemConfig> Serializer = new FileSerializer<SystemConfig>();
namespace TweetDuck.Configuration {
sealed class SystemConfig : BaseConfig {
// CONFIGURATION DATA
public static readonly bool IsHardwareAccelerationSupported = File.Exists(Path.Combine(Program.ProgramPath, "libEGL.dll")) &&
File.Exists(Path.Combine(Program.ProgramPath, "libGLESv2.dll"));
private bool _hardwareAcceleration = true;
// CONFIGURATION DATA
public bool ClearCacheAutomatically { get; set; } = true;
public int ClearCacheThreshold { get; set; } = 250;
private bool _hardwareAcceleration = true;
// SPECIAL PROPERTIES
public bool ClearCacheAutomatically { get; set; } = true;
public int ClearCacheThreshold { get; set; } = 250;
public bool HardwareAcceleration {
get => _hardwareAcceleration;
set => UpdatePropertyWithRestartRequest(ref _hardwareAcceleration, value);
}
// SPECIAL PROPERTIES
// END OF CONFIG
public bool HardwareAcceleration{
get => _hardwareAcceleration && IsHardwareAccelerationSupported;
set => _hardwareAcceleration = value;
}
public SystemConfig(IConfigManager configManager) : base(configManager) {}
// END OF CONFIG
private readonly string file;
private SystemConfig(string file){
this.file = file;
}
public void Save(){
try{
Serializer.Write(file, this);
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not save the system configuration file.", true, e);
}
}
public static SystemConfig Load(string file){
SystemConfig config = new SystemConfig(file);
try{
Serializer.ReadIfExists(file, config);
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not open the system configuration file. If you continue, you will lose system specific configuration such as Hardware Acceleration.", true, e);
}
return config;
}
}
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
return new SystemConfig(configManager);
}
}
}

View File

@@ -1,238 +1,168 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.IO;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Data.Serialization;
using TweetDuck.Browser;
using TweetDuck.Browser.Data;
using TweetDuck.Controls;
using TweetLib.Core.Features.Notifications;
using TweetLib.Core.Features.Twitter;
using TweetLib.Core.Systems.Configuration;
namespace TweetDuck.Configuration{
sealed class UserConfig{
private static readonly FileSerializer<UserConfig> Serializer = new FileSerializer<UserConfig>();
namespace TweetDuck.Configuration {
sealed class UserConfig : BaseConfig {
// CONFIGURATION DATA
static UserConfig(){
Serializer.RegisterTypeConverter(typeof(WindowState), WindowState.Converter);
public bool FirstRun { get; set; } = true;
Serializer.RegisterTypeConverter(typeof(Point), new SingleTypeConverter<Point>{
ConvertToString = value => $"{value.X} {value.Y}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Point(elements[0], elements[1]);
}
});
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public bool AllowDataCollection { get; set; } = false;
Serializer.RegisterTypeConverter(typeof(Size), new SingleTypeConverter<Size>{
ConvertToString = value => $"{value.Width} {value.Height}",
ConvertToObject = value => {
int[] elements = StringUtils.ParseInts(value, ' ');
return new Size(elements[0], elements[1]);
}
});
}
public WindowState BrowserWindow { get; set; } = new WindowState();
public Size PluginsWindowSize { get; set; } = Size.Empty;
// CONFIGURATION DATA
public bool ExpandLinksOnHover { get; set; } = true;
public bool FocusDmInput { get; set; } = true;
public bool OpenSearchInFirstColumn { get; set; } = true;
public bool KeepLikeFollowDialogsOpen { get; set; } = true;
public bool BestImageQuality { get; set; } = true;
public bool EnableAnimatedImages { get; set; } = true;
public bool FirstRun { get; set; } = true;
public bool AllowDataCollection { get; set; } = false;
private bool _enableSmoothScrolling = true;
private bool _enableTouchAdjustment = false;
private bool _enableColorProfileDetection = false;
private string _customCefArgs = null;
public WindowState BrowserWindow { get; set; } = new WindowState();
public WindowState PluginsWindow { get; set; } = new WindowState();
public string BrowserPath { get; set; } = null;
public string BrowserPathArgs { get; set; } = null;
public bool IgnoreTrackingUrlWarning { get; set; } = false;
public string SearchEngineUrl { get; set; } = null;
private int _zoomLevel = 100;
public bool ExpandLinksOnHover { get; set; } = true;
public bool SwitchAccountSelectors { get; set; } = true;
public bool OpenSearchInFirstColumn { get; set; } = true;
public bool KeepLikeFollowDialogsOpen { get; set; } = true;
public bool BestImageQuality { get; set; } = true;
public bool EnableAnimatedImages { get; set; } = true;
public string VideoPlayerPath { get; set; } = null;
public string VideoPlayerPathArgs { get; set; } = null;
public int VideoPlayerVolume { get; set; } = 50;
public bool IgnoreTrackingUrlWarning { get; set; } = false;
public bool EnableSmoothScrolling { get; set; } = true;
public string BrowserPath { get; set; } = null;
private int _zoomLevel = 100;
private bool _muteNotifications;
public bool EnableSpellCheck { get; set; } = false;
private string _spellCheckLanguage = "en-US";
public int VideoPlayerVolume { get; set; } = 50;
public string TranslationTarget { get; set; } = "en";
public int CalendarFirstDay { get; set; } = -1;
public bool EnableSpellCheck { get; set; } = false;
public string SpellCheckLanguage { get; set; } = "en-US";
public string TranslationTarget { get; set; } = "en";
private TrayIcon.Behavior _trayBehavior = TrayIcon.Behavior.Disabled;
public bool EnableTrayHighlight { get; set; } = true;
private TrayIcon.Behavior _trayBehavior = TrayIcon.Behavior.Disabled;
public bool EnableTrayHighlight { get; set; } = true;
public bool EnableUpdateCheck { get; set; } = true;
public string DismissedUpdate { get; set; } = null;
public bool EnableUpdateCheck { get; set; } = true;
public string DismissedUpdate { get; set; } = null;
public bool DisplayNotificationColumn { get; set; } = false;
public bool NotificationMediaPreviews { get; set; } = true;
public bool NotificationSkipOnLinkClick { get; set; } = false;
public bool NotificationNonIntrusiveMode { get; set; } = true;
public int NotificationIdlePauseSeconds { get; set; } = 0;
public bool DisplayNotificationColumn { get; set; } = false;
public bool NotificationMediaPreviews { get; set; } = true;
public bool NotificationSkipOnLinkClick { get; set; } = false;
public bool NotificationNonIntrusiveMode { get; set; } = true;
public int NotificationIdlePauseSeconds { get; set; } = 0;
public bool DisplayNotificationTimer { get; set; } = true;
public bool NotificationTimerCountDown { get; set; } = false;
public int NotificationDurationValue { get; set; } = 25;
public bool DisplayNotificationTimer { get; set; } = true;
public bool NotificationTimerCountDown { get; set; } = false;
public int NotificationDurationValue { get; set; } = 25;
public DesktopNotification.Position NotificationPosition { get; set; } = DesktopNotification.Position.TopRight;
public Point CustomNotificationPosition { get; set; } = ControlExtensions.InvisibleLocation;
public int NotificationDisplay { get; set; } = 0;
public int NotificationEdgeDistance { get; set; } = 8;
public int NotificationWindowOpacity { get; set; } = 100;
public TweetNotification.Position NotificationPosition { get; set; } = TweetNotification.Position.TopRight;
public Point CustomNotificationPosition { get; set; } = ControlExtensions.InvisibleLocation;
public int NotificationDisplay { get; set; } = 0;
public int NotificationEdgeDistance { get; set; } = 8;
public DesktopNotification.Size NotificationSize { get; set; } = DesktopNotification.Size.Auto;
public Size CustomNotificationSize { get; set; } = Size.Empty;
public int NotificationScrollSpeed { get; set; } = 100;
public TweetNotification.Size NotificationSize { get; set; } = TweetNotification.Size.Auto;
public Size CustomNotificationSize { get; set; } = Size.Empty;
public int NotificationScrollSpeed { get; set; } = 100;
private string _notificationSoundPath;
private int _notificationSoundVolume = 100;
private string _notificationSoundPath;
private int _notificationSoundVolume = 100;
private bool _muteNotifications;
public string CustomCefArgs { get; set; } = null;
public string CustomBrowserCSS { get; set; } = null;
public string CustomNotificationCSS { get; set; } = null;
public string CustomBrowserCSS { get; set; } = null;
public string CustomNotificationCSS { get; set; } = null;
// SPECIAL PROPERTIES
private bool _useSystemProxyForAllConnections;
public bool IsCustomNotificationPositionSet => CustomNotificationPosition != ControlExtensions.InvisibleLocation;
public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty;
public bool DevToolsInContextMenu { get; set; } = false;
public bool DevToolsWindowOnTop { get; set; } = true;
public TwitterUtils.ImageQuality TwitterImageQuality => BestImageQuality ? TwitterUtils.ImageQuality.Orig : TwitterUtils.ImageQuality.Default;
// SPECIAL PROPERTIES
public string NotificationSoundPath{
get => _notificationSoundPath ?? string.Empty;
set => UpdatePropertyWithEvent(ref _notificationSoundPath, value, SoundNotificationChanged);
}
public bool IsCustomNotificationPositionSet => CustomNotificationPosition != ControlExtensions.InvisibleLocation;
public bool IsCustomNotificationSizeSet => CustomNotificationSize != Size.Empty;
public bool IsCustomSoundNotificationSet => NotificationSoundPath != string.Empty;
public int NotificationSoundVolume{
get => _notificationSoundVolume;
set => UpdatePropertyWithEvent(ref _notificationSoundVolume, value, SoundNotificationChanged);
}
public ImageQuality TwitterImageQuality => BestImageQuality ? ImageQuality.Best : ImageQuality.Default;
public bool MuteNotifications{
get => _muteNotifications;
set => UpdatePropertyWithEvent(ref _muteNotifications, value, MuteToggled);
}
public string NotificationSoundPath {
get => _notificationSoundPath ?? string.Empty;
set => UpdatePropertyWithEvent(ref _notificationSoundPath, value, SoundNotificationChanged);
}
public int ZoomLevel{
get => _zoomLevel;
set => UpdatePropertyWithEvent(ref _zoomLevel, value, ZoomLevelChanged);
}
public int NotificationSoundVolume {
get => _notificationSoundVolume;
set => UpdatePropertyWithEvent(ref _notificationSoundVolume, value, SoundNotificationChanged);
}
public TrayIcon.Behavior TrayBehavior{
get => _trayBehavior;
set => UpdatePropertyWithEvent(ref _trayBehavior, value, TrayBehaviorChanged);
}
public bool MuteNotifications {
get => _muteNotifications;
set => UpdatePropertyWithEvent(ref _muteNotifications, value, MuteToggled);
}
// EVENTS
public int ZoomLevel {
get => _zoomLevel;
set => UpdatePropertyWithEvent(ref _zoomLevel, value, ZoomLevelChanged);
}
public event EventHandler MuteToggled;
public event EventHandler ZoomLevelChanged;
public event EventHandler TrayBehaviorChanged;
public event EventHandler SoundNotificationChanged;
public TrayIcon.Behavior TrayBehavior {
get => _trayBehavior;
set => UpdatePropertyWithEvent(ref _trayBehavior, value, TrayBehaviorChanged);
}
// END OF CONFIG
public bool EnableSmoothScrolling {
get => _enableSmoothScrolling;
set => UpdatePropertyWithRestartRequest(ref _enableSmoothScrolling, value);
}
private readonly string file;
public bool EnableTouchAdjustment {
get => _enableTouchAdjustment;
set => UpdatePropertyWithRestartRequest(ref _enableTouchAdjustment, value);
}
private UserConfig(string file){
this.file = file;
}
public bool EnableColorProfileDetection {
get => _enableColorProfileDetection;
set => UpdatePropertyWithRestartRequest(ref _enableColorProfileDetection, value);
}
private void UpdatePropertyWithEvent<T>(ref T field, T value, EventHandler eventHandler){
if (!EqualityComparer<T>.Default.Equals(field, value)){
field = value;
eventHandler?.Invoke(this, EventArgs.Empty);
}
}
public bool UseSystemProxyForAllConnections {
get => _useSystemProxyForAllConnections;
set => UpdatePropertyWithRestartRequest(ref _useSystemProxyForAllConnections, value);
}
public void Save(){
try{
if (File.Exists(file)){
string backupFile = GetBackupFile(file);
File.Delete(backupFile);
File.Move(file, backupFile);
}
public string CustomCefArgs {
get => _customCefArgs;
set => UpdatePropertyWithRestartRequest(ref _customCefArgs, value);
}
Serializer.Write(file, this);
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not save the configuration file.", true, e);
}
}
public string SpellCheckLanguage {
get => _spellCheckLanguage;
set => UpdatePropertyWithRestartRequest(ref _spellCheckLanguage, value);
}
public void Reload(){
try{
LoadInternal(false);
}catch(FileNotFoundException){
try{
Serializer.Write(file, new UserConfig(file));
LoadInternal(false);
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not regenerate configuration file.", true, e);
}
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not reload configuration file.", true, e);
}
}
// EVENTS
public void Reset(){
try{
File.Delete(file);
File.Delete(GetBackupFile(file));
}catch(Exception e){
Program.Reporter.HandleException("Configuration Error", "Could not delete configuration files to reset the options.", true, e);
return;
}
public event EventHandler MuteToggled;
public event EventHandler ZoomLevelChanged;
public event EventHandler TrayBehaviorChanged;
public event EventHandler SoundNotificationChanged;
Reload();
}
// END OF CONFIG
private void LoadInternal(bool backup){
Serializer.Read(backup ? GetBackupFile(file) : file, this);
public UserConfig(IConfigManager configManager) : base(configManager) {}
if (NotificationScrollSpeed == 10){ // incorrect initial value
NotificationScrollSpeed = 100;
Save();
}
}
public static UserConfig Load(string file){
Exception firstException = null;
for(int attempt = 0; attempt < 2; attempt++){
try{
UserConfig config = new UserConfig(file);
config.LoadInternal(attempt > 0);
return config;
}catch(FileNotFoundException){
}catch(DirectoryNotFoundException){
break;
}catch(Exception e){
if (attempt == 0){
firstException = e;
Program.Reporter.Log(e.ToString());
}
else if (firstException is FormatException){
Program.Reporter.HandleException("Configuration Error", "The configuration file is outdated or corrupted. If you continue, your program options will be reset.", true, e);
return new UserConfig(file);
}
else if (firstException != null){
Program.Reporter.HandleException("Configuration Error", "Could not open the backup configuration file. If you continue, your program options will be reset.", true, e);
return new UserConfig(file);
}
}
}
if (firstException != null){
Program.Reporter.HandleException("Configuration Error", "Could not open the configuration file.", true, firstException);
}
return new UserConfig(file);
}
public static string GetBackupFile(string file){
return file+".bak";
}
}
protected override BaseConfig ConstructWithDefaults(IConfigManager configManager) {
return new UserConfig(configManager);
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace TweetDuck.Controls {
static class ControlExtensions {
public static readonly Point InvisibleLocation = new Point(-32000, -32000);
public static void InvokeSafe(this Control control, Action func) {
if (control.InvokeRequired) {
control.Invoke(func);
}
else {
func();
}
}
public static void InvokeAsyncSafe(this Control control, Action func) {
control.BeginInvoke(func);
}
public static float GetDPIScale(this Control control) {
using Graphics graphics = control.CreateGraphics();
return graphics.DpiY / 96F;
}
public static bool IsFullyOutsideView(this Form form) {
return !Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(form.Bounds));
}
public static void MoveToCenter(this Form targetForm, Form parentForm) {
targetForm.Location = new Point(parentForm.Location.X + (parentForm.Width / 2) - (targetForm.Width / 2), parentForm.Location.Y + (parentForm.Height / 2) - (targetForm.Height / 2));
}
public static void SetValueInstant(this ProgressBar bar, int value) {
if (value == bar.Maximum) {
bar.Value = value;
bar.Value = value - 1;
bar.Value = value;
}
else {
bar.Value = value + 1;
bar.Value = value;
}
}
public static void SetValueSafe(this NumericUpDown numUpDown, int value) {
if (value >= numUpDown.Minimum && value <= numUpDown.Maximum) {
numUpDown.Value = value;
}
}
public static void SetValueSafe(this TrackBar trackBar, int value) {
if (value >= trackBar.Minimum && value <= trackBar.Maximum) {
trackBar.Value = value;
}
}
public static bool AlignValueToTick(this TrackBar trackBar) {
if (trackBar.Value % trackBar.SmallChange != 0) {
trackBar.Value = trackBar.SmallChange * (int) Math.Floor(((double) trackBar.Value / trackBar.SmallChange) + 0.5);
return false;
}
return true;
}
public static void EnableMultilineShortcuts(this TextBox textBox) {
textBox.KeyDown += (sender, args) => {
if (args.Control && args.KeyCode == Keys.A) {
((TextBox) sender).SelectAll();
args.SuppressKeyPress = true;
args.Handled = true;
}
};
}
}
}

16
Controls/FlatButton.cs Normal file
View File

@@ -0,0 +1,16 @@
using System;
using System.Windows.Forms;
namespace TweetDuck.Controls {
sealed class FlatButton : Button {
protected override bool ShowFocusCues => false;
public FlatButton() {
GotFocus += FlatButton_GotFocus;
}
private void FlatButton_GotFocus(object sender, EventArgs e) { // removes extra border when focused
NotifyDefault(false);
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Controls {
sealed class FlatProgressBar : ProgressBar {
private readonly SolidBrush brush;
public FlatProgressBar() {
brush = new SolidBrush(Color.White);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
public void SetValueInstant(int value) {
ControlExtensions.SetValueInstant(this, Math.Max(Minimum, Math.Min(Maximum, value)));
}
protected override void OnPaint(PaintEventArgs e) {
if (brush.Color != ForeColor) {
brush.Color = ForeColor;
}
Rectangle rect = e.ClipRectangle;
rect.Width = (int) (rect.Width * ((double) Value / Maximum));
e.Graphics.FillRectangle(brush, rect);
}
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
if (disposing) {
brush.Dispose();
}
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Windows.Forms;
using TweetDuck.Utils;
namespace TweetDuck.Controls {
sealed class FlowLayoutPanelNoHScroll : FlowLayoutPanel {
protected override void WndProc(ref Message m) {
if (m.Msg == 0x85) { // WM_NCPAINT
NativeMethods.ShowScrollBar(Handle, NativeMethods.SB_HORZ, false); // basically fuck the horizontal scrollbar very much
}
base.WndProc(ref m);
}
}
}

22
Controls/LabelVertical.cs Normal file
View File

@@ -0,0 +1,22 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Controls {
sealed class LabelVertical : Label {
public int LineHeight { get; set; }
protected override void OnPaint(PaintEventArgs e) {
int y = (int) Math.Floor((ClientRectangle.Height - Text.Length * LineHeight) / 2F) - 1;
using Brush brush = new SolidBrush(ForeColor);
foreach (char chr in Text) {
string str = chr.ToString();
float x = (ClientRectangle.Width - e.Graphics.MeasureString(str, Font).Width) / 2F;
e.Graphics.DrawString(str, Font, brush, x, y);
y += LineHeight;
}
}
}
}

View File

@@ -0,0 +1,18 @@
using System.ComponentModel;
using System.Windows.Forms;
namespace TweetDuck.Controls {
sealed class NumericUpDownEx : NumericUpDown {
public string TextSuffix { get; set ; }
protected override void UpdateEditText() {
base.UpdateEditText();
if (LicenseManager.UsageMode != LicenseUsageMode.Designtime) {
ChangingText = true;
Text += TextSuffix;
ChangingText = false;
}
}
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
namespace TweetDuck.Core.Bridge{
sealed class CallbackBridge{
private readonly Control owner;
private readonly Action safeCallback;
public CallbackBridge(Control owner, Action safeCallback){
this.owner = owner;
this.safeCallback = safeCallback;
}
public void Trigger(){
owner.InvokeSafe(safeCallback);
}
}
}

View File

@@ -1,33 +0,0 @@
using System.Text;
namespace TweetDuck.Core.Bridge{
static class PropertyBridge{
public enum Environment{
Browser, Notification
}
public static string GenerateScript(Environment environment){
string Bool(bool value) => value ? "true;" : "false;";
string Str(string value) => '"'+value+"\";";
StringBuilder build = new StringBuilder().Append("(function(x){");
build.Append("x.expandLinksOnHover=").Append(Bool(Program.UserConfig.ExpandLinksOnHover));
if (environment == Environment.Browser){
build.Append("x.switchAccountSelectors=").Append(Bool(Program.UserConfig.SwitchAccountSelectors));
build.Append("x.openSearchInFirstColumn=").Append(Bool(Program.UserConfig.OpenSearchInFirstColumn));
build.Append("x.keepLikeFollowDialogsOpen=").Append(Bool(Program.UserConfig.KeepLikeFollowDialogsOpen));
build.Append("x.muteNotifications=").Append(Bool(Program.UserConfig.MuteNotifications));
build.Append("x.notificationMediaPreviews=").Append(Bool(Program.UserConfig.NotificationMediaPreviews));
build.Append("x.translationTarget=").Append(Str(Program.UserConfig.TranslationTarget));
}
if (environment == Environment.Notification){
build.Append("x.skipOnLinkClick=").Append(Bool(Program.UserConfig.NotificationSkipOnLinkClick));
}
return build.Append("})(window.$TDX=window.$TDX||{})").ToString();
}
}
}

View File

@@ -1,173 +0,0 @@
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetDuck.Resources;
namespace TweetDuck.Core.Bridge{
class TweetDeckBridge{
public static string FontSize { get; private set; }
public static string NotificationHeadLayout { get; private set; }
public static string LastHighlightedTweetUrl = string.Empty;
public static string LastHighlightedQuoteUrl = string.Empty;
private static string LastHighlightedTweetAuthors = string.Empty;
private static string LastHighlightedTweetImages = string.Empty;
public static string[] LastHighlightedTweetAuthorsArray => LastHighlightedTweetAuthors.Split(';');
public static string[] LastHighlightedTweetImagesArray => LastHighlightedTweetImages.Split(';');
private static readonly Dictionary<string, string> SessionData = new Dictionary<string, string>(2);
public static void ResetStaticProperties(){
FontSize = NotificationHeadLayout = null;
LastHighlightedTweetUrl = LastHighlightedQuoteUrl = LastHighlightedTweetAuthors = LastHighlightedTweetImages = string.Empty;
}
public static void RestoreSessionData(IFrame frame){
if (SessionData.Count > 0){
StringBuilder build = new StringBuilder().Append("window.TD_SESSION={");
foreach(KeyValuePair<string, string> kvp in SessionData){
build.Append(kvp.Key).Append(":'").Append(kvp.Value.Replace("'", "\\'")).Append("',");
}
ScriptLoader.ExecuteScript(frame, build.Append("}").ToString(), "gen:session");
SessionData.Clear();
}
}
private readonly FormBrowser form;
private readonly FormNotificationMain notification;
protected TweetDeckBridge(FormBrowser form, FormNotificationMain notification){
this.form = form;
this.notification = notification;
}
// Browser only
public sealed class Browser : TweetDeckBridge{
public Browser(FormBrowser form, FormNotificationMain notification) : base(form, notification){}
public void OpenContextMenu(){
form.InvokeAsyncSafe(form.OpenContextMenu);
}
public void OnIntroductionClosed(bool showGuide, bool allowDataCollection){
form.InvokeAsyncSafe(() => {
form.OnIntroductionClosed(showGuide, allowDataCollection);
});
}
public void LoadNotificationLayout(string fontSize, string headLayout){
form.InvokeAsyncSafe(() => {
FontSize = fontSize;
NotificationHeadLayout = headLayout;
});
}
public void SetLastHighlightedTweet(string tweetUrl, string quoteUrl, string authors, string imageList){
form.InvokeAsyncSafe(() => {
LastHighlightedTweetUrl = tweetUrl;
LastHighlightedQuoteUrl = quoteUrl;
LastHighlightedTweetAuthors = authors;
LastHighlightedTweetImages = imageList;
});
}
public void DisplayTooltip(string text){
form.InvokeAsyncSafe(() => form.DisplayTooltip(text));
}
public void SetSessionData(string key, string value){
form.InvokeSafe(() => { // do not use InvokeAsyncSafe, return only after invocation
SessionData.Add(key, value);
});
}
}
// Notification only
public sealed class Notification : TweetDeckBridge{
public Notification(FormBrowser form, FormNotificationMain notification) : base(form, notification){}
public void DisplayTooltip(string text){
notification.InvokeAsyncSafe(() => notification.DisplayTooltip(text));
}
public void LoadNextNotification(){
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
}
public void ShowTweetDetail(){
notification.InvokeAsyncSafe(notification.ShowTweetDetail);
}
}
// Global
public void SetLastRightClickInfo(string type, string link){
form.InvokeAsyncSafe(() => ContextMenuBase.SetContextInfo(type, link));
}
public void OnTweetPopup(string columnId, string chirpId, string columnName, string tweetHtml, int tweetCharacters, string tweetUrl, string quoteUrl){
notification.InvokeAsyncSafe(() => {
form.OnTweetNotification();
notification.ShowNotification(new TweetNotification(columnId, chirpId, columnName, tweetHtml, tweetCharacters, tweetUrl, quoteUrl));
});
}
public void OnTweetSound(){
form.InvokeAsyncSafe(() => {
form.OnTweetNotification();
form.OnTweetSound();
});
}
public void ScreenshotTweet(string html, int width, int height){
form.InvokeAsyncSafe(() => form.OnTweetScreenshotReady(html, width, height));
}
public void PlayVideo(string url, string username){
form.InvokeAsyncSafe(() => form.PlayVideo(url, username));
}
public void FixClipboard(){
form.InvokeAsyncSafe(WindowsUtils.ClipboardStripHtmlStyles);
}
public void OpenBrowser(string url){
form.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
}
public int GetIdleSeconds(){
return NativeMethods.GetIdleSeconds();
}
public void Alert(string type, string contents){
MessageBoxIcon icon;
switch(type){
case "error": icon = MessageBoxIcon.Error; break;
case "warning": icon = MessageBoxIcon.Warning; break;
case "info": icon = MessageBoxIcon.Information; break;
default: icon = MessageBoxIcon.None; break;
}
FormMessage.Show("TweetDuck Browser Message", contents, icon, FormMessage.OK);
}
public void CrashDebug(string message){
#if DEBUG
System.Diagnostics.Debug.WriteLine(message);
System.Diagnostics.Debugger.Break();
#endif
}
}
}

View File

@@ -1,79 +0,0 @@
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace TweetDuck.Core.Controls{
static class ControlExtensions{
public static readonly Point InvisibleLocation = new Point(-32000, -32000);
public static void InvokeSafe(this Control control, Action func){
if (control.InvokeRequired){
control.Invoke(func);
}
else{
func();
}
}
public static void InvokeAsyncSafe(this Control control, Action func){
control.BeginInvoke(func);
}
public static float GetDPIScale(this Control control){
using(Graphics graphics = control.CreateGraphics()){
return graphics.DpiY/96F;
}
}
public static bool IsFullyOutsideView(this Form form){
return !Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(form.Bounds));
}
public static void MoveToCenter(this Form targetForm, Form parentForm){
targetForm.Location = new Point(parentForm.Location.X+parentForm.Width/2-targetForm.Width/2, parentForm.Location.Y+parentForm.Height/2-targetForm.Height/2);
}
public static void SetValueInstant(this ProgressBar bar, int value){
if (value == bar.Maximum){
bar.Value = value;
bar.Value = value-1;
bar.Value = value;
}
else{
bar.Value = value+1;
bar.Value = value;
}
}
public static void SetValueSafe(this NumericUpDown numUpDown, int value){
if (value >= numUpDown.Minimum && value <= numUpDown.Maximum){
numUpDown.Value = value;
}
}
public static void SetValueSafe(this TrackBar trackBar, int value){
if (value >= trackBar.Minimum && value <= trackBar.Maximum){
trackBar.Value = value;
}
}
public static bool AlignValueToTick(this TrackBar trackBar){
if (trackBar.Value % trackBar.SmallChange != 0){
trackBar.Value = trackBar.SmallChange*(int)Math.Floor(((double)trackBar.Value/trackBar.SmallChange)+0.5);
return false;
}
else return true;
}
public static void EnableMultilineShortcuts(this TextBox textBox){
textBox.KeyDown += (sender, args) => {
if (args.Control && args.KeyCode == Keys.A){
((TextBox)sender).SelectAll();
args.SuppressKeyPress = true;
args.Handled = true;
}
};
}
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Windows.Forms;
namespace TweetDuck.Core.Controls{
sealed class FlatButton : Button{
protected override bool ShowFocusCues => false;
public FlatButton(){
GotFocus += FlatButton_GotFocus;
}
private void FlatButton_GotFocus(object sender, EventArgs e){ // removes extra border when focused
NotifyDefault(false);
}
}
}

View File

@@ -1,38 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Core.Controls{
sealed class FlatProgressBar : ProgressBar{
private readonly SolidBrush brush;
public FlatProgressBar(){
brush = new SolidBrush(Color.White);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
public void SetValueInstant(int value){
ControlExtensions.SetValueInstant(this, Math.Max(Minimum, Math.Min(Maximum, value)));
}
protected override void OnPaint(PaintEventArgs e){
if (brush.Color != ForeColor){
brush.Color = ForeColor;
}
Rectangle rect = e.ClipRectangle;
rect.Width = (int)(rect.Width*((double)Value/Maximum));
e.Graphics.FillRectangle(brush, rect);
}
protected override void Dispose(bool disposing){
base.Dispose(disposing);
if (disposing){
brush.Dispose();
}
}
}
}

View File

@@ -1,23 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace TweetDuck.Core.Controls{
sealed class LabelVertical : Label{
public int LineHeight { get; set; }
protected override void OnPaint(PaintEventArgs e){
int y = (int)Math.Floor((ClientRectangle.Height-Text.Length*LineHeight)/2F)-2; // 2 = random
using(Brush brush = new SolidBrush(ForeColor)){
foreach(char chr in Text){
string str = chr.ToString();
float x = (ClientRectangle.Width-e.Graphics.MeasureString(str, Font).Width)/2F;
e.Graphics.DrawString(str, Font, brush, x, y);
y += LineHeight;
}
}
}
}
}

View File

@@ -1,18 +0,0 @@
using System.ComponentModel;
using System.Windows.Forms;
namespace TweetDuck.Core.Controls{
sealed class NumericUpDownEx : NumericUpDown{
public string TextSuffix { get; set ; }
protected override void UpdateEditText(){
base.UpdateEditText();
if (LicenseManager.UsageMode != LicenseUsageMode.Designtime){
ChangingText = true;
Text += TextSuffix;
ChangingText = false;
}
}
}
}

View File

@@ -1,475 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Management;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Screenshot;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Events;
using TweetDuck.Updates;
namespace TweetDuck.Core{
sealed partial class FormBrowser : Form, AnalyticsFile.IProvider{
private static UserConfig Config => Program.UserConfig;
public bool IsWaiting{
set{
if (value){
browser.Enabled = false;
Cursor = Cursors.WaitCursor;
}
else{
browser.Enabled = true;
Cursor = Cursors.Default;
if (Focused){ // re-focus browser only if the window or a child is activated
browser.Focus();
}
}
}
}
public string UpdateInstallerPath { get; private set; }
public AnalyticsFile AnalyticsFile => analytics?.File ?? AnalyticsFile.Dummy;
private readonly TweetDeckBrowser browser;
private readonly PluginManager plugins;
private readonly UpdateHandler updates;
private readonly FormNotificationTweet notification;
private readonly ContextMenu contextMenu;
private bool isLoaded;
private FormWindowState prevState;
private TweetScreenshotManager notificationScreenshotManager;
private VideoPlayer videoPlayer;
private AnalyticsManager analytics;
public FormBrowser(UpdaterSettings updaterSettings){
InitializeComponent();
Text = Program.BrandName;
this.browser = new TweetDeckBrowser(this, new TweetDeckBridge.Browser(this, notification));
this.contextMenu = ContextMenuBrowser.CreateMenu(this);
this.plugins = new PluginManager(browser, Program.PluginPath, Program.PluginConfigFilePath);
this.plugins.Reloaded += plugins_Reloaded;
this.plugins.Executed += plugins_Executed;
this.plugins.Reload();
this.notification = new FormNotificationTweet(this, plugins);
this.notification.Show();
Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
Disposed += (sender, args) => {
Config.MuteToggled -= Config_MuteToggled;
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
browser.Dispose();
contextMenu.Dispose();
notificationScreenshotManager?.Dispose();
videoPlayer?.Dispose();
analytics?.Dispose();
};
Config.MuteToggled += Config_MuteToggled;
this.trayIcon.ClickRestore += trayIcon_ClickRestore;
this.trayIcon.ClickClose += trayIcon_ClickClose;
Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;
UpdateTrayIcon();
this.updates = new UpdateHandler(browser, updaterSettings);
this.updates.UpdateAccepted += updates_UpdateAccepted;
this.updates.UpdateDismissed += updates_UpdateDismissed;
if (Config.AllowDataCollection){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
RestoreWindow();
}
private void ShowChildForm(Form form){
form.VisibleChanged += (sender, args) => form.MoveToCenter(this);
form.Show(this);
}
public void ForceClose(){
trayIcon.Visible = false; // checked in FormClosing event
Close();
}
// window setup
private void RestoreWindow(){
Config.BrowserWindow.Restore(this, true);
prevState = WindowState;
isLoaded = true;
}
private void UpdateTrayIcon(){
trayIcon.Visible = Config.TrayBehavior.ShouldDisplayIcon();
}
// event handlers
private void timerResize_Tick(object sender, EventArgs e){
FormBrowser_ResizeEnd(this, e); // also stops timer
}
private void FormBrowser_Activated(object sender, EventArgs e){
if (!isLoaded)return;
trayIcon.HasNotifications = false;
if (!browser.Enabled){ // when taking a screenshot, the window is unfocused and
browser.Enabled = true; // the browser is disabled; if the user clicks back into
} // the window, enable the browser again
}
private void FormBrowser_LocationChanged(object sender, EventArgs e){
if (!isLoaded)return;
timerResize.Stop();
timerResize.Start();
}
private void FormBrowser_Resize(object sender, EventArgs e){
if (!isLoaded)return;
if (WindowState != prevState){
prevState = WindowState;
if (WindowState == FormWindowState.Minimized){
if (Config.TrayBehavior.ShouldHideOnMinimize()){
Hide(); // hides taskbar too?! welp that works I guess
}
}
else{
FormBrowser_ResizeEnd(sender, e);
}
}
else{
timerResize.Stop();
timerResize.Start();
}
}
private void FormBrowser_ResizeEnd(object sender, EventArgs e){ // also triggers when the window moves
if (!isLoaded)return;
timerResize.Stop();
if (Location != ControlExtensions.InvisibleLocation){
Config.BrowserWindow.Save(this);
Config.Save();
}
}
private void FormBrowser_FormClosing(object sender, FormClosingEventArgs e){
if (!isLoaded)return;
if (Config.TrayBehavior.ShouldHideOnClose() && trayIcon.Visible && e.CloseReason == CloseReason.UserClosing){
Hide(); // hides taskbar too?! welp that works I guess
e.Cancel = true;
}
}
private void FormBrowser_FormClosed(object sender, FormClosedEventArgs e){
if (isLoaded && UpdateInstallerPath == null){
updates.CleanupDownload();
}
}
private void Config_MuteToggled(object sender, EventArgs e){
AnalyticsFile.NotificationMutes.Trigger();
}
private void Config_TrayBehaviorChanged(object sender, EventArgs e){
UpdateTrayIcon();
}
private void trayIcon_ClickRestore(object sender, EventArgs e){
Show();
RestoreWindow();
Activate();
UpdateTrayIcon();
}
private void trayIcon_ClickClose(object sender, EventArgs e){
ForceClose();
}
private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
FormMessage.Error("Error Loading Plugins", "The following plugins will not be available until the issues are resolved:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
}
if (isLoaded){
browser.ReloadToTweetDeck();
}
}
private static void plugins_Executed(object sender, PluginErrorEventArgs e){
if (e.HasErrors){
FormMessage.Error("Error Executing Plugins", "Failed to execute the following plugins:\n\n"+string.Join("\n\n", e.Errors), FormMessage.OK);
}
}
private void updates_UpdateAccepted(object sender, UpdateEventArgs e){
this.InvokeAsyncSafe(() => {
FormManager.CloseAllDialogs();
if (!string.IsNullOrEmpty(Config.DismissedUpdate)){
Config.DismissedUpdate = null;
Config.Save();
}
updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
if (update.DownloadStatus == UpdateDownloadStatus.Done){
UpdateInstallerPath = update.InstallerPath;
}
ForceClose();
});
});
}
private void updates_UpdateDismissed(object sender, UpdateEventArgs e){
this.InvokeAsyncSafe(() => {
Config.DismissedUpdate = e.UpdateInfo.VersionTag;
Config.Save();
});
}
protected override void WndProc(ref Message m){
if (isLoaded && m.Msg == Program.WindowRestoreMessage){
if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
trayIcon_ClickRestore(trayIcon, EventArgs.Empty);
}
return;
}
if (browser.Ready && m.Msg == NativeMethods.WM_PARENTNOTIFY && (m.WParam.ToInt32() & 0xFFFF) == NativeMethods.WM_XBUTTONDOWN){
if (videoPlayer != null && videoPlayer.Running){
videoPlayer.Close();
}
else{
browser.OnMouseClickExtra(m.WParam);
AnalyticsFile.BrowserExtraMouseButtons.Trigger();
}
return;
}
base.WndProc(ref m);
}
// bridge methods
public void PauseNotification(){
notification.PauseNotification();
}
public void ResumeNotification(){
notification.ResumeNotification();
}
public void ReinjectCustomCSS(string css){
browser.ReinjectCustomCSS(css);
}
public void ReloadToTweetDeck(){
browser.ReloadToTweetDeck();
AnalyticsFile.BrowserReloads.Trigger();
}
public void TriggerTweetScreenshot(){
browser.TriggerTweetScreenshot();
}
public void ReloadColumns(){
browser.ReloadColumns();
}
public void PlaySoundNotification(){
browser.PlaySoundNotification();
}
public void ApplyROT13(){
browser.ApplyROT13();
AnalyticsFile.UsedROT13.Trigger();
}
// callback handlers
public void OnIntroductionClosed(bool showGuide, bool allowDataCollection){
if (Config.FirstRun){
Config.FirstRun = false;
Config.AllowDataCollection = allowDataCollection;
Config.Save();
if (allowDataCollection && analytics == null){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
}
if (showGuide){
FormGuide.Show();
}
}
public void OpenContextMenu(){
contextMenu.Show(this, PointToClient(Cursor.Position));
}
public void OpenSettings(){
OpenSettings(null);
}
public void OpenSettings(Type startTab){
if (!FormManager.TryBringToFront<FormSettings>()){
bool prevEnableUpdateCheck = Config.EnableUpdateCheck;
FormSettings form = new FormSettings(this, plugins, updates, analytics, startTab);
form.FormClosed += (sender, args) => {
if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
Config.DismissedUpdate = null;
Config.Save();
updates.Check(true);
}
if (!Config.EnableTrayHighlight){
trayIcon.HasNotifications = false;
}
if (Config.AllowDataCollection){
if (analytics == null){
analytics = new AnalyticsManager(this, plugins, Program.AnalyticsFilePath);
}
}
else if (analytics != null){
analytics.Dispose();
analytics = null;
}
BrowserCache.RefreshTimer();
if (form.ShouldReloadBrowser){
FormManager.TryFind<FormPlugins>()?.Close();
plugins.Reload(); // also reloads the browser
}
else{
browser.UpdateProperties();
}
notification.RequiresResize = true;
form.Dispose();
};
AnalyticsFile.OpenOptions.Trigger();
ShowChildForm(form);
}
}
public void OpenAbout(){
if (!FormManager.TryBringToFront<FormAbout>()){
AnalyticsFile.OpenAbout.Trigger();
ShowChildForm(new FormAbout());
}
}
public void OpenPlugins(){
if (!FormManager.TryBringToFront<FormPlugins>()){
AnalyticsFile.OpenPlugins.Trigger();
ShowChildForm(new FormPlugins(plugins));
}
}
public void OnTweetNotification(){ // may be called multiple times, once for each type of notification
if (Config.EnableTrayHighlight && !ContainsFocus){
trayIcon.HasNotifications = true;
}
}
public void OnTweetSound(){
AnalyticsFile.SoundNotifications.Trigger();
}
public void PlayVideo(string url, string username){
if (string.IsNullOrEmpty(url)){
videoPlayer?.Close();
return;
}
if (videoPlayer == null){
videoPlayer = new VideoPlayer(this);
videoPlayer.ProcessExited += (sender, args) => {
browser.HideVideoOverlay(true);
};
}
videoPlayer.Launch(url, username);
AnalyticsFile.VideoPlays.Trigger();
}
public bool ProcessBrowserKey(Keys key){
if (videoPlayer != null && videoPlayer.Running){
videoPlayer.SendKeyEvent(key);
return true;
}
return false;
}
public void ShowTweetDetail(string columnId, string chirpId, string fallbackUrl){
Activate();
if (!browser.IsTweetDeckWebsite){
FormMessage.Error("View Tweet Detail", "TweetDeck is not currently loaded.", FormMessage.OK);
return;
}
notification.FinishCurrentNotification();
browser.ShowTweetDetail(columnId, chirpId, fallbackUrl);
AnalyticsFile.TweetDetails.Trigger();
}
public void OnTweetScreenshotReady(string html, int width, int height){
if (notificationScreenshotManager == null){
notificationScreenshotManager = new TweetScreenshotManager(this, plugins);
}
notificationScreenshotManager.Trigger(html, width, height);
AnalyticsFile.TweetScreenshots.Trigger();
}
public void DisplayTooltip(string text){
if (string.IsNullOrEmpty(text)){
toolTip.Hide(this);
}
else{
Point position = PointToClient(Cursor.Position);
position.Offset(20, 10);
toolTip.Show(text, this, position);
}
}
}
}

View File

@@ -1,29 +0,0 @@
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Core.Other;
namespace TweetDuck.Core{
static class FormManager{
public static T TryFind<T>() where T : Form{
return Application.OpenForms.OfType<T>().FirstOrDefault();
}
public static bool TryBringToFront<T>() where T : Form{
T form = TryFind<T>();
if (form != null){
form.BringToFront();
return true;
}
else return false;
}
public static void CloseAllDialogs(){
foreach(Form form in Application.OpenForms.Cast<Form>().Reverse()){
if (form is FormSettings || form is FormPlugins || form is FormAbout || form is FormGuide){
form.Close();
}
}
}
}
}

View File

@@ -1,217 +0,0 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using CefSharp;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
using System.Collections.Generic;
using System.Linq;
using TweetDuck.Core.Management;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Handling{
abstract class ContextMenuBase : IContextMenuHandler{
public static readonly bool HasDevTools = File.Exists(Path.Combine(Program.ProgramPath, "devtools_resources.pak"));
private static TwitterUtils.ImageQuality ImageQuality => Program.UserConfig.TwitterImageQuality;
private static KeyValuePair<string, string> ContextInfo;
private static bool IsLink => ContextInfo.Key == "link";
private static bool IsImage => ContextInfo.Key == "image";
private static bool IsVideo => ContextInfo.Key == "video";
public static void SetContextInfo(string type, string link){
ContextInfo = new KeyValuePair<string, string>(string.IsNullOrEmpty(link) ? null : type, link);
}
private static string GetMediaLink(IContextMenuParams parameters){
return IsImage || IsVideo ? ContextInfo.Value : parameters.SourceUrl;
}
private const CefMenuCommand MenuOpenLinkUrl = (CefMenuCommand)26500;
private const CefMenuCommand MenuCopyLinkUrl = (CefMenuCommand)26501;
private const CefMenuCommand MenuCopyUsername = (CefMenuCommand)26502;
private const CefMenuCommand MenuViewImage = (CefMenuCommand)26503;
private const CefMenuCommand MenuOpenMediaUrl = (CefMenuCommand)26504;
private const CefMenuCommand MenuCopyMediaUrl = (CefMenuCommand)26505;
private const CefMenuCommand MenuSaveMedia = (CefMenuCommand)26506;
private const CefMenuCommand MenuSaveTweetImages = (CefMenuCommand)26507;
private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand)26599;
private string[] lastHighlightedTweetAuthors;
private string[] lastHighlightedTweetImageList;
private readonly AnalyticsFile.IProvider analytics;
protected ContextMenuBase(AnalyticsFile.IProvider analytics){
this.analytics = analytics;
}
public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
lastHighlightedTweetAuthors = StringUtils.EmptyArray;
lastHighlightedTweetImageList = StringUtils.EmptyArray;
ContextInfo = default(KeyValuePair<string, string>);
}
else{
lastHighlightedTweetAuthors = TweetDeckBridge.LastHighlightedTweetAuthorsArray;
lastHighlightedTweetImageList = TweetDeckBridge.LastHighlightedTweetImagesArray;
}
bool hasTweetImage = IsImage;
bool hasTweetVideo = IsVideo;
string TextOpen(string name) => "Open "+name+" in browser";
string TextCopy(string name) => "Copy "+name+" address";
string TextSave(string name) => "Save "+name+" as...";
if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && !parameters.UnfilteredLinkUrl.EndsWith("tweetdeck.twitter.com/#", StringComparison.Ordinal) && !hasTweetImage && !hasTweetVideo){
if (TwitterUtils.RegexAccount.IsMatch(parameters.UnfilteredLinkUrl)){
model.AddItem(MenuOpenLinkUrl, TextOpen("account"));
model.AddItem(MenuCopyLinkUrl, TextCopy("account"));
model.AddItem(MenuCopyUsername, "Copy account username");
}
else{
model.AddItem(MenuOpenLinkUrl, TextOpen("link"));
model.AddItem(MenuCopyLinkUrl, TextCopy("link"));
}
model.AddSeparator();
}
if (hasTweetVideo){
model.AddItem(MenuOpenMediaUrl, TextOpen("video"));
model.AddItem(MenuCopyMediaUrl, TextCopy("video"));
model.AddItem(MenuSaveMedia, TextSave("video"));
model.AddSeparator();
}
else if (((parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) || hasTweetImage) && parameters.SourceUrl != TweetNotification.AppLogo.Url){
model.AddItem(MenuViewImage, "View image in photo viewer");
model.AddItem(MenuOpenMediaUrl, TextOpen("image"));
model.AddItem(MenuCopyMediaUrl, TextCopy("image"));
model.AddItem(MenuSaveMedia, TextSave("image"));
if (lastHighlightedTweetImageList.Length > 1){
model.AddItem(MenuSaveTweetImages, TextSave("all images"));
}
model.AddSeparator();
}
}
public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
Control control = browserControl.AsControl();
switch(commandId){
case MenuOpenLinkUrl:
OpenBrowser(control, IsLink ? ContextInfo.Value : parameters.LinkUrl);
break;
case MenuCopyLinkUrl:
SetClipboardText(control, IsLink ? ContextInfo.Value : parameters.UnfilteredLinkUrl);
break;
case MenuCopyUsername:
Match match = TwitterUtils.RegexAccount.Match(parameters.UnfilteredLinkUrl);
SetClipboardText(control, match.Success ? match.Groups[1].Value : parameters.UnfilteredLinkUrl);
control.InvokeAsyncSafe(analytics.AnalyticsFile.CopiedUsernames.Trigger);
break;
case MenuOpenMediaUrl:
OpenBrowser(control, TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
break;
case MenuCopyMediaUrl:
SetClipboardText(control, TwitterUtils.GetMediaLink(GetMediaLink(parameters), ImageQuality));
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{
control.InvokeAsyncSafe(analytics.AnalyticsFile.ViewedImages.Trigger);
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:
if (IsVideo){
control.InvokeAsyncSafe(analytics.AnalyticsFile.DownloadedVideos.Trigger);
TwitterUtils.DownloadVideo(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault());
}
else{
control.InvokeAsyncSafe(analytics.AnalyticsFile.DownloadedImages.Trigger);
TwitterUtils.DownloadImage(GetMediaLink(parameters), lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality);
}
break;
case MenuSaveTweetImages:
control.InvokeAsyncSafe(analytics.AnalyticsFile.DownloadedImages.Trigger);
TwitterUtils.DownloadImages(lastHighlightedTweetImageList, lastHighlightedTweetAuthors.LastOrDefault(), ImageQuality);
break;
case MenuOpenDevTools:
browserControl.ShowDevTools();
break;
}
return false;
}
public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
ContextInfo = default(KeyValuePair<string, string>);
}
public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback){
return false;
}
protected void OpenBrowser(Control control, string url){
control.InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(url));
}
protected void SetClipboardText(Control control, string text){
control.InvokeAsyncSafe(() => WindowsUtils.SetClipboard(text, TextDataFormat.UnicodeText));
}
protected static void AddDebugMenuItems(IMenuModel model){
model.AddItem(MenuOpenDevTools, "Open dev tools");
}
protected static void RemoveSeparatorIfLast(IMenuModel model){
if (model.Count > 0 && model.GetTypeAt(model.Count-1) == MenuItemType.Separator){
model.RemoveAt(model.Count-1);
}
}
protected static void AddSeparator(IMenuModel model){
if (model.Count > 0 && model.GetTypeAt(model.Count-1) != MenuItemType.Separator){ // do not add separators if there is nothing to separate
model.AddSeparator();
}
}
}
}

View File

@@ -1,179 +0,0 @@
using CefSharp;
using System.Windows.Forms;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuBrowser : ContextMenuBase{
private const CefMenuCommand MenuGlobal = (CefMenuCommand)26600;
private const CefMenuCommand MenuMute = (CefMenuCommand)26601;
private const CefMenuCommand MenuSettings = (CefMenuCommand)26602;
private const CefMenuCommand MenuPlugins = (CefMenuCommand)26003;
private const CefMenuCommand MenuAbout = (CefMenuCommand)26604;
private const CefMenuCommand MenuOpenTweetUrl = (CefMenuCommand)26610;
private const CefMenuCommand MenuCopyTweetUrl = (CefMenuCommand)26611;
private const CefMenuCommand MenuOpenQuotedTweetUrl = (CefMenuCommand)26612;
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand)26613;
private const CefMenuCommand MenuScreenshotTweet = (CefMenuCommand)26614;
private const CefMenuCommand MenuInputApplyROT13 = (CefMenuCommand)26615;
private const string TitleReloadBrowser = "Reload browser";
private const string TitleMuteNotifications = "Mute notifications";
private const string TitleSettings = "Options";
private const string TitlePlugins = "Plugins";
private const string TitleAboutProgram = "About "+Program.BrandName;
private readonly FormBrowser form;
private string lastHighlightedTweetUrl;
private string lastHighlightedQuoteUrl;
public ContextMenuBrowser(FormBrowser form) : base(form){
this.form = form;
}
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
model.Remove(CefMenuCommand.Back);
model.Remove(CefMenuCommand.Forward);
model.Remove(CefMenuCommand.Print);
model.Remove(CefMenuCommand.ViewSource);
RemoveSeparatorIfLast(model);
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
if (parameters.TypeFlags.HasFlag(ContextMenuType.Editable)){
model.AddSeparator();
model.AddItem(MenuInputApplyROT13, "Apply ROT13");
}
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
lastHighlightedTweetUrl = TweetDeckBridge.LastHighlightedTweetUrl;
lastHighlightedQuoteUrl = TweetDeckBridge.LastHighlightedQuoteUrl;
if (!TwitterUtils.IsTweetDeckWebsite(frame) || browser.IsLoading){
lastHighlightedTweetUrl = string.Empty;
lastHighlightedQuoteUrl = string.Empty;
}
if (!string.IsNullOrEmpty(lastHighlightedTweetUrl) && (parameters.TypeFlags & (ContextMenuType.Editable | ContextMenuType.Selection)) == 0){
model.AddItem(MenuOpenTweetUrl, "Open tweet in browser");
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
model.AddItem(MenuScreenshotTweet, "Screenshot tweet to clipboard");
if (!string.IsNullOrEmpty(lastHighlightedQuoteUrl)){
model.AddSeparator();
model.AddItem(MenuOpenQuotedTweetUrl, "Open quoted tweet in browser");
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
}
model.AddSeparator();
}
if ((parameters.TypeFlags & (ContextMenuType.Editable | ContextMenuType.Selection)) == 0){
AddSeparator(model);
IMenuModel globalMenu = model.Count == 0 ? model : model.AddSubMenu(MenuGlobal, Program.BrandName);
globalMenu.AddItem(CefMenuCommand.Reload, TitleReloadBrowser);
globalMenu.AddCheckItem(MenuMute, TitleMuteNotifications);
globalMenu.SetChecked(MenuMute, Program.UserConfig.MuteNotifications);
globalMenu.AddSeparator();
globalMenu.AddItem(MenuSettings, TitleSettings);
globalMenu.AddItem(MenuPlugins, TitlePlugins);
globalMenu.AddItem(MenuAbout, TitleAboutProgram);
if (HasDevTools){
globalMenu.AddSeparator();
AddDebugMenuItems(globalMenu);
}
}
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(form.AnalyticsFile.BrowserContextMenus.Trigger);
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)){
return true;
}
switch(commandId){
case CefMenuCommand.Reload:
form.InvokeAsyncSafe(form.ReloadToTweetDeck);
return true;
case MenuSettings:
form.InvokeAsyncSafe(form.OpenSettings);
return true;
case MenuAbout:
form.InvokeAsyncSafe(form.OpenAbout);
return true;
case MenuPlugins:
form.InvokeAsyncSafe(form.OpenPlugins);
return true;
case MenuMute:
form.InvokeAsyncSafe(ToggleMuteNotifications);
return true;
case MenuOpenTweetUrl:
OpenBrowser(form, lastHighlightedTweetUrl);
return true;
case MenuCopyTweetUrl:
SetClipboardText(form, lastHighlightedTweetUrl);
return true;
case MenuScreenshotTweet:
form.InvokeAsyncSafe(form.TriggerTweetScreenshot);
return true;
case MenuOpenQuotedTweetUrl:
OpenBrowser(form, lastHighlightedQuoteUrl);
return true;
case MenuCopyQuotedTweetUrl:
SetClipboardText(form, lastHighlightedQuoteUrl);
return true;
case MenuInputApplyROT13:
form.InvokeAsyncSafe(form.ApplyROT13);
return true;
}
return false;
}
public static ContextMenu CreateMenu(FormBrowser form){
ContextMenu menu = new ContextMenu();
menu.MenuItems.Add(TitleReloadBrowser, (sender, args) => form.ReloadToTweetDeck());
menu.MenuItems.Add(TitleMuteNotifications, (sender, args) => ToggleMuteNotifications());
menu.MenuItems.Add("-");
menu.MenuItems.Add(TitleSettings, (sender, args) => form.OpenSettings());
menu.MenuItems.Add(TitlePlugins, (sender, args) => form.OpenPlugins());
menu.MenuItems.Add(TitleAboutProgram, (sender, args) => form.OpenAbout());
menu.Popup += (sender, args) => {
menu.MenuItems[1].Checked = Program.UserConfig.MuteNotifications;
form.AnalyticsFile.BrowserContextMenus.Trigger();
};
return menu;
}
private static void ToggleMuteNotifications(){
Program.UserConfig.MuteNotifications = !Program.UserConfig.MuteNotifications;
Program.UserConfig.Save();
}
}
}

View File

@@ -1,18 +0,0 @@
using CefSharp;
using TweetDuck.Core.Other.Analytics;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuGuide : ContextMenuBase{
public ContextMenuGuide(AnalyticsFile.IProvider analytics) : base(analytics){}
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
model.Clear();
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (HasDevTools){
AddSeparator(model);
AddDebugMenuItems(model);
}
}
}
}

View File

@@ -1,98 +0,0 @@
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
namespace TweetDuck.Core.Handling{
sealed class ContextMenuNotification : ContextMenuBase{
private const CefMenuCommand MenuViewDetail = (CefMenuCommand)26600;
private const CefMenuCommand MenuSkipTweet = (CefMenuCommand)26601;
private const CefMenuCommand MenuFreeze = (CefMenuCommand)26602;
private const CefMenuCommand MenuCopyTweetUrl = (CefMenuCommand)26603;
private const CefMenuCommand MenuCopyQuotedTweetUrl = (CefMenuCommand)26604;
private readonly FormNotificationBase form;
private readonly bool enableCustomMenu;
public ContextMenuNotification(FormNotificationBase form, bool enableCustomMenu) : base(form){
this.form = form;
this.enableCustomMenu = enableCustomMenu;
}
public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model){
model.Clear();
if (parameters.TypeFlags.HasFlag(ContextMenuType.Selection)){
model.AddItem(CefMenuCommand.Copy, "Copy");
model.AddSeparator();
}
base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model);
if (enableCustomMenu){
if (form.CanViewDetail){
model.AddItem(MenuViewDetail, "View detail");
}
model.AddItem(MenuSkipTweet, "Skip tweet");
model.AddCheckItem(MenuFreeze, "Freeze");
model.SetChecked(MenuFreeze, form.FreezeTimer);
if (!string.IsNullOrEmpty(form.CurrentTweetUrl)){
model.AddSeparator();
model.AddItem(MenuCopyTweetUrl, "Copy tweet address");
if (!string.IsNullOrEmpty(form.CurrentQuoteUrl)){
model.AddItem(MenuCopyQuotedTweetUrl, "Copy quoted tweet address");
}
}
}
if (HasDevTools){
AddSeparator(model);
AddDebugMenuItems(model);
}
RemoveSeparatorIfLast(model);
form.InvokeAsyncSafe(() => {
form.ContextMenuOpen = true;
form.AnalyticsFile.NotificationContextMenus.Trigger();
});
}
public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags){
if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)){
return true;
}
switch(commandId){
case MenuSkipTweet:
form.InvokeAsyncSafe(form.FinishCurrentNotification);
return true;
case MenuFreeze:
form.InvokeAsyncSafe(() => form.FreezeTimer = !form.FreezeTimer);
return true;
case MenuViewDetail:
form.InvokeSafe(form.ShowTweetDetail);
return true;
case MenuCopyTweetUrl:
SetClipboardText(form, form.CurrentTweetUrl);
return true;
case MenuCopyQuotedTweetUrl:
SetClipboardText(form, form.CurrentQuoteUrl);
return true;
}
return false;
}
public override void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame){
base.OnContextMenuDismissed(browserControl, browser, frame);
form.InvokeAsyncSafe(() => form.ContextMenuOpen = false);
}
}
}

View File

@@ -1,26 +0,0 @@
using System.Collections.Generic;
using CefSharp;
namespace TweetDuck.Core.Handling{
sealed class DragHandlerBrowser : IDragHandler{
public bool OnDragEnter(IWebBrowser browserControl, IBrowser browser, IDragData dragData, DragOperationsMask mask){
void TriggerDragStart(string type, string data = null){
browserControl.ExecuteScriptAsync("window.TDGF_onGlobalDragStart", type, data);
}
if (dragData.IsLink){
TriggerDragStart("link", dragData.LinkUrl);
}
else if (dragData.IsFragment){
TriggerDragStart("text", dragData.FragmentText.Trim());
}
else{
TriggerDragStart("unknown");
}
return false;
}
public void OnDraggableRegionsChanged(IWebBrowser browserControl, IBrowser browser, IList<DraggableRegion> regions){}
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Threading.Tasks;
using CefSharp;
namespace TweetDuck.Core.Handling.General{
sealed class BrowserProcessHandler : IBrowserProcessHandler{
public static Task UpdatePrefs(){
return Cef.UIThreadTaskFactory.StartNew(UpdatePrefsInternal);
}
private static void UpdatePrefsInternal(){
using(IRequestContext ctx = Cef.GetGlobalRequestContext()){
ctx.SetPreference("browser.enable_spellchecking", Program.UserConfig.EnableSpellCheck, out string _);
ctx.SetPreference("spellcheck.dictionary", Program.UserConfig.SpellCheckLanguage, out string _);
ctx.SetPreference("settings.a11y.animation_policy", Program.UserConfig.EnableAnimatedImages ? "allowed" : "none", out string _);
}
}
void IBrowserProcessHandler.OnContextInitialized(){
UpdatePrefsInternal();
}
void IBrowserProcessHandler.OnScheduleMessagePumpWork(long delay){}
void IDisposable.Dispose(){}
}
}

View File

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

View File

@@ -1,90 +0,0 @@
using System.Drawing;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling.General{
sealed class JavaScriptDialogHandler : IJsDialogHandler{
private static FormMessage CreateMessageForm(string caption, string text){
MessageBoxIcon icon = MessageBoxIcon.None;
int pipe = text.IndexOf('|');
if (pipe != -1){
switch(text.Substring(0, pipe)){
case "error": icon = MessageBoxIcon.Error; break;
case "warning": icon = MessageBoxIcon.Warning; break;
case "info": icon = MessageBoxIcon.Information; break;
case "question": icon = MessageBoxIcon.Question; break;
default: return new FormMessage(caption, text, icon);
}
text = text.Substring(pipe+1);
}
return new FormMessage(caption, text, icon);
}
bool IJsDialogHandler.OnJSDialog(IWebBrowser browserControl, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage){
((ChromiumWebBrowser)browserControl).InvokeSafe(() => {
FormMessage form;
TextBox input = null;
if (dialogType == CefJsDialogType.Alert){
form = CreateMessageForm("Browser Message", messageText);
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
}
else if (dialogType == CefJsDialogType.Confirm){
form = CreateMessageForm("Browser Confirmation", messageText);
form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel);
form.AddButton(FormMessage.Yes, ControlType.Focused);
}
else if (dialogType == CefJsDialogType.Prompt){
form = CreateMessageForm("Browser Prompt", messageText);
form.AddButton(FormMessage.Cancel, DialogResult.Cancel, ControlType.Cancel);
form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused);
float dpiScale = form.GetDPIScale();
int inputPad = form.HasIcon ? 43 : 0;
input = new TextBox{
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
Location = new Point(BrowserUtils.Scale(22+inputPad, dpiScale), form.ActionPanelY-BrowserUtils.Scale(46, dpiScale)),
Size = new Size(form.ClientSize.Width-BrowserUtils.Scale(44+inputPad, dpiScale), 20)
};
form.Controls.Add(input);
form.ActiveControl = input;
form.Height += input.Size.Height+input.Margin.Vertical;
}
else{
callback.Continue(false);
return;
}
bool success = form.ShowDialog() == DialogResult.OK;
if (input == null){
callback.Continue(success);
}
else{
callback.Continue(success, input.Text);
input.Dispose();
}
form.Dispose();
});
return true;
}
bool IJsDialogHandler.OnJSBeforeUnload(IWebBrowser browserControl, IBrowser browser, string message, bool isReload, IJsDialogCallback callback){
return false;
}
void IJsDialogHandler.OnResetDialogState(IWebBrowser browserControl, IBrowser browser){}
void IJsDialogHandler.OnDialogClosed(IWebBrowser browserControl, IBrowser browser){}
}
}

View File

@@ -1,34 +0,0 @@
using CefSharp;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling.General{
sealed class LifeSpanHandler : ILifeSpanHandler{
public static bool HandleLinkClick(IWebBrowser browserControl, WindowOpenDisposition targetDisposition, string targetUrl){
switch(targetDisposition){
case WindowOpenDisposition.NewBackgroundTab:
case WindowOpenDisposition.NewForegroundTab:
case WindowOpenDisposition.NewPopup:
case WindowOpenDisposition.NewWindow:
browserControl.AsControl().InvokeAsyncSafe(() => BrowserUtils.OpenExternalBrowser(targetUrl));
return true;
default:
return false;
}
}
public bool OnBeforePopup(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, string targetFrameName, WindowOpenDisposition targetDisposition, bool userGesture, IPopupFeatures popupFeatures, IWindowInfo windowInfo, IBrowserSettings browserSettings, ref bool noJavascriptAccess, out IWebBrowser newBrowser){
newBrowser = null;
return HandleLinkClick(browserControl, targetDisposition, targetUrl);
}
public void OnAfterCreated(IWebBrowser browserControl, IBrowser browser){}
public bool DoClose(IWebBrowser browserControl, IBrowser browser){
return false;
}
public void OnBeforeClose(IWebBrowser browserControl, IBrowser browser){}
}
}

View File

@@ -1,20 +0,0 @@
using System.Windows.Forms;
using CefSharp;
namespace TweetDuck.Core.Handling{
sealed class KeyboardHandlerBrowser : IKeyboardHandler{
private readonly FormBrowser form;
public KeyboardHandlerBrowser(FormBrowser form){
this.form = form;
}
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){
return type == KeyType.RawKeyDown && form.ProcessBrowserKey((Keys)windowsKeyCode);
}
bool IKeyboardHandler.OnKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey){
return false;
}
}
}

View File

@@ -1,45 +0,0 @@
using CefSharp;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Notification;
namespace TweetDuck.Core.Handling {
sealed class KeyboardHandlerNotification : IKeyboardHandler{
private readonly FormNotificationBase notification;
public KeyboardHandlerNotification(FormNotificationBase notification){
this.notification = notification;
}
private void TriggerKeyboardShortcutAnalytics(){
notification.InvokeAsyncSafe(notification.AnalyticsFile.NotificationKeyboardShortcuts.Trigger);
}
bool IKeyboardHandler.OnPreKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey, ref bool isKeyboardShortcut){
if (type == KeyType.RawKeyDown && !browser.FocusedFrame.Url.StartsWith("chrome-devtools://")){
switch((Keys)windowsKeyCode){
case Keys.Enter:
notification.InvokeAsyncSafe(notification.FinishCurrentNotification);
TriggerKeyboardShortcutAnalytics();
return true;
case Keys.Escape:
notification.InvokeAsyncSafe(notification.HideNotification);
TriggerKeyboardShortcutAnalytics();
return true;
case Keys.Space:
notification.InvokeAsyncSafe(() => notification.FreezeTimer = !notification.FreezeTimer);
TriggerKeyboardShortcutAnalytics();
return true;
}
}
return false;
}
bool IKeyboardHandler.OnKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int windowsKeyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey){
return false;
}
}
}

View File

@@ -1,22 +0,0 @@
using System.Collections.Specialized;
using CefSharp;
using CefSharp.Handler;
using TweetDuck.Core.Handling.General;
namespace TweetDuck.Core.Handling{
class RequestHandlerBase : DefaultRequestHandler{
public override bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture){
return LifeSpanHandler.HandleLinkClick(browserControl, targetDisposition, targetUrl);
}
public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){
if (ContextMenuBase.HasDevTools){
NameValueCollection headers = request.Headers;
headers.Remove("x-devtools-emulate-network-conditions-client-id");
request.Headers = headers;
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
}
}

View File

@@ -1,28 +0,0 @@
using CefSharp;
using TweetDuck.Core.Utils;
namespace TweetDuck.Core.Handling{
sealed class RequestHandlerBrowser : RequestHandlerBase{
public override void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status){
browser.Reload();
}
public override CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback){
if (request.ResourceType == ResourceType.Script && request.Url.Contains("analytics.")){
callback.Dispose();
return CefReturnValue.Cancel;
}
return base.OnBeforeResourceLoad(browserControl, browser, frame, request, callback);
}
public override bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response){
if (request.ResourceType == ResourceType.Image && request.Url.Contains("backgrounds/spinner_blue")){
request.Url = TwitterUtils.LoadingSpinner.Url;
return true;
}
return base.OnResourceResponse(browserControl, browser, frame, request, response);
}
}
}

View File

@@ -1,63 +0,0 @@
using CefSharp;
using System.Collections.Specialized;
using System.IO;
using System.Text;
namespace TweetDuck.Core.Handling{
sealed class ResourceHandlerNotification : IResourceHandler{
private readonly NameValueCollection headers = new NameValueCollection(0);
private MemoryStream dataIn;
public void SetHTML(string html){
dataIn?.Dispose();
dataIn = ResourceHandler.GetMemoryStream(html, Encoding.UTF8);
}
public void Dispose(){
if (dataIn != null){
dataIn.Dispose();
dataIn = null;
}
}
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback){
callback.Continue();
return true;
}
void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl){
redirectUrl = null;
response.MimeType = "text/html";
response.StatusCode = 200;
response.StatusText = "OK";
response.ResponseHeaders = headers;
responseLength = dataIn?.Length ?? -1;
}
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback){
callback.Dispose();
try{
int length = (int)dataIn.Length;
dataIn.CopyTo(dataOut, length);
bytesRead = length;
return true;
}catch{ // catch IOException, possibly NullReferenceException if dataIn is null
bytesRead = 0;
return false;
}
}
bool IResourceHandler.CanGetCookie(Cookie cookie){
return true;
}
bool IResourceHandler.CanSetCookie(Cookie cookie){
return true;
}
void IResourceHandler.Cancel(){}
}
}

View File

@@ -1,10 +0,0 @@
using System;
using CefSharp;
namespace TweetDuck.Core{
interface ITweetDeckBrowser{
void RegisterBridge(string name, object obj);
void OnFrameLoaded(Action<IFrame> callback);
void ExecuteFunction(string name, params object[] args);
}
}

View File

@@ -1,72 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace TweetDuck.Core.Management{
static class BrowserCache{
public static string CacheFolder => Path.Combine(Program.StoragePath, "Cache");
private static bool ClearOnExit;
private static Timer AutoClearTimer;
private static long CalculateCacheSize(){
return new DirectoryInfo(CacheFolder).EnumerateFiles().Select(file => {
try{
return file.Length;
}catch{
return 0L;
}
}).Sum();
}
public static void GetCacheSize(Action<Task<long>> callbackBytes){
Task<long> task = new Task<long>(CalculateCacheSize);
task.ContinueWith(callbackBytes);
task.Start();
}
public static void RefreshTimer(){
bool shouldRun = Program.SystemConfig.ClearCacheAutomatically && !ClearOnExit;
if (!shouldRun && AutoClearTimer != null){
AutoClearTimer.Dispose();
AutoClearTimer = null;
}
else if (shouldRun && AutoClearTimer == null){
AutoClearTimer = new Timer(state => {
if (AutoClearTimer != null){
try{
if (CalculateCacheSize() >= Program.SystemConfig.ClearCacheThreshold*1024L*1024L){
SetClearOnExit();
}
}catch(Exception){
// TODO should probably log errors and report them at some point
}
}
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromHours(4));
}
}
public static void SetClearOnExit(){
ClearOnExit = true;
RefreshTimer();
}
public static void Exit(){
if (AutoClearTimer != null){
AutoClearTimer.Dispose();
AutoClearTimer = null;
}
if (ClearOnExit){
try{
Directory.Delete(CacheFolder, true);
}catch{
// welp, too bad
}
}
}
}
}

View File

@@ -1,219 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TweetDuck.Core.Other;
using TweetDuck.Data;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
namespace TweetDuck.Core.Management{
sealed class ProfileManager{
private static readonly string CookiesPath = Path.Combine(Program.StoragePath, "Cookies");
private static readonly string TempCookiesPath = Path.Combine(Program.StoragePath, "CookiesTmp");
[Flags]
public enum Items{
None = 0,
UserConfig = 1,
SystemConfig = 2,
Session = 4,
PluginData = 8,
All = UserConfig|SystemConfig|Session|PluginData
}
public bool IsRestarting { get; private set; }
public Exception LastException { get; private set; }
private readonly string file;
private readonly PluginManager plugins;
public ProfileManager(string file, PluginManager plugins){
this.file = file;
this.plugins = plugins;
}
public bool Export(Items items){
try{
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))){
if (items.HasFlag(Items.UserConfig)){
stream.WriteFile("config", Program.UserConfigFilePath);
}
if (items.HasFlag(Items.SystemConfig)){
stream.WriteFile("system", Program.SystemConfigFilePath);
}
if (items.HasFlag(Items.PluginData)){
stream.WriteFile("plugin.config", Program.PluginConfigFilePath);
foreach(Plugin plugin in plugins.Plugins){
foreach(PathInfo path in EnumerateFilesRelative(plugin.GetPluginFolder(PluginFolder.Data))){
try{
stream.WriteFile(new string[]{ "plugin.data", plugin.Identifier, path.Relative }, path.Full);
}catch(ArgumentOutOfRangeException e){
FormMessage.Warning("Export Profile", "Could not include a plugin file in the export. "+e.Message, FormMessage.OK);
}
}
}
}
if (items.HasFlag(Items.Session)){
stream.WriteFile("cookies", CookiesPath);
}
stream.Flush();
}
return true;
}catch(Exception e){
LastException = e;
return false;
}
}
public Items FindImportItems(){
Items items = Items.None;
try{
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))){
string key;
while((key = stream.SkipFile()) != null){
switch(key){
case "config":
items |= Items.UserConfig;
break;
case "system":
items |= Items.SystemConfig;
break;
case "plugin.config":
case "plugin.data":
items |= Items.PluginData;
break;
case "cookies":
items |= Items.Session;
break;
}
}
}
}catch(Exception e){
LastException = e;
items = Items.None;
}
return items;
}
public bool Import(Items items){
try{
HashSet<string> missingPlugins = new HashSet<string>();
using(CombinedFileStream stream = new CombinedFileStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))){
CombinedFileStream.Entry entry;
while((entry = stream.ReadFile()) != null){
switch(entry.KeyName){
case "config":
if (items.HasFlag(Items.UserConfig)){
entry.WriteToFile(Program.UserConfigFilePath);
}
break;
case "system":
if (items.HasFlag(Items.SystemConfig)){
entry.WriteToFile(Program.SystemConfigFilePath);
IsRestarting = true;
}
break;
case "plugin.config":
if (items.HasFlag(Items.PluginData)){
entry.WriteToFile(Program.PluginConfigFilePath);
}
break;
case "plugin.data":
if (items.HasFlag(Items.PluginData)){
string[] value = entry.KeyValue;
entry.WriteToFile(Path.Combine(Program.PluginDataPath, value[0], value[1]), true);
if (!plugins.IsPluginInstalled(value[0])){
missingPlugins.Add(value[0]);
}
}
break;
case "cookies":
if (items.HasFlag(Items.Session)){
entry.WriteToFile(Path.Combine(Program.StoragePath, TempCookiesPath));
IsRestarting = true;
}
break;
}
}
}
if (missingPlugins.Count > 0){
FormMessage.Information("Importing TweetDuck Profile", "Detected missing plugins when importing plugin data:\n"+string.Join("\n", missingPlugins), FormMessage.OK);
}
return true;
}catch(Exception e){
LastException = e;
return false;
}
}
public static void ImportCookies(){
if (File.Exists(TempCookiesPath)){
try{
if (File.Exists(CookiesPath)){
File.Delete(CookiesPath);
}
File.Move(TempCookiesPath, CookiesPath);
}catch(Exception e){
Program.Reporter.HandleException("Profile Import Error", "Could not import the cookie file to restore login session.", true, e);
}
}
}
public static void DeleteCookies(){
try{
if (File.Exists(CookiesPath)){
File.Delete(CookiesPath);
}
}catch(Exception e){
Program.Reporter.HandleException("Session Reset Error", "Could not remove the cookie file to reset the login session.", true, e);
}
}
private static IEnumerable<PathInfo> EnumerateFilesRelative(string root){
return Directory.Exists(root) ? Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories).Select(fullPath => new PathInfo{
Full = fullPath,
Relative = fullPath.Substring(root.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) // strip leading separator character
}) : Enumerable.Empty<PathInfo>();
}
private sealed class PathInfo{
public string Full { get; set; }
public string Relative { get; set; }
}
}
static class ProfileManagerExtensions{
public static bool NeedsRestart(this ProfileManager.Items items){
return items.HasFlag(ProfileManager.Items.SystemConfig) || items.HasFlag(ProfileManager.Items.Session);
}
}
}

View File

@@ -1,187 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
using TweetLib.Communication;
namespace TweetDuck.Core.Management{
sealed class VideoPlayer : IDisposable{
public bool Running{
get{
if (currentProcess == null){
return false;
}
currentProcess.Refresh();
return !currentProcess.HasExited;
}
}
public event EventHandler ProcessExited;
private readonly FormBrowser owner;
private string lastUrl;
private string lastUsername;
private Process currentProcess;
private DuplexPipe.Server currentPipe;
private bool isClosing;
public VideoPlayer(FormBrowser owner){
this.owner = owner;
this.owner.FormClosing += owner_FormClosing;
}
public void Launch(string url, string username){
if (Running){
Destroy();
isClosing = false;
}
lastUrl = url;
lastUsername = username;
try{
currentPipe = DuplexPipe.CreateServer();
currentPipe.DataIn += currentPipe_DataIn;
if ((currentProcess = Process.Start(new ProcessStartInfo{
FileName = Path.Combine(Program.ProgramPath, "TweetDuck.Video.exe"),
Arguments = $"{owner.Handle} {Program.UserConfig.VideoPlayerVolume} \"{url}\" \"{currentPipe.GenerateToken()}\"",
UseShellExecute = false,
RedirectStandardOutput = true
})) != null){
currentProcess.EnableRaisingEvents = true;
currentProcess.Exited += process_Exited;
currentProcess.BeginOutputReadLine();
currentProcess.OutputDataReceived += process_OutputDataReceived;
}
currentPipe.DisposeToken();
}catch(Exception e){
Program.Reporter.HandleException("Video Playback Error", "Error launching video player.", true, e);
}
}
public void SendKeyEvent(Keys key){
currentPipe?.Write("key", ((int)key).ToString());
}
private void currentPipe_DataIn(object sender, DuplexPipe.PipeReadEventArgs e){
owner.InvokeSafe(() => {
switch(e.Key){
case "vol":
if (int.TryParse(e.Data, out int volume) && volume != Program.UserConfig.VideoPlayerVolume){
Program.UserConfig.VideoPlayerVolume = volume;
Program.UserConfig.Save();
}
break;
case "download":
owner.AnalyticsFile.DownloadedVideos.Trigger();
TwitterUtils.DownloadVideo(lastUrl, lastUsername);
break;
case "rip":
currentPipe.Dispose();
currentPipe = null;
currentProcess.Dispose();
currentProcess = null;
isClosing = false;
TriggerProcessExitEventUnsafe();
break;
}
});
}
public void Close(){
if (currentProcess != null){
if (isClosing){
Destroy();
isClosing = false;
}
else{
isClosing = true;
currentProcess.Exited -= process_Exited;
currentPipe.Write("die");
}
}
}
public void Dispose(){
ProcessExited = null;
isClosing = true;
Destroy();
}
private void Destroy(){
if (currentProcess != null){
try{
currentProcess.Kill();
}catch{
// kill me instead then
}
currentProcess.Dispose();
currentProcess = null;
currentPipe.Dispose();
currentPipe = null;
TriggerProcessExitEventUnsafe();
}
}
private void owner_FormClosing(object sender, FormClosingEventArgs e){
if (currentProcess != null){
currentProcess.Exited -= process_Exited;
}
}
private void process_OutputDataReceived(object sender, DataReceivedEventArgs e){
if (!string.IsNullOrEmpty(e.Data)){
Program.Reporter.Log("[VideoPlayer] "+e.Data);
}
}
private void process_Exited(object sender, EventArgs e){
int exitCode = currentProcess.ExitCode;
currentProcess.Dispose();
currentProcess = null;
currentPipe.Dispose();
currentPipe = null;
switch(exitCode){
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 your browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl);
}
break;
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 your browser?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(lastUrl);
}
break;
}
owner.InvokeAsyncSafe(TriggerProcessExitEventUnsafe);
}
private void TriggerProcessExitEventUnsafe(){
ProcessExited?.Invoke(this, EventArgs.Empty);
}
}
}

Some files were not shown because too many files have changed in this diff Show More