initial push

This commit is contained in:
ti_mo
2026-01-02 01:41:58 +01:00
commit c446ff9afb
19 changed files with 438 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Photos.App"
xmlns:local="using:Photos"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
+47
View File
@@ -0,0 +1,47 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml;
using Photos.ViewModels;
using Photos.Views;
namespace Photos;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

+27
View File
@@ -0,0 +1,27 @@
using System;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
namespace Photos.Models;
public class Photo
{
public string DisplayName { get; set; }
public string FullPath { get; set; }
public string DisplayPath { get; set; }
public IImage Source { get; set; }
public Photo(string path, string relativeTo)
{
DisplayName = System.IO.Path.GetFileName(path);
FullPath = path;
DisplayPath = path.Remove(0, relativeTo.Length + 1);
using (var stream = File.OpenRead(path))
{
Source = new Bitmap(stream);
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.10"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.10"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.10">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
<PackageReference Include="Xaml.Behaviors" Version="11.3.9" />
</ItemGroup>
</Project>
+21
View File
@@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace Photos;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Photos.ViewModels;
namespace Photos;
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
{
if (param is null)
return null;
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
+95
View File
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Net;
using Avalonia.Controls;
using Avalonia.Input;
using Photos.Models;
using Avalonia.Platform.Storage;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Photos.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<Photo> Photos { get; private set; } = [];
[ObservableProperty] private Photo? _selectedPhoto;
public string LibraryPath { get; private set; } = string.Empty;
public MainWindowViewModel()
{
}
void OpenDirectory()
{
}
private void SanitizeFiles(List<string> files, IEnumerable<string> extensions)
=> files.RemoveAll(x => !extensions.Any(ex => x.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
private async Task AddPhotosAsync(string path)
{
try
{
var files = await Task.Run(() =>
{
var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories).ToList();
SanitizeFiles(files, [".png", ".jpg", ".jpeg"]);
return files;
});
var photosToAdd = await Task.Run(() =>
{
var photos = new List<Photo>();
foreach (var file in files)
{
photos.Add(new Photo(file, LibraryPath));
}
return photos;
});
await Dispatcher.UIThread.InvokeAsync(() =>
{
foreach (var photo in photosToAdd) Photos.Add(photo);
});
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
public async void WindowDropHandler(DragEventArgs e)
{
var files = e.DataTransfer.TryGetFiles();
if (files is null || files.Length == 0) return;
var localPath = Path.GetDirectoryName(files[0].TryGetLocalPath());
if (String.IsNullOrWhiteSpace(localPath)) return;
LibraryPath = localPath;
Photos.Clear();
await AddPhotosAsync(localPath);
}
public void PhotoClicked(PointerReleasedEventArgs e)
{
if(e.Source is not Image img) return;
SelectedPhoto = Photos.FirstOrDefault(x => x.Source == img.Source);
}
public void SelectedPhotoClicked()
{
SelectedPhoto = null;
}
}
+7
View File
@@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Photos.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}
+62
View File
@@ -0,0 +1,62 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Photos.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="using:Avalonia.Xaml.Interactivity"
xmlns:ic="using:Avalonia.Xaml.Interactions.Core"
xmlns:models="using:Photos.Models"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Photos.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
x:Name="Root"
Icon="/Assets/avalonia-logo.ico"
Title="Photos"
DragDrop.AllowDrop="True"
DragDrop.Drop="DropHandler"
>
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<vm:MainWindowViewModel/>
</Design.DataContext>
<Grid ColumnDefinitions="1*, 5*" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Menu>
<Button Content="Open Directory" Command="{Binding OpenDirectory}"/>
</Menu>
<ScrollViewer Grid.Column="1">
<ItemsControl ItemsSource="{Binding Photos}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="models:Photo">
<StackPanel Orientation="Vertical" Margin="20">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="PointerReleased">
<ic:InvokeCommandAction Command="{Binding DataContext.PhotoClicked, ElementName=Root}" PassEventArgsToCommand="True"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{Binding Source}" Width="150" Height="150"/>
<TextBlock Text="{Binding DisplayName}" HorizontalAlignment="Center" Width="150"/>
<TextBlock Text="{Binding DisplayPath}" HorizontalAlignment="Center" Width="150"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Grid Grid.Column="0" Grid.ColumnSpan="2" IsEnabled="{Binding SelectedPhoto}">
<Image Source="{Binding SelectedPhoto.Source}" Margin="50">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="PointerReleased">
<ic:InvokeCommandAction Command="{Binding Path=SelectedPhotoClicked}"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
</Image>
</Grid>
</Grid>
</Window>
+20
View File
@@ -0,0 +1,20 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Xaml.Interactions.DragAndDrop;
using Photos.ViewModels;
namespace Photos.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
public void DropHandler(object sender, DragEventArgs e)
{
(DataContext as MainWindowViewModel)?.WindowDropHandler(e);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Photos.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>