mirror of
https://github.com/greenshot/greenshot
synced 2025-07-16 10:03:44 -07:00
BUG-1811: Improved the Jira destination "sub" icons, by showing the issue types.
This commit is contained in:
parent
fcd18225ef
commit
f88672b5f6
8 changed files with 423 additions and 54 deletions
246
GreenshotJiraPlugin/AsyncMemoryCache.cs
Normal file
246
GreenshotJiraPlugin/AsyncMemoryCache.cs
Normal file
|
@ -0,0 +1,246 @@
|
|||
#region Dapplo 2016 - GNU Lesser General Public License
|
||||
|
||||
// Dapplo - building blocks for .NET applications
|
||||
// Copyright (C) 2016 Dapplo
|
||||
//
|
||||
// For more information see: http://dapplo.net/
|
||||
// Dapplo repositories are hosted on GitHub: https://github.com/dapplo
|
||||
//
|
||||
// This file is part of Dapplo.Utils
|
||||
//
|
||||
// Dapplo.Utils is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Dapplo.Utils is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have a copy of the GNU Lesser General Public License
|
||||
// along with Dapplo.Utils. If not, see <http://www.gnu.org/licenses/lgpl.txt>.
|
||||
|
||||
#endregion
|
||||
|
||||
#region Usings
|
||||
|
||||
using System;
|
||||
using System.Runtime.Caching;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dapplo.Log.Facade;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
namespace GreenshotJiraPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This abstract class builds a base for a simple async memory cache.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">Type for the key</typeparam>
|
||||
/// <typeparam name="TResult">Type for the stored value</typeparam>
|
||||
public abstract class AsyncMemoryCache<TKey, TResult> where TResult : class
|
||||
{
|
||||
private static readonly Task<TResult> EmptyValueTask = Task.FromResult<TResult>(null);
|
||||
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
private readonly MemoryCache _cache = new MemoryCache(Guid.NewGuid().ToString());
|
||||
private readonly LogSource _log = new LogSource();
|
||||
|
||||
/// <summary>
|
||||
/// Set the timespan for items to expire.
|
||||
/// </summary>
|
||||
public TimeSpan? ExpireTimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the timespan for items to slide.
|
||||
/// </summary>
|
||||
public TimeSpan? SlidingTimeSpan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies if the RemovedCallback needs to be called
|
||||
/// If this is active, ActivateUpdateCallback should be false
|
||||
/// </summary>
|
||||
protected bool ActivateRemovedCallback { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies if the UpdateCallback needs to be called.
|
||||
/// If this is active, ActivateRemovedCallback should be false
|
||||
/// </summary>
|
||||
protected bool ActivateUpdateCallback { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Implement this method, it should create an instance of TResult via the supplied TKey.
|
||||
/// </summary>
|
||||
/// <param name="key">TKey</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
/// <returns>TResult</returns>
|
||||
protected abstract Task<TResult> CreateAsync(TKey key, CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a key under which the object is stored or retrieved, default is a toString on the object.
|
||||
/// </summary>
|
||||
/// <param name="keyObject">TKey</param>
|
||||
/// <returns>string</returns>
|
||||
protected virtual string CreateKey(TKey keyObject)
|
||||
{
|
||||
return keyObject.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an element from the cache, if this is not available call the create function.
|
||||
/// </summary>
|
||||
/// <param name="keyObject">object for the key</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
/// <returns>TResult</returns>
|
||||
public async Task DeleteAsync(TKey keyObject, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var key = CreateKey(keyObject);
|
||||
await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
_cache.Remove(key);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a task element from the cache, if this is not available return null.
|
||||
/// You probably want to call GetOrCreateAsync
|
||||
/// </summary>
|
||||
/// <param name="keyObject">object for the key</param>
|
||||
/// <returns>Task with TResult, null if no value</returns>
|
||||
public Task<TResult> GetAsync(TKey keyObject)
|
||||
{
|
||||
var key = CreateKey(keyObject);
|
||||
return _cache.Get(key) as Task<TResult> ?? EmptyValueTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a task element from the cache, if this is not available call the create function.
|
||||
/// </summary>
|
||||
/// <param name="keyObject">object for the key</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
/// <returns>Task with TResult</returns>
|
||||
public Task<TResult> GetOrCreateAsync(TKey keyObject, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var key = CreateKey(keyObject);
|
||||
return _cache.Get(key) as Task<TResult> ?? GetOrCreateInternalAsync(keyObject, null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a task element from the cache, if this is not available call the create function.
|
||||
/// </summary>
|
||||
/// <param name="keyObject">object for the key</param>
|
||||
/// <param name="cacheItemPolicy">CacheItemPolicy for when you want more control over the item</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
/// <returns>Task with TResult</returns>
|
||||
public Task<TResult> GetOrCreateAsync(TKey keyObject, CacheItemPolicy cacheItemPolicy, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var key = CreateKey(keyObject);
|
||||
return _cache.Get(key) as Task<TResult> ?? GetOrCreateInternalAsync(keyObject, cacheItemPolicy, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This takes care of the real async part of the code.
|
||||
/// </summary>
|
||||
/// <param name="keyObject"></param>
|
||||
/// <param name="cacheItemPolicy">CacheItemPolicy for when you want more control over the item</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
/// <returns>TResult</returns>
|
||||
private async Task<TResult> GetOrCreateInternalAsync(TKey keyObject, CacheItemPolicy cacheItemPolicy = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var key = CreateKey(keyObject);
|
||||
var completionSource = new TaskCompletionSource<TResult>();
|
||||
|
||||
if (cacheItemPolicy == null)
|
||||
{
|
||||
cacheItemPolicy = new CacheItemPolicy
|
||||
{
|
||||
AbsoluteExpiration = ExpireTimeSpan.HasValue ? DateTimeOffset.Now.Add(ExpireTimeSpan.Value) : ObjectCache.InfiniteAbsoluteExpiration,
|
||||
SlidingExpiration = SlidingTimeSpan ?? ObjectCache.NoSlidingExpiration
|
||||
};
|
||||
if (ActivateUpdateCallback)
|
||||
{
|
||||
cacheItemPolicy.UpdateCallback = UpdateCallback;
|
||||
}
|
||||
if (ActivateRemovedCallback)
|
||||
{
|
||||
cacheItemPolicy.RemovedCallback = RemovedCallback;
|
||||
}
|
||||
}
|
||||
|
||||
var result = _cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) as Task<TResult>;
|
||||
// Test if we got an existing object or our own
|
||||
if (result != null && !completionSource.Task.Equals(result))
|
||||
{
|
||||
return await result.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
result = _cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) as Task<TResult>;
|
||||
if (result != null && !completionSource.Task.Equals(result))
|
||||
{
|
||||
return await result.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Now, start the background task, which will set the completionSource with the correct response
|
||||
// ReSharper disable once MethodSupportsCancellation
|
||||
// ReSharper disable once UnusedVariable
|
||||
var ignoreBackgroundTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var backgroundResult = await CreateAsync(keyObject, cancellationToken).ConfigureAwait(false);
|
||||
completionSource.TrySetResult(backgroundResult);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
completionSource.TrySetCanceled();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completionSource.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
return await completionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to know when an item is removed, make sure to configure ActivateUpdateCallback / ActivateRemovedCallback
|
||||
/// </summary>
|
||||
/// <param name="cacheEntryRemovedArguments">CacheEntryRemovedArguments</param>
|
||||
protected virtual void RemovedCallback(CacheEntryRemovedArguments cacheEntryRemovedArguments)
|
||||
{
|
||||
_log.Verbose().WriteLine("Item {0} removed due to {1}.", cacheEntryRemovedArguments.CacheItem.Key, cacheEntryRemovedArguments.RemovedReason);
|
||||
var disposable = cacheEntryRemovedArguments.CacheItem.Value as IDisposable;
|
||||
if (disposable != null)
|
||||
{
|
||||
_log.Debug().WriteLine("Disposed cached item.");
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to modify the cache behaviour when an item is about to be removed, make sure to configure
|
||||
/// ActivateUpdateCallback / ActivateRemovedCallback
|
||||
/// </summary>
|
||||
/// <param name="cacheEntryUpdateArguments">CacheEntryUpdateArguments</param>
|
||||
protected virtual void UpdateCallback(CacheEntryUpdateArguments cacheEntryUpdateArguments)
|
||||
{
|
||||
_log.Verbose().WriteLine("Update request for {0} due to {1}.", cacheEntryUpdateArguments.Key, cacheEntryUpdateArguments.RemovedReason);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -118,12 +118,8 @@ namespace GreenshotJiraPlugin.Forms {
|
|||
|
||||
public async Task UploadAsync(IBinaryContainer attachment) {
|
||||
_config.LastUsedJira = _selectedIssue.Key;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
attachment.WriteToStream(memoryStream);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
await _jiraConnector.AttachAsync(_selectedIssue.Key, memoryStream, jiraFilenameBox.Text, attachment.ContentType);
|
||||
}
|
||||
attachment.Filename = jiraFilenameBox.Text;
|
||||
await _jiraConnector.AttachAsync(_selectedIssue.Key, attachment);
|
||||
|
||||
if (!string.IsNullOrEmpty(jiraCommentBox.Text)) {
|
||||
await _jiraConnector.AddCommentAsync(_selectedIssue.Key, jiraCommentBox.Text);
|
||||
|
@ -141,8 +137,7 @@ namespace GreenshotJiraPlugin.Forms {
|
|||
IList<Issue> issues = null;
|
||||
try
|
||||
{
|
||||
var searchResult = await _jiraConnector.SearchAsync(filter.Jql, fields: new [] { "summary", "reporter", "assignee", "created" });
|
||||
issues = searchResult.Issues;
|
||||
issues = await _jiraConnector.SearchAsync(filter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -38,8 +38,8 @@
|
|||
<HintPath>..\packages\Dapplo.HttpExtensions.0.5.31\lib\net45\Dapplo.HttpExtensions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Dapplo.Jira, Version=0.1.57.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Dapplo.Jira.0.1.57\lib\net45\Dapplo.Jira.dll</HintPath>
|
||||
<Reference Include="Dapplo.Jira, Version=0.1.58.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Dapplo.Jira.0.1.58\lib\net45\Dapplo.Jira.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Dapplo.Log.Facade, Version=0.5.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
|
@ -51,11 +51,13 @@
|
|||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Runtime.Caching" />
|
||||
<Reference Include="System.Web.Services" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AsyncMemoryCache.cs" />
|
||||
<Compile Include="Forms\JiraForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
|
@ -71,6 +73,7 @@
|
|||
<Compile Include="Forms\SettingsForm.Designer.cs">
|
||||
<DependentUpon>SettingsForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="IssueTypeBitmapCache.cs" />
|
||||
<Compile Include="JiraConnector.cs" />
|
||||
<Compile Include="JiraConfiguration.cs" />
|
||||
<Compile Include="JiraDestination.cs" />
|
||||
|
|
50
GreenshotJiraPlugin/IssueTypeBitmapCache.cs
Normal file
50
GreenshotJiraPlugin/IssueTypeBitmapCache.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Greenshot - a free and open source screenshot tool
|
||||
* Copyright (C) 2007-2016 Thomas Braun, Jens Klingen, Robin Krom
|
||||
*
|
||||
* For more information see: http://getgreenshot.org/
|
||||
* The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 1 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dapplo.Jira;
|
||||
using Dapplo.Jira.Entities;
|
||||
|
||||
namespace GreenshotJiraPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the bach for the IssueType bitmaps
|
||||
/// </summary>
|
||||
public class IssueTypeBitmapCache : AsyncMemoryCache<Issue, Bitmap>
|
||||
{
|
||||
private readonly JiraApi _jiraApi;
|
||||
|
||||
public IssueTypeBitmapCache(JiraApi jiraApi)
|
||||
{
|
||||
_jiraApi = jiraApi;
|
||||
// Set the expire timeout to an hour
|
||||
ExpireTimeSpan = TimeSpan.FromHours(1);
|
||||
}
|
||||
|
||||
protected override async Task<Bitmap> CreateAsync(Issue issue, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
return await _jiraApi.GetUriContentAsync<Bitmap>(issue.Fields.IssueType.IconUri, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,8 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
@ -31,15 +33,21 @@ using Greenshot.IniFile;
|
|||
using GreenshotPlugin.Core;
|
||||
|
||||
namespace GreenshotJiraPlugin {
|
||||
/// <summary>
|
||||
/// This encapsulates the JiraApi to make it possible to change as less old Greenshot code as needed
|
||||
/// </summary>
|
||||
public class JiraConnector : IDisposable {
|
||||
private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(JiraConnector));
|
||||
private static readonly JiraConfiguration Config = IniConfig.GetIniSection<JiraConfiguration>();
|
||||
// Used to remove the wsdl information from the old SOAP Uri
|
||||
public const string DefaultPostfix = "/rpc/soap/jirasoapservice-v2?wsdl";
|
||||
private DateTime _loggedInTime = DateTime.Now;
|
||||
private DateTimeOffset _loggedInTime = DateTimeOffset.MinValue;
|
||||
private bool _loggedIn;
|
||||
private readonly int _timeout;
|
||||
private readonly object _lock = new object();
|
||||
private string _url;
|
||||
private JiraApi _jiraApi;
|
||||
private IssueTypeBitmapCache _issueTypeBitmapCache;
|
||||
|
||||
public void Dispose() {
|
||||
if (_jiraApi != null)
|
||||
|
@ -51,7 +59,6 @@ namespace GreenshotJiraPlugin {
|
|||
public JiraConnector() {
|
||||
_url = Config.Url.Replace(DefaultPostfix, "");
|
||||
_timeout = Config.Timeout;
|
||||
_jiraApi = new JiraApi(new Uri(_url));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -60,11 +67,18 @@ namespace GreenshotJiraPlugin {
|
|||
/// <returns>true if login was done sucessfully</returns>
|
||||
private async Task<bool> DoLogin(string user, string password)
|
||||
{
|
||||
if (_url.EndsWith("wsdl"))
|
||||
lock (_lock)
|
||||
{
|
||||
_url = _url.Replace(DefaultPostfix, "");
|
||||
// recreate the service with the new url
|
||||
_jiraApi = new JiraApi(new Uri(_url));
|
||||
if (_url.EndsWith("wsdl"))
|
||||
{
|
||||
_url = _url.Replace(DefaultPostfix, "");
|
||||
}
|
||||
if (_jiraApi == null)
|
||||
{
|
||||
// recreate the service with the new url
|
||||
_jiraApi = new JiraApi(new Uri(_url));
|
||||
_issueTypeBitmapCache = new IssueTypeBitmapCache(_jiraApi);
|
||||
}
|
||||
}
|
||||
|
||||
LoginInfo loginInfo;
|
||||
|
@ -118,14 +132,22 @@ namespace GreenshotJiraPlugin {
|
|||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End the session, if there was one
|
||||
/// </summary>
|
||||
public async Task Logout() {
|
||||
if (_jiraApi != null)
|
||||
if (_jiraApi != null && _loggedIn)
|
||||
{
|
||||
await _jiraApi.EndSessionAsync();
|
||||
_loggedIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// check the login credentials, to prevent timeouts of the session, or makes a login
|
||||
/// Do not use ConfigureAwait to call this, as it will move await from the UI thread.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task CheckCredentials() {
|
||||
if (_loggedIn) {
|
||||
if (_loggedInTime.AddMinutes(_timeout-1).CompareTo(DateTime.Now) < 0) {
|
||||
|
@ -137,32 +159,81 @@ namespace GreenshotJiraPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the favourite filters
|
||||
/// </summary>
|
||||
/// <returns>List with filters</returns>
|
||||
public async Task<IList<Filter>> GetFavoriteFiltersAsync()
|
||||
{
|
||||
await CheckCredentials();
|
||||
return await _jiraApi.GetFavoriteFiltersAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the issue for a key
|
||||
/// </summary>
|
||||
/// <param name="issueKey">Jira issue key</param>
|
||||
/// <returns>Issue</returns>
|
||||
public async Task<Issue> GetIssueAsync(string issueKey)
|
||||
{
|
||||
await CheckCredentials();
|
||||
return await _jiraApi.GetIssueAsync(issueKey).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<Attachment> AttachAsync<TContent>(string issueKey, TContent content, string filename, string contentType = null, CancellationToken cancellationToken = default(CancellationToken)) where TContent : class
|
||||
|
||||
/// <summary>
|
||||
/// Attach the content to the jira
|
||||
/// </summary>
|
||||
/// <param name="issueKey"></param>
|
||||
/// <param name="content">IBinaryContainer</param>
|
||||
/// <param name="filename"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task AttachAsync(string issueKey, IBinaryContainer content, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
await CheckCredentials().ConfigureAwait(false);
|
||||
return await _jiraApi.AttachAsync(issueKey, content, filename, contentType, cancellationToken).ConfigureAwait(false);
|
||||
await CheckCredentials();
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
content.WriteToStream(memoryStream);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
await _jiraApi.AttachAsync(issueKey, memoryStream, content.Filename, content.ContentType, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a comment to the supplied issue
|
||||
/// </summary>
|
||||
/// <param name="issueKey">Jira issue key</param>
|
||||
/// <param name="body">text</param>
|
||||
/// <param name="visibility">the visibility role</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
public async Task AddCommentAsync(string issueKey, string body, string visibility = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
await CheckCredentials();
|
||||
await _jiraApi.AddCommentAsync(issueKey, body, visibility, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<SearchResult> SearchAsync(string jql, int maxResults = 20, IList<string> fields = null, CancellationToken cancellationToken = default(CancellationToken))
|
||||
|
||||
/// <summary>
|
||||
/// Get the search results for the specified filter
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IList<Issue>> SearchAsync(Filter filter, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
await CheckCredentials();
|
||||
return await _jiraApi.SearchAsync(jql, maxResults, fields, cancellationToken).ConfigureAwait(false);
|
||||
var searchResult = await _jiraApi.SearchAsync(filter.Jql, 20, new[] { "summary", "reporter", "assignee", "created" }, cancellationToken).ConfigureAwait(false);
|
||||
return searchResult.Issues;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the bitmap representing the issue type of an issue, from cache.
|
||||
/// </summary>
|
||||
/// <param name="issue">Issue</param>
|
||||
/// <param name="cancellationToken">CancellationToken</param>
|
||||
/// <returns>Bitmap</returns>
|
||||
public async Task<Bitmap> GetIssueTypeBitmapAsync(Issue issue, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return await _issueTypeBitmapCache.GetOrCreateAsync(issue, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Uri JiraBaseUri => _jiraApi.JiraBaseUri;
|
||||
|
|
|
@ -41,30 +41,27 @@ namespace GreenshotJiraPlugin {
|
|||
private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(JiraDestination));
|
||||
private static readonly JiraConfiguration Config = IniConfig.GetIniSection<JiraConfiguration>();
|
||||
private readonly JiraPlugin _jiraPlugin;
|
||||
private readonly Issue _jira;
|
||||
private readonly Issue _jiraIssue;
|
||||
|
||||
public JiraDestination(JiraPlugin jiraPlugin) {
|
||||
_jiraPlugin = jiraPlugin;
|
||||
}
|
||||
|
||||
public JiraDestination(JiraPlugin jiraPlugin, Issue jira) {
|
||||
public JiraDestination(JiraPlugin jiraPlugin, Issue jiraIssue) {
|
||||
_jiraPlugin = jiraPlugin;
|
||||
_jira = jira;
|
||||
_jiraIssue = jiraIssue;
|
||||
}
|
||||
|
||||
public override string Designation => "Jira";
|
||||
|
||||
private string FormatUpload(Issue jira) {
|
||||
return Designation + " - " + jira.Key + ": " + jira.Fields.Summary.Substring(0, Math.Min(20, jira.Fields.Summary.Length));
|
||||
}
|
||||
|
||||
public override string Description {
|
||||
get
|
||||
{
|
||||
if (_jira == null) {
|
||||
if (_jiraIssue?.Fields?.Summary == null) {
|
||||
return Language.GetString("jira", LangKey.upload_menu_item);
|
||||
}
|
||||
return FormatUpload(_jira);
|
||||
// Format the title of this destination
|
||||
return Designation + " - " + _jiraIssue.Key + ": " + _jiraIssue.Fields.Summary.Substring(0, Math.Min(20, _jiraIssue.Fields.Summary.Length));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,9 +70,19 @@ namespace GreenshotJiraPlugin {
|
|||
public override bool isDynamic => true;
|
||||
|
||||
public override Image DisplayIcon {
|
||||
get {
|
||||
var resources = new ComponentResourceManager(typeof(JiraPlugin));
|
||||
return (Image)resources.GetObject("Jira");
|
||||
get
|
||||
{
|
||||
Image displayIcon = null;
|
||||
if (_jiraIssue != null && JiraPlugin.Instance.CurrentJiraConnector != null)
|
||||
{
|
||||
displayIcon = JiraPlugin.Instance.CurrentJiraConnector.GetIssueTypeBitmapAsync(_jiraIssue).Result;
|
||||
}
|
||||
if (displayIcon == null)
|
||||
{
|
||||
var resources = new ComponentResourceManager(typeof(JiraPlugin));
|
||||
displayIcon = (Image)resources.GetObject("Jira");
|
||||
}
|
||||
return displayIcon;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,23 +102,18 @@ namespace GreenshotJiraPlugin {
|
|||
ExportInformation exportInformation = new ExportInformation(Designation, Description);
|
||||
string filename = Path.GetFileName(FilenameHelper.GetFilename(Config.UploadFormat, captureDetails));
|
||||
SurfaceOutputSettings outputSettings = new SurfaceOutputSettings(Config.UploadFormat, Config.UploadJpegQuality, Config.UploadReduceColors);
|
||||
if (_jira != null) {
|
||||
if (_jiraIssue != null) {
|
||||
try {
|
||||
// Run upload in the background
|
||||
new PleaseWaitForm().ShowAndWait(Description, Language.GetString("jira", LangKey.communication_wait),
|
||||
async () =>
|
||||
{
|
||||
var surfaceContainer = new SurfaceContainer(surfaceToUpload, outputSettings, filename);
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
surfaceContainer.WriteToStream(memoryStream);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
await _jiraPlugin.JiraConnector.AttachAsync(_jira.Key, memoryStream, filename, surfaceContainer.ContentType);
|
||||
}
|
||||
surfaceToUpload.UploadURL = _jiraPlugin.JiraConnector.JiraBaseUri.AppendSegments("browse", _jira.Key).AbsoluteUri;
|
||||
await _jiraPlugin.JiraConnector.AttachAsync(_jiraIssue.Key, surfaceContainer);
|
||||
surfaceToUpload.UploadURL = _jiraPlugin.JiraConnector.JiraBaseUri.AppendSegments("browse", _jiraIssue.Key).AbsoluteUri;
|
||||
}
|
||||
);
|
||||
Log.DebugFormat("Uploaded to Jira {0}", _jira.Key);
|
||||
Log.DebugFormat("Uploaded to Jira {0}", _jiraIssue.Key);
|
||||
exportInformation.ExportMade = true;
|
||||
exportInformation.Uri = surfaceToUpload.UploadURL;
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Dapplo.HttpExtensions" version="0.5.31" targetFramework="net45" />
|
||||
<package id="Dapplo.Jira" version="0.1.57" targetFramework="net45" />
|
||||
<package id="Dapplo.Jira" version="0.1.58" targetFramework="net45" />
|
||||
<package id="Dapplo.Log.Facade" version="0.5.4" targetFramework="net45" />
|
||||
<package id="LibZ.Tool" version="1.2.0.0" targetFramework="net45" />
|
||||
</packages>
|
|
@ -557,6 +557,7 @@ namespace GreenshotPlugin.Core {
|
|||
void Upload(HttpWebRequest webRequest);
|
||||
|
||||
string ContentType { get; }
|
||||
string Filename { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -564,7 +565,6 @@ namespace GreenshotPlugin.Core {
|
|||
/// </summary>
|
||||
public class ByteContainer : IBinaryContainer {
|
||||
private readonly byte[] _file;
|
||||
private readonly string _fileName;
|
||||
private readonly string _contentType;
|
||||
private readonly int _fileSize;
|
||||
public ByteContainer(byte[] file) : this(file, null) {
|
||||
|
@ -575,7 +575,7 @@ namespace GreenshotPlugin.Core {
|
|||
}
|
||||
public ByteContainer(byte[] file, string filename, string contenttype, int filesize) {
|
||||
_file = file;
|
||||
_fileName = filename;
|
||||
Filename = filename;
|
||||
_contentType = contenttype;
|
||||
_fileSize = filesize == 0 ? file.Length : filesize;
|
||||
}
|
||||
|
@ -607,7 +607,7 @@ namespace GreenshotPlugin.Core {
|
|||
string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n",
|
||||
boundary,
|
||||
name,
|
||||
_fileName ?? name,
|
||||
Filename ?? name,
|
||||
_contentType ?? "application/octet-stream");
|
||||
|
||||
formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header));
|
||||
|
@ -638,6 +638,7 @@ namespace GreenshotPlugin.Core {
|
|||
}
|
||||
|
||||
public string ContentType => _contentType;
|
||||
public string Filename { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -646,12 +647,11 @@ namespace GreenshotPlugin.Core {
|
|||
public class BitmapContainer : IBinaryContainer {
|
||||
private readonly Bitmap _bitmap;
|
||||
private readonly SurfaceOutputSettings _outputSettings;
|
||||
private readonly string _fileName;
|
||||
|
||||
public BitmapContainer(Bitmap bitmap, SurfaceOutputSettings outputSettings, string filename) {
|
||||
_bitmap = bitmap;
|
||||
_outputSettings = outputSettings;
|
||||
_fileName = filename;
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -689,7 +689,7 @@ namespace GreenshotPlugin.Core {
|
|||
string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n",
|
||||
boundary,
|
||||
name,
|
||||
_fileName ?? name,
|
||||
Filename ?? name,
|
||||
ContentType);
|
||||
|
||||
formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header));
|
||||
|
@ -717,6 +717,8 @@ namespace GreenshotPlugin.Core {
|
|||
}
|
||||
|
||||
public string ContentType => "image/" + _outputSettings.Format;
|
||||
|
||||
public string Filename { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -725,12 +727,11 @@ namespace GreenshotPlugin.Core {
|
|||
public class SurfaceContainer : IBinaryContainer {
|
||||
private readonly ISurface _surface;
|
||||
private readonly SurfaceOutputSettings _outputSettings;
|
||||
private readonly string _fileName;
|
||||
|
||||
public SurfaceContainer(ISurface surface, SurfaceOutputSettings outputSettings, string filename) {
|
||||
_surface = surface;
|
||||
_outputSettings = outputSettings;
|
||||
_fileName = filename;
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -768,7 +769,7 @@ namespace GreenshotPlugin.Core {
|
|||
string header = string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\";\r\nContent-Type: {3}\r\n\r\n",
|
||||
boundary,
|
||||
name,
|
||||
_fileName ?? name,
|
||||
Filename ?? name,
|
||||
ContentType);
|
||||
|
||||
formDataStream.Write(Encoding.UTF8.GetBytes(header), 0, Encoding.UTF8.GetByteCount(header));
|
||||
|
@ -796,5 +797,6 @@ namespace GreenshotPlugin.Core {
|
|||
}
|
||||
|
||||
public string ContentType => "image/" + _outputSettings.Format;
|
||||
public string Filename { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue