Skip to content

Feature: Drag to clone #16920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/Files.App/Actions/Git/GitCloneAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

namespace Files.App.Actions
{
internal sealed partial class GitCloneAction : ObservableObject, IAction
{
private readonly IContentPageContext pageContext = Ioc.Default.GetRequiredService<IContentPageContext>();
private readonly IDialogService dialogService = Ioc.Default.GetRequiredService<IDialogService>();

public string Label { get; } = Strings.GitClone.GetLocalizedResource();

public string Description { get; } = Strings.GitCloneDescription.GetLocalizedResource();

public bool IsExecutable
=> pageContext.CanCreateItem && !pageContext.IsGitRepository;

public RichGlyph Glyph
=> new(themedIconStyle: "App.ThemedIcons.Git");

public GitCloneAction()
{
pageContext.PropertyChanged += Context_PropertyChanged;
}

public Task ExecuteAsync(object? parameter = null)
{
if (pageContext.ShellPage is null)
return Task.CompletedTask;

var repoUrl = parameter?.ToString() ?? string.Empty;
var viewModel = new CloneRepoDialogViewModel(repoUrl, pageContext.ShellPage.ShellViewModel.WorkingDirectory);
return dialogService.ShowDialogAsync(viewModel);
}

private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(IContentPageContext.CanCreateItem):
case nameof(IContentPageContext.IsGitRepository):
OnPropertyChanged(nameof(IsExecutable));
break;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ partial class StatusCenterStateToStateIconConverter : IValueConverter
StatusCenterItemIconKind.Compress => Application.Current.Resources["App.Theme.PathIcon.ActionExtract"] as string,
StatusCenterItemIconKind.Successful => Application.Current.Resources["App.Theme.PathIcon.ActionSuccess"] as string,
StatusCenterItemIconKind.Error => Application.Current.Resources["App.Theme.PathIcon.ActionInfo"] as string,
StatusCenterItemIconKind.GitClone => Application.Current.Resources["App.Theme.PathIcon.ActionGitClone"] as string,
_ => ""
};

Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Data/Commands/Manager/CommandCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ public enum CommandCodes
PlayAll,

// Git
GitClone,
GitFetch,
GitInit,
GitPull,
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Data/Commands/Manager/CommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ public IRichCommand this[HotKey hotKey]
public IRichCommand ArrangePanesHorizontally => commands[CommandCodes.ArrangePanesHorizontally];
public IRichCommand OpenFileLocation => commands[CommandCodes.OpenFileLocation];
public IRichCommand PlayAll => commands[CommandCodes.PlayAll];
public IRichCommand GitClone => commands[CommandCodes.GitClone];
public IRichCommand GitFetch => commands[CommandCodes.GitFetch];
public IRichCommand GitInit => commands[CommandCodes.GitInit];
public IRichCommand GitPull => commands[CommandCodes.GitPull];
Expand Down Expand Up @@ -421,6 +422,7 @@ public IEnumerator<IRichCommand> GetEnumerator() =>
[CommandCodes.ArrangePanesHorizontally] = new ArrangePanesHorizontallyAction(),
[CommandCodes.OpenFileLocation] = new OpenFileLocationAction(),
[CommandCodes.PlayAll] = new PlayAllAction(),
[CommandCodes.GitClone] = new GitCloneAction(),
[CommandCodes.GitFetch] = new GitFetchAction(),
[CommandCodes.GitInit] = new GitInitAction(),
[CommandCodes.GitPull] = new GitPullAction(),
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Data/Commands/Manager/ICommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public interface ICommandManager : IEnumerable<IRichCommand>

IRichCommand PlayAll { get; }

IRichCommand GitClone { get; }
IRichCommand GitFetch { get; }
IRichCommand GitInit { get; }
IRichCommand GitPull { get; }
Expand Down
5 changes: 5 additions & 0 deletions src/Files.App/Data/Enums/FileOperationType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,10 @@ public enum FileOperationType : byte
/// An item has been added to an archive
/// </summary>
Compressed = 11,

/// <summary>
/// A git repo has been cloned
/// </summary>
GitClone = 12,
}
}
1 change: 1 addition & 0 deletions src/Files.App/Data/Enums/StatusCenterItemIconKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ public enum StatusCenterItemIconKind
Compress,
Successful,
Error,
GitClone
}
}
30 changes: 30 additions & 0 deletions src/Files.App/Dialogs/CloneRepoDialog.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- Copyright (c) Files Community. Licensed under the MIT License. -->
<ContentDialog
x:Class="Files.App.Dialogs.CloneRepoDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{helpers:ResourceString Name=CloneRepo}"
HighContrastAdjustment="None"
IsPrimaryButtonEnabled="{x:Bind ViewModel.CanCloneRepo, Mode=OneWay}"
PrimaryButtonCommand="{x:Bind ViewModel.CloneRepoCommand, Mode=OneWay}"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
PrimaryButtonText="{helpers:ResourceString Name=Clone}"
RequestedTheme="{x:Bind RootAppElement.RequestedTheme, Mode=OneWay}"
SecondaryButtonText="{helpers:ResourceString Name=Cancel}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">

<Grid Width="340">
<StackPanel Spacing="8">
<TextBox
Header="{helpers:ResourceString Name=RepositoryURL}"
PlaceholderText="https://github.com/files-community/Files"
Text="{x:Bind ViewModel.RepoUrl, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>

</ContentDialog>
31 changes: 31 additions & 0 deletions src/Files.App/Dialogs/CloneRepoDialog.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel.DataTransfer;

namespace Files.App.Dialogs
{
public sealed partial class CloneRepoDialog : ContentDialog, IDialog<CloneRepoDialogViewModel>
{
private FrameworkElement RootAppElement
=> (FrameworkElement)MainWindow.Instance.Content;

public CloneRepoDialogViewModel ViewModel
{
get => (CloneRepoDialogViewModel)DataContext;
set => DataContext = value;
}

public CloneRepoDialog()
{
InitializeComponent();
}

public new async Task<DialogResult> ShowAsync()
{
return (DialogResult)await base.ShowAsync();
}
}
}
105 changes: 59 additions & 46 deletions src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ public static DynamicDialog GetFor_PropertySaveErrorDialog()
{
DynamicDialog dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "PropertySaveErrorDialog/Title".GetLocalizedResource(),
SubtitleText = "PropertySaveErrorMessage/Text".GetLocalizedResource(), // We can use subtitle here as our content
PrimaryButtonText = "Retry".GetLocalizedResource(),
SecondaryButtonText = "PropertySaveErrorDialog/SecondaryButtonText".GetLocalizedResource(),
CloseButtonText = "Cancel".GetLocalizedResource(),
TitleText = Strings.PropertySaveErrorDialog_Title.GetLocalizedResource(),
SubtitleText = Strings.PropertySaveErrorMessage_Text.GetLocalizedResource(), // We can use subtitle here as our content
PrimaryButtonText = Strings.Retry.GetLocalizedResource(),
SecondaryButtonText = Strings.PropertySaveErrorDialog_SecondaryButtonText.GetLocalizedResource(),
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Secondary | DynamicDialogButtons.Cancel
});
return dialog;
Expand All @@ -33,9 +33,9 @@ public static DynamicDialog GetFor_ConsentDialog()
{
DynamicDialog dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "WelcomeDialog/Title".GetLocalizedResource(),
SubtitleText = "WelcomeDialogTextBlock/Text".GetLocalizedResource(), // We can use subtitle here as our content
PrimaryButtonText = "WelcomeDialog/PrimaryButtonText".GetLocalizedResource(),
TitleText = Strings.WelcomeDialog_Title.GetLocalizedResource(),
SubtitleText = Strings.WelcomeDialogTextBlock_Text.GetLocalizedResource(), // We can use subtitle here as our content
PrimaryButtonText = Strings.WelcomeDialog_PrimaryButtonText.GetLocalizedResource(),
PrimaryButtonAction = async (vm, e) => await Launcher.LaunchUriAsync(new Uri("ms-settings:privacy-broadfilesystemaccess")),
DynamicButtons = DynamicDialogButtons.Primary
});
Expand All @@ -46,10 +46,10 @@ public static DynamicDialog GetFor_ShortcutNotFound(string targetPath)
{
DynamicDialog dialog = new(new DynamicDialogViewModel
{
TitleText = "ShortcutCannotBeOpened".GetLocalizedResource(),
SubtitleText = string.Format("DeleteShortcutDescription".GetLocalizedResource(), targetPath),
PrimaryButtonText = "Delete".GetLocalizedResource(),
SecondaryButtonText = "No".GetLocalizedResource(),
TitleText = Strings.ShortcutCannotBeOpened.GetLocalizedResource(),
SubtitleText = string.Format(Strings.DeleteShortcutDescription.GetLocalizedResource(), targetPath),
PrimaryButtonText = Strings.Delete.GetLocalizedResource(),
SecondaryButtonText = Strings.No.GetLocalizedResource(),
DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Secondary
});
return dialog;
Expand All @@ -60,12 +60,12 @@ public static DynamicDialog GetFor_CreateItemDialog(string itemType)
DynamicDialog? dialog = null;
TextBox inputText = new()
{
PlaceholderText = "EnterAnItemName".GetLocalizedResource()
PlaceholderText = Strings.EnterAnItemName.GetLocalizedResource()
};

TeachingTip warning = new()
{
Title = "InvalidFilename/Text".GetLocalizedResource(),
Title = Strings.InvalidFilename_Text.GetLocalizedResource(),
PreferredPlacement = TeachingTipPlacementMode.Bottom,
DataContext = new CreateItemDialogViewModel(),
};
Expand Down Expand Up @@ -101,7 +101,7 @@ public static DynamicDialog GetFor_CreateItemDialog(string itemType)

dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = string.Format("CreateNewItemTitle".GetLocalizedResource(), itemType),
TitleText = string.Format(Strings.CreateNewItemTitle.GetLocalizedResource(), itemType),
SubtitleText = null,
DisplayControl = new Grid()
{
Expand All @@ -115,8 +115,8 @@ public static DynamicDialog GetFor_CreateItemDialog(string itemType)
{
vm.HideDialog(); // Rename successful
},
PrimaryButtonText = "Create".GetLocalizedResource(),
CloseButtonText = "Cancel".GetLocalizedResource(),
PrimaryButtonText = Strings.Create.GetLocalizedResource(),
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
DynamicButtonsEnabled = DynamicDialogButtons.Cancel,
DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel
});
Expand All @@ -133,9 +133,9 @@ public static DynamicDialog GetFor_FileInUseDialog(List<Win32Process> lockingPro
{
DynamicDialog dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "FileInUseDialog/Title".GetLocalizedResource(),
SubtitleText = lockingProcess.IsEmpty() ? "FileInUseDialog/Text".GetLocalizedResource() :
string.Format("FileInUseByDialog/Text".GetLocalizedResource(), string.Join(", ", lockingProcess.Select(x => $"{x.AppName ?? x.Name} (PID: {x.Pid})"))),
TitleText = Strings.FileInUseDialog_Title.GetLocalizedResource(),
SubtitleText = lockingProcess.IsEmpty() ? Strings.FileInUseDialog_Text.GetLocalizedResource() :
string.Format(Strings.FileInUseByDialog_Text.GetLocalizedResource(), string.Join(", ", lockingProcess.Select(x => $"{x.AppName ?? x.Name} (PID: {x.Pid})"))),
PrimaryButtonText = "OK",
DynamicButtons = DynamicDialogButtons.Primary
});
Expand All @@ -149,17 +149,17 @@ public static DynamicDialog GetFor_CredentialEntryDialog(string path)

TextBox inputUsername = new()
{
PlaceholderText = "CredentialDialogUserName/PlaceholderText".GetLocalizedResource()
PlaceholderText = Strings.CredentialDialogUserName_PlaceholderText.GetLocalizedResource()
};

PasswordBox inputPassword = new()
{
PlaceholderText = "Password".GetLocalizedResource()
PlaceholderText = Strings.Password.GetLocalizedResource()
};

CheckBox saveCreds = new()
{
Content = "NetworkAuthenticationSaveCheckbox".GetLocalizedResource()
Content = Strings.NetworkAuthenticationSaveCheckbox.GetLocalizedResource()
};

inputUsername.TextChanged += (textBox, args) =>
Expand Down Expand Up @@ -188,10 +188,10 @@ public static DynamicDialog GetFor_CredentialEntryDialog(string path)

dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "NetworkAuthenticationDialogTitle".GetLocalizedResource(),
PrimaryButtonText = "OK".GetLocalizedResource(),
CloseButtonText = "Cancel".GetLocalizedResource(),
SubtitleText = string.Format("NetworkAuthenticationDialogMessage".GetLocalizedResource(), path.Substring(2)),
TitleText = Strings.NetworkAuthenticationDialogTitle.GetLocalizedResource(),
PrimaryButtonText = Strings.OK.GetLocalizedResource(),
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
SubtitleText = string.Format(Strings.NetworkAuthenticationDialogMessage.GetLocalizedResource(), path.Substring(2)),
DisplayControl = new Grid()
{
MinWidth = 250d,
Expand Down Expand Up @@ -228,9 +228,9 @@ public static DynamicDialog GetFor_GitCheckoutConflicts(string checkoutBranchNam
{
ItemsSource = new string[]
{
string.Format("BringChanges".GetLocalizedResource(), checkoutBranchName),
string.Format("StashChanges".GetLocalizedResource(), headBranchName),
"DiscardChanges".GetLocalizedResource()
string.Format(Strings.BringChanges.GetLocalizedResource(), checkoutBranchName),
string.Format(Strings.StashChanges.GetLocalizedResource(), headBranchName),
Strings.DiscardChanges.GetLocalizedResource()
},
SelectionMode = ListViewSelectionMode.Single
};
Expand All @@ -243,10 +243,10 @@ public static DynamicDialog GetFor_GitCheckoutConflicts(string checkoutBranchNam

dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "SwitchBranch".GetLocalizedResource(),
PrimaryButtonText = "Switch".GetLocalizedResource(),
CloseButtonText = "Cancel".GetLocalizedResource(),
SubtitleText = "UncommittedChanges".GetLocalizedResource(),
TitleText = Strings.SwitchBranch.GetLocalizedResource(),
PrimaryButtonText = Strings.Switch.GetLocalizedResource(),
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
SubtitleText = Strings.UncommittedChanges.GetLocalizedResource(),
DisplayControl = new Grid()
{
MinWidth = 250d,
Expand All @@ -271,8 +271,8 @@ public static DynamicDialog GetFor_GitHubConnectionError()
DynamicDialog dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "Error".GetLocalizedResource(),
SubtitleText = "CannotReachGitHubError".GetLocalizedResource(),
PrimaryButtonText = "Close".GetLocalizedResource(),
SubtitleText = Strings.CannotReachGitHubError.GetLocalizedResource(),
PrimaryButtonText = Strings.Close.GetLocalizedResource(),
DynamicButtons = DynamicDialogButtons.Primary
});
return dialog;
Expand All @@ -283,8 +283,8 @@ public static DynamicDialog GetFor_GitCannotInitializeqRepositoryHere()
return new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "Error".GetLocalizedResource(),
SubtitleText = "CannotInitializeGitRepo".GetLocalizedResource(),
PrimaryButtonText = "Close".GetLocalizedResource(),
SubtitleText = Strings.CannotInitializeGitRepo.GetLocalizedResource(),
PrimaryButtonText = Strings.Close.GetLocalizedResource(),
DynamicButtons = DynamicDialogButtons.Primary
});
}
Expand All @@ -294,10 +294,10 @@ public static DynamicDialog GetFor_DeleteGitBranchConfirmation(string branchName
DynamicDialog dialog = null!;
dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "GitDeleteBranch".GetLocalizedResource(),
SubtitleText = string.Format("GitDeleteBranchSubtitle".GetLocalizedResource(), branchName),
PrimaryButtonText = "OK".GetLocalizedResource(),
CloseButtonText = "Cancel".GetLocalizedResource(),
TitleText = Strings.GitDeleteBranch.GetLocalizedResource(),
SubtitleText = string.Format(Strings.GitDeleteBranchSubtitle.GetLocalizedResource(), branchName),
PrimaryButtonText = Strings.OK.GetLocalizedResource(),
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
AdditionalData = true,
CloseButtonAction = (vm, e) =>
{
Expand All @@ -314,10 +314,10 @@ public static DynamicDialog GetFor_RenameRequiresHigherPermissions(string path)
DynamicDialog dialog = null!;
dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = "ItemRenameFailed".GetLocalizedResource(),
SubtitleText = string.Format("HigherPermissionsRequired".GetLocalizedResource(), path),
PrimaryButtonText = "OK".GetLocalizedResource(),
SecondaryButtonText = "EditPermissions".GetLocalizedResource(),
TitleText = Strings.ItemRenameFailed.GetLocalizedResource(),
SubtitleText = string.Format(Strings.HigherPermissionsRequired.GetLocalizedResource(), path),
PrimaryButtonText = Strings.OK.GetLocalizedResource(),
SecondaryButtonText = Strings.EditPermissions.GetLocalizedResource(),
SecondaryButtonAction = (vm, e) =>
{
var context = Ioc.Default.GetRequiredService<IContentPageContext>();
Expand Down Expand Up @@ -424,5 +424,18 @@ await commands.OpenSettings.ExecuteAsync(
new SettingsNavigationParams() { PageKind = SettingsPageKind.DevToolsPage }
);
}

public static async Task ShowFor_CannotCloneRepo(string exception)
{
var dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = Strings.CannotCloneRepoTitle.GetLocalizedResource(),
SubtitleText = exception,
PrimaryButtonText = Strings.OK.GetLocalizedResource(),
DynamicButtons = DynamicDialogButtons.Primary
});

await dialog.TryShowAsync();
}
}
}
Loading
Loading