mirror of
https://github.com/greenshot/greenshot
synced 2025-08-20 13:33:27 -07:00
Fixes for the UpdateService and AboutForm, they were using the wrong version.
This commit is contained in:
parent
7e79c58ab0
commit
f1c8a34a93
3 changed files with 444 additions and 440 deletions
|
@ -24,6 +24,7 @@
|
||||||
#region Usings
|
#region Usings
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.ServiceModel.Syndication;
|
using System.ServiceModel.Syndication;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -42,39 +43,39 @@ using Greenshot.Ui.Notifications.ViewModels;
|
||||||
|
|
||||||
namespace Greenshot.Components
|
namespace Greenshot.Components
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This processes the information, if there are updates available.
|
/// This processes the information, if there are updates available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Service(nameof(UpdateService), nameof(MainFormStartup))]
|
[Service(nameof(UpdateService), nameof(MainFormStartup))]
|
||||||
public class UpdateService : IStartup, IShutdown, IVersionProvider
|
public class UpdateService : IStartup, IShutdown, IVersionProvider
|
||||||
{
|
{
|
||||||
private static readonly LogSource Log = new LogSource();
|
private static readonly LogSource Log = new LogSource();
|
||||||
private static readonly Regex VersionRegex = new Regex(@"^.*[^-]-(?<version>[0-9\.]+)\-(?<type>(release|beta|rc[0-9]+))\.exe.*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex VersionRegex = new Regex(@"^.*[^-]-(?<version>[0-9\.]+)\-(?<type>(release|beta|rc[0-9]+))\.exe.*", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private static readonly Uri UpdateFeed = new Uri("http://getgreenshot.org/project-feed/");
|
private static readonly Uri UpdateFeed = new Uri("http://getgreenshot.org/project-feed/");
|
||||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
private readonly ICoreConfiguration _coreConfiguration;
|
private readonly ICoreConfiguration _coreConfiguration;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly Func<Version, Owned<UpdateNotificationViewModel>> _updateNotificationViewModelFactory;
|
private readonly Func<Version, Owned<UpdateNotificationViewModel>> _updateNotificationViewModelFactory;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Version CurrentVersion { get; }
|
public Version CurrentVersion { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Version LatestVersion { get; private set; }
|
public Version LatestVersion { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The latest beta version
|
/// The latest beta version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Version BetaVersion { get; private set; }
|
public Version BetaVersion { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The latest RC version
|
/// The latest RC version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Version ReleaseCandidateVersion { get; private set; }
|
public Version ReleaseCandidateVersion { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsUpdateAvailable => LatestVersion > CurrentVersion;
|
public bool IsUpdateAvailable => LatestVersion > CurrentVersion;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor with dependencies
|
/// Constructor with dependencies
|
||||||
|
@ -86,26 +87,27 @@ namespace Greenshot.Components
|
||||||
ICoreConfiguration coreConfiguration,
|
ICoreConfiguration coreConfiguration,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
Func<Version, Owned<UpdateNotificationViewModel>> updateNotificationViewModelFactory)
|
Func<Version, Owned<UpdateNotificationViewModel>> updateNotificationViewModelFactory)
|
||||||
{
|
{
|
||||||
_coreConfiguration = coreConfiguration;
|
_coreConfiguration = coreConfiguration;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_updateNotificationViewModelFactory = updateNotificationViewModelFactory;
|
_updateNotificationViewModelFactory = updateNotificationViewModelFactory;
|
||||||
LatestVersion = CurrentVersion = GetType().Assembly.GetName().Version;
|
var version = FileVersionInfo.GetVersionInfo(GetType().Assembly.Location);
|
||||||
_coreConfiguration.LastSaveWithVersion = CurrentVersion.ToString();
|
LatestVersion = CurrentVersion = new Version(version.FileMajorPart, version.FileMinorPart, version.FileBuildPart);
|
||||||
}
|
_coreConfiguration.LastSaveWithVersion = CurrentVersion.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Startup()
|
public void Startup()
|
||||||
{
|
{
|
||||||
var ignore = BackgroundTask(() => TimeSpan.FromDays(_coreConfiguration.UpdateCheckInterval), UpdateCheck, _cancellationTokenSource.Token);
|
var ignore = BackgroundTask(() => TimeSpan.FromDays(_coreConfiguration.UpdateCheckInterval), UpdateCheck, _cancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Shutdown()
|
public void Shutdown()
|
||||||
{
|
{
|
||||||
if (!_cancellationTokenSource.IsCancellationRequested)
|
if (!_cancellationTokenSource.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,48 +118,48 @@ namespace Greenshot.Components
|
||||||
/// <param name="reoccurringTask">Func which returns a task</param>
|
/// <param name="reoccurringTask">Func which returns a task</param>
|
||||||
/// <param name="cancellationToken">CancellationToken</param>
|
/// <param name="cancellationToken">CancellationToken</param>
|
||||||
/// <returns>Task</returns>
|
/// <returns>Task</returns>
|
||||||
private async Task BackgroundTask(Func<TimeSpan> intervalFactory, Func<CancellationToken, Task> reoccurringTask, CancellationToken cancellationToken = default)
|
private async Task BackgroundTask(Func<TimeSpan> intervalFactory, Func<CancellationToken, Task> reoccurringTask, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// Initial delay, to make sure this doesn't happen at the startup
|
// Initial delay, to make sure this doesn't happen at the startup
|
||||||
await Task.Delay(20000, cancellationToken);
|
await Task.Delay(20000, cancellationToken);
|
||||||
Log.Info().WriteLine("Starting background task to check for updates");
|
Log.Info().WriteLine("Starting background task to check for updates");
|
||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var interval = intervalFactory();
|
var interval = intervalFactory();
|
||||||
var task = reoccurringTask;
|
var task = reoccurringTask;
|
||||||
// If the check is disabled, handle that here
|
// If the check is disabled, handle that here
|
||||||
if (TimeSpan.Zero == interval)
|
if (TimeSpan.Zero == interval)
|
||||||
{
|
{
|
||||||
interval = TimeSpan.FromMinutes(10);
|
interval = TimeSpan.FromMinutes(10);
|
||||||
task = c => Task.FromResult(true);
|
task = c => Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await task(cancellationToken).ConfigureAwait(false);
|
await task(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error().WriteLine(ex, "Error occured when trying to check for updates.");
|
Log.Error().WriteLine(ex, "Error occured when trying to check for updates.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
// Ignore, this always happens
|
// Ignore, this always happens
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error().WriteLine(ex, "Error occured await for the next update check.");
|
Log.Error().WriteLine(ex, "Error occured await for the next update check.");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -165,50 +167,50 @@ namespace Greenshot.Components
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">CancellationToken</param>
|
/// <param name="cancellationToken">CancellationToken</param>
|
||||||
/// <returns>Task</returns>
|
/// <returns>Task</returns>
|
||||||
private async Task UpdateCheck(CancellationToken cancellationToken = default)
|
private async Task UpdateCheck(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Log.Info().WriteLine("Checking for updates from {0}", UpdateFeed);
|
Log.Info().WriteLine("Checking for updates from {0}", UpdateFeed);
|
||||||
var updateFeed = await UpdateFeed.GetAsAsync<SyndicationFeed>(cancellationToken);
|
var updateFeed = await UpdateFeed.GetAsAsync<SyndicationFeed>(cancellationToken);
|
||||||
if (updateFeed == null)
|
if (updateFeed == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_coreConfiguration.LastUpdateCheck = DateTime.Now;
|
_coreConfiguration.LastUpdateCheck = DateTime.Now;
|
||||||
|
|
||||||
ProcessFeed(updateFeed);
|
ProcessFeed(updateFeed);
|
||||||
|
|
||||||
if (IsUpdateAvailable)
|
if (IsUpdateAvailable)
|
||||||
{
|
{
|
||||||
ShowUpdate(LatestVersion);
|
ShowUpdate(LatestVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This takes care of creating the toast view model, publishing it, and disposing afterwards
|
/// This takes care of creating the toast view model, publishing it, and disposing afterwards
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ShowUpdate(Version latestVersion)
|
private void ShowUpdate(Version latestVersion)
|
||||||
{
|
{
|
||||||
// Create the ViewModel "part"
|
// Create the ViewModel "part"
|
||||||
var message = _updateNotificationViewModelFactory(latestVersion);
|
var message = _updateNotificationViewModelFactory(latestVersion);
|
||||||
// Prepare to dispose the view model parts automatically if it's finished
|
// Prepare to dispose the view model parts automatically if it's finished
|
||||||
void DisposeHandler(object sender, DeactivationEventArgs args)
|
void DisposeHandler(object sender, DeactivationEventArgs args)
|
||||||
{
|
{
|
||||||
message.Value.Deactivated -= DisposeHandler;
|
message.Value.Deactivated -= DisposeHandler;
|
||||||
message.Dispose();
|
message.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
message.Value.Deactivated += DisposeHandler;
|
message.Value.Deactivated += DisposeHandler;
|
||||||
|
|
||||||
// Show the ViewModel as toast
|
// Show the ViewModel as toast
|
||||||
_eventAggregator.PublishOnUIThread(message.Value);
|
_eventAggregator.PublishOnUIThread(message.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process the update feed to get the latest version
|
/// Process the update feed to get the latest version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="updateFeed"></param>
|
/// <param name="updateFeed"></param>
|
||||||
public void ProcessFeed(SyndicationFeed updateFeed)
|
public void ProcessFeed(SyndicationFeed updateFeed)
|
||||||
{
|
{
|
||||||
var versions =
|
var versions =
|
||||||
from link in updateFeed.Items.SelectMany(i => i.Links)
|
from link in updateFeed.Items.SelectMany(i => i.Links)
|
||||||
|
@ -217,28 +219,28 @@ namespace Greenshot.Components
|
||||||
group match by Regex.Replace(match.Groups["type"].Value, @"[\d-]", string.Empty) into groupedVersions
|
group match by Regex.Replace(match.Groups["type"].Value, @"[\d-]", string.Empty) into groupedVersions
|
||||||
select groupedVersions.OrderByDescending(m => new Version(m.Groups["version"].Value)).First();
|
select groupedVersions.OrderByDescending(m => new Version(m.Groups["version"].Value)).First();
|
||||||
|
|
||||||
foreach (var versionMatch in versions)
|
foreach (var versionMatch in versions)
|
||||||
{
|
{
|
||||||
var version = new Version(versionMatch.Groups["version"].Value);
|
var version = new Version(versionMatch.Groups["version"].Value);
|
||||||
var type = versionMatch.Groups["type"].Value;
|
var type = versionMatch.Groups["type"].Value;
|
||||||
if (string.IsNullOrEmpty(type))
|
if (string.IsNullOrEmpty(type))
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Log.Debug().WriteLine("Got {0} {1}", type, version);
|
|
||||||
if ("release".Equals(type, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
LatestVersion = version;
|
|
||||||
}
|
|
||||||
if ("beta".Equals(type, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
BetaVersion = version;
|
|
||||||
}
|
|
||||||
if ("rc".Equals(type, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
ReleaseCandidateVersion = version;
|
continue;
|
||||||
}
|
}
|
||||||
|
Log.Debug().WriteLine("Got {0} {1}", type, version);
|
||||||
|
if ("release".Equals(type, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
LatestVersion = version;
|
||||||
|
}
|
||||||
|
if ("beta".Equals(type, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
BetaVersion = version;
|
||||||
|
}
|
||||||
|
if ("rc".Equals(type, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ReleaseCandidateVersion = version;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
4
src/Greenshot/Forms/AboutForm.Designer.cs
generated
4
src/Greenshot/Forms/AboutForm.Designer.cs
generated
|
@ -117,7 +117,7 @@ namespace Greenshot.Forms {
|
||||||
this.linkLblBugs.Size = new System.Drawing.Size(465, 23);
|
this.linkLblBugs.Size = new System.Drawing.Size(465, 23);
|
||||||
this.linkLblBugs.TabIndex = 8;
|
this.linkLblBugs.TabIndex = 8;
|
||||||
this.linkLblBugs.TabStop = true;
|
this.linkLblBugs.TabStop = true;
|
||||||
this.linkLblBugs.Text = "http://getgreenshot.org/tickets/?version=" + Assembly.GetEntryAssembly().GetName().Version;
|
this.linkLblBugs.Text = "http://getgreenshot.org/tickets/?version=" + _versionProvider.CurrentVersion;
|
||||||
this.linkLblBugs.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabelClicked);
|
this.linkLblBugs.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabelClicked);
|
||||||
//
|
//
|
||||||
// lblBugs
|
// lblBugs
|
||||||
|
@ -135,7 +135,7 @@ namespace Greenshot.Forms {
|
||||||
this.linkLblDonations.Size = new System.Drawing.Size(465, 23);
|
this.linkLblDonations.Size = new System.Drawing.Size(465, 23);
|
||||||
this.linkLblDonations.TabIndex = 10;
|
this.linkLblDonations.TabIndex = 10;
|
||||||
this.linkLblDonations.TabStop = true;
|
this.linkLblDonations.TabStop = true;
|
||||||
this.linkLblDonations.Text = "http://getgreenshot.org/support/?version=" + Assembly.GetEntryAssembly().GetName().Version;
|
this.linkLblDonations.Text = "http://getgreenshot.org/support/?version=" + _versionProvider.CurrentVersion;
|
||||||
this.linkLblDonations.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabelClicked);
|
this.linkLblDonations.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.LinkLabelClicked);
|
||||||
//
|
//
|
||||||
// lblDonations
|
// lblDonations
|
||||||
|
|
|
@ -45,371 +45,373 @@ using Greenshot.Gfx;
|
||||||
|
|
||||||
namespace Greenshot.Forms
|
namespace Greenshot.Forms
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The about form
|
/// The about form
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class AboutForm : AnimatingForm
|
public sealed partial class AboutForm : AnimatingForm
|
||||||
{
|
{
|
||||||
private readonly IGreenshotLanguage _greenshotlanguage;
|
private readonly IGreenshotLanguage _greenshotlanguage;
|
||||||
|
private readonly IVersionProvider _versionProvider;
|
||||||
|
|
||||||
private static readonly LogSource Log = new LogSource();
|
private static readonly LogSource Log = new LogSource();
|
||||||
// Variables are used to define the location of the dots
|
// Variables are used to define the location of the dots
|
||||||
private const int w = 13;
|
private const int w = 13;
|
||||||
private const int p1 = 7;
|
private const int p1 = 7;
|
||||||
private const int p2 = p1 + w;
|
private const int p2 = p1 + w;
|
||||||
private const int p3 = p2 + w;
|
private const int p3 = p2 + w;
|
||||||
private const int p4 = p3 + w;
|
private const int p4 = p3 + w;
|
||||||
private const int p5 = p4 + w;
|
private const int p5 = p4 + w;
|
||||||
private const int p6 = p5 + w;
|
private const int p6 = p5 + w;
|
||||||
private const int p7 = p6 + w;
|
private const int p7 = p6 + w;
|
||||||
private readonly Color _backColor = Color.FromArgb(61, 61, 61);
|
private readonly Color _backColor = Color.FromArgb(61, 61, 61);
|
||||||
private readonly ColorAnimator _backgroundAnimation;
|
private readonly ColorAnimator _backgroundAnimation;
|
||||||
private readonly IList<Color> _colorFlow = new List<Color>();
|
private readonly IList<Color> _colorFlow = new List<Color>();
|
||||||
private readonly IDisposable _dpiSubscription;
|
private readonly IDisposable _dpiSubscription;
|
||||||
|
|
||||||
// 0 1 2 3 4
|
// 0 1 2 3 4
|
||||||
// 5 6
|
// 5 6
|
||||||
// 7 8
|
// 7 8
|
||||||
// 9 10 11 12 13
|
// 9 10 11 12 13
|
||||||
// 14 15 16 17
|
// 14 15 16 17
|
||||||
// 18 19 20 21 22 23
|
// 18 19 20 21 22 23
|
||||||
|
|
||||||
// The order in which we draw the dots & flow the collors.
|
// The order in which we draw the dots & flow the collors.
|
||||||
private readonly IList<int> _flowOrder = new List<int> {4, 3, 2, 1, 0, 5, 6, 7, 8, 9, 10, 14, 15, 18, 19, 20, 21, 22, 23, 16, 17, 13, 12, 11};
|
private readonly IList<int> _flowOrder = new List<int> {4, 3, 2, 1, 0, 5, 6, 7, 8, 9, 10, 14, 15, 18, 19, 20, 21, 22, 23, 16, 17, 13, 12, 11};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The location of every dot in the "G"
|
/// The location of every dot in the "G"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IList<Point> _gSpots = new List<Point>
|
private readonly IList<Point> _gSpots = new List<Point>
|
||||||
{
|
{
|
||||||
// Top row
|
// Top row
|
||||||
new Point(p2, p1), // 0
|
new Point(p2, p1), // 0
|
||||||
new Point(p3, p1), // 1
|
new Point(p3, p1), // 1
|
||||||
new Point(p4, p1), // 2
|
new Point(p4, p1), // 2
|
||||||
new Point(p5, p1), // 3
|
new Point(p5, p1), // 3
|
||||||
new Point(p6, p1), // 4
|
new Point(p6, p1), // 4
|
||||||
|
|
||||||
// Second row
|
// Second row
|
||||||
new Point(p1, p2), // 5
|
new Point(p1, p2), // 5
|
||||||
new Point(p2, p2), // 6
|
new Point(p2, p2), // 6
|
||||||
|
|
||||||
// Third row
|
// Third row
|
||||||
new Point(p1, p3), // 7
|
new Point(p1, p3), // 7
|
||||||
new Point(p2, p3), // 8
|
new Point(p2, p3), // 8
|
||||||
|
|
||||||
// Fourth row
|
// Fourth row
|
||||||
new Point(p1, p4), // 9
|
new Point(p1, p4), // 9
|
||||||
new Point(p2, p4), // 10
|
new Point(p2, p4), // 10
|
||||||
new Point(p5, p4), // 11
|
new Point(p5, p4), // 11
|
||||||
new Point(p6, p4), // 12
|
new Point(p6, p4), // 12
|
||||||
new Point(p7, p4), // 13
|
new Point(p7, p4), // 13
|
||||||
|
|
||||||
// Fifth row
|
// Fifth row
|
||||||
new Point(p1, p5), // 14
|
new Point(p1, p5), // 14
|
||||||
new Point(p2, p5), // 15
|
new Point(p2, p5), // 15
|
||||||
new Point(p6, p5), // 16
|
new Point(p6, p5), // 16
|
||||||
new Point(p7, p5), // 17
|
new Point(p7, p5), // 17
|
||||||
|
|
||||||
// Sixth row
|
// Sixth row
|
||||||
new Point(p1, p6), // 18
|
new Point(p1, p6), // 18
|
||||||
new Point(p2, p6), // 19
|
new Point(p2, p6), // 19
|
||||||
new Point(p3, p6), // 20
|
new Point(p3, p6), // 20
|
||||||
new Point(p4, p6), // 21
|
new Point(p4, p6), // 21
|
||||||
new Point(p5, p6), // 22
|
new Point(p5, p6), // 22
|
||||||
new Point(p6, p6) // 23
|
new Point(p6, p6) // 23
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Color _pixelColor = Color.FromArgb(138, 255, 0);
|
private readonly Color _pixelColor = Color.FromArgb(138, 255, 0);
|
||||||
private readonly IList<Color> _pixelColors = new List<Color>();
|
private readonly IList<Color> _pixelColors = new List<Color>();
|
||||||
private readonly IList<RectangleAnimator> _pixels = new List<RectangleAnimator>();
|
private readonly IList<RectangleAnimator> _pixels = new List<RectangleAnimator>();
|
||||||
private readonly Random _rand = new Random();
|
private readonly Random _rand = new Random();
|
||||||
private Bitmap _bitmap;
|
private Bitmap _bitmap;
|
||||||
private int _colorIndex;
|
private int _colorIndex;
|
||||||
private bool _hasAnimationsLeft;
|
private bool _hasAnimationsLeft;
|
||||||
private int _scrollCount;
|
private int _scrollCount;
|
||||||
|
|
||||||
// Variables used for the color-cycle
|
// Variables used for the color-cycle
|
||||||
private int _waitFrames;
|
private int _waitFrames;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AboutForm(
|
public AboutForm(
|
||||||
ICoreConfiguration coreConfiguration,
|
ICoreConfiguration coreConfiguration,
|
||||||
IGreenshotLanguage greenshotlanguage,
|
IGreenshotLanguage greenshotlanguage,
|
||||||
IVersionProvider versionProvider
|
IVersionProvider versionProvider
|
||||||
) : base(coreConfiguration, greenshotlanguage)
|
) : base(coreConfiguration, greenshotlanguage)
|
||||||
{
|
{
|
||||||
_greenshotlanguage = greenshotlanguage;
|
_greenshotlanguage = greenshotlanguage;
|
||||||
// Make sure our resources are removed again.
|
_versionProvider = versionProvider;
|
||||||
Disposed += Cleanup;
|
// Make sure our resources are removed again.
|
||||||
FormClosing += Cleanup;
|
Disposed += Cleanup;
|
||||||
|
FormClosing += Cleanup;
|
||||||
|
|
||||||
// Enable animation for this form, when we don't set this the timer doesn't start as soon as the form is loaded.
|
// Enable animation for this form, when we don't set this the timer doesn't start as soon as the form is loaded.
|
||||||
EnableAnimation = true;
|
EnableAnimation = true;
|
||||||
//
|
//
|
||||||
// The InitializeComponent() call is required for Windows Forms designer support.
|
// The InitializeComponent() call is required for Windows Forms designer support.
|
||||||
//
|
//
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
// Use the self drawn image, first we create the background to be the backcolor (as we animate from this)
|
// Use the self drawn image, first we create the background to be the backcolor (as we animate from this)
|
||||||
_bitmap = BitmapFactory.CreateEmpty(90, 90, PixelFormat.Format24bppRgb, BackColor);
|
_bitmap = BitmapFactory.CreateEmpty(90, 90, PixelFormat.Format24bppRgb, BackColor);
|
||||||
pictureBox1.Image = _bitmap;
|
pictureBox1.Image = _bitmap;
|
||||||
|
|
||||||
_dpiSubscription = FormDpiHandler.OnDpiChanged.Subscribe(info =>
|
_dpiSubscription = FormDpiHandler.OnDpiChanged.Subscribe(info =>
|
||||||
{
|
{
|
||||||
pictureBox1.Size = FormDpiHandler.ScaleWithCurrentDpi(new NativeSize(90,90));
|
pictureBox1.Size = FormDpiHandler.ScaleWithCurrentDpi(new NativeSize(90,90));
|
||||||
});
|
});
|
||||||
|
|
||||||
var versionInfo = $@"Greenshot {versionProvider.CurrentVersion} {(coreConfiguration.IsPortable ? " Portable" : "")} ({OsInfo.Bits} bit)";
|
var versionInfo = $@"Greenshot {versionProvider.CurrentVersion} {(coreConfiguration.IsPortable ? " Portable" : "")} ({OsInfo.Bits} bit)";
|
||||||
if (versionProvider.IsUpdateAvailable)
|
if (versionProvider.IsUpdateAvailable)
|
||||||
{
|
{
|
||||||
versionInfo += $" latest is: {versionProvider.LatestVersion}";
|
versionInfo += $" latest is: {versionProvider.LatestVersion}";
|
||||||
}
|
}
|
||||||
lblTitle.Text = versionInfo;
|
lblTitle.Text = versionInfo;
|
||||||
// Number of frames the pixel animation takes
|
// Number of frames the pixel animation takes
|
||||||
var frames = FramesForMillis(2000);
|
var frames = FramesForMillis(2000);
|
||||||
// The number of frames the color-cycle waits before it starts
|
// The number of frames the color-cycle waits before it starts
|
||||||
_waitFrames = FramesForMillis(6000);
|
_waitFrames = FramesForMillis(6000);
|
||||||
|
|
||||||
// Every pixel is created after pixelWaitFrames frames, which is increased in the loop.
|
// Every pixel is created after pixelWaitFrames frames, which is increased in the loop.
|
||||||
var pixelWaitFrames = FramesForMillis(2000);
|
var pixelWaitFrames = FramesForMillis(2000);
|
||||||
// Create pixels
|
// Create pixels
|
||||||
for (var index = 0; index < _gSpots.Count; index++)
|
for (var index = 0; index < _gSpots.Count; index++)
|
||||||
{
|
{
|
||||||
// Read the pixels in the order of the flow
|
// Read the pixels in the order of the flow
|
||||||
var gSpot = _gSpots[_flowOrder[index]];
|
var gSpot = _gSpots[_flowOrder[index]];
|
||||||
// Create the animation, first we do nothing (on the final destination)
|
// Create the animation, first we do nothing (on the final destination)
|
||||||
RectangleAnimator pixelAnimation;
|
RectangleAnimator pixelAnimation;
|
||||||
|
|
||||||
// Make the pixel grom from the middle, if this offset isn't used it looks like it's shifted
|
// Make the pixel grom from the middle, if this offset isn't used it looks like it's shifted
|
||||||
var offset = (w - 2) / 2;
|
var offset = (w - 2) / 2;
|
||||||
|
|
||||||
// If the optimize for Terminal Server is set we make the animation without much ado
|
// If the optimize for Terminal Server is set we make the animation without much ado
|
||||||
if (IsTerminalServerSession)
|
if (IsTerminalServerSession)
|
||||||
{
|
{
|
||||||
// No animation
|
// No animation
|
||||||
pixelAnimation = new RectangleAnimator(new Rectangle(gSpot.X, gSpot.Y, w - 2, w - 2), new Rectangle(gSpot.X, gSpot.Y, w - 2, w - 2), 1, EasingTypes.Cubic);
|
pixelAnimation = new RectangleAnimator(new Rectangle(gSpot.X, gSpot.Y, w - 2, w - 2), new Rectangle(gSpot.X, gSpot.Y, w - 2, w - 2), 1, EasingTypes.Cubic);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Create the animation, first we do nothing (on the final destination)
|
// Create the animation, first we do nothing (on the final destination)
|
||||||
var standingStill = new Rectangle(gSpot.X + offset, gSpot.Y + offset, 0, 0);
|
var standingStill = new Rectangle(gSpot.X + offset, gSpot.Y + offset, 0, 0);
|
||||||
pixelAnimation = new RectangleAnimator(standingStill, standingStill, pixelWaitFrames, EasingTypes.Quintic);
|
pixelAnimation = new RectangleAnimator(standingStill, standingStill, pixelWaitFrames, EasingTypes.Quintic);
|
||||||
// And than we size to the wanted size.
|
// And than we size to the wanted size.
|
||||||
pixelAnimation.QueueDestinationLeg(new Rectangle(gSpot.X, gSpot.Y, w - 2, w - 2), frames);
|
pixelAnimation.QueueDestinationLeg(new Rectangle(gSpot.X, gSpot.Y, w - 2, w - 2), frames);
|
||||||
}
|
}
|
||||||
// Increase the wait frames
|
// Increase the wait frames
|
||||||
pixelWaitFrames += FramesForMillis(100);
|
pixelWaitFrames += FramesForMillis(100);
|
||||||
// Add to the list of to be animated pixels
|
// Add to the list of to be animated pixels
|
||||||
_pixels.Add(pixelAnimation);
|
_pixels.Add(pixelAnimation);
|
||||||
// Add a color to the list for this pixel.
|
// Add a color to the list for this pixel.
|
||||||
_pixelColors.Add(_pixelColor);
|
_pixelColors.Add(_pixelColor);
|
||||||
}
|
}
|
||||||
// Make sure the frame "loop" knows we have to animate
|
// Make sure the frame "loop" knows we have to animate
|
||||||
_hasAnimationsLeft = true;
|
_hasAnimationsLeft = true;
|
||||||
|
|
||||||
// Pixel Color cycle colors, here we use a pre-animated loop which stores the values.
|
// Pixel Color cycle colors, here we use a pre-animated loop which stores the values.
|
||||||
var pixelColorAnimator = new ColorAnimator(_pixelColor, Color.FromArgb(255, 255, 255), 6, EasingTypes.Quadratic);
|
var pixelColorAnimator = new ColorAnimator(_pixelColor, Color.FromArgb(255, 255, 255), 6, EasingTypes.Quadratic);
|
||||||
pixelColorAnimator.QueueDestinationLeg(_pixelColor, 6, EasingTypes.Quadratic, EasingModes.EaseOut);
|
pixelColorAnimator.QueueDestinationLeg(_pixelColor, 6, EasingTypes.Quadratic, EasingModes.EaseOut);
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
_colorFlow.Add(pixelColorAnimator.Current);
|
_colorFlow.Add(pixelColorAnimator.Current);
|
||||||
pixelColorAnimator.Next();
|
pixelColorAnimator.Next();
|
||||||
} while (pixelColorAnimator.HasNext);
|
} while (pixelColorAnimator.HasNext);
|
||||||
|
|
||||||
// color animation for the background
|
// color animation for the background
|
||||||
_backgroundAnimation = new ColorAnimator(BackColor, _backColor, FramesForMillis(5000));
|
_backgroundAnimation = new ColorAnimator(BackColor, _backColor, FramesForMillis(5000));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleanup all the allocated resources
|
/// Cleanup all the allocated resources
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void Cleanup(object sender, EventArgs e)
|
private void Cleanup(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_bitmap != null)
|
if (_bitmap != null)
|
||||||
{
|
{
|
||||||
_bitmap.Dispose();
|
_bitmap.Dispose();
|
||||||
_bitmap = null;
|
_bitmap = null;
|
||||||
}
|
}
|
||||||
_dpiSubscription.Dispose();
|
_dpiSubscription.Dispose();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is called when a link is clicked
|
/// This is called when a link is clicked
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sender"></param>
|
/// <param name="sender"></param>
|
||||||
/// <param name="e"></param>
|
/// <param name="e"></param>
|
||||||
private void LinkLabelClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
private void LinkLabelClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!(sender is LinkLabel linkLabel))
|
if (!(sender is LinkLabel linkLabel))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
linkLabel.LinkVisited = true;
|
linkLabel.LinkVisited = true;
|
||||||
Process.Start(linkLabel.Text);
|
Process.Start(linkLabel.Text);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
MessageBox.Show(string.Format(_greenshotlanguage.ErrorOpenlink, linkLabel.Text), _greenshotlanguage.Error);
|
MessageBox.Show(string.Format(_greenshotlanguage.ErrorOpenlink, linkLabel.Text), _greenshotlanguage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called from the AnimatingForm, for every frame
|
/// Called from the AnimatingForm, for every frame
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void Animate()
|
protected override void Animate()
|
||||||
{
|
{
|
||||||
if (_bitmap == null)
|
if (_bitmap == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!IsTerminalServerSession)
|
if (!IsTerminalServerSession)
|
||||||
{
|
{
|
||||||
// Color cycle
|
// Color cycle
|
||||||
if (_waitFrames != 0)
|
if (_waitFrames != 0)
|
||||||
{
|
{
|
||||||
_waitFrames--;
|
_waitFrames--;
|
||||||
// Check if there is something else to do, if not we return so we don't occupy the CPU
|
// Check if there is something else to do, if not we return so we don't occupy the CPU
|
||||||
if (!_hasAnimationsLeft)
|
if (!_hasAnimationsLeft)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_scrollCount < _pixelColors.Count + _colorFlow.Count)
|
else if (_scrollCount < _pixelColors.Count + _colorFlow.Count)
|
||||||
{
|
{
|
||||||
// Scroll colors, the scrollCount is the amount of pixels + the amount of colors to cycle.
|
// Scroll colors, the scrollCount is the amount of pixels + the amount of colors to cycle.
|
||||||
for (var index = _pixelColors.Count - 1; index > 0; index--)
|
for (var index = _pixelColors.Count - 1; index > 0; index--)
|
||||||
{
|
{
|
||||||
_pixelColors[index] = _pixelColors[index - 1];
|
_pixelColors[index] = _pixelColors[index - 1];
|
||||||
}
|
}
|
||||||
// Keep adding from the colors to cycle until there is nothing left
|
// Keep adding from the colors to cycle until there is nothing left
|
||||||
if (_colorIndex < _colorFlow.Count)
|
if (_colorIndex < _colorFlow.Count)
|
||||||
{
|
{
|
||||||
_pixelColors[0] = _colorFlow[_colorIndex++];
|
_pixelColors[0] = _colorFlow[_colorIndex++];
|
||||||
}
|
}
|
||||||
_scrollCount++;
|
_scrollCount++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Reset values, wait X time for the next one
|
// Reset values, wait X time for the next one
|
||||||
_waitFrames = FramesForMillis(3000 + _rand.Next(35000));
|
_waitFrames = FramesForMillis(3000 + _rand.Next(35000));
|
||||||
_colorIndex = 0;
|
_colorIndex = 0;
|
||||||
_scrollCount = 0;
|
_scrollCount = 0;
|
||||||
// Check if there is something else to do, if not we return so we don't occupy the CPU
|
// Check if there is something else to do, if not we return so we don't occupy the CPU
|
||||||
if (!_hasAnimationsLeft)
|
if (!_hasAnimationsLeft)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!_hasAnimationsLeft)
|
else if (!_hasAnimationsLeft)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the "G"
|
// Draw the "G"
|
||||||
using (var graphics = Graphics.FromImage(_bitmap))
|
using (var graphics = Graphics.FromImage(_bitmap))
|
||||||
{
|
{
|
||||||
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
graphics.InterpolationMode = InterpolationMode.HighQualityBilinear;
|
graphics.InterpolationMode = InterpolationMode.HighQualityBilinear;
|
||||||
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
|
||||||
graphics.Clear(_backgroundAnimation.Next());
|
graphics.Clear(_backgroundAnimation.Next());
|
||||||
|
|
||||||
graphics.TranslateTransform(2, -2);
|
graphics.TranslateTransform(2, -2);
|
||||||
graphics.RotateTransform(20);
|
graphics.RotateTransform(20);
|
||||||
|
|
||||||
using (var brush = new SolidBrush(_pixelColor))
|
using (var brush = new SolidBrush(_pixelColor))
|
||||||
{
|
{
|
||||||
var index = 0;
|
var index = 0;
|
||||||
// We asume there is nothing to animate in the next Animate loop
|
// We asume there is nothing to animate in the next Animate loop
|
||||||
_hasAnimationsLeft = false;
|
_hasAnimationsLeft = false;
|
||||||
// Pixels of the G
|
// Pixels of the G
|
||||||
foreach (var pixel in _pixels)
|
foreach (var pixel in _pixels)
|
||||||
{
|
{
|
||||||
brush.Color = _pixelColors[index++];
|
brush.Color = _pixelColors[index++];
|
||||||
graphics.FillEllipse(brush, pixel.Current);
|
graphics.FillEllipse(brush, pixel.Current);
|
||||||
// If a pixel still has frames left, the hasAnimationsLeft will be true
|
// If a pixel still has frames left, the hasAnimationsLeft will be true
|
||||||
_hasAnimationsLeft = _hasAnimationsLeft || pixel.HasNext;
|
_hasAnimationsLeft = _hasAnimationsLeft || pixel.HasNext;
|
||||||
pixel.Next();
|
pixel.Next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pictureBox1.Invalidate();
|
pictureBox1.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CmdKey handler
|
/// CmdKey handler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="msg"></param>
|
/// <param name="msg"></param>
|
||||||
/// <param name="keyData"></param>
|
/// <param name="keyData"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
|
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
|
||||||
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
switch (keyData)
|
switch (keyData)
|
||||||
{
|
{
|
||||||
case Keys.Escape:
|
case Keys.Escape:
|
||||||
DialogResult = DialogResult.Cancel;
|
DialogResult = DialogResult.Cancel;
|
||||||
break;
|
break;
|
||||||
case Keys.E:
|
case Keys.E:
|
||||||
MessageBox.Show(EnvironmentInfo.EnvironmentToString(true));
|
MessageBox.Show(EnvironmentInfo.EnvironmentToString(true));
|
||||||
break;
|
break;
|
||||||
case Keys.L:
|
case Keys.L:
|
||||||
// TODO: Open the log file
|
// TODO: Open the log file
|
||||||
/*
|
/*
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(MainForm.LogFileLocation))
|
if (File.Exists(MainForm.LogFileLocation))
|
||||||
{
|
{
|
||||||
using (Process.Start("\"" + MainForm.LogFileLocation + "\""))
|
using (Process.Start("\"" + MainForm.LogFileLocation + "\""))
|
||||||
{
|
{
|
||||||
// nothing to do, just using dispose to cleanup
|
// nothing to do, just using dispose to cleanup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MessageBox.Show("Greenshot can't find the logfile, it should have been here: " + MainForm.LogFileLocation);
|
MessageBox.Show("Greenshot can't find the logfile, it should have been here: " + MainForm.LogFileLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
MessageBox.Show("Couldn't open the greenshot.log, it's located here: " + MainForm.LogFileLocation, "Error opening greeenshot.log", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
|
MessageBox.Show("Couldn't open the greenshot.log, it's located here: " + MainForm.LogFileLocation, "Error opening greeenshot.log", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
break;
|
break;
|
||||||
// TODO: Open configuration location
|
// TODO: Open configuration location
|
||||||
// case Keys.I:
|
// case Keys.I:
|
||||||
//try
|
//try
|
||||||
//{
|
//{
|
||||||
// using (Process.Start("\"" + IniConfig.Current.IniLocation + "\""))
|
// using (Process.Start("\"" + IniConfig.Current.IniLocation + "\""))
|
||||||
// {
|
// {
|
||||||
// // Ignore
|
// // Ignore
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
//catch (Exception)
|
//catch (Exception)
|
||||||
//{
|
//{
|
||||||
// MessageBox.Show("Couldn't open the greenshot.ini, it's located here: " + IniConfig.Current.IniLocation, "Error opening greeenshot.ini", MessageBoxButtons.OK,
|
// MessageBox.Show("Couldn't open the greenshot.ini, it's located here: " + IniConfig.Current.IniLocation, "Error opening greeenshot.ini", MessageBoxButtons.OK,
|
||||||
// MessageBoxIcon.Asterisk);
|
// MessageBoxIcon.Asterisk);
|
||||||
//}
|
//}
|
||||||
//break;
|
//break;
|
||||||
default:
|
default:
|
||||||
return base.ProcessCmdKey(ref msg, keyData);
|
return base.ProcessCmdKey(ref msg, keyData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error().WriteLine(ex, $"Error handling key '{keyData}'");
|
Log.Error().WriteLine(ex, $"Error handling key '{keyData}'");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue