Cloud drives
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.Core.Helper;
|
||||
@@ -33,6 +34,7 @@ public static class PathHelper
|
||||
|
||||
return string.Join(Constants.SeparatorChar, commonPathParts);
|
||||
}
|
||||
|
||||
public static string GetCommonPath(string? path1, string? path2)
|
||||
{
|
||||
var path1Parts = path1?.Split(Constants.SeparatorChar) ?? Array.Empty<string>();
|
||||
@@ -51,4 +53,18 @@ public static class PathHelper
|
||||
|
||||
return string.Join(Constants.SeparatorChar, commonPathParts);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ReplaceEnvironmentVariablePlaceHolders(string path)
|
||||
{
|
||||
foreach (DictionaryEntry environmentVariable in Environment.GetEnvironmentVariables())
|
||||
{
|
||||
var value = environmentVariable.Value?.ToString();
|
||||
|
||||
if (value is null) continue;
|
||||
|
||||
path = path.Replace($"%{environmentVariable.Key}%", value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace FileTime.Core.Models;
|
||||
|
||||
public interface ISymlinkElement
|
||||
{
|
||||
IItem RealItem { get; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using FileTime.Core.Models;
|
||||
|
||||
namespace FileTime.GuiApp.App.CloudDrives;
|
||||
|
||||
public record CloudDrive(string Name, NativePath Path);
|
||||
@@ -0,0 +1,8 @@
|
||||
using FileTime.App.Core.Services;
|
||||
|
||||
namespace FileTime.GuiApp.App.CloudDrives;
|
||||
|
||||
public interface ICloudDriveService : IStartupHandler
|
||||
{
|
||||
IReadOnlyList<CloudDrive> CloudDrives { get; }
|
||||
}
|
||||
@@ -28,4 +28,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace FileTime.GuiApp.App.IconProviders;
|
||||
public interface IIconProvider
|
||||
{
|
||||
ImagePath GetImage(IItem item);
|
||||
ImagePath GetImage(string? localPath, bool isContainer, bool isLocalItem);
|
||||
bool EnableAdvancedIcons { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace FileTime.GuiApp.App.CloudDrives;
|
||||
|
||||
public partial class LinuxCloudDriveService : ICloudDriveService
|
||||
{
|
||||
[Notify] private IReadOnlyList<CloudDrive> _cloudDrives = new List<CloudDrive>();
|
||||
|
||||
public Task InitAsync() => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Based on: https://github.com/files-community/Files/blob/main/src/Files.App/Utils/Cloud/CloudDrivesDetector.cs
|
||||
|
||||
using System.Runtime.Versioning;
|
||||
using FileTime.Core.Helper;
|
||||
using FileTime.Core.Models;
|
||||
using Microsoft.Win32;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace FileTime.GuiApp.App.CloudDrives;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class WindowsCloudDriveService : ICloudDriveService
|
||||
{
|
||||
[Notify] private IReadOnlyList<CloudDrive> _cloudDrives = new List<CloudDrive>();
|
||||
|
||||
private async Task<List<CloudDrive>> GetCloudDrives()
|
||||
{
|
||||
var cloudDrives = new List<CloudDrive>();
|
||||
cloudDrives.AddRange(await GetOneDrive());
|
||||
cloudDrives.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
|
||||
return cloudDrives;
|
||||
}
|
||||
|
||||
private static Task<IEnumerable<CloudDrive>> GetOneDrive()
|
||||
{
|
||||
using var oneDriveAccountsKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\OneDrive\Accounts");
|
||||
if (oneDriveAccountsKey is null)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<CloudDrive>());
|
||||
}
|
||||
|
||||
var oneDriveAccounts = new List<CloudDrive>();
|
||||
foreach (var account in oneDriveAccountsKey.GetSubKeyNames())
|
||||
{
|
||||
var accountKeyName = @$"{oneDriveAccountsKey.Name}\{account}";
|
||||
var displayName = (string?) Registry.GetValue(accountKeyName, "DisplayName", null);
|
||||
var userFolder = (string?) Registry.GetValue(accountKeyName, "UserFolder", null);
|
||||
var accountName = string.IsNullOrWhiteSpace(displayName) ? "OneDrive" : $"OneDrive - {displayName}";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userFolder) || oneDriveAccounts.Any(x => x.Name == accountName)) continue;
|
||||
|
||||
userFolder = PathHelper.ReplaceEnvironmentVariablePlaceHolders(userFolder);
|
||||
|
||||
oneDriveAccounts.Add(new CloudDrive(accountName, new NativePath(userFolder)));
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<CloudDrive>>(oneDriveAccounts);
|
||||
}
|
||||
|
||||
public async Task InitAsync() => CloudDrives = (await GetCloudDrives()).AsReadOnly();
|
||||
}
|
||||
@@ -4,40 +4,34 @@ using Avalonia.Svg.Skia;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.GuiApp.App.IconProviders;
|
||||
using FileTime.GuiApp.App.Models;
|
||||
using FileTime.Providers.Local;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FileTime.GuiApp.App.Converters;
|
||||
|
||||
public class ItemToImageConverter : IValueConverter
|
||||
{
|
||||
private readonly IIconProvider _iconProvider;
|
||||
|
||||
public ItemToImageConverter()
|
||||
{
|
||||
_iconProvider = DI.ServiceProvider.GetRequiredService<IIconProvider>();
|
||||
}
|
||||
private readonly IIconProvider _iconProvider = DI.ServiceProvider.GetRequiredService<IIconProvider>();
|
||||
private readonly ILocalContentProvider _localContentProvider = DI.ServiceProvider.GetRequiredService<ILocalContentProvider>();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null) return null;
|
||||
|
||||
IItem item = value switch
|
||||
{
|
||||
IContainerViewModel container => container.Container!,
|
||||
IElementViewModel element => element.Element!,
|
||||
IItem i => i,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
SvgSource? source;
|
||||
try
|
||||
{
|
||||
var path = _iconProvider.GetImage(item)!;
|
||||
if (path.Type == Models.ImagePathType.Absolute)
|
||||
var path = GetImageFromPath(value);
|
||||
path ??= GetImageFromItem(value);
|
||||
|
||||
if (path is null) return null;
|
||||
|
||||
if (path.Type == ImagePathType.Absolute)
|
||||
{
|
||||
source = SvgSource.Load<SvgSource>(path.Path!, null);
|
||||
}
|
||||
else if (path.Type == Models.ImagePathType.Raw)
|
||||
else if (path.Type == ImagePathType.Raw)
|
||||
{
|
||||
return path.Image;
|
||||
}
|
||||
@@ -54,6 +48,35 @@ public class ItemToImageConverter : IValueConverter
|
||||
return new SvgImage {Source = source};
|
||||
}
|
||||
|
||||
private ImagePath? GetImageFromItem(object value)
|
||||
{
|
||||
var item = value switch
|
||||
{
|
||||
IContainerViewModel container => container.Container!,
|
||||
IElementViewModel element => element.Element!,
|
||||
IItem i => i,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (item is null) return null;
|
||||
|
||||
return _iconProvider.GetImage(item)!;
|
||||
}
|
||||
|
||||
private ImagePath? GetImageFromPath(object value)
|
||||
{
|
||||
if (value is not NativePath nativePath) return null;
|
||||
|
||||
var canHandlePathTask = _localContentProvider.CanHandlePathAsync(nativePath);
|
||||
canHandlePathTask.Wait();
|
||||
var isLocal = canHandlePathTask.Result;
|
||||
|
||||
|
||||
var isDirectory = Directory.Exists(nativePath.Path);
|
||||
|
||||
return _iconProvider.GetImage(nativePath.Path, isDirectory, isLocal);
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -33,32 +33,38 @@ public class MaterialIconProvider : IIconProvider
|
||||
});
|
||||
}
|
||||
|
||||
public ImagePath GetImage(IItem item)
|
||||
public ImagePath GetImage(IItem item) => GetImage(item.NativePath?.Path, item is IContainer, item.Provider is ILocalContentProvider);
|
||||
|
||||
public ImagePath GetImage(string? localPath, bool isContainer, bool isLocalItem)
|
||||
{
|
||||
item = item is ISymlinkElement symlinkElement ? symlinkElement.RealItem : item;
|
||||
var icon = item is IContainer ? "folder.svg" : "file.svg";
|
||||
var localPath = item.NativePath?.Path.TrimEnd(Path.DirectorySeparatorChar);
|
||||
var icon = isContainer ? "folder.svg" : "file.svg";
|
||||
localPath = localPath?.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
if (!EnableAdvancedIcons) return GetAssetPath(icon);
|
||||
|
||||
if (localPath != null && _specialPaths.Value.Find(p => p.Path == localPath) is SpecialPathWithIcon specialPath)
|
||||
if (_specialPaths.Value.Find(p => p.Path == localPath) is { } specialPath)
|
||||
{
|
||||
return specialPath.IconPath;
|
||||
}
|
||||
|
||||
if (item is not IElement element) return GetAssetPath(icon);
|
||||
if (isContainer || localPath is null) return GetAssetPath(icon);
|
||||
|
||||
if (element.Provider is ILocalContentProvider && (localPath?.EndsWith(".svg") ?? false))
|
||||
if (isLocalItem && localPath.EndsWith(".svg"))
|
||||
{
|
||||
return new ImagePath(ImagePathType.Absolute, localPath);
|
||||
}
|
||||
|
||||
string? possibleIcon = null;
|
||||
var fileName = element.Name;
|
||||
var extension = element.Name.Contains('.') ? element.Name.Split('.').Last() : null;
|
||||
string? possibleIcon;
|
||||
var fileName = localPath.Split(Path.DirectorySeparatorChar).Last();
|
||||
|
||||
if (_iconsByFileName.TryGetValue(fileName, out var value)) possibleIcon = value;
|
||||
else if (_iconsByExtension.FirstOrDefault(k => fileName.EndsWith("." + k.Key)) is KeyValuePair<string, string> {Key: { }} matchingExtension) possibleIcon = matchingExtension.Value;
|
||||
if (_iconsByFileName.TryGetValue(fileName, out var value))
|
||||
{
|
||||
possibleIcon = value;
|
||||
}
|
||||
else if (_iconsByExtension.FirstOrDefault(k => fileName.EndsWith("." + k.Key)) is var matchingExtension)
|
||||
{
|
||||
possibleIcon = matchingExtension.Value;
|
||||
}
|
||||
|
||||
if (possibleIcon != null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using FileTime.App.Core.Services;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.App.Core.ViewModels.Timeline;
|
||||
using FileTime.App.FrequencyNavigation.Services;
|
||||
using FileTime.GuiApp.App.CloudDrives;
|
||||
using FileTime.GuiApp.App.Services;
|
||||
using FileTime.Providers.LocalAdmin;
|
||||
|
||||
@@ -21,6 +22,7 @@ public interface IMainWindowViewModel : IMainWindowViewModelBase
|
||||
IClipboardService ClipboardService { get; }
|
||||
ITimelineViewModel TimelineViewModel { get; }
|
||||
IPossibleCommandsViewModel PossibleCommands { get; }
|
||||
ICloudDriveService CloudDriveService { get; }
|
||||
Action? ShowWindow { get; set; }
|
||||
Thickness IconStatusPanelMargin { get; }
|
||||
Task RunOrOpenItem(IItemViewModel itemViewModel);
|
||||
|
||||
@@ -13,6 +13,7 @@ using FileTime.App.Core.ViewModels.Timeline;
|
||||
using FileTime.App.FrequencyNavigation.Services;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Timeline;
|
||||
using FileTime.GuiApp.App.CloudDrives;
|
||||
using FileTime.GuiApp.App.InstanceManagement;
|
||||
using FileTime.GuiApp.App.Services;
|
||||
using FileTime.Providers.Local;
|
||||
@@ -43,6 +44,7 @@ namespace FileTime.GuiApp.App.ViewModels;
|
||||
[Inject(typeof(ITimelineViewModel), PropertyAccessModifier = AccessModifier.Public)]
|
||||
[Inject(typeof(IPossibleCommandsViewModel), PropertyName = "PossibleCommands", PropertyAccessModifier = AccessModifier.Public)]
|
||||
[Inject(typeof(IInstanceMessageHandler), PropertyName = "_instanceMessageHandler")]
|
||||
[Inject(typeof(ICloudDriveService), PropertyAccessModifier = AccessModifier.Public)]
|
||||
public partial class MainWindowViewModel : IMainWindowViewModel
|
||||
{
|
||||
public bool Loading => false;
|
||||
@@ -57,7 +59,7 @@ public partial class MainWindowViewModel : IMainWindowViewModel
|
||||
|
||||
partial void OnInitialize()
|
||||
{
|
||||
_logger?.LogInformation($"Starting {nameof(MainWindowViewModel)} initialization...");
|
||||
_logger.LogInformation($"Starting {nameof(MainWindowViewModel)} initialization...");
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:appCoreModels="using:FileTime.App.Core.Models"
|
||||
xmlns:cloudDrives="clr-namespace:FileTime.GuiApp.App.CloudDrives;assembly=FileTime.GuiApp.App.Abstractions"
|
||||
xmlns:corevm="using:FileTime.App.Core.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
|
||||
@@ -116,13 +117,13 @@
|
||||
|
||||
<Border
|
||||
Margin="10"
|
||||
Padding="10"
|
||||
Padding="0,10"
|
||||
Background="{DynamicResource ContainerBackgroundBrush}"
|
||||
CornerRadius="10">
|
||||
<Grid RowDefinitions="Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
Margin="0,0,0,10"
|
||||
Margin="10,0,10,10"
|
||||
Text="Drives" />
|
||||
|
||||
<ItemsRepeater
|
||||
@@ -135,7 +136,7 @@
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnHasContainerPointerPressed">
|
||||
<Grid
|
||||
Margin="0,5"
|
||||
Margin="10,5"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
RowDefinitions="Auto,Auto">
|
||||
<Image
|
||||
@@ -209,7 +210,8 @@
|
||||
Margin="10"
|
||||
Padding="0,10"
|
||||
Background="{DynamicResource ContainerBackgroundBrush}"
|
||||
CornerRadius="10">
|
||||
CornerRadius="10"
|
||||
IsVisible="{Binding AppState.Places.Count, Converter={StaticResource GreaterThanConverter}, ConverterParameter=0}">
|
||||
<Grid RowDefinitions="Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
@@ -250,6 +252,52 @@
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Margin="10"
|
||||
Padding="0,10"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource ContainerBackgroundBrush}"
|
||||
CornerRadius="10"
|
||||
IsVisible="{Binding CloudDriveService.CloudDrives.Count, Converter={StaticResource GreaterThanConverter}, ConverterParameter=0}">
|
||||
<Grid RowDefinitions="Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
Margin="10,0,10,10"
|
||||
Text="Cloud drives" />
|
||||
|
||||
<ItemsRepeater
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding CloudDriveService.CloudDrives}">
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="cloudDrives:CloudDrive">
|
||||
<Grid
|
||||
Classes="SidebarContainerPresenter"
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnHasContainerPointerPressed">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Horizontal"
|
||||
ToolTip.Tip="{Binding Name}">
|
||||
<Image
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Source="{Binding Path, Converter={StaticResource ItemToImageConverter}}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="5,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Row="3"
|
||||
Margin="10"
|
||||
Padding="10"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource ContainerBackgroundBrush}"
|
||||
|
||||
@@ -7,6 +7,8 @@ using Avalonia.VisualTree;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Timeline;
|
||||
using FileTime.GuiApp.App.CloudDrives;
|
||||
using FileTime.GuiApp.App.Services;
|
||||
using FileTime.GuiApp.App.Settings;
|
||||
using FileTime.GuiApp.App.ViewModels;
|
||||
@@ -138,6 +140,11 @@ public partial class MainWindow : Window, IUiAccessor
|
||||
{
|
||||
path = placeInfoPath;
|
||||
}
|
||||
else if (control.DataContext is CloudDrive {Path: { } cloudDrivePath})
|
||||
{
|
||||
var timelessContentProvider = DI.ServiceProvider.GetRequiredService<ITimelessContentProvider>();
|
||||
path = await timelessContentProvider.GetFullNameByNativePathAsync(cloudDrivePath);
|
||||
}
|
||||
|
||||
if (path is null) return;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using FileTime.App.Core.Configuration;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.GuiApp.App.CloudDrives;
|
||||
using FileTime.GuiApp.App.Configuration;
|
||||
using FileTime.GuiApp.App.ContextMenu;
|
||||
using FileTime.GuiApp.CustomImpl.ViewModels;
|
||||
@@ -84,18 +85,21 @@ public static class Startup
|
||||
{
|
||||
serviceCollection
|
||||
.AddSingleton<IContextMenuProvider, WindowsContextMenuProvider>()
|
||||
.AddSingleton<IPlacesService, WindowsPlacesService>();
|
||||
.AddSingleton<IPlacesService, WindowsPlacesService>()
|
||||
.AddSingleton<ICloudDriveService, WindowsCloudDriveService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
serviceCollection
|
||||
.AddSingleton<IContextMenuProvider, LinuxContextMenuProvider>()
|
||||
.AddSingleton<IPlacesService, LinuxPlacesService>();
|
||||
.AddSingleton<IPlacesService, LinuxPlacesService>()
|
||||
.AddSingleton<ICloudDriveService, LinuxCloudDriveService>();
|
||||
}
|
||||
|
||||
return serviceCollection
|
||||
.AddSingleton<IExitHandler, RootDriveInfoService>()
|
||||
.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<IPlacesService>());
|
||||
.AddSingleton<IExitHandler>(sp => sp.GetRequiredService<IRootDriveInfoService>())
|
||||
.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<IPlacesService>())
|
||||
.AddSingleton<IStartupHandler>(sp => sp.GetRequiredService<ICloudDriveService>());
|
||||
}
|
||||
|
||||
internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using FileTime.App.Core.Services;
|
||||
|
||||
namespace FileTime.Providers.Local;
|
||||
|
||||
public interface IRootDriveInfoService
|
||||
public interface IRootDriveInfoService : IExitHandler
|
||||
{
|
||||
ObservableCollection<RootDriveInfo> RootDriveInfos { get; set; }
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.Core.Models;
|
||||
using ObservableComputations;
|
||||
|
||||
namespace FileTime.Providers.Local;
|
||||
|
||||
public class RootDriveInfoService : IRootDriveInfoService, IExitHandler
|
||||
public class RootDriveInfoService : IRootDriveInfoService
|
||||
{
|
||||
private readonly ILocalContentProvider _localContentProvider;
|
||||
private readonly List<DriveInfo> _rootDrives = new();
|
||||
|
||||
Reference in New Issue
Block a user