├── src
├── LauncherAppAvalonia
│ ├── Assets
│ │ └── avalonia-logo.ico
│ ├── ViewModels
│ │ ├── ViewModelBase.cs
│ │ ├── LauncherItemViewModel.cs
│ │ ├── AppSettingsViewModel.cs
│ │ ├── MainWindowViewModel.cs
│ │ ├── ItemEditorViewModel.cs
│ │ └── MainWindowViewModel.Commands.cs
│ ├── Views
│ │ ├── ItemEditorView.axaml.cs
│ │ ├── AppSettingsView.axaml.cs
│ │ ├── MainWindow.axaml.cs
│ │ ├── ItemEditorView.axaml
│ │ ├── MainWindow.axaml
│ │ └── AppSettingsView.axaml
│ ├── Models
│ │ ├── LauncherItem.cs
│ │ └── AppSettings.cs
│ ├── App.axaml
│ ├── Program.cs
│ ├── ViewLocator.cs
│ ├── app.manifest
│ ├── Converters
│ │ └── LauncherItemTypeToStringConverter.cs
│ ├── LauncherAppAvalonia.csproj
│ ├── App.axaml.cs
│ ├── Services
│ │ └── DataService.cs
│ └── Utility
│ │ └── Utility.cs
└── LauncherAppAvalonia.sln
├── .vscode
└── settings.json
├── LICENSE
├── .gitignore
├── README.md
└── .github
└── copilot-instructions.md
/src/LauncherAppAvalonia/Assets/avalonia-logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SolarianZ/LauncherAppAvalonia/main/src/LauncherAppAvalonia/Assets/avalonia-logo.ico
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 |
3 | namespace LauncherAppAvalonia.ViewModels;
4 |
5 | public class ViewModelBase : ObservableObject;
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Views/ItemEditorView.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 | namespace LauncherAppAvalonia.Views;
4 |
5 | public partial class ItemEditorView : UserControl
6 | {
7 | public ItemEditorView()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Views/AppSettingsView.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 |
3 | namespace LauncherAppAvalonia.Views;
4 |
5 | public partial class AppSettingsView : UserControl
6 | {
7 | public AppSettingsView()
8 | {
9 | InitializeComponent();
10 | }
11 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.git": true,
4 | "**/.svn": true,
5 | "**/.hg": true,
6 | "**/.DS_Store": true,
7 | "**/Thumbs.db": true,
8 | "**/bin": true,
9 | "**/obj": true,
10 | "**/.idea": true
11 | },
12 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Models/LauncherItem.cs:
--------------------------------------------------------------------------------
1 | using CommunityToolkit.Mvvm.ComponentModel;
2 |
3 | namespace LauncherAppAvalonia.Models;
4 |
5 | public enum LauncherItemType
6 | {
7 | File,
8 | Folder,
9 | Url,
10 | Command,
11 | }
12 |
13 | public partial class LauncherItem(LauncherItemType type, string path, string? name)
14 | : ObservableObject
15 | {
16 | [ObservableProperty]
17 | private LauncherItemType _type = type;
18 |
19 | [ObservableProperty]
20 | private string _path = path;
21 |
22 | [ObservableProperty]
23 | private string? _name = name;
24 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/App.axaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Views/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Input;
3 | using LauncherAppAvalonia.ViewModels;
4 |
5 | namespace LauncherAppAvalonia.Views;
6 |
7 | public partial class MainWindow : Window
8 | {
9 | public MainWindow()
10 | {
11 | InitializeComponent();
12 | }
13 |
14 | private void OnItemListBoxItemDoubleTapped(object? sender, TappedEventArgs e)
15 | {
16 | LauncherItemViewModel? itemVM = (sender as ListBox)?.SelectedItem as LauncherItemViewModel;
17 | if (itemVM == null)
18 | return;
19 |
20 | if (DataContext is MainWindowViewModel viewModel)
21 | viewModel.OpenItemCommand.Execute(itemVM);
22 | }
23 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | namespace LauncherAppAvalonia;
5 |
6 | sealed class Program
7 | {
8 | // Initialization code. Don't use any Avalonia, third-party APIs or any
9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
10 | // yet and stuff might break.
11 | [STAThread]
12 | public static void Main(string[] args) => BuildAvaloniaApp()
13 | .StartWithClassicDesktopLifetime(args);
14 |
15 | // Avalonia configuration, don't remove; also used by visual designer.
16 | public static AppBuilder BuildAvaloniaApp()
17 | => AppBuilder.Configure()
18 | .UsePlatformDetect()
19 | .WithInterFont()
20 | .LogToTrace();
21 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewModels/LauncherItemViewModel.cs:
--------------------------------------------------------------------------------
1 | using LauncherAppAvalonia.Models;
2 |
3 | namespace LauncherAppAvalonia.ViewModels;
4 |
5 | public class LauncherItemViewModel(LauncherItem launcherItem) : ViewModelBase
6 | {
7 | public LauncherItem LauncherItem { get; } = launcherItem;
8 | public LauncherItemType Type => LauncherItem.Type;
9 | public string Path => LauncherItem.Path;
10 | public string? Name => string.IsNullOrEmpty(LauncherItem.Name)
11 | ? LauncherItem.Path
12 | : LauncherItem.Name;
13 | public string Icon => LauncherItem.Type switch
14 | {
15 | LauncherItemType.File => "📄",
16 | LauncherItemType.Folder => "📂",
17 | LauncherItemType.Url => "🌐",
18 | LauncherItemType.Command => "⌨",
19 | _ => "❓"
20 | };
21 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewLocator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Avalonia.Controls;
3 | using Avalonia.Controls.Templates;
4 | using LauncherAppAvalonia.ViewModels;
5 |
6 | namespace LauncherAppAvalonia;
7 |
8 | public class ViewLocator : IDataTemplate
9 | {
10 | public Control? Build(object? param)
11 | {
12 | if (param is null)
13 | return null;
14 |
15 | string name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
16 | Type? type = Type.GetType(name);
17 |
18 | if (type != null)
19 | {
20 | return (Control)Activator.CreateInstance(type)!;
21 | }
22 |
23 | return new TextBlock { Text = "Not Found: " + name };
24 | }
25 |
26 | public bool Match(object? data)
27 | {
28 | return data is ViewModelBase;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LauncherAppAvalonia", "LauncherAppAvalonia\LauncherAppAvalonia.csproj", "{34192E6D-1EA1-41BF-8176-7E5CC66D48DB}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | Release|Any CPU = Release|Any CPU
9 | EndGlobalSection
10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 | {34192E6D-1EA1-41BF-8176-7E5CC66D48DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 | {34192E6D-1EA1-41BF-8176-7E5CC66D48DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 | {34192E6D-1EA1-41BF-8176-7E5CC66D48DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 | {34192E6D-1EA1-41BF-8176-7E5CC66D48DB}.Release|Any CPU.Build.0 = Release|Any CPU
15 | EndGlobalSection
16 | EndGlobal
17 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Converters/LauncherItemTypeToStringConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Avalonia.Data.Converters;
4 | using LauncherAppAvalonia.Models;
5 |
6 | namespace LauncherAppAvalonia.Converters;
7 |
8 | public class LauncherItemTypeToStringConverter : IValueConverter
9 | {
10 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
11 | {
12 | if (value is LauncherItemType type)
13 | {
14 | return type switch
15 | {
16 | LauncherItemType.File => "文件",
17 | LauncherItemType.Folder => "文件夹",
18 | LauncherItemType.Url => "网址",
19 | LauncherItemType.Command => "命令",
20 | _ => value.ToString()
21 | };
22 | }
23 |
24 | return value;
25 | }
26 |
27 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
28 | {
29 | throw new NotImplementedException();
30 | }
31 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 ZQY
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Models/AppSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using CommunityToolkit.Mvvm.ComponentModel;
3 |
4 | namespace LauncherAppAvalonia.Models;
5 |
6 | public partial class AppSettings
7 | : ObservableObject
8 | {
9 | public static IReadOnlyList Themes { get; } = ["System", "Light", "Dark"];
10 | public static IReadOnlyList Languages { get; } = ["System", "zh-CN", "en-US"];
11 |
12 | [ObservableProperty]
13 | private string _theme = Themes[0];
14 |
15 | [ObservableProperty]
16 | private string _language = Languages[0];
17 |
18 | [ObservableProperty]
19 | private string? _hotkey = "Alt+Shift+Q";
20 |
21 |
22 | public AppSettings() { }
23 |
24 | public AppSettings(AppSettings source) => Clone(source);
25 |
26 | public void Reset()
27 | {
28 | Theme = Themes[0];
29 | Language = Languages[0];
30 | Hotkey = "Alt+Shift+Q";
31 | }
32 |
33 | public void Clone(AppSettings source)
34 | {
35 | Theme = source.Theme;
36 | Language = source.Language;
37 | Hotkey = source.Hotkey;
38 | }
39 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## A streamlined .gitignore for modern .NET projects
2 | ## including temporary files, build results, and
3 | ## files generated by popular .NET tools. If you are
4 | ## developing with Visual Studio, the VS .gitignore
5 | ## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
6 | ## has more thorough IDE-specific entries.
7 | ##
8 | ## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
9 |
10 | # Build results
11 | [Dd]ebug/
12 | [Dd]ebugPublic/
13 | [Rr]elease/
14 | [Rr]eleases/
15 | x64/
16 | x86/
17 | [Ww][Ii][Nn]32/
18 | [Aa][Rr][Mm]/
19 | [Aa][Rr][Mm]64/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 | [Ll]ogs/
25 |
26 | # .NET Core
27 | project.lock.json
28 | project.fragment.lock.json
29 | artifacts/
30 |
31 | # ASP.NET Scaffolding
32 | ScaffoldingReadMe.txt
33 |
34 | # NuGet Packages
35 | *.nupkg
36 | # NuGet Symbol Packages
37 | *.snupkg
38 |
39 | # MSBuild Binary and Structured Log
40 | *.binlog
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # IDE
52 | .idea/
53 | .vs/
54 |
55 | # Others
56 | .DS_Store
57 | ~$*
58 | *~
59 | CodeCoverage/
60 | src/LauncherAppAvalonia.sln.DotSettings.user
61 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/LauncherAppAvalonia.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net9.0
5 | enable
6 | true
7 | app.manifest
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | None
23 | All
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Launcher App
2 |
3 | [English](./README.md)
4 |
5 | > 🚀 快速访问常用文件、文件夹、URL 和命令的桌面启动器应用
6 |
7 | 
8 | 
9 |
10 | 
11 |
12 | ## 📋 项目简介
13 |
14 | Launcher App 是一款基于 Avalonia UI 开发的桌面快速启动器工具,帮助用户快速访问常用的文件、文件夹、网站和命令。
15 |
16 | > ⚠️ **声明:**
17 | > 1. 本项目的代码主要由人工智能辅助生成。项目结构、功能实现及界面设计均使用了 AI 技术。
18 | > 2. 此文档内容亦主要由 AI 生成。
19 | > 3. 软件未经严格测试。
20 |
21 | ## ✨ 主要功能
22 |
23 | - 🗂️ 添加和管理多种类型的项目
24 | - 文件
25 | - 文件夹
26 | - 网址和 Deep Link
27 | - 命令行指令
28 | - 🔍 快速搜索项目
29 | - 🖱️ 支持拖放文件/文件夹直接添加
30 | - 📋 右键菜单提供丰富操作选项
31 | - 🌓 支持深色和浅色主题
32 | - 🌍 支持多语言
33 | - 内置中文、英文支持
34 | - 可以通过添加翻译文件来支持更多语言
35 | - ⌨️ 自定义全局快捷键呼出应用(默认是Alt+Shift+Q)
36 | - 🧩 系统托盘集成,显示最近使用的项目
37 | - 🔄 支持拖拽重新排序列表
38 | - ⚡ 通过双击或回车快速打开项目
39 | - 💬 跨平台支持 (Windows, macOS, Linux,未严格测试)
40 |
41 | ## 📥 构建
42 |
43 | 1. 确保已安装 [Node.js](https://nodejs.org/) (推荐 22 LTS 或更高版本)
44 |
45 | 2. 克隆仓库
46 |
47 | ```bash
48 | git clone https://github.com/SolarianZ/LauncherAppAvalonia.git
49 | cd LauncherAppAvalonia
50 | ```
51 |
52 | 3. 安装依赖
53 |
54 | TODO
55 |
56 | 4. 启动应用
57 |
58 | TODO
59 |
60 | ### 📦 打包应用
61 |
62 | TODO
63 |
64 | ## 🧩 技术实现
65 |
66 | - **Avalonia UI**:跨平台桌面应用框架
67 | - **本地存储**:JSON文件持久化数据
68 | - **国际化**:多语言支持系统
69 | - **响应式UI**:适配不同尺寸和主题
70 |
71 | 更多项目细节参考 [copilot-instructions](./.github/copilot-instructions.md) 。
72 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewModels/AppSettingsViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.InteropServices;
4 | using Avalonia.Media;
5 | using CommunityToolkit.Mvvm.ComponentModel;
6 | using CommunityToolkit.Mvvm.Input;
7 | using LauncherAppAvalonia.Models;
8 | using LauncherAppAvalonia.Services;
9 |
10 | namespace LauncherAppAvalonia.ViewModels;
11 |
12 | public partial class AppSettingsViewModel : ViewModelBase
13 | {
14 | [ObservableProperty]
15 | private AppSettings _appSettings;
16 |
17 | [ObservableProperty]
18 | private IBrush? _viewBackground;
19 |
20 |
21 | private readonly Action _onClose;
22 | private readonly Action _onSave;
23 |
24 |
25 | // For Preview
26 | public AppSettingsViewModel() : this(new AppSettings(), () => { }, _ => { }) { }
27 |
28 | public AppSettingsViewModel(AppSettings appSettings, Action onClose, Action onSave)
29 | {
30 | _appSettings = appSettings;
31 |
32 | _onClose = onClose;
33 | _onSave = onSave;
34 | }
35 |
36 |
37 | #region Command
38 |
39 | [RelayCommand]
40 | private void SaveSettings() => _onSave(AppSettings);
41 |
42 | [RelayCommand]
43 | private void CloseSettings() => _onClose();
44 |
45 | [RelayCommand]
46 | private void ClearItems()
47 | {
48 | // TODO
49 | }
50 |
51 | [RelayCommand]
52 | private void OpenDataFolder()
53 | {
54 | string dataFolder = DataService.UserDataFolder;
55 | if (!Directory.Exists(dataFolder))
56 | Directory.CreateDirectory(dataFolder);
57 |
58 | Utility.OpenFolder(dataFolder);
59 | }
60 |
61 | #endregion
62 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Avalonia;
3 | using Avalonia.Controls.ApplicationLifetimes;
4 | using Avalonia.Data.Core.Plugins;
5 | using Avalonia.Markup.Xaml;
6 | using LauncherAppAvalonia.Services;
7 | using LauncherAppAvalonia.ViewModels;
8 | using LauncherAppAvalonia.Views;
9 |
10 | namespace LauncherAppAvalonia;
11 |
12 | public partial class App : Application
13 | {
14 | public override void Initialize()
15 | {
16 | AvaloniaXamlLoader.Load(this);
17 | }
18 |
19 | public override void OnFrameworkInitializationCompleted()
20 | {
21 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
22 | {
23 | // Avoid duplicate validations from both Avalonia and the CommunityToolkit.
24 | // More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
25 | DisableAvaloniaDataAnnotationValidation();
26 |
27 | DataService dataService = new DataService();
28 | desktop.MainWindow = new MainWindow
29 | {
30 | DataContext = new MainWindowViewModel(dataService),
31 | };
32 | }
33 |
34 | base.OnFrameworkInitializationCompleted();
35 | }
36 |
37 | private void DisableAvaloniaDataAnnotationValidation()
38 | {
39 | // Get an array of plugins to remove
40 | var dataValidationPluginsToRemove =
41 | BindingPlugins.DataValidators.OfType().ToArray();
42 |
43 | // remove each entry found
44 | foreach (var plugin in dataValidationPluginsToRemove)
45 | {
46 | BindingPlugins.DataValidators.Remove(plugin);
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.ComponentModel;
5 | using CommunityToolkit.Mvvm.ComponentModel;
6 | using LauncherAppAvalonia.Models;
7 | using LauncherAppAvalonia.Services;
8 |
9 | namespace LauncherAppAvalonia.ViewModels;
10 |
11 | public partial class MainWindowViewModel : ViewModelBase
12 | {
13 | public bool IsItemEditorViewVisible => ItemEditorViewModel is not null;
14 | public bool IsAppSettingsViewVisible => AppSettingsViewModel is not null;
15 |
16 | [ObservableProperty]
17 | private ObservableCollection _items;
18 |
19 | [ObservableProperty]
20 | private string? _searchText = string.Empty;
21 |
22 | [ObservableProperty]
23 | private ObservableCollection _filteredItems = new();
24 |
25 | [ObservableProperty]
26 | private LauncherItemViewModel? _selectedItem;
27 |
28 | [ObservableProperty]
29 | [NotifyPropertyChangedFor(nameof(IsItemEditorViewVisible))]
30 | private ItemEditorViewModel? _itemEditorViewModel;
31 |
32 | [ObservableProperty]
33 | [NotifyPropertyChangedFor(nameof(IsAppSettingsViewVisible))]
34 | private AppSettingsViewModel? _appSettingsViewModel;
35 |
36 | private readonly DataService _dataService;
37 |
38 |
39 | public MainWindowViewModel(DataService dataService)
40 | {
41 | _dataService = dataService;
42 |
43 | // 初始化数据
44 | List items = _dataService.GetItems();
45 | _items = new ObservableCollection(items);
46 | Items.CollectionChanged += (_, _) => UpdateFilteredItems();
47 | UpdateFilteredItems();
48 | }
49 |
50 | ///
51 | protected override void OnPropertyChanged(PropertyChangedEventArgs e)
52 | {
53 | base.OnPropertyChanged(e);
54 |
55 | if (e.PropertyName is nameof(SearchText) or nameof(Items))
56 | {
57 | UpdateFilteredItems();
58 | }
59 | }
60 |
61 | private void UpdateFilteredItems()
62 | {
63 | FilteredItems.Clear();
64 | foreach (LauncherItem item in Items)
65 | {
66 | if (MatchesSearchText(item, SearchText))
67 | FilteredItems.Add(new LauncherItemViewModel(item));
68 | }
69 | }
70 |
71 | private static bool MatchesSearchText(LauncherItem item, string? searchText)
72 | {
73 | if (string.IsNullOrWhiteSpace(searchText))
74 | return true;
75 |
76 | return item.Name?.Contains(searchText, StringComparison.OrdinalIgnoreCase) == true ||
77 | item.Path.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
78 | item.Type.ToString().Contains(searchText, StringComparison.OrdinalIgnoreCase);
79 | }
80 | }
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # GitHub Copilot 指导文件
2 |
3 | ## 项目概述
4 |
5 | Launcher App 是一个基于 Avalonia UI 的启动器应用程序,用于管理用户常用的文件、文件夹、URL和指令。
6 | 该应用程序包含项目列表界面(主界面)、项目编辑界面、项目右键菜单、设置界面、托盘图标、托盘右键菜单等。
7 |
8 |
9 | ## 主要功能
10 |
11 | 1. **项目管理**
12 | - 添加、编辑和移除项目(文件、文件夹、URL、命令)
13 | - 拖放添加文件/文件夹
14 | - 项目排序与搜索
15 | - 右键菜单操作(打开、复制路径、在文件夹中显示等)
16 |
17 | 2. **用户界面**
18 | - 主窗口(项目列表)
19 | - 项目编辑窗口
20 | - 设置窗口
21 | - 系统托盘集成(显示最近使用的8个项目和退出选项)
22 | - 键盘导航与快捷键(Alt+Shift+Q 呼出应用)
23 | - 统一的Toast提示组件
24 |
25 |
26 | ## 项目结构
27 |
28 | ```
29 | LauncherAppAvalonia/src/
30 | ├── Program.cs # 应用入口
31 | └── TODO 其他待补充...
32 | ```
33 |
34 |
35 | ## 编码规范
36 |
37 | 此项目是 Avalonia UI 技术的示例项目,需要遵守以下规范,以便入门者阅读学习。
38 |
39 | 1. 注重代码质量和可读性,使用有意义的变量和函数名称。
40 | 2. 添加详细的注释,尤其是处理平台特定代码时。
41 | 3. 分离关注点,将不同功能模块化处理。
42 | 4. 使用异步编程处理IO操作,避免阻塞主线程。
43 | 5. 避免代码重复,将常用功能提取为模块或共享函数。
44 | 6. 遵守Avalonia UI推荐的最佳实践。
45 | 7. 打印日志时使用英文,不需要多语言支持,避免乱码。
46 |
47 | 关注Avalonia UI的版本变更:https://docs.avaloniaui.net/docs/stay-up-to-date/upgrade-from-0.10
48 |
49 |
50 | ### 代码优化建议
51 |
52 | 1. **UI/UX 一致性**
53 | - 确保所有窗口在主题切换、语言切换时有一致的行为
54 |
55 | 2. **模块化改进**
56 | - 将重复的主题和语言处理函数抽象为共享模块
57 | - 通过预加载脚本提供共享的类型和常量定义,避免重复定义
58 |
59 | 3. **错误处理**
60 | - 添加更全面的错误捕获和用户友好的错误显示
61 | - 实现操作日志记录功能
62 |
63 |
64 | ## Avalonia UI 常见错误及解决方案
65 |
66 | 在开发过程中遇到的一些常见错误和解决方案,可以作为参考:
67 |
68 | 1. **类型转换错误**
69 | - **错误描述**: 从 `IEnumerable` 转换为 `IReadOnlyList` 失败
70 | - **解决方案**: 使用 `.ToList()` 方法进行显式转换,例如:
71 | ```csharp
72 | // 转换为List以匹配需要的参数类型
73 | ViewModel.HandleDroppedItem(files.ToList(), this);
74 | ```
75 |
76 | 2. **XAML绑定错误**
77 | - **错误描述**: 无法解析转换器,如 `ObjectConverters.IsZero` 或 `BoolConverters.ToString`
78 | - **解决方案**:
79 | - 使用内联表达式代替转换器,例如 `IsVisible="{Binding !FilteredItems.Count}"`
80 | - 对于标题等简单文本,可以使用静态文本而非复杂绑定
81 |
82 | 3. **DataTemplate绑定错误**
83 | - **错误描述**: 无法解析DataTemplate中的属性
84 | - **解决方案**:
85 | - 显式声明DataTemplate的数据类型:``
86 | - 确保绑定到公共属性而非方法,如将 `{Binding GetIcon()}` 替换为 `{Binding Icon}`
87 | - 在ViewModel中添加相应的属性:`public string Icon => GetIcon();`
88 |
89 | 4. **ContextMenu绑定问题**
90 | - **错误描述**: ContextMenu默认不继承DataContext,导致绑定失败
91 | - **解决方案**: 使用特殊的绑定语法访问父元素的DataContext:
92 | ```xml
93 |
94 |
95 |
96 | ```
97 |
98 | 5. **编译时文件锁定**
99 | - **错误描述**: DLL被其他进程锁定,无法覆盖
100 | - **解决方案**:
101 | - 使用 `dotnet clean` 清理项目后再构建
102 | - 关闭所有可能运行的应用实例
103 | - 重启IDE或开发工具
104 |
105 | 6. **ListBox控件命名错误**
106 | - **错误描述**: `Items` 属性不存在
107 | - **解决方案**: 在Avalonia中使用 `ItemsSource` 而非 `Items` 绑定集合数据
108 |
109 | 这些问题大多与Avalonia UI的特定实现方式有关,特别是与其他XAML框架(如WPF或UWP)的微小差异造成的。在Avalonia开发中,推荐经常参考[官方文档](https://docs.avaloniaui.net)以及示例项目。
110 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Views/ItemEditorView.axaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 | 编辑项目
24 |
25 |
26 |
27 |
28 |
29 |
30 |
36 |
37 |
38 |
39 |
44 |
45 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
67 | ERROR TEXT 错误信息
68 |
69 | ERROR TEXT 错误信息
70 |
71 |
72 |
73 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Services/DataService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Text.Json;
6 | using System.Text.Json.Serialization;
7 | using LauncherAppAvalonia.Models;
8 |
9 | namespace LauncherAppAvalonia.Services;
10 |
11 | public class DataService
12 | {
13 | public static string UserDataFolder { get; }
14 | public static string ItemsFilePath { get; }
15 | public static string SettingsFilePath { get; }
16 |
17 | private readonly List _items = new();
18 | private readonly AppSettings _settings = new();
19 | private readonly JsonSerializerOptions _jsonSerializerOptions = new()
20 | {
21 | WriteIndented = true,
22 | Converters =
23 | {
24 | new JsonStringEnumConverter()
25 | }
26 | };
27 |
28 |
29 | static DataService()
30 | {
31 | string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
32 | UserDataFolder = Path.Combine(appDataFolder, "LauncherAppAvalonia", "UserData");
33 | ItemsFilePath = Path.Combine(UserDataFolder, "items.json");
34 | SettingsFilePath = Path.Combine(UserDataFolder, "settings.json");
35 | }
36 |
37 | public DataService()
38 | {
39 | LoadItems();
40 | LoadSettings();
41 | }
42 |
43 |
44 | #region Items
45 |
46 | public List GetItems()
47 | {
48 | return [.._items];
49 | }
50 |
51 | public void SetItems(IEnumerable? items)
52 | {
53 | _items.Clear();
54 | if (items != null)
55 | _items.AddRange(items);
56 | SaveItems();
57 | }
58 |
59 | private void LoadItems()
60 | {
61 | _items.Clear();
62 |
63 | try
64 | {
65 | if (!File.Exists(ItemsFilePath))
66 | return;
67 |
68 | string json = File.ReadAllText(ItemsFilePath);
69 | List? loadedItems = JsonSerializer.Deserialize>(json, _jsonSerializerOptions);
70 | if (loadedItems != null)
71 | _items.AddRange(loadedItems);
72 | }
73 | catch (Exception ex)
74 | {
75 | Debug.WriteLine($"Error loading items: {ex.Message}");
76 | }
77 | }
78 |
79 | private void SaveItems()
80 | {
81 | try
82 | {
83 | Directory.CreateDirectory(UserDataFolder);
84 | string json = JsonSerializer.Serialize(_items, _jsonSerializerOptions);
85 | File.WriteAllText(ItemsFilePath, json);
86 | }
87 | catch (Exception ex)
88 | {
89 | Debug.WriteLine($"Error saving items: {ex.Message}");
90 | }
91 | }
92 |
93 | #endregion
94 |
95 |
96 | #region Settings
97 |
98 | public AppSettings GetSettings()
99 | {
100 | return new AppSettings(_settings);
101 | }
102 |
103 | public void SetSettings(AppSettings? settings)
104 | {
105 | if (settings != null)
106 | _settings.Clone(settings);
107 | else
108 | _settings.Reset();
109 |
110 | SaveSettings();
111 | }
112 |
113 | private void LoadSettings()
114 | {
115 | _settings.Reset();
116 |
117 | try
118 | {
119 | if (!File.Exists(SettingsFilePath))
120 | return;
121 |
122 | string json = File.ReadAllText(SettingsFilePath);
123 | AppSettings? loadedSettings = JsonSerializer.Deserialize(json, _jsonSerializerOptions);
124 | if (loadedSettings != null)
125 | _settings.Clone(loadedSettings);
126 | }
127 | catch (Exception ex)
128 | {
129 | Debug.WriteLine($"Error loading settings: {ex.Message}");
130 | }
131 | }
132 |
133 | private void SaveSettings()
134 | {
135 | try
136 | {
137 | Directory.CreateDirectory(UserDataFolder);
138 | string json = JsonSerializer.Serialize(_settings, _jsonSerializerOptions);
139 | File.WriteAllText(SettingsFilePath, json);
140 | }
141 | catch (Exception ex)
142 | {
143 | Debug.WriteLine($"Error saving settings: {ex.Message}");
144 | }
145 | }
146 |
147 | #endregion
148 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Views/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
46 |
49 |
52 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
68 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Views/AppSettingsView.axaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
30 |
36 |
37 |
38 |
39 |
40 |
42 |
43 |
47 |
48 |
52 |
53 |
57 |
62 |
63 |
64 |
68 |
73 |
74 |
75 |
76 |
77 |
78 |
81 |
82 |
86 |
87 |
91 |
92 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewModels/ItemEditorViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.IO;
5 | using System.Text.RegularExpressions;
6 | using System.Threading.Tasks;
7 | using Avalonia;
8 | using Avalonia.Controls;
9 | using Avalonia.Controls.ApplicationLifetimes;
10 | using Avalonia.Media;
11 | using Avalonia.Platform.Storage;
12 | using CommunityToolkit.Mvvm.ComponentModel;
13 | using CommunityToolkit.Mvvm.Input;
14 | using LauncherAppAvalonia.Models;
15 |
16 | namespace LauncherAppAvalonia.ViewModels;
17 |
18 | public partial class ItemEditorViewModel : ViewModelBase
19 | {
20 | public static LauncherItemType[] ItemTypes { get; } = Enum.GetValues();
21 |
22 | [ObservableProperty]
23 | private LauncherItem _launcherItem;
24 |
25 | [ObservableProperty]
26 | private IBrush? _viewBackground;
27 |
28 | // 匹配如http://, https://, ftp://, app://, myapp://等协议格式
29 | private readonly Regex _protocolRegex = new(@"^[a-z][a-z0-9+.-]*:\/\/", RegexOptions.IgnoreCase);
30 | // 匹配标准域名格式 (包括www开头和不带www的域名)
31 | private readonly Regex _domainRegex = new(@"^([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,})(:[0-9]{1,5})?(\/.*)?$", RegexOptions.IgnoreCase);
32 |
33 | private readonly Action _onCancel;
34 | private readonly Action _onSave;
35 |
36 |
37 | public ItemEditorViewModel(LauncherItem item, Action onCancel, Action onSave)
38 | {
39 | LauncherItem = item;
40 |
41 | _onCancel = onCancel;
42 | _onSave = onSave;
43 |
44 | LauncherItem.PropertyChanged += OnLauncherItemPropertyChanged;
45 | }
46 |
47 | private void OnLauncherItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
48 | {
49 | if (e.PropertyName == nameof(Path))
50 | DetectItemTypeByPath(LauncherItem.Path);
51 | }
52 |
53 | private void DetectItemTypeByPath(string? path)
54 | {
55 | if (string.IsNullOrWhiteSpace(path))
56 | {
57 | LauncherItem.Type = LauncherItemType.Command;
58 | return;
59 | }
60 |
61 | if (File.Exists(path))
62 | {
63 | // Windows特殊逻辑:将BAT视为Command
64 | if (OperatingSystem.IsWindows())
65 | {
66 | string extension = Path.GetExtension(path).ToLowerInvariant();
67 | if (extension == ".bat")
68 | {
69 | LauncherItem.Type = LauncherItemType.Command;
70 | return;
71 | }
72 | }
73 |
74 | LauncherItem.Type = LauncherItemType.File;
75 | return;
76 | }
77 |
78 | if (Directory.Exists(path))
79 | {
80 | LauncherItem.Type = LauncherItemType.Folder;
81 | return;
82 | }
83 |
84 | // 判断是否是标准协议URL和Deep Link
85 | if (_protocolRegex.IsMatch(path) || _domainRegex.IsMatch(path))
86 | {
87 | LauncherItem.Type = LauncherItemType.Url;
88 | return;
89 | }
90 |
91 | // 默认视为 Command
92 | LauncherItem.Type = LauncherItemType.Command;
93 | }
94 |
95 |
96 | #region Commands
97 |
98 | [RelayCommand]
99 | private async Task SelectFileAsync()
100 | {
101 | // 调用内置文件选择对话框
102 | TopLevel? topLevel = GetTopLevel();
103 | if (topLevel is null)
104 | return;
105 |
106 | IReadOnlyList files = await topLevel.StorageProvider.OpenFilePickerAsync(
107 | new FilePickerOpenOptions
108 | {
109 | Title = "Select File",
110 | AllowMultiple = false
111 | });
112 | if (files.Count > 0)
113 | LauncherItem.Path = files[0].Path.LocalPath;
114 | }
115 |
116 | [RelayCommand]
117 | private async Task SelectFolderAsync()
118 | {
119 | // 调用内置文件夹选择对话框
120 | TopLevel? topLevel = GetTopLevel();
121 | if (topLevel is null)
122 | return;
123 |
124 | IReadOnlyList folders = await topLevel.StorageProvider.OpenFolderPickerAsync(
125 | new FolderPickerOpenOptions
126 | {
127 | Title = "Select Folder",
128 | AllowMultiple = false
129 | });
130 | if (folders.Count > 0)
131 | LauncherItem.Path = folders[0].Path.LocalPath;
132 | }
133 |
134 | [RelayCommand]
135 | private void CancelEdit()
136 | {
137 | _onCancel.Invoke();
138 | }
139 |
140 | [RelayCommand]
141 | private void SaveEdit()
142 | {
143 | _onSave.Invoke(LauncherItem);
144 | }
145 |
146 | #endregion
147 |
148 |
149 | private static TopLevel? GetTopLevel()
150 | {
151 | if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
152 | return desktop.MainWindow;
153 |
154 | return null;
155 | }
156 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/Utility/Utility.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace LauncherAppAvalonia;
7 |
8 | public static class Utility
9 | {
10 | #region Show Item in Folder
11 |
12 | public static void ShowItemInFolder(string path)
13 | {
14 | try
15 | {
16 | if (OperatingSystem.IsWindows())
17 | {
18 | Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{path}\"")
19 | {
20 | UseShellExecute = true
21 | });
22 | }
23 | else if (OperatingSystem.IsMacOS())
24 | {
25 | Process.Start(new ProcessStartInfo("open", $"-R \"{path}\"")
26 | {
27 | UseShellExecute = true
28 | });
29 | }
30 | else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
31 | {
32 | bool isFolder = Directory.Exists(path);
33 | string command = isFolder ? "xdg-open" : "xdg-open --select";
34 | Process.Start(new ProcessStartInfo(command, $"\"{path}\"")
35 | {
36 | UseShellExecute = true
37 | });
38 | }
39 | else
40 | Debug.WriteLine($"Unsupported OS for showing item in folder: {RuntimeInformation.OSDescription}");
41 | }
42 | catch (Exception ex)
43 | {
44 | Debug.WriteLine($"Error showing item in folder: {ex.Message}");
45 | }
46 | }
47 |
48 | #endregion
49 |
50 |
51 | #region Open Item
52 |
53 | public static void OpenFile(string filePath)
54 | {
55 | if (OperatingSystem.IsWindows())
56 | {
57 | Process.Start(new ProcessStartInfo(filePath)
58 | {
59 | UseShellExecute = true
60 | });
61 | }
62 | else if (OperatingSystem.IsMacOS())
63 | {
64 | Process.Start(new ProcessStartInfo("open", $"\"{filePath}\"")
65 | {
66 | UseShellExecute = true
67 | });
68 | }
69 | else if (OperatingSystem.IsLinux() ||
70 | OperatingSystem.IsFreeBSD())
71 | {
72 | Process.Start(new ProcessStartInfo("xdg-open", $"\"{filePath}\"")
73 | {
74 | UseShellExecute = true
75 | });
76 | }
77 | else
78 | {
79 | Debug.WriteLine($"Unsupported OS for opening file: {RuntimeInformation.OSDescription}");
80 | }
81 | }
82 |
83 | public static void OpenFolder(string folderPath)
84 | {
85 | if (OperatingSystem.IsWindows())
86 | {
87 | Process.Start(new ProcessStartInfo("explorer.exe", $"\"{folderPath}\"")
88 | {
89 | UseShellExecute = true
90 | });
91 | }
92 | else if (OperatingSystem.IsMacOS())
93 | {
94 | Process.Start(new ProcessStartInfo("open", $"\"{folderPath}\"")
95 | {
96 | UseShellExecute = true
97 | });
98 | }
99 | else if (OperatingSystem.IsLinux() ||
100 | OperatingSystem.IsFreeBSD())
101 | {
102 | Process.Start(new ProcessStartInfo("xdg-open", $"\"{folderPath}\"")
103 | {
104 | UseShellExecute = true
105 | });
106 | }
107 | else
108 | {
109 | Debug.WriteLine($"Unsupported OS for opening folder: {RuntimeInformation.OSDescription}");
110 | }
111 | }
112 |
113 | public static void OpenUrl(string url)
114 | {
115 | if (OperatingSystem.IsWindows())
116 | {
117 | Process.Start(new ProcessStartInfo(url)
118 | {
119 | UseShellExecute = true
120 | });
121 | }
122 | else if (OperatingSystem.IsMacOS())
123 | {
124 | Process.Start(new ProcessStartInfo("open", url)
125 | {
126 | UseShellExecute = true
127 | });
128 | }
129 | else if (OperatingSystem.IsLinux() ||
130 | OperatingSystem.IsFreeBSD())
131 | {
132 | Process.Start(new ProcessStartInfo("xdg-open", url)
133 | {
134 | UseShellExecute = true
135 | });
136 | }
137 | else
138 | {
139 | Debug.WriteLine($"Unsupported OS for opening URL: {RuntimeInformation.OSDescription}");
140 | }
141 | }
142 |
143 | public static void OpenCommand(string command)
144 | {
145 | if (OperatingSystem.IsWindows())
146 | {
147 | if (!command.StartsWith("/C", StringComparison.OrdinalIgnoreCase) &&
148 | !command.StartsWith("/K", StringComparison.OrdinalIgnoreCase))
149 | command = $"/K {command}";
150 |
151 | Process.Start(new ProcessStartInfo("cmd.exe", command)
152 | {
153 | UseShellExecute = false
154 | });
155 | }
156 | else if (OperatingSystem.IsMacOS() ||
157 | OperatingSystem.IsLinux() ||
158 | OperatingSystem.IsFreeBSD())
159 | {
160 | Process.Start(new ProcessStartInfo("/bin/bash", $"-c \"{command}\"")
161 | {
162 | UseShellExecute = false
163 | });
164 | }
165 | else
166 | {
167 | Debug.WriteLine($"Unsupported OS for executing command: {RuntimeInformation.OSDescription}");
168 | }
169 | }
170 |
171 | #endregion
172 | }
--------------------------------------------------------------------------------
/src/LauncherAppAvalonia/ViewModels/MainWindowViewModel.Commands.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using Avalonia;
5 | using Avalonia.Controls;
6 | using Avalonia.Controls.ApplicationLifetimes;
7 | using Avalonia.Input.Platform;
8 | using Avalonia.Media;
9 | using CommunityToolkit.Mvvm.Input;
10 | using LauncherAppAvalonia.Models;
11 |
12 | namespace LauncherAppAvalonia.ViewModels;
13 |
14 | public partial class MainWindowViewModel
15 | {
16 | #region AddItem & EditItem
17 |
18 | [RelayCommand]
19 | private void AddItem(IBrush? background)
20 | {
21 | Debug.Assert(IsItemEditorViewVisible == false);
22 |
23 | LauncherItem newItem = new LauncherItem(LauncherItemType.Command, string.Empty, null);
24 | ItemEditorViewModel = new ItemEditorViewModel(newItem, CloseItemEditorView, SaveItemAndCloseItemEditorView)
25 | {
26 | ViewBackground = background,
27 | };
28 | }
29 |
30 | [RelayCommand]
31 | private void EditItem(LauncherItemViewModel itemVM)
32 | {
33 | Debug.Assert(itemVM != null);
34 | Debug.Assert(IsItemEditorViewVisible == false);
35 |
36 | LauncherItem item = itemVM.LauncherItem;
37 | ItemEditorViewModel = new ItemEditorViewModel(item, CloseItemEditorView, SaveItemAndCloseItemEditorView);
38 | }
39 |
40 | private void CloseItemEditorView()
41 | {
42 | Debug.Assert(IsItemEditorViewVisible);
43 |
44 | ItemEditorViewModel = null;
45 | }
46 |
47 | private void SaveItemAndCloseItemEditorView(LauncherItem item)
48 | {
49 | Debug.Assert(IsItemEditorViewVisible);
50 |
51 | if (!Items.Contains(item))
52 | Items.Add(item);
53 | _dataService.SetItems(Items);
54 |
55 | CloseItemEditorView();
56 | }
57 |
58 | #endregion
59 |
60 |
61 | #region OpenSettings
62 |
63 | [RelayCommand]
64 | private void OpenAppSettingsView(IBrush? background)
65 | {
66 | Debug.Assert(IsAppSettingsViewVisible == false);
67 |
68 | AppSettings appSettings = _dataService.GetSettings();
69 | AppSettingsViewModel = new AppSettingsViewModel(appSettings, CloseAppSettingsView, SaveAppSettings)
70 | {
71 | ViewBackground = background,
72 | };
73 | }
74 |
75 | private void CloseAppSettingsView()
76 | {
77 | Debug.Assert(IsAppSettingsViewVisible);
78 |
79 | AppSettingsViewModel = null;
80 | }
81 |
82 | private void SaveAppSettings(AppSettings appSettings)
83 | {
84 | Debug.Assert(IsAppSettingsViewVisible);
85 |
86 | _dataService.SetSettings(appSettings);
87 | }
88 |
89 | #endregion
90 |
91 |
92 | #region RemoveItem
93 |
94 | [RelayCommand]
95 | private void RemoveItem(LauncherItemViewModel itemVM)
96 | {
97 | LauncherItem item = itemVM.LauncherItem;
98 | Items.Remove(item);
99 | _dataService.SetItems(Items);
100 | }
101 |
102 | #endregion
103 |
104 |
105 | #region OpenItem
106 |
107 | [RelayCommand]
108 | private void OpenItem(LauncherItemViewModel itemVM)
109 | {
110 | Debug.Assert(itemVM != null);
111 |
112 | try
113 | {
114 | switch (itemVM.Type)
115 | {
116 | case LauncherItemType.File:
117 | Utility.OpenFile(itemVM.Path);
118 | break;
119 | case LauncherItemType.Folder:
120 | Utility.OpenFolder(itemVM.Path);
121 | break;
122 | case LauncherItemType.Url:
123 | Utility.OpenUrl(itemVM.Path);
124 | break;
125 | case LauncherItemType.Command:
126 | Utility.OpenCommand(itemVM.Path);
127 | break;
128 | default:
129 | Debug.WriteLine("Unsupported item type for opening: " + itemVM.Type);
130 | break;
131 | }
132 | }
133 | catch (Exception ex)
134 | {
135 | Debug.WriteLine($"Error opening item: {ex.Message}");
136 | }
137 | }
138 |
139 | #endregion
140 |
141 |
142 | #region ShowItemInFolder
143 |
144 | private bool CanShowItemInFolder(LauncherItemViewModel? itemVM)
145 | {
146 | if (itemVM == null)
147 | return false;
148 |
149 | return itemVM.Type switch
150 | {
151 | LauncherItemType.File => File.Exists(itemVM.Path),
152 | LauncherItemType.Folder => Directory.Exists(itemVM.Path),
153 | _ => false
154 | };
155 | }
156 |
157 | [RelayCommand(CanExecute = nameof(CanShowItemInFolder))]
158 | private void ShowItemInFolder(LauncherItemViewModel itemVM)
159 | {
160 | Debug.Assert(itemVM is { Type: LauncherItemType.File or LauncherItemType.Folder });
161 |
162 | Utility.ShowItemInFolder(itemVM.Path);
163 | }
164 |
165 | #endregion
166 |
167 |
168 | #region CopyItemPath
169 |
170 | [RelayCommand]
171 | private void CopyItemPath(LauncherItemViewModel itemVM)
172 | {
173 | try
174 | {
175 | if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
176 | {
177 | IClipboard? clipboard = TopLevel.GetTopLevel(desktop.MainWindow)?.Clipboard;
178 | if (clipboard != null)
179 | {
180 | clipboard.SetTextAsync(itemVM.Path);
181 | return;
182 | }
183 | }
184 |
185 | Debug.WriteLine("Clipboard service is not available.");
186 | }
187 | catch (Exception ex)
188 | {
189 | Debug.WriteLine($"Error copying item path: {ex.Message}");
190 | }
191 | }
192 |
193 | #endregion
194 | }
--------------------------------------------------------------------------------