mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
[AdvancedPaste]Add paste actions to allow transcoding of media files (#37188)
* [AdvancedPaste] Additional actions, including Image to text * Spellcheck issue * [AdvancedPaste] Paste as file and many other improvements * Fixed typo * Fixed typo * [AdvancedPaste] Improved paste window menu layout * [AdvancedPaste] Improved settings window layout * [AdvancedPaste] Removed AudioToText for the moment * Code cleanup * Minor fixes * [AdvancedPaste] Semantic Kernel support * Changed log-line with potentially sensitive info * Spellcheck issues * Various improvements for Semantic Kernel * Spellcheck issue * Refactored Clipboard routines * Added integration tests for KernelService * Extra telemetry for AdvancedPaste * Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event * Added IsSavedQuery * Added KernelQueryCache * Refactoring * Added KernelQueryCache to BugReportTool delete list * Added opt-n for Semantic Kernel * Fixed bug with KernelQueryCache * Ability to view last AI chat message on error * Improved kernel query cache * Used System.IO.Abstractions and improved tests * Fixed under-count of token usage * Used Semantic Kernel icon * Cleanup * Add missing EndProject line * Fix dependency version conflicts * Fix NOTICE.md * Correct place of SemanticKernel in NOTICE.md * Unlinked CustomPreview toggle from AI * Added Microsoft.Bcl.AsyncInterfaces dependency to AdvancedPaste * Fixed NOTICE.md order * Moved Custom Preview to behaviour section * Made Image to Text raise error on empty output * Added AIServiceBatchIntegrationTests * Updated AIServiceBatchIntegrationTests * Added prompt moderation * [AdvancedPaste] Media Transcoding support * Spellcheck issue * Improved transcoding output profile and added tests * Moved GPO Infobar to better location * Added cancel button and minor bug fixes * Fixed crash * Minor cleanups * Improved transcoding error messages * Used software back when transcoding fails with hardware accerlation * Added Reencode to spellcheck * Spellcheck issue --------- Co-authored-by: Jaime Bernardo <jaime@janeasystems.com> Co-authored-by: Dustin L. Howett <dustin@howett.net> Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
This commit is contained in:
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -1283,7 +1283,7 @@ rectp
|
|||||||
RECTSOURCE
|
RECTSOURCE
|
||||||
recyclebin
|
recyclebin
|
||||||
Redist
|
Redist
|
||||||
reencode
|
Reencode
|
||||||
reencoded
|
reencoded
|
||||||
REFCLSID
|
REFCLSID
|
||||||
REFGUID
|
REFGUID
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Models.KernelQueryCache;
|
using AdvancedPaste.Models.KernelQueryCache;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AdvancedPaste.UnitTests.Mocks;
|
||||||
|
|
||||||
|
internal sealed class NoOpProgress : IProgress<double>
|
||||||
|
{
|
||||||
|
public void Report(double value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -131,17 +132,18 @@ public sealed class AIServiceBatchIntegrationTests
|
|||||||
{
|
{
|
||||||
VaultCredentialsProvider credentialsProvider = new();
|
VaultCredentialsProvider credentialsProvider = new();
|
||||||
PromptModerationService promptModerationService = new(credentialsProvider);
|
PromptModerationService promptModerationService = new(credentialsProvider);
|
||||||
|
NoOpProgress progress = new();
|
||||||
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
|
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
|
||||||
|
|
||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
case PasteFormats.CustomTextTransformation:
|
case PasteFormats.CustomTextTransformation:
|
||||||
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
|
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress));
|
||||||
|
|
||||||
case PasteFormats.KernelQuery:
|
case PasteFormats.KernelQuery:
|
||||||
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
|
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
|
||||||
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
|
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
|
||||||
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
|
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException($"Unexpected format {format}");
|
throw new InvalidOperationException($"Unexpected format {format}");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -130,7 +131,7 @@ public sealed class KernelServiceIntegrationTests : IDisposable
|
|||||||
|
|
||||||
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
|
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
|
||||||
{
|
{
|
||||||
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
|
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false, CancellationToken.None, new NoOpProgress());
|
||||||
|
|
||||||
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
|
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
|
||||||
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);
|
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using AdvancedPaste.Helpers;
|
||||||
|
using AdvancedPaste.Models;
|
||||||
|
using AdvancedPaste.UnitTests.Mocks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Windows.Storage;
|
||||||
|
using Windows.Storage.FileProperties;
|
||||||
|
|
||||||
|
namespace AdvancedPaste.UnitTests.ServicesTests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class TranscodeHelperIntegrationTests
|
||||||
|
{
|
||||||
|
private sealed record class MediaProperties(BasicProperties Basic, MusicProperties Music, VideoProperties Video);
|
||||||
|
|
||||||
|
private const string InputRootFolder = @"%USERPROFILE%\AdvancedPasteTranscodeMediaTestData";
|
||||||
|
|
||||||
|
/// <summary> Tests transforming a folder of media files.
|
||||||
|
/// - Verifies that the output file has the same basic properties (e.g. duration) as the input file.
|
||||||
|
/// - Copies the output file to a subfolder of the input folder for manual inspection.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(@"audio", PasteFormats.TranscodeToMp3)]
|
||||||
|
[DataRow(@"video", PasteFormats.TranscodeToMp4)]
|
||||||
|
public async Task TestTransformFolder(string inputSubfolder, PasteFormats format)
|
||||||
|
{
|
||||||
|
var inputFolder = Environment.ExpandEnvironmentVariables(Path.Combine(InputRootFolder, inputSubfolder));
|
||||||
|
|
||||||
|
if (!Directory.Exists(inputFolder))
|
||||||
|
{
|
||||||
|
Assert.Inconclusive($"Skipping tests for {inputFolder} as it does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputPath = Path.Combine(inputFolder, $"test_output_{format}");
|
||||||
|
|
||||||
|
foreach (var inputPath in Directory.EnumerateFiles(inputFolder))
|
||||||
|
{
|
||||||
|
await RunTestTransformFileAsync(inputPath, outputPath, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunTestTransformFileAsync(string inputPath, string finalOutputPath, PasteFormats format)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Running {nameof(RunTestTransformFileAsync)} for {inputPath}/{format}");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(finalOutputPath);
|
||||||
|
|
||||||
|
var inputPackage = await DataPackageHelpers.CreateFromFileAsync(inputPath);
|
||||||
|
var inputProperties = await GetPropertiesAsync(await StorageFile.GetFileFromPathAsync(inputPath));
|
||||||
|
|
||||||
|
var outputPackage = await TransformHelpers.TransformAsync(format, inputPackage.GetView(), CancellationToken.None, new NoOpProgress());
|
||||||
|
|
||||||
|
var outputItems = await outputPackage.GetView().GetStorageItemsAsync();
|
||||||
|
Assert.AreEqual(1, outputItems.Count);
|
||||||
|
var outputFile = outputItems.Single() as StorageFile;
|
||||||
|
Assert.IsNotNull(outputFile);
|
||||||
|
var outputProperties = await GetPropertiesAsync(outputFile);
|
||||||
|
AssertPropertiesMatch(format, inputProperties, outputProperties);
|
||||||
|
|
||||||
|
await outputFile.CopyAsync(await StorageFolder.GetFolderFromPathAsync(finalOutputPath), outputFile.Name, NameCollisionOption.ReplaceExisting);
|
||||||
|
await outputPackage.GetView().TryCleanupAfterDelayAsync(TimeSpan.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertPropertiesMatch(PasteFormats format, MediaProperties inputProperties, MediaProperties outputProperties)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(outputProperties.Basic.Size > 0);
|
||||||
|
|
||||||
|
Assert.AreEqual(inputProperties.Music.Title, outputProperties.Music.Title);
|
||||||
|
Assert.AreEqual(inputProperties.Music.Album, outputProperties.Music.Album);
|
||||||
|
Assert.AreEqual(inputProperties.Music.Artist, outputProperties.Music.Artist);
|
||||||
|
AssertDurationsApproxEqual(inputProperties.Music.Duration, outputProperties.Music.Duration);
|
||||||
|
|
||||||
|
if (format == PasteFormats.TranscodeToMp4)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(inputProperties.Video.Title, outputProperties.Video.Title);
|
||||||
|
AssertDurationsApproxEqual(inputProperties.Video.Duration, outputProperties.Video.Duration);
|
||||||
|
|
||||||
|
var inputVideoDimensions = GetNormalizedDimensions(inputProperties.Video);
|
||||||
|
if (inputVideoDimensions != null)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(inputVideoDimensions, GetNormalizedDimensions(outputProperties.Video));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<MediaProperties> GetPropertiesAsync(StorageFile file) =>
|
||||||
|
new(await file.GetBasicPropertiesAsync(), await file.Properties.GetMusicPropertiesAsync(), await file.Properties.GetVideoPropertiesAsync());
|
||||||
|
|
||||||
|
private static void AssertDurationsApproxEqual(TimeSpan expected, TimeSpan actual) =>
|
||||||
|
Assert.AreEqual(expected.Ticks, actual.Ticks, delta: TimeSpan.FromSeconds(1).Ticks);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the dimensions of a video, if available. Accounts for the fact that the dimensions may sometimes be swapped.
|
||||||
|
/// </summary>
|
||||||
|
private static (uint Width, uint Height)? GetNormalizedDimensions(VideoProperties properties) =>
|
||||||
|
properties.Width == 0 || properties.Height == 0
|
||||||
|
? null
|
||||||
|
: (Math.Max(properties.Width, properties.Height), Math.Min(properties.Width, properties.Height));
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
BorderThickness="4"
|
BorderThickness="4"
|
||||||
CornerRadius="{TemplateBinding CornerRadius}"
|
CornerRadius="{TemplateBinding CornerRadius}"
|
||||||
|
IsHitTestVisible="False"
|
||||||
Visibility="Collapsed">
|
Visibility="Collapsed">
|
||||||
<!-- CornerRadius needs to be > 0 -->
|
<!-- CornerRadius needs to be > 0 -->
|
||||||
<Grid.BorderBrush>
|
<Grid.BorderBrush>
|
||||||
|
|||||||
@@ -178,17 +178,36 @@
|
|||||||
Padding="0"
|
Padding="0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch">
|
VerticalAlignment="Stretch">
|
||||||
<Image
|
<ProgressRing
|
||||||
x:Name="AIGlyphImage"
|
Width="30"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
Height="30"
|
||||||
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
|
HorizontalAlignment="Right"
|
||||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
|
VerticalAlignment="Center"
|
||||||
<PathIcon
|
IsActive="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||||
x:Name="AIGlyph"
|
IsIndeterminate="{Binding DataContext.HasIndeterminateTransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
|
||||||
AutomationProperties.AccessibilityView="Raw"
|
Maximum="100"
|
||||||
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
|
Minimum="0"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||||
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
Value="{Binding DataContext.TransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Margin="0"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||||
|
<Image
|
||||||
|
x:Name="AIGlyphImage"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
|
||||||
|
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||||
|
<PathIcon
|
||||||
|
x:Name="AIGlyph"
|
||||||
|
AutomationProperties.AccessibilityView="Raw"
|
||||||
|
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
@@ -572,6 +591,24 @@
|
|||||||
Duration="0:0:0.167" />
|
Duration="0:0:0.167" />
|
||||||
</animations:Implicit.HideAnimations>
|
</animations:Implicit.HideAnimations>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
x:Name="CancelBtn"
|
||||||
|
x:Uid="CancelBtnAutomation"
|
||||||
|
Padding="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||||
|
Command="{x:Bind CancelPasteActionCommand}"
|
||||||
|
Content="{ui:FontIcon Glyph=,
|
||||||
|
FontSize=16}"
|
||||||
|
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||||
|
IsEnabled="False"
|
||||||
|
Style="{StaticResource SubtleButtonStyle}"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<ToolTipService.ToolTip>
|
||||||
|
<TextBlock x:Uid="CancelBtnToolTip" TextWrapping="WrapWholeWords" />
|
||||||
|
</ToolTipService.ToolTip>
|
||||||
|
</Button>
|
||||||
<!-- Transparent overlay to show tooltip -->
|
<!-- Transparent overlay to show tooltip -->
|
||||||
<Grid
|
<Grid
|
||||||
x:Name="SendBtnOverlay"
|
x:Name="SendBtnOverlay"
|
||||||
@@ -679,6 +716,10 @@
|
|||||||
<Setter Target="Loader.IsLoading" Value="True" />
|
<Setter Target="Loader.IsLoading" Value="True" />
|
||||||
<Setter Target="InputTxtBox.IsEnabled" Value="False" />
|
<Setter Target="InputTxtBox.IsEnabled" Value="False" />
|
||||||
<Setter Target="SendBtn.IsEnabled" Value="False" />
|
<Setter Target="SendBtn.IsEnabled" Value="False" />
|
||||||
|
<Setter Target="SendBtn.Visibility" Value="Collapsed" />
|
||||||
|
<Setter Target="SendBtnOverlay.Visibility" Value="Collapsed" />
|
||||||
|
<Setter Target="CancelBtn.IsEnabled" Value="True" />
|
||||||
|
<Setter Target="CancelBtn.Visibility" Value="Visible" />
|
||||||
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
|
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
|
||||||
<Setter Target="LoadingText.Visibility" Value="Visible" />
|
<Setter Target="LoadingText.Visibility" Value="Visible" />
|
||||||
</VisualState.Setters>
|
</VisualState.Setters>
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ namespace AdvancedPaste.Controls
|
|||||||
|
|
||||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteActionError))
|
if (e.PropertyName is nameof(ViewModel.IsBusy) or nameof(ViewModel.PasteActionError))
|
||||||
{
|
{
|
||||||
var state = ViewModel.Busy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
||||||
VisualStateManager.GoToState(this, state, true);
|
VisualStateManager.GoToState(this, state, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,9 @@ namespace AdvancedPaste.Controls
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task GenerateCustomAIAsync() => await ViewModel.ExecuteCustomAIFormatFromCurrentQueryAsync(PasteActionSource.PromptBox);
|
private async Task GenerateCustomAIAsync() => await ViewModel.ExecuteCustomAIFormatFromCurrentQueryAsync(PasteActionSource.PromptBox);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task CancelPasteActionAsync() => await ViewModel.CancelPasteActionAsync();
|
||||||
|
|
||||||
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIAvailable)
|
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIAvailable)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace AdvancedPaste
|
|||||||
{
|
{
|
||||||
private readonly WindowMessageMonitor _msgMonitor;
|
private readonly WindowMessageMonitor _msgMonitor;
|
||||||
private readonly IUserSettings _userSettings;
|
private readonly IUserSettings _userSettings;
|
||||||
|
private readonly OptionsViewModel _optionsViewModel;
|
||||||
|
|
||||||
private bool _disposedValue;
|
private bool _disposedValue;
|
||||||
|
|
||||||
@@ -32,8 +33,7 @@ namespace AdvancedPaste
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
_userSettings = App.GetService<IUserSettings>();
|
_userSettings = App.GetService<IUserSettings>();
|
||||||
|
_optionsViewModel = App.GetService<OptionsViewModel>();
|
||||||
var optionsViewModel = App.GetService<OptionsViewModel>();
|
|
||||||
|
|
||||||
var baseHeight = MinHeight;
|
var baseHeight = MinHeight;
|
||||||
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
|
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
|
||||||
@@ -43,7 +43,7 @@ namespace AdvancedPaste
|
|||||||
double GetHeight(int maxCustomActionCount) =>
|
double GetHeight(int maxCustomActionCount) =>
|
||||||
baseHeight +
|
baseHeight +
|
||||||
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
|
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
|
||||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
|
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
|
||||||
|
|
||||||
MinHeight = GetHeight(1);
|
MinHeight = GetHeight(1);
|
||||||
Height = GetHeight(5);
|
Height = GetHeight(5);
|
||||||
@@ -52,9 +52,9 @@ namespace AdvancedPaste
|
|||||||
UpdateHeight();
|
UpdateHeight();
|
||||||
|
|
||||||
_userSettings.Changed += (_, _) => UpdateHeight();
|
_userSettings.Changed += (_, _) => UpdateHeight();
|
||||||
optionsViewModel.PropertyChanged += (_, e) =>
|
_optionsViewModel.PropertyChanged += (_, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(optionsViewModel.IsCustomAIServiceEnabled))
|
if (e.PropertyName == nameof(_optionsViewModel.IsCustomAIServiceEnabled))
|
||||||
{
|
{
|
||||||
UpdateHeight();
|
UpdateHeight();
|
||||||
}
|
}
|
||||||
@@ -111,8 +111,9 @@ namespace AdvancedPaste
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
private async void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||||
{
|
{
|
||||||
|
await _optionsViewModel.CancelPasteActionAsync();
|
||||||
Hide();
|
Hide();
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Models;
|
using AdvancedPaste.Models;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.Win32;
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
using Windows.Data.Html;
|
using Windows.Data.Html;
|
||||||
using Windows.Graphics.Imaging;
|
using Windows.Graphics.Imaging;
|
||||||
@@ -18,8 +22,6 @@ namespace AdvancedPaste.Helpers;
|
|||||||
|
|
||||||
internal static class DataPackageHelpers
|
internal static class DataPackageHelpers
|
||||||
{
|
{
|
||||||
private static readonly Lazy<HashSet<string>> ImageFileTypes = new(GetImageFileTypes());
|
|
||||||
|
|
||||||
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
|
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
|
||||||
[
|
[
|
||||||
(StandardDataFormats.Text, ClipboardFormat.Text),
|
(StandardDataFormats.Text, ClipboardFormat.Text),
|
||||||
@@ -27,6 +29,14 @@ internal static class DataPackageHelpers
|
|||||||
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
|
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly Lazy<(ClipboardFormat Format, HashSet<string> FileTypes)[]> SupportedFileTypes =
|
||||||
|
new(() =>
|
||||||
|
[
|
||||||
|
(ClipboardFormat.Image, GetImageFileTypes()),
|
||||||
|
(ClipboardFormat.Audio, GetMediaFileTypes("audio")),
|
||||||
|
(ClipboardFormat.Video, GetMediaFileTypes("video")),
|
||||||
|
]);
|
||||||
|
|
||||||
internal static DataPackage CreateFromText(string text)
|
internal static DataPackage CreateFromText(string text)
|
||||||
{
|
{
|
||||||
DataPackage dataPackage = new();
|
DataPackage dataPackage = new();
|
||||||
@@ -57,9 +67,12 @@ internal static class DataPackageHelpers
|
|||||||
{
|
{
|
||||||
availableFormats |= ClipboardFormat.File;
|
availableFormats |= ClipboardFormat.File;
|
||||||
|
|
||||||
if (ImageFileTypes.Value.Contains(file.FileType))
|
foreach (var (format, fileTypes) in SupportedFileTypes.Value)
|
||||||
{
|
{
|
||||||
availableFormats |= ClipboardFormat.Image;
|
if (fileTypes.Contains(file.FileType))
|
||||||
|
{
|
||||||
|
availableFormats |= format;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +106,60 @@ internal static class DataPackageHelpers
|
|||||||
return availableFormats == ClipboardFormat.Text ? !string.IsNullOrEmpty(await dataPackageView.GetTextAsync()) : availableFormats != ClipboardFormat.None;
|
return availableFormats == ClipboardFormat.Text ? !string.IsNullOrEmpty(await dataPackageView.GetTextAsync()) : availableFormats != ClipboardFormat.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static async Task TryCleanupAfterDelayAsync(this DataPackageView dataPackageView, TimeSpan delay)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tempFile = await GetSingleTempFileOrNullAsync(dataPackageView);
|
||||||
|
|
||||||
|
if (tempFile != null)
|
||||||
|
{
|
||||||
|
await Task.Delay(delay);
|
||||||
|
|
||||||
|
Logger.LogDebug($"Cleaning up temporary file with extension [{tempFile.Extension}] from data package after delay");
|
||||||
|
|
||||||
|
tempFile.Delete();
|
||||||
|
if (NormalizeDirectoryPath(tempFile.Directory?.Parent?.FullName) == NormalizeDirectoryPath(Path.GetTempPath()))
|
||||||
|
{
|
||||||
|
tempFile.Directory?.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError("Failed to clean up temporary files", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<FileInfo> GetSingleTempFileOrNullAsync(this DataPackageView dataPackageView)
|
||||||
|
{
|
||||||
|
if (!dataPackageView.Contains(StandardDataFormats.StorageItems))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storageItems = await dataPackageView.GetStorageItemsAsync();
|
||||||
|
|
||||||
|
if (storageItems.Count != 1 || storageItems.Single() is not StorageFile file)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo fileInfo = new(file.Path);
|
||||||
|
var tempPathDirectory = NormalizeDirectoryPath(Path.GetTempPath());
|
||||||
|
|
||||||
|
var directoryPaths = new[] { fileInfo.Directory, fileInfo.Directory?.Parent }
|
||||||
|
.Where(directory => directory != null)
|
||||||
|
.Select(directory => NormalizeDirectoryPath(directory.FullName));
|
||||||
|
|
||||||
|
return directoryPaths.Contains(NormalizeDirectoryPath(Path.GetTempPath())) ? fileInfo : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeDirectoryPath(string path) =>
|
||||||
|
Path.GetFullPath(new Uri(path).LocalPath)
|
||||||
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||||
|
.ToUpperInvariant();
|
||||||
|
|
||||||
internal static async Task<string> GetTextOrEmptyAsync(this DataPackageView dataPackageView) =>
|
internal static async Task<string> GetTextOrEmptyAsync(this DataPackageView dataPackageView) =>
|
||||||
dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty;
|
dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty;
|
||||||
|
|
||||||
@@ -153,4 +220,27 @@ internal static class DataPackageHelpers
|
|||||||
BitmapDecoder.GetDecoderInformationEnumerator()
|
BitmapDecoder.GetDecoderInformationEnumerator()
|
||||||
.SelectMany(di => di.FileExtensions)
|
.SelectMany(di => di.FileExtensions)
|
||||||
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
private static HashSet<string> GetMediaFileTypes(string mediaKind)
|
||||||
|
{
|
||||||
|
static string AssocQueryString(NativeMethods.AssocStr assocStr, string extension)
|
||||||
|
{
|
||||||
|
uint pcchOut = 0;
|
||||||
|
|
||||||
|
NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, null, ref pcchOut);
|
||||||
|
|
||||||
|
StringBuilder pszOut = new((int)pcchOut);
|
||||||
|
var hResult = NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, pszOut, ref pcchOut);
|
||||||
|
return hResult == NativeMethods.HResult.Ok ? pszOut.ToString() : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparison = StringComparison.OrdinalIgnoreCase;
|
||||||
|
var extensions = from extension in Registry.ClassesRoot.GetSubKeyNames()
|
||||||
|
where extension.StartsWith('.')
|
||||||
|
where AssocQueryString(NativeMethods.AssocStr.PerceivedType, extension).Equals(mediaKind, comparison) ||
|
||||||
|
AssocQueryString(NativeMethods.AssocStr.ContentType, extension).StartsWith($"{mediaKind}/", comparison)
|
||||||
|
select extension;
|
||||||
|
|
||||||
|
return extensions.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Models;
|
using AdvancedPaste.Models;
|
||||||
@@ -17,6 +18,8 @@ internal static class KernelExtensions
|
|||||||
private const string DataPackageKey = "DataPackage";
|
private const string DataPackageKey = "DataPackage";
|
||||||
private const string LastErrorKey = "LastError";
|
private const string LastErrorKey = "LastError";
|
||||||
private const string ActionChainKey = "ActionChain";
|
private const string ActionChainKey = "ActionChain";
|
||||||
|
private const string CancellationTokenKey = "CancellationToken";
|
||||||
|
private const string ProgressKey = "Progress";
|
||||||
|
|
||||||
internal static DataPackageView GetDataPackageView(this Kernel kernel)
|
internal static DataPackageView GetDataPackageView(this Kernel kernel)
|
||||||
{
|
{
|
||||||
@@ -40,6 +43,14 @@ internal static class KernelExtensions
|
|||||||
|
|
||||||
internal static void SetDataPackageView(this Kernel kernel, DataPackageView dataPackageView) => kernel.Data[DataPackageKey] = dataPackageView;
|
internal static void SetDataPackageView(this Kernel kernel, DataPackageView dataPackageView) => kernel.Data[DataPackageKey] = dataPackageView;
|
||||||
|
|
||||||
|
internal static CancellationToken GetCancellationToken(this Kernel kernel) => kernel.Data.TryGetValue(CancellationTokenKey, out object value) ? (CancellationToken)value : CancellationToken.None;
|
||||||
|
|
||||||
|
internal static void SetCancellationToken(this Kernel kernel, CancellationToken cancellationToken) => kernel.Data[CancellationTokenKey] = cancellationToken;
|
||||||
|
|
||||||
|
internal static IProgress<double> GetProgress(this Kernel kernel) => kernel.Data.TryGetValue(ProgressKey, out object obj) ? obj as IProgress<double> : null;
|
||||||
|
|
||||||
|
internal static void SetProgress(this Kernel kernel, IProgress<double> progress) => kernel.Data[ProgressKey] = progress;
|
||||||
|
|
||||||
internal static Exception GetLastError(this Kernel kernel) => kernel.Data.TryGetValue(LastErrorKey, out object obj) ? obj as Exception : null;
|
internal static Exception GetLastError(this Kernel kernel) => kernel.Data.TryGetValue(LastErrorKey, out object obj) ? obj as Exception : null;
|
||||||
|
|
||||||
internal static void SetLastError(this Kernel kernel, Exception error) => kernel.Data[LastErrorKey] = error;
|
internal static void SetLastError(this Kernel kernel, Exception error) => kernel.Data[LastErrorKey] = error;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace AdvancedPaste.Helpers
|
namespace AdvancedPaste.Helpers
|
||||||
{
|
{
|
||||||
@@ -83,6 +84,68 @@ namespace AdvancedPaste.Helpers
|
|||||||
Scancode = 0x0008,
|
Scancode = 0x0008,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HResult
|
||||||
|
{
|
||||||
|
Ok = 0x0000,
|
||||||
|
False = 0x0001,
|
||||||
|
InvalidArguments = unchecked((int)0x80070057),
|
||||||
|
OutOfMemory = unchecked((int)0x8007000E),
|
||||||
|
NoInterface = unchecked((int)0x80004002),
|
||||||
|
Fail = unchecked((int)0x80004005),
|
||||||
|
ExtractionFailed = unchecked((int)0x8004B200),
|
||||||
|
ElementNotFound = unchecked((int)0x80070490),
|
||||||
|
TypeElementNotFound = unchecked((int)0x8002802B),
|
||||||
|
NoObject = unchecked((int)0x800401E5),
|
||||||
|
Win32ErrorCanceled = 1223,
|
||||||
|
Canceled = unchecked((int)0x800704C7),
|
||||||
|
ResourceInUse = unchecked((int)0x800700AA),
|
||||||
|
AccessDenied = unchecked((int)0x80030005),
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum AssocF
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Init_NoRemapCLSID = 0x1,
|
||||||
|
Init_ByExeName = 0x2,
|
||||||
|
Open_ByExeName = 0x3,
|
||||||
|
Init_DefaultToStar = 0x4,
|
||||||
|
Init_DefaultToFolder = 0x8,
|
||||||
|
NoUserSettings = 0x10,
|
||||||
|
NoTruncate = 0x20,
|
||||||
|
Verify = 0x40,
|
||||||
|
RemapRunDll = 0x80,
|
||||||
|
NoFixUps = 0x100,
|
||||||
|
IgnoreBaseClass = 0x200,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AssocStr
|
||||||
|
{
|
||||||
|
Command = 1,
|
||||||
|
Executable,
|
||||||
|
FriendlyDocName,
|
||||||
|
FriendlyAppName,
|
||||||
|
NoOpen,
|
||||||
|
ShellNewValue,
|
||||||
|
DDECommand,
|
||||||
|
DDEIfExec,
|
||||||
|
DDEApplication,
|
||||||
|
DDETopic,
|
||||||
|
InfoTip,
|
||||||
|
QuickTip,
|
||||||
|
TileInfo,
|
||||||
|
ContentType,
|
||||||
|
DefaultIcon,
|
||||||
|
ShellExtension,
|
||||||
|
PerceivedType,
|
||||||
|
DelegateExecute,
|
||||||
|
SupportedUriProtocols,
|
||||||
|
ProgId,
|
||||||
|
AppId,
|
||||||
|
AppPublisher,
|
||||||
|
AppIconReference,
|
||||||
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||||
|
|
||||||
@@ -100,5 +163,8 @@ namespace AdvancedPaste.Helpers
|
|||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern bool GetCursorPos(out PointInter lpPoint);
|
internal static extern bool GetCursorPos(out PointInter lpPoint);
|
||||||
|
|
||||||
|
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Windows.Globalization;
|
using Windows.Globalization;
|
||||||
@@ -15,11 +16,14 @@ namespace AdvancedPaste.Helpers;
|
|||||||
|
|
||||||
public static class OcrHelpers
|
public static class OcrHelpers
|
||||||
{
|
{
|
||||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap)
|
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
|
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
|
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
|
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
|
||||||
|
|
||||||
return string.IsNullOrWhiteSpace(ocrResult.Text)
|
return string.IsNullOrWhiteSpace(ocrResult.Text)
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using AdvancedPaste.Models;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
|
using Windows.Media.MediaProperties;
|
||||||
|
using Windows.Media.Transcoding;
|
||||||
|
using Windows.Storage;
|
||||||
|
|
||||||
|
namespace AdvancedPaste.Helpers;
|
||||||
|
|
||||||
|
internal static class TranscodeHelpers
|
||||||
|
{
|
||||||
|
public static async Task<DataPackage> TranscodeToMp3Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||||
|
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", cancellationToken, progress);
|
||||||
|
|
||||||
|
public static async Task<DataPackage> TranscodeToMp4Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||||
|
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", cancellationToken, progress);
|
||||||
|
|
||||||
|
private static async Task<DataPackage> TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
Logger.LogTrace();
|
||||||
|
|
||||||
|
var inputFiles = await clipboardData.GetStorageItemsAsync();
|
||||||
|
|
||||||
|
if (inputFiles.Count != 1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} does not support multiple files");
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputFile = inputFiles.Single() as StorageFile ?? throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} only supports files");
|
||||||
|
var inputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFile.Path);
|
||||||
|
|
||||||
|
var inputProfile = await MediaEncodingProfile.CreateFromFileAsync(inputFile);
|
||||||
|
var outputProfile = CreateOutputProfile(inputProfile, baseOutputProfile);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
static string ProfileToString(MediaEncodingProfile profile) => System.Text.Json.JsonSerializer.Serialize(profile, options: new() { WriteIndented = true });
|
||||||
|
Logger.LogDebug($"{nameof(inputProfile)}: {ProfileToString(inputProfile)}");
|
||||||
|
Logger.LogDebug($"{nameof(outputProfile)}: {ProfileToString(outputProfile)}");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var outputFolder = await Task.Run(() => Directory.CreateTempSubdirectory("PowerToys_AdvancedPaste_"), cancellationToken);
|
||||||
|
var outputFileName = StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(inputFile.Path), extension) ? inputFileNameWithoutExtension + "_1" : inputFileNameWithoutExtension;
|
||||||
|
var outputFilePath = Path.Combine(outputFolder.FullName, Path.ChangeExtension(outputFileName, extension));
|
||||||
|
await File.WriteAllBytesAsync(outputFilePath, [], cancellationToken); // TranscodeAsync seems to require the output file to exist
|
||||||
|
|
||||||
|
await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, cancellationToken, progress);
|
||||||
|
|
||||||
|
return await DataPackageHelpers.CreateFromFileAsync(outputFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaEncodingProfile CreateOutputProfile(MediaEncodingProfile inputProfile, MediaEncodingProfile baseOutputProfile)
|
||||||
|
{
|
||||||
|
MediaEncodingProfile outputProfile = new()
|
||||||
|
{
|
||||||
|
Video = null,
|
||||||
|
Audio = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
outputProfile.Container = baseOutputProfile.Container.Copy();
|
||||||
|
|
||||||
|
if (inputProfile.Video != null && baseOutputProfile.Video != null)
|
||||||
|
{
|
||||||
|
outputProfile.Video = baseOutputProfile.Video.Copy();
|
||||||
|
|
||||||
|
if (inputProfile.Video.Bitrate != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Video.Bitrate = inputProfile.Video.Bitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Video.FrameRate.Numerator != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Video.FrameRate.Numerator = inputProfile.Video.FrameRate.Numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Video.FrameRate.Denominator != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Video.FrameRate.Denominator = inputProfile.Video.FrameRate.Denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Video.PixelAspectRatio.Numerator != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Video.PixelAspectRatio.Numerator = inputProfile.Video.PixelAspectRatio.Numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Video.PixelAspectRatio.Denominator != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Video.PixelAspectRatio.Denominator = inputProfile.Video.PixelAspectRatio.Denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputProfile.Video.Width = inputProfile.Video.Width;
|
||||||
|
outputProfile.Video.Height = inputProfile.Video.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Audio != null && baseOutputProfile.Audio != null)
|
||||||
|
{
|
||||||
|
outputProfile.Audio = baseOutputProfile.Audio.Copy();
|
||||||
|
|
||||||
|
if (inputProfile.Audio.Bitrate != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Audio.Bitrate = inputProfile.Audio.Bitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Audio.BitsPerSample != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Audio.BitsPerSample = inputProfile.Audio.BitsPerSample;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Audio.ChannelCount != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Audio.ChannelCount = inputProfile.Audio.ChannelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputProfile.Audio.SampleRate != 0)
|
||||||
|
{
|
||||||
|
outputProfile.Audio.SampleRate = inputProfile.Audio.SampleRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
|
{
|
||||||
|
if (outputProfile.Video == null && outputProfile.Audio == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Target profile does not contain media");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<PrepareTranscodeResult> GetPrepareResult(bool hardwareAccelerationEnabled)
|
||||||
|
{
|
||||||
|
MediaTranscoder transcoder = new()
|
||||||
|
{
|
||||||
|
AlwaysReencode = false,
|
||||||
|
HardwareAccelerationEnabled = hardwareAccelerationEnabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await transcoder.PrepareFileTranscodeAsync(inputFile, outputFile, outputProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
var prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: true);
|
||||||
|
|
||||||
|
if (!prepareResult.CanTranscode)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Unable to transcode with hardware acceleration enabled, falling back to software; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}");
|
||||||
|
|
||||||
|
prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prepareResult.CanTranscode)
|
||||||
|
{
|
||||||
|
var message = ResourceLoaderInstance.ResourceLoader.GetString(prepareResult.FailureReason == TranscodeFailureReason.CodecNotFound ? "TranscodeErrorUnsupportedCodec" : "TranscodeErrorGeneral");
|
||||||
|
throw new PasteActionException(message, new InvalidOperationException($"Error transcoding; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
await prepareResult.TranscodeAsync().AsTask(cancellationToken, progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Models;
|
using AdvancedPaste.Models;
|
||||||
@@ -17,17 +18,19 @@ namespace AdvancedPaste.Helpers;
|
|||||||
|
|
||||||
public static class TransformHelpers
|
public static class TransformHelpers
|
||||||
{
|
{
|
||||||
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData)
|
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
{
|
{
|
||||||
return format switch
|
return format switch
|
||||||
{
|
{
|
||||||
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
|
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
|
||||||
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
|
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
|
||||||
PasteFormats.Json => await ToJsonAsync(clipboardData),
|
PasteFormats.Json => await ToJsonAsync(clipboardData),
|
||||||
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData),
|
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData, cancellationToken),
|
||||||
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData),
|
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData, cancellationToken),
|
||||||
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData),
|
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData, cancellationToken),
|
||||||
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData),
|
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData, cancellationToken),
|
||||||
|
PasteFormats.TranscodeToMp3 => await TranscodeHelpers.TranscodeToMp3Async(clipboardData, cancellationToken, progress),
|
||||||
|
PasteFormats.TranscodeToMp4 => await TranscodeHelpers.TranscodeToMp4Async(clipboardData, cancellationToken, progress),
|
||||||
PasteFormats.KernelQuery => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
|
PasteFormats.KernelQuery => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
|
||||||
PasteFormats.CustomTextTransformation => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
|
PasteFormats.CustomTextTransformation => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
|
||||||
_ => throw new ArgumentException($"Unknown value {format}", nameof(format)),
|
_ => throw new ArgumentException($"Unknown value {format}", nameof(format)),
|
||||||
@@ -52,16 +55,16 @@ public static class TransformHelpers
|
|||||||
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
|
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData)
|
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.LogTrace();
|
Logger.LogTrace();
|
||||||
|
|
||||||
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
|
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
|
||||||
var text = await OcrHelpers.ExtractTextAsync(bitmap);
|
var text = await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
|
||||||
return CreateDataPackageFromText(text);
|
return CreateDataPackageFromText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData)
|
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.LogTrace();
|
Logger.LogTrace();
|
||||||
|
|
||||||
@@ -72,25 +75,25 @@ public static class TransformHelpers
|
|||||||
encoder.SetSoftwareBitmap(clipboardBitmap);
|
encoder.SetSoftwareBitmap(clipboardBitmap);
|
||||||
await encoder.FlushAsync();
|
await encoder.FlushAsync();
|
||||||
|
|
||||||
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png");
|
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png", cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData)
|
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.LogTrace();
|
Logger.LogTrace();
|
||||||
|
|
||||||
var text = await clipboardData.GetTextOrHtmlTextAsync();
|
var text = await clipboardData.GetTextOrHtmlTextAsync();
|
||||||
return await CreateDataPackageFromFileContentAsync(text, "txt");
|
return await CreateDataPackageFromFileContentAsync(text, "txt", cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData)
|
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.LogTrace();
|
Logger.LogTrace();
|
||||||
|
|
||||||
var cfHtml = await clipboardData.GetHtmlContentAsync();
|
var cfHtml = await clipboardData.GetHtmlContentAsync();
|
||||||
var html = RemoveHtmlMetadata(cfHtml);
|
var html = RemoveHtmlMetadata(cfHtml);
|
||||||
|
|
||||||
return await CreateDataPackageFromFileContentAsync(html, "html");
|
return await CreateDataPackageFromFileContentAsync(html, "html", cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -114,7 +117,7 @@ public static class TransformHelpers
|
|||||||
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
|
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension)
|
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(data))
|
if (string.IsNullOrEmpty(data))
|
||||||
{
|
{
|
||||||
@@ -123,16 +126,16 @@ public static class TransformHelpers
|
|||||||
|
|
||||||
var path = GetPasteAsFileTempFilePath(fileExtension);
|
var path = GetPasteAsFileTempFilePath(fileExtension);
|
||||||
|
|
||||||
await File.WriteAllTextAsync(path, data);
|
await File.WriteAllTextAsync(path, data, cancellationToken);
|
||||||
return await DataPackageHelpers.CreateFromFileAsync(path);
|
return await DataPackageHelpers.CreateFromFileAsync(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension)
|
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var path = GetPasteAsFileTempFilePath(fileExtension);
|
var path = GetPasteAsFileTempFilePath(fileExtension);
|
||||||
|
|
||||||
using var fileStream = File.Create(path);
|
using var fileStream = File.Create(path);
|
||||||
await stream.CopyToAsync(fileStream);
|
await stream.CopyToAsync(fileStream, cancellationToken);
|
||||||
|
|
||||||
return await DataPackageHelpers.CreateFromFileAsync(path);
|
return await DataPackageHelpers.CreateFromFileAsync(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ namespace AdvancedPaste.Settings
|
|||||||
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
|
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
|
||||||
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
|
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
|
||||||
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
|
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
|
||||||
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile])
|
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile]),
|
||||||
|
(PasteFormats.TranscodeToMp3, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp3]),
|
||||||
|
(PasteFormats.TranscodeToMp4, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp4]),
|
||||||
];
|
];
|
||||||
|
|
||||||
_additionalActions.Clear();
|
_additionalActions.Clear();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public enum ClipboardFormat
|
|||||||
Text = 1 << 0,
|
Text = 1 << 0,
|
||||||
Html = 1 << 1,
|
Html = 1 << 1,
|
||||||
Audio = 1 << 2,
|
Audio = 1 << 2,
|
||||||
Image = 1 << 3,
|
Video = 1 << 3,
|
||||||
File = 1 << 4, // output only for now
|
Image = 1 << 4,
|
||||||
|
File = 1 << 5, // output only for now
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public sealed class PasteActionError
|
|||||||
public static PasteActionError FromException(Exception ex) =>
|
public static PasteActionError FromException(Exception ex) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
|
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString(ex is OperationCanceledException ? "PasteActionCanceled" : "PasteError"),
|
||||||
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
|
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,12 +82,34 @@ public enum PasteFormats
|
|||||||
KernelFunctionDescription = "Takes HTML data in the clipboard and transforms it to an HTML file.")]
|
KernelFunctionDescription = "Takes HTML data in the clipboard and transforms it to an HTML file.")]
|
||||||
PasteAsHtmlFile,
|
PasteAsHtmlFile,
|
||||||
|
|
||||||
|
[PasteFormatMetadata(
|
||||||
|
IsCoreAction = false,
|
||||||
|
ResourceId = "TranscodeToMp3",
|
||||||
|
IconGlyph = "\uE8D6",
|
||||||
|
RequiresAIService = false,
|
||||||
|
CanPreview = false,
|
||||||
|
SupportedClipboardFormats = ClipboardFormat.Audio | ClipboardFormat.Video,
|
||||||
|
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp3,
|
||||||
|
KernelFunctionDescription = "Takes an audio or video file in the clipboard and transcodes it to MP3.")]
|
||||||
|
TranscodeToMp3,
|
||||||
|
|
||||||
|
[PasteFormatMetadata(
|
||||||
|
IsCoreAction = false,
|
||||||
|
ResourceId = "TranscodeToMp4",
|
||||||
|
IconGlyph = "\uE714",
|
||||||
|
RequiresAIService = false,
|
||||||
|
CanPreview = false,
|
||||||
|
SupportedClipboardFormats = ClipboardFormat.Video,
|
||||||
|
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp4,
|
||||||
|
KernelFunctionDescription = "Takes a video file in the clipboard and transcodes it to MP4 (H.264/AAC).")]
|
||||||
|
TranscodeToMp4,
|
||||||
|
|
||||||
[PasteFormatMetadata(
|
[PasteFormatMetadata(
|
||||||
IsCoreAction = false,
|
IsCoreAction = false,
|
||||||
IconGlyph = "\uE945",
|
IconGlyph = "\uE945",
|
||||||
RequiresAIService = true,
|
RequiresAIService = true,
|
||||||
CanPreview = true,
|
CanPreview = true,
|
||||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Image,
|
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Video | ClipboardFormat.Image,
|
||||||
RequiresPrompt = true)]
|
RequiresPrompt = true)]
|
||||||
KernelQuery,
|
KernelQuery,
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AdvancedPaste.Services;
|
namespace AdvancedPaste.Services;
|
||||||
|
|
||||||
public interface ICustomTextTransformService
|
public interface ICustomTextTransformService
|
||||||
{
|
{
|
||||||
Task<string> TransformTextAsync(string prompt, string inputText);
|
Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
@@ -10,5 +12,5 @@ namespace AdvancedPaste.Services;
|
|||||||
|
|
||||||
public interface IKernelService
|
public interface IKernelService
|
||||||
{
|
{
|
||||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery);
|
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Models;
|
using AdvancedPaste.Models;
|
||||||
@@ -11,5 +13,5 @@ namespace AdvancedPaste.Services;
|
|||||||
|
|
||||||
public interface IPasteFormatExecutor
|
public interface IPasteFormatExecutor
|
||||||
{
|
{
|
||||||
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
|
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AdvancedPaste.Services;
|
namespace AdvancedPaste.Services;
|
||||||
|
|
||||||
public interface IPromptModerationService
|
public interface IPromptModerationService
|
||||||
{
|
{
|
||||||
Task ValidateAsync(string fullPrompt);
|
Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -36,12 +37,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
|
|
||||||
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
|
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
|
||||||
|
|
||||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery)
|
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
{
|
{
|
||||||
Logger.LogTrace();
|
Logger.LogTrace();
|
||||||
|
|
||||||
var kernel = CreateKernel();
|
var kernel = CreateKernel();
|
||||||
kernel.SetDataPackageView(clipboardData);
|
kernel.SetDataPackageView(clipboardData);
|
||||||
|
kernel.SetCancellationToken(cancellationToken);
|
||||||
|
kernel.SetProgress(progress);
|
||||||
|
|
||||||
CacheKey cacheKey = new() { Prompt = prompt, AvailableFormats = await clipboardData.GetAvailableFormatsAsync() };
|
CacheKey cacheKey = new() { Prompt = prompt, AvailableFormats = await clipboardData.GetAvailableFormatsAsync() };
|
||||||
var maybeCacheValue = _queryCacheService.ReadOrNull(cacheKey);
|
var maybeCacheValue = _queryCacheService.ReadOrNull(cacheKey);
|
||||||
@@ -51,7 +54,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt);
|
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt, cancellationToken);
|
||||||
|
|
||||||
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
||||||
|
|
||||||
@@ -84,7 +87,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
AdvancedPasteSemanticKernelErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
AdvancedPasteSemanticKernelErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||||
|
|
||||||
if (ex is PasteActionException)
|
if (ex is PasteActionException or OperationCanceledException)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@@ -127,7 +130,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
return $"{combinedSystemMessage}{newLine}{newLine}User instructions:{newLine}{userPromptMessage.Content}";
|
return $"{combinedSystemMessage}{newLine}{newLine}User instructions:{newLine}{userPromptMessage.Content}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt)
|
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ChatHistory chatHistory = [];
|
ChatHistory chatHistory = [];
|
||||||
|
|
||||||
@@ -141,10 +144,10 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||||
chatHistory.AddUserMessage(prompt);
|
chatHistory.AddUserMessage(prompt);
|
||||||
|
|
||||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory));
|
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||||
|
|
||||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
|
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
|
||||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel);
|
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
|
||||||
chatHistory.Add(chatResult);
|
chatHistory.Add(chatResult);
|
||||||
|
|
||||||
var totalUsage = chatHistory.Select(GetAIServiceUsage)
|
var totalUsage = chatHistory.Select(GetAIServiceUsage)
|
||||||
@@ -157,6 +160,8 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
{
|
{
|
||||||
foreach (var item in actionChain)
|
foreach (var item in actionChain)
|
||||||
{
|
{
|
||||||
|
kernel.GetCancellationToken().ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (item.Arguments.Count > 0)
|
if (item.Arguments.Count > 0)
|
||||||
{
|
{
|
||||||
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
|
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
|
||||||
@@ -208,14 +213,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
async dataPackageView =>
|
async dataPackageView =>
|
||||||
{
|
{
|
||||||
var input = await dataPackageView.GetTextAsync();
|
var input = await dataPackageView.GetTextAsync();
|
||||||
string output = await GetPromptBasedOutput(format, prompt, input);
|
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||||
return DataPackageHelpers.CreateFromText(output);
|
return DataPackageHelpers.CreateFromText(output);
|
||||||
});
|
});
|
||||||
|
|
||||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input) =>
|
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||||
format switch
|
format switch
|
||||||
{
|
{
|
||||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input),
|
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
|
||||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,7 +228,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
|||||||
ExecuteTransformAsync(
|
ExecuteTransformAsync(
|
||||||
kernel,
|
kernel,
|
||||||
new ActionChainItem(format, Arguments: []),
|
new ActionChainItem(format, Arguments: []),
|
||||||
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView));
|
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView, kernel.GetCancellationToken(), kernel.GetProgress()));
|
||||||
|
|
||||||
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
|
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -23,11 +24,11 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
|||||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||||
|
|
||||||
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage)
|
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var fullPrompt = systemInstructions + "\n\n" + userMessage;
|
var fullPrompt = systemInstructions + "\n\n" + userMessage;
|
||||||
|
|
||||||
await _promptModerationService.ValidateAsync(fullPrompt);
|
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||||
|
|
||||||
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
|
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
|
||||||
|
|
||||||
@@ -41,7 +42,8 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
|||||||
},
|
},
|
||||||
Temperature = 0.01F,
|
Temperature = 0.01F,
|
||||||
MaxTokens = 2000,
|
MaxTokens = 2000,
|
||||||
});
|
},
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
if (response.Value.Choices[0].FinishReason == "length")
|
if (response.Value.Choices[0].FinishReason == "length")
|
||||||
{
|
{
|
||||||
@@ -51,7 +53,7 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> TransformTextAsync(string prompt, string inputText)
|
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(prompt))
|
if (string.IsNullOrWhiteSpace(prompt))
|
||||||
{
|
{
|
||||||
@@ -80,7 +82,7 @@ Output:
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await GetAICompletionAsync(systemInstructions, userMessage);
|
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
|
||||||
|
|
||||||
var usage = response.Usage;
|
var usage = response.Usage;
|
||||||
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
|
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
|
||||||
@@ -98,7 +100,7 @@ Output:
|
|||||||
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||||
|
|
||||||
if (ex is PasteActionException)
|
if (ex is PasteActionException or OperationCanceledException)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System.ClientModel;
|
using System.ClientModel;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -18,12 +19,12 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
|
|||||||
|
|
||||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||||
|
|
||||||
public async Task ValidateAsync(string fullPrompt)
|
public async Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
|
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
|
||||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt);
|
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
|
||||||
var moderationResult = moderationClientResult.Value;
|
var moderationResult = moderationClientResult.Value;
|
||||||
|
|
||||||
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");
|
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -17,7 +18,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
|
|||||||
private readonly IKernelService _kernelService = kernelService;
|
private readonly IKernelService _kernelService = kernelService;
|
||||||
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
|
||||||
|
|
||||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
|
||||||
{
|
{
|
||||||
if (!pasteFormat.IsEnabled)
|
if (!pasteFormat.IsEnabled)
|
||||||
{
|
{
|
||||||
@@ -34,9 +35,9 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
|
|||||||
return await Task.Run(async () =>
|
return await Task.Run(async () =>
|
||||||
pasteFormat.Format switch
|
pasteFormat.Format switch
|
||||||
{
|
{
|
||||||
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery),
|
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
|
||||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync())),
|
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
|
||||||
_ => await TransformHelpers.TransformAsync(format, clipboardData),
|
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,9 @@
|
|||||||
<data name="OpenAIApiKeyError" xml:space="preserve">
|
<data name="OpenAIApiKeyError" xml:space="preserve">
|
||||||
<value>OpenAI request failed with status code: </value>
|
<value>OpenAI request failed with status code: </value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PasteActionCanceled" xml:space="preserve">
|
||||||
|
<value>The paste operation was canceled</value>
|
||||||
|
</data>
|
||||||
<data name="PasteError" xml:space="preserve">
|
<data name="PasteError" xml:space="preserve">
|
||||||
<value>An error occurred during the paste operation</value>
|
<value>An error occurred during the paste operation</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -188,7 +191,19 @@
|
|||||||
</data>
|
</data>
|
||||||
<data name="PasteAsHtmlFile" xml:space="preserve">
|
<data name="PasteAsHtmlFile" xml:space="preserve">
|
||||||
<value>Paste as .html file</value>
|
<value>Paste as .html file</value>
|
||||||
|
</data>
|
||||||
|
<data name="TranscodeToMp3" xml:space="preserve">
|
||||||
|
<value>Transcode to .mp3</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TranscodeToMp4" xml:space="preserve">
|
||||||
|
<value>Transcode to .mp4 (H.264/AAC)</value>
|
||||||
|
</data>
|
||||||
|
<data name="TranscodeErrorGeneral" xml:space="preserve">
|
||||||
|
<value>An error occurred while transcoding media file</value>
|
||||||
|
</data>
|
||||||
|
<data name="TranscodeErrorUnsupportedCodec" xml:space="preserve">
|
||||||
|
<value>The media file contains an unsupported codec</value>
|
||||||
|
</data>
|
||||||
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
<value>Paste</value>
|
<value>Paste</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -207,6 +222,9 @@
|
|||||||
<data name="SendBtnToolTip.Text" xml:space="preserve">
|
<data name="SendBtnToolTip.Text" xml:space="preserve">
|
||||||
<value>Generate and paste data</value>
|
<value>Generate and paste data</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CancelBtnToolTip.Text" xml:space="preserve">
|
||||||
|
<value>Cancel paste operation</value>
|
||||||
|
</data>
|
||||||
<data name="RegenerateBtnToolTip.Text" xml:space="preserve">
|
<data name="RegenerateBtnToolTip.Text" xml:space="preserve">
|
||||||
<value>Regenerate</value>
|
<value>Regenerate</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -216,6 +234,9 @@
|
|||||||
<data name="SendButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
<data name="SendButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
<value>Generate and paste data</value>
|
<value>Generate and paste data</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CancelBtnAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||||
|
<value>Cancel paste operation</value>
|
||||||
|
</data>
|
||||||
<data name="SettingsBtn.Content" xml:space="preserve">
|
<data name="SettingsBtn.Content" xml:space="preserve">
|
||||||
<value>Open settings</value>
|
<value>Open settings</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using AdvancedPaste.Helpers;
|
using AdvancedPaste.Helpers;
|
||||||
@@ -29,7 +31,7 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
|||||||
|
|
||||||
namespace AdvancedPaste.ViewModels
|
namespace AdvancedPaste.ViewModels
|
||||||
{
|
{
|
||||||
public sealed partial class OptionsViewModel : ObservableObject, IDisposable
|
public sealed partial class OptionsViewModel : ObservableObject, IProgress<double>, IDisposable
|
||||||
{
|
{
|
||||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
private readonly DispatcherTimer _clipboardTimer;
|
private readonly DispatcherTimer _clipboardTimer;
|
||||||
@@ -37,6 +39,8 @@ namespace AdvancedPaste.ViewModels
|
|||||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||||
private readonly IAICredentialsProvider _aiCredentialsProvider;
|
private readonly IAICredentialsProvider _aiCredentialsProvider;
|
||||||
|
|
||||||
|
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||||
|
|
||||||
public DataPackageView ClipboardData { get; set; }
|
public DataPackageView ClipboardData { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -65,7 +69,11 @@ namespace AdvancedPaste.ViewModels
|
|||||||
private bool _pasteFormatsDirty;
|
private bool _pasteFormatsDirty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _busy;
|
private bool _isBusy;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasIndeterminateTransformProgress))]
|
||||||
|
private double _transformProgress = double.NaN;
|
||||||
|
|
||||||
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
|
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
|
||||||
|
|
||||||
@@ -81,9 +89,24 @@ namespace AdvancedPaste.ViewModels
|
|||||||
|
|
||||||
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
|
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
|
||||||
|
|
||||||
|
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||||
|
|
||||||
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
|
||||||
|
|
||||||
private bool Visible => GetMainWindow()?.Visible is true;
|
private bool Visible
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return GetMainWindow()?.Visible is true;
|
||||||
|
}
|
||||||
|
catch (COMException)
|
||||||
|
{
|
||||||
|
return false; // window is closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public event EventHandler PreviewRequested;
|
public event EventHandler PreviewRequested;
|
||||||
|
|
||||||
@@ -189,7 +212,12 @@ namespace AdvancedPaste.ViewModels
|
|||||||
|
|
||||||
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
|
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
|
||||||
{
|
{
|
||||||
collection.Clear();
|
// Hack: Clear collection via repeated RemoveAt to avoid this crash, which seems to occasionally occur when using Clear:
|
||||||
|
// https://github.com/microsoft/microsoft-ui-xaml/issues/8684
|
||||||
|
while (collection.Count > 0)
|
||||||
|
{
|
||||||
|
collection.RemoveAt(collection.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var format in FilterAndSort(pasteFormats))
|
foreach (var format in FilterAndSort(pasteFormats))
|
||||||
{
|
{
|
||||||
@@ -214,12 +242,13 @@ namespace AdvancedPaste.ViewModels
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_clipboardTimer.Stop();
|
_clipboardTimer.Stop();
|
||||||
|
_pasteActionCancellationTokenSource?.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReadClipboardAsync()
|
public async Task ReadClipboardAsync()
|
||||||
{
|
{
|
||||||
if (Busy)
|
if (IsBusy)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -324,6 +353,10 @@ namespace AdvancedPaste.ViewModels
|
|||||||
{
|
{
|
||||||
await ClipboardHelper.TryCopyPasteAsync(package, HideWindow);
|
await ClipboardHelper.TryCopyPasteAsync(package, HideWindow);
|
||||||
Query = string.Empty;
|
Query = string.Empty;
|
||||||
|
|
||||||
|
// Delete any temp files created. A delay is needed to ensure the file is not in use by the target application -
|
||||||
|
// for example, when pasting onto File Explorer, the paste operation will trigger a file copy.
|
||||||
|
_ = Task.Run(() => package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to select the previous custom format
|
// Command to select the previous custom format
|
||||||
@@ -362,7 +395,7 @@ namespace AdvancedPaste.ViewModels
|
|||||||
|
|
||||||
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||||
{
|
{
|
||||||
if (Busy)
|
if (IsBusy)
|
||||||
{
|
{
|
||||||
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
|
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
|
||||||
return;
|
return;
|
||||||
@@ -377,16 +410,18 @@ namespace AdvancedPaste.ViewModels
|
|||||||
var elapsedWatch = Stopwatch.StartNew();
|
var elapsedWatch = Stopwatch.StartNew();
|
||||||
Logger.LogDebug($"Started executing {pasteFormat.Format} from source {source}");
|
Logger.LogDebug($"Started executing {pasteFormat.Format} from source {source}");
|
||||||
|
|
||||||
Busy = true;
|
IsBusy = true;
|
||||||
|
_pasteActionCancellationTokenSource = new();
|
||||||
|
TransformProgress = double.NaN;
|
||||||
PasteActionError = PasteActionError.None;
|
PasteActionError = PasteActionError.None;
|
||||||
Query = pasteFormat.Query;
|
Query = pasteFormat.Query;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Minimum time to show busy spinner for AI actions when triggered by global keyboard shortcut.
|
// Minimum time to show busy spinner for AI actions when triggered by global keyboard shortcut.
|
||||||
var aiActionMinTaskTime = TimeSpan.FromSeconds(2);
|
var aiActionMinTaskTime = TimeSpan.FromSeconds(1.5);
|
||||||
var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask;
|
var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask;
|
||||||
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source);
|
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source, _pasteActionCancellationTokenSource.Token, this);
|
||||||
|
|
||||||
await delayTask;
|
await delayTask;
|
||||||
|
|
||||||
@@ -410,7 +445,9 @@ namespace AdvancedPaste.ViewModels
|
|||||||
PasteActionError = PasteActionError.FromException(ex);
|
PasteActionError = PasteActionError.FromException(ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Busy = false;
|
IsBusy = false;
|
||||||
|
_pasteActionCancellationTokenSource?.Dispose();
|
||||||
|
_pasteActionCancellationTokenSource = null;
|
||||||
elapsedWatch.Stop();
|
elapsedWatch.Stop();
|
||||||
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
|
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
|
||||||
}
|
}
|
||||||
@@ -484,5 +521,26 @@ namespace AdvancedPaste.ViewModels
|
|||||||
|
|
||||||
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
|
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CancelPasteActionAsync()
|
||||||
|
{
|
||||||
|
if (_pasteActionCancellationTokenSource != null)
|
||||||
|
{
|
||||||
|
await _pasteActionCancellationTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IProgress<double>.Report(double value)
|
||||||
|
{
|
||||||
|
ReportProgress(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReportProgress(double value)
|
||||||
|
{
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
TransformProgress = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ void Trace::AdvancedPaste_SettingsTelemetry(const PowertoyModuleIface::Hotkey& p
|
|||||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"ImageToText"), "ImageToTextHotkey"),
|
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"ImageToText"), "ImageToTextHotkey"),
|
||||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsTxtFile"), "PasteAsTxtFileHotkey"),
|
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsTxtFile"), "PasteAsTxtFileHotkey"),
|
||||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsPngFile"), "PasteAsPngFileHotkey"),
|
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsPngFile"), "PasteAsPngFileHotkey"),
|
||||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey")
|
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey"),
|
||||||
|
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp3"), "TranscodeToMp3Hotkey"),
|
||||||
|
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp4"), "TranscodeToMp4Hotkey")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||||
@@ -36,4 +37,7 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
|
|||||||
get => _isShown;
|
get => _isShown;
|
||||||
set => Set(ref _isShown, value);
|
set => Set(ref _isShown, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<IAdvancedPasteAction> SubActions => [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class AdvancedPasteAdditionalActions
|
|||||||
{
|
{
|
||||||
public const string ImageToText = "image-to-text";
|
public const string ImageToText = "image-to-text";
|
||||||
public const string PasteAsFile = "paste-as-file";
|
public const string PasteAsFile = "paste-as-file";
|
||||||
|
public const string Transcode = "transcode";
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.ImageToText)]
|
[JsonPropertyName(PropertyNames.ImageToText)]
|
||||||
@@ -22,6 +23,22 @@ public sealed class AdvancedPasteAdditionalActions
|
|||||||
[JsonPropertyName(PropertyNames.PasteAsFile)]
|
[JsonPropertyName(PropertyNames.PasteAsFile)]
|
||||||
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
|
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonPropertyName(PropertyNames.Transcode)]
|
||||||
public IEnumerable<IAdvancedPasteAction> AllActions => new IAdvancedPasteAction[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
|
public AdvancedPasteTranscodeAction Transcode { get; init; } = new();
|
||||||
|
|
||||||
|
public IEnumerable<IAdvancedPasteAction> GetAllActions()
|
||||||
|
{
|
||||||
|
Queue<IAdvancedPasteAction> queue = new([ImageToText, PasteAsFile, Transcode]);
|
||||||
|
|
||||||
|
while (queue.Count != 0)
|
||||||
|
{
|
||||||
|
var action = queue.Dequeue();
|
||||||
|
yield return action;
|
||||||
|
|
||||||
|
foreach (var subAction in action.SubActions)
|
||||||
|
{
|
||||||
|
queue.Enqueue(subAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||||
@@ -98,6 +99,9 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
|
|||||||
private set => Set(ref _isValid, value);
|
private set => Set(ref _isValid, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<IAdvancedPasteAction> SubActions => [];
|
||||||
|
|
||||||
public object Clone()
|
public object Clone()
|
||||||
{
|
{
|
||||||
AdvancedPasteCustomAction clone = new();
|
AdvancedPasteCustomAction clone = new();
|
||||||
|
|||||||
@@ -52,5 +52,5 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public IEnumerable<AdvancedPasteAdditionalAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
|
public IEnumerable<IAdvancedPasteAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||||
|
|
||||||
|
public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAction
|
||||||
|
{
|
||||||
|
public static class PropertyNames
|
||||||
|
{
|
||||||
|
public const string TranscodeToMp3 = "transcode-to-mp3";
|
||||||
|
public const string TranscodeToMp4 = "transcode-to-mp4";
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdvancedPasteAdditionalAction _transcodeToMp3 = new();
|
||||||
|
private AdvancedPasteAdditionalAction _transcodeToMp4 = new();
|
||||||
|
private bool _isShown = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("isShown")]
|
||||||
|
public bool IsShown
|
||||||
|
{
|
||||||
|
get => _isShown;
|
||||||
|
set => Set(ref _isShown, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName(PropertyNames.TranscodeToMp3)]
|
||||||
|
public AdvancedPasteAdditionalAction TranscodeToMp3
|
||||||
|
{
|
||||||
|
get => _transcodeToMp3;
|
||||||
|
init => Set(ref _transcodeToMp3, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
|
||||||
|
public AdvancedPasteAdditionalAction TranscodeToMp4
|
||||||
|
{
|
||||||
|
get => _transcodeToMp4;
|
||||||
|
init => Set(ref _transcodeToMp4, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<IAdvancedPasteAction> SubActions => [TranscodeToMp3, TranscodeToMp4];
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.Settings.UI.Library;
|
namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||||
@@ -9,4 +10,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
|
|||||||
public interface IAdvancedPasteAction : INotifyPropertyChanged
|
public interface IAdvancedPasteAction : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public bool IsShown { get; }
|
public bool IsShown { get; }
|
||||||
|
|
||||||
|
public IEnumerable<IAdvancedPasteAction> SubActions { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,37 @@
|
|||||||
</tkcontrols:SettingsExpander.Items>
|
</tkcontrols:SettingsExpander.Items>
|
||||||
</tkcontrols:SettingsExpander>
|
</tkcontrols:SettingsExpander>
|
||||||
|
|
||||||
|
<tkcontrols:SettingsExpander
|
||||||
|
x:Uid="Transcode"
|
||||||
|
DataContext="{x:Bind ViewModel.AdditionalActions.Transcode, Mode=OneWay}"
|
||||||
|
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||||
|
IsExpanded="{Binding IsShown, Mode=OneWay}">
|
||||||
|
<tkcontrols:SettingsExpander.Content>
|
||||||
|
<ToggleSwitch
|
||||||
|
IsOn="{Binding IsShown, Mode=TwoWay}"
|
||||||
|
OffContent=""
|
||||||
|
OnContent="" />
|
||||||
|
</tkcontrols:SettingsExpander.Content>
|
||||||
|
<tkcontrols:SettingsExpander.Items>
|
||||||
|
<!-- HACK: For some weird reason, a ShortcutControl does not work correctly if it's the first or last item in the expander, so we add an invisible card. -->
|
||||||
|
<tkcontrols:SettingsCard Visibility="Collapsed" />
|
||||||
|
<tkcontrols:SettingsCard
|
||||||
|
x:Uid="TranscodeToMp3"
|
||||||
|
DataContext="{Binding TranscodeToMp3, Mode=TwoWay}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.AdditionalActions.Transcode.IsShown, Mode=OneWay}">
|
||||||
|
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
<tkcontrols:SettingsCard
|
||||||
|
x:Uid="TranscodeToMp4"
|
||||||
|
DataContext="{Binding TranscodeToMp4, Mode=TwoWay}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.AdditionalActions.Transcode.IsShown, Mode=OneWay}">
|
||||||
|
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
|
<!-- HACK: For some weird reason, a ShortcutControl does not work correctly if it's the first or last item in the expander, so we add an invisible card. -->
|
||||||
|
<tkcontrols:SettingsCard Visibility="Collapsed" />
|
||||||
|
</tkcontrols:SettingsExpander.Items>
|
||||||
|
</tkcontrols:SettingsExpander>
|
||||||
|
|
||||||
<InfoBar
|
<InfoBar
|
||||||
x:Uid="AdvancedPaste_ShortcutWarning"
|
x:Uid="AdvancedPaste_ShortcutWarning"
|
||||||
IsClosable="False"
|
IsClosable="False"
|
||||||
|
|||||||
@@ -1934,6 +1934,15 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
|||||||
<data name="PasteAsHtmlFile.Header" xml:space="preserve">
|
<data name="PasteAsHtmlFile.Header" xml:space="preserve">
|
||||||
<value>Paste as .html file</value>
|
<value>Paste as .html file</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Transcode.Header" xml:space="preserve">
|
||||||
|
<value>Transcode audio / video</value>
|
||||||
|
</data>
|
||||||
|
<data name="TranscodeToMp3.Header" xml:space="preserve">
|
||||||
|
<value>Transcode to .mp3</value>
|
||||||
|
</data>
|
||||||
|
<data name="TranscodeToMp4.Header" xml:space="preserve">
|
||||||
|
<value>Transcode to .mp4 (H.264/AAC)</value>
|
||||||
|
</data>
|
||||||
<data name="AdvancedPaste_EnableAIDialogOpenAIApiKey.Text" xml:space="preserve">
|
<data name="AdvancedPaste_EnableAIDialogOpenAIApiKey.Text" xml:space="preserve">
|
||||||
<value>OpenAI API key:</value>
|
<value>OpenAI API key:</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
_delayedTimer.Elapsed += DelayedTimer_Tick;
|
_delayedTimer.Elapsed += DelayedTimer_Tick;
|
||||||
_delayedTimer.AutoReset = false;
|
_delayedTimer.AutoReset = false;
|
||||||
|
|
||||||
foreach (var action in _additionalActions.AllActions)
|
foreach (var action in _additionalActions.GetAllActions())
|
||||||
{
|
{
|
||||||
action.PropertyChanged += OnAdditionalActionPropertyChanged;
|
action.PropertyChanged += OnAdditionalActionPropertyChanged;
|
||||||
}
|
}
|
||||||
@@ -366,7 +366,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||||
|
|
||||||
public bool IsAdditionalActionConflictingCopyShortcut =>
|
public bool IsAdditionalActionConflictingCopyShortcut =>
|
||||||
_additionalActions.AllActions
|
_additionalActions.GetAllActions()
|
||||||
.OfType<AdvancedPasteAdditionalAction>()
|
.OfType<AdvancedPasteAdditionalAction>()
|
||||||
.Select(additionalAction => additionalAction.Shortcut)
|
.Select(additionalAction => additionalAction.Shortcut)
|
||||||
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||||
|
|||||||
Reference in New Issue
Block a user