This commit is contained in:
commit
01acd4d6e6
107
.gitea/workflows/release.yaml
Normal file
107
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-gitea
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Prep For Local Builds
|
||||||
|
run: echo "${LOCIP} gitea.comnenos" >> /etc/hosts
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Build Dependencies
|
||||||
|
run: |
|
||||||
|
apt update
|
||||||
|
apt -y --no-install-recommends install \
|
||||||
|
wget \
|
||||||
|
file \
|
||||||
|
fuse \
|
||||||
|
libfuse2 \
|
||||||
|
python3 \
|
||||||
|
python3-pip
|
||||||
|
|
||||||
|
- name: Install .NET SDK
|
||||||
|
run: |
|
||||||
|
wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
|
||||||
|
chmod +x dotnet-install.sh
|
||||||
|
./dotnet-install.sh --channel 8.0 --install-dir /usr/local/dotnet
|
||||||
|
ln -sf /usr/local/dotnet/dotnet /usr/bin/dotnet
|
||||||
|
|
||||||
|
- name: Install AppImageTool
|
||||||
|
run: |
|
||||||
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage \
|
||||||
|
-O /usr/local/bin/appimagetool
|
||||||
|
chmod +x /usr/local/bin/appimagetool
|
||||||
|
|
||||||
|
- name: Build Linux Packages
|
||||||
|
run: |
|
||||||
|
cd NotePad
|
||||||
|
python3 publish.py linux both
|
||||||
|
|
||||||
|
- name: Create Release Archive
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
mkdir -p release
|
||||||
|
|
||||||
|
# Copy AppImage
|
||||||
|
cp publish/appimage/NotePad-0.1.0-x86_64.AppImage \
|
||||||
|
release/NotePad-${VERSION}-x86_64.AppImage
|
||||||
|
|
||||||
|
# Copy Tarball
|
||||||
|
cp publish/tarball/notepad-0.1.0-linux-x64.tar.gz \
|
||||||
|
release/notepad-${VERSION}-linux-x64.tar.gz
|
||||||
|
|
||||||
|
# Generate checksums
|
||||||
|
cd release
|
||||||
|
sha256sum NotePad-${VERSION}-x86_64.AppImage > NotePad-${VERSION}-x86_64.AppImage.sha256
|
||||||
|
sha256sum notepad-${VERSION}-linux-x64.tar.gz > notepad-${VERSION}-linux-x64.tar.gz.sha256
|
||||||
|
ls -lh # For debugging
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"${VERSION}\",
|
||||||
|
\"name\": \"NotePad ${VERSION}\",
|
||||||
|
\"body\": \"Release ${VERSION}\n\n## Linux Downloads\n- **AppImage**: Portable, single-file executable (no installation needed)\n- **Tarball**: Traditional installation with \`sudo ./install.sh\`\"
|
||||||
|
}" > release_response.json
|
||||||
|
|
||||||
|
RELEASE_ID=$(cat release_response.json | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||||
|
|
||||||
|
# Upload AppImage
|
||||||
|
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=NotePad-${VERSION}-x86_64.AppImage" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @release/NotePad-${VERSION}-x86_64.AppImage
|
||||||
|
|
||||||
|
# Upload AppImage checksum
|
||||||
|
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=NotePad-${VERSION}-x86_64.AppImage.sha256" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: text/plain" \
|
||||||
|
--data-binary @release/NotePad-${VERSION}-x86_64.AppImage.sha256
|
||||||
|
|
||||||
|
# Upload tarball
|
||||||
|
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=notepad-${VERSION}-linux-x64.tar.gz" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/gzip" \
|
||||||
|
--data-binary @release/notepad-${VERSION}-linux-x64.tar.gz
|
||||||
|
|
||||||
|
# Upload tarball checksum
|
||||||
|
curl -X POST "https://repos.gmgauthier.com/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=notepad-${VERSION}-linux-x64.tar.gz.sha256" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: text/plain" \
|
||||||
|
--data-binary @release/notepad-${VERSION}-linux-x64.tar.gz.sha256
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.idea/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
publish/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
poetry.lock
|
||||||
18
NotePad.sln
Normal file
18
NotePad.sln
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotePad", "NotePad\NotePad.csproj", "{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6FE2D646-2FF3-4E35-9876-F75901ABC1DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
10
NotePad/App.axaml
Normal file
10
NotePad/App.axaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="NotePad.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||||
|
|
||||||
|
<Application.Styles>
|
||||||
|
<FluentTheme />
|
||||||
|
</Application.Styles>
|
||||||
|
</Application>
|
||||||
23
NotePad/App.axaml.cs
Normal file
23
NotePad/App.axaml.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace NotePad;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnFrameworkInitializationCompleted()
|
||||||
|
{
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new MainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
74
NotePad/MainWindow.axaml
Normal file
74
NotePad/MainWindow.axaml
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="NotePad.MainWindow"
|
||||||
|
Title="Untitled - Notepad"
|
||||||
|
Width="600" Height="400">
|
||||||
|
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Menu Bar -->
|
||||||
|
<Menu DockPanel.Dock="Top">
|
||||||
|
<MenuItem Header="_File">
|
||||||
|
<MenuItem Header="_New" Click="OnNewClick" InputGesture="Ctrl+N"/>
|
||||||
|
<MenuItem Header="_Open..." Click="OnOpenClick" InputGesture="Ctrl+O"/>
|
||||||
|
<MenuItem Header="_Save" Click="OnSaveClick" InputGesture="Ctrl+S"/>
|
||||||
|
<MenuItem Header="Save _As..." Click="OnSaveAsClick"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="E_xit" Click="OnExitClick"/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_Edit">
|
||||||
|
<MenuItem Header="_Undo" Click="OnUndoClick" InputGesture="Ctrl+Z"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="Cu_t" Click="OnCutClick" InputGesture="Ctrl+X"/>
|
||||||
|
<MenuItem Header="_Copy" Click="OnCopyClick" InputGesture="Ctrl+C"/>
|
||||||
|
<MenuItem Header="_Paste" Click="OnPasteClick" InputGesture="Ctrl+V"/>
|
||||||
|
<MenuItem Header="De_lete" Click="OnDeleteClick" InputGesture="Delete"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="Select _All" Click="OnSelectAllClick" InputGesture="Ctrl+A"/>
|
||||||
|
<MenuItem Header="Time/_Date" Click="OnTimeDateClick" InputGesture="F5"/>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="F_ormat">
|
||||||
|
<MenuItem Header="_Word Wrap" x:Name="WordWrapMenuItem" Click="OnWordWrapToggle">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<CheckBox x:Name="WordWrapCheckBox" IsChecked="True" IsHitTestVisible="False"/>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="_Help">
|
||||||
|
<MenuItem Header="_About Notepad" Click="OnAboutClick"/>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<!-- Text Editor Area -->
|
||||||
|
<Border Background="White" BorderThickness="0">
|
||||||
|
<TextBox x:Name="EditorTextBox"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
AcceptsTab="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||||
|
BorderThickness="0"
|
||||||
|
Background="White"
|
||||||
|
Foreground="Black"
|
||||||
|
CaretBrush="Black"
|
||||||
|
FontFamily="Consolas,Courier New,monospace"
|
||||||
|
FontSize="12">
|
||||||
|
<TextBox.Styles>
|
||||||
|
<Style Selector="TextBox">
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="Foreground" Value="Black"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||||
|
<Setter Property="Background" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
</TextBox.Styles>
|
||||||
|
</TextBox>
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
</Window>
|
||||||
374
NotePad/MainWindow.axaml.cs
Normal file
374
NotePad/MainWindow.axaml.cs
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace NotePad
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private string? _currentFilePath;
|
||||||
|
private string? _currentFileName;
|
||||||
|
private bool _isModified;
|
||||||
|
private string? _originalText;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
UpdateTitle();
|
||||||
|
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
textBox.TextChanged += OnTextChanged;
|
||||||
|
|
||||||
|
Closing += OnWindowClosing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTitle()
|
||||||
|
{
|
||||||
|
var modified = _isModified ? "*" : "";
|
||||||
|
Title = $"{modified}{_currentFileName ?? "Untitled"} - Notepad";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTextChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var currentText = this.FindControl<TextBox>("EditorTextBox")!.Text ?? string.Empty;
|
||||||
|
_isModified = currentText != _originalText;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnWindowClosing(object? sender, WindowClosingEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isModified)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
var result = await ShowSavePrompt();
|
||||||
|
|
||||||
|
if (result == SavePromptResult.Save)
|
||||||
|
{
|
||||||
|
await PerformSave();
|
||||||
|
if (!_isModified) // Only close if save succeeded
|
||||||
|
{
|
||||||
|
Closing -= OnWindowClosing;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (result == SavePromptResult.DontSave)
|
||||||
|
{
|
||||||
|
Closing -= OnWindowClosing;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
// Cancel = do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<SavePromptResult> ShowSavePrompt()
|
||||||
|
{
|
||||||
|
var dialog = new Window
|
||||||
|
{
|
||||||
|
Title = "Notepad",
|
||||||
|
Width = 400,
|
||||||
|
Height = 150,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||||
|
CanResize = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = SavePromptResult.Cancel;
|
||||||
|
|
||||||
|
var panel = new StackPanel { Margin = new Thickness(20) };
|
||||||
|
|
||||||
|
var message = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"Do you want to save changes to {_currentFileName ?? "Untitled"}?",
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Margin = new Thickness(0, 0, 0, 20)
|
||||||
|
};
|
||||||
|
|
||||||
|
var buttonPanel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
Spacing = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var yesButton = new Button { Content = "Yes", Width = 80 };
|
||||||
|
yesButton.Click += (s, e) => { result = SavePromptResult.Save; dialog.Close(); };
|
||||||
|
|
||||||
|
var noButton = new Button { Content = "No", Width = 80 };
|
||||||
|
noButton.Click += (s, e) => { result = SavePromptResult.DontSave; dialog.Close(); };
|
||||||
|
|
||||||
|
var cancelButton = new Button { Content = "Cancel", Width = 80 };
|
||||||
|
cancelButton.Click += (s, e) => { result = SavePromptResult.Cancel; dialog.Close(); };
|
||||||
|
|
||||||
|
buttonPanel.Children.Add(yesButton);
|
||||||
|
buttonPanel.Children.Add(noButton);
|
||||||
|
buttonPanel.Children.Add(cancelButton);
|
||||||
|
|
||||||
|
panel.Children.Add(message);
|
||||||
|
panel.Children.Add(buttonPanel);
|
||||||
|
|
||||||
|
dialog.Content = panel;
|
||||||
|
|
||||||
|
await dialog.ShowDialog(this);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SavePromptResult
|
||||||
|
{
|
||||||
|
Save,
|
||||||
|
DontSave,
|
||||||
|
Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// New File
|
||||||
|
private async void OnNewClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isModified)
|
||||||
|
{
|
||||||
|
var result = await ShowSavePrompt();
|
||||||
|
if (result == SavePromptResult.Save)
|
||||||
|
{
|
||||||
|
await PerformSave();
|
||||||
|
if (_isModified) return; // Save was cancelled
|
||||||
|
}
|
||||||
|
else if (result == SavePromptResult.Cancel)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.FindControl<TextBox>("EditorTextBox")!.Text = string.Empty;
|
||||||
|
_currentFilePath = null;
|
||||||
|
_currentFileName = null;
|
||||||
|
_originalText = string.Empty;
|
||||||
|
_isModified = false;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open File
|
||||||
|
private async void OnOpenClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isModified)
|
||||||
|
{
|
||||||
|
var result = await ShowSavePrompt();
|
||||||
|
if (result == SavePromptResult.Save)
|
||||||
|
{
|
||||||
|
await PerformSave();
|
||||||
|
if (_isModified) return; // Save was cancelled
|
||||||
|
}
|
||||||
|
else if (result == SavePromptResult.Cancel)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var topLevel = GetTopLevel(this);
|
||||||
|
if (topLevel == null) return;
|
||||||
|
|
||||||
|
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = "Open",
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter = new[] { FilePickerFileTypes.TextPlain, FilePickerFileTypes.All }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files?.Count > 0)
|
||||||
|
{
|
||||||
|
var file = files[0];
|
||||||
|
_currentFilePath = file.TryGetLocalPath();
|
||||||
|
_currentFileName = file.Name;
|
||||||
|
|
||||||
|
await using var stream = await file.OpenReadAsync();
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var text = await reader.ReadToEndAsync();
|
||||||
|
|
||||||
|
this.FindControl<TextBox>("EditorTextBox")!.Text = text;
|
||||||
|
_originalText = text;
|
||||||
|
_isModified = false;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save File (if path exists, else Save As)
|
||||||
|
private async void OnSaveClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await PerformSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PerformSave()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_currentFilePath))
|
||||||
|
{
|
||||||
|
await SaveAs();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SaveToPath(_currentFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save As
|
||||||
|
private async void OnSaveAsClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await SaveAs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAs()
|
||||||
|
{
|
||||||
|
var topLevel = GetTopLevel(this);
|
||||||
|
if (topLevel == null) return;
|
||||||
|
|
||||||
|
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||||
|
{
|
||||||
|
Title = "Save As",
|
||||||
|
DefaultExtension = "txt",
|
||||||
|
FileTypeChoices = new[] { FilePickerFileTypes.TextPlain, FilePickerFileTypes.All },
|
||||||
|
SuggestedFileName = _currentFileName ?? "Untitled.txt"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
_currentFilePath = file.TryGetLocalPath();
|
||||||
|
_currentFileName = file.Name;
|
||||||
|
await SaveToPath(_currentFilePath!);
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveToPath(string path)
|
||||||
|
{
|
||||||
|
var text = this.FindControl<TextBox>("EditorTextBox")!.Text ?? string.Empty;
|
||||||
|
await File.WriteAllTextAsync(path, text);
|
||||||
|
_originalText = text;
|
||||||
|
_isModified = false;
|
||||||
|
UpdateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit
|
||||||
|
private void OnExitClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word Wrap Toggle
|
||||||
|
private void OnWordWrapToggle(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var checkBox = this.FindControl<CheckBox>("WordWrapCheckBox")!;
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
|
||||||
|
checkBox.IsChecked = !checkBox.IsChecked;
|
||||||
|
textBox.TextWrapping = checkBox.IsChecked == true ? TextWrapping.Wrap : TextWrapping.NoWrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit Menu
|
||||||
|
private void OnUndoClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
textBox.Undo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCutClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
textBox.Cut();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCopyClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
textBox.Copy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPasteClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
textBox.Paste();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDeleteClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
var selectionStart = textBox.SelectionStart;
|
||||||
|
var selectionEnd = textBox.SelectionEnd;
|
||||||
|
|
||||||
|
if (selectionStart != selectionEnd)
|
||||||
|
{
|
||||||
|
var text = textBox.Text ?? string.Empty;
|
||||||
|
textBox.Text = text.Remove(selectionStart, selectionEnd - selectionStart);
|
||||||
|
textBox.SelectionStart = selectionStart;
|
||||||
|
textBox.SelectionEnd = selectionStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSelectAllClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
textBox.SelectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTimeDateClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var textBox = this.FindControl<TextBox>("EditorTextBox")!;
|
||||||
|
var timeDate = DateTime.Now.ToString("h:mm tt M/d/yyyy");
|
||||||
|
var position = textBox.SelectionStart;
|
||||||
|
var text = textBox.Text ?? string.Empty;
|
||||||
|
textBox.Text = text.Insert(position, timeDate);
|
||||||
|
textBox.SelectionStart = position + timeDate.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help Menu
|
||||||
|
private async void OnAboutClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dialog = new Window
|
||||||
|
{
|
||||||
|
Title = "About Notepad",
|
||||||
|
Width = 350,
|
||||||
|
Height = 200,
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||||
|
CanResize = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var panel = new StackPanel { Margin = new Thickness(20) };
|
||||||
|
|
||||||
|
var title = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "Notepad",
|
||||||
|
FontSize = 16,
|
||||||
|
FontWeight = FontWeight.Bold,
|
||||||
|
Margin = new Thickness(0, 0, 0, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
var info = new TextBlock
|
||||||
|
{
|
||||||
|
Text = "A simple text editor built with Avalonia",
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Margin = new Thickness(0, 0, 0, 20)
|
||||||
|
};
|
||||||
|
|
||||||
|
var okButton = new Button
|
||||||
|
{
|
||||||
|
Content = "OK",
|
||||||
|
Width = 80,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center
|
||||||
|
};
|
||||||
|
okButton.Click += (s, e) => dialog.Close();
|
||||||
|
|
||||||
|
panel.Children.Add(title);
|
||||||
|
panel.Children.Add(info);
|
||||||
|
panel.Children.Add(okButton);
|
||||||
|
|
||||||
|
dialog.Content = panel;
|
||||||
|
|
||||||
|
await dialog.ShowDialog(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
NotePad/NotePad.csproj
Normal file
21
NotePad/NotePad.csproj
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.12"/>
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.12"/>
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12"/>
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12"/>
|
||||||
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
|
||||||
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
21
NotePad/Program.cs
Normal file
21
NotePad/Program.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NotePad;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
18
NotePad/app.manifest
Normal file
18
NotePad/app.manifest
Normal 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="NotePad.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>
|
||||||
106
NotePad/build-appimage.sh
Executable file
106
NotePad/build-appimage.sh
Executable file
@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build AppImage for NotePad
|
||||||
|
# Requires appimagetool: https://github.com/AppImage/AppImageKit/releases
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_NAME="NotePad"
|
||||||
|
VERSION="0.1.0"
|
||||||
|
ARCH="x86_64"
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
PUBLISH_DIR="../publish/linux-x64"
|
||||||
|
APPDIR="NotePad.AppDir"
|
||||||
|
OUTPUT_DIR="../publish/appimage"
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Building AppImage for $APP_NAME v$VERSION"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# Check if publish directory exists
|
||||||
|
if [ ! -d "$PUBLISH_DIR" ]; then
|
||||||
|
echo "❌ ERROR: $PUBLISH_DIR not found. Run 'python3 publish.py linux' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean and create AppDir structure
|
||||||
|
echo "Creating AppDir structure..."
|
||||||
|
rm -rf "$APPDIR"
|
||||||
|
mkdir -p "$APPDIR/usr/bin"
|
||||||
|
mkdir -p "$APPDIR/usr/share/applications"
|
||||||
|
mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps"
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
echo "Copying application files..."
|
||||||
|
cp -r "$PUBLISH_DIR"/* "$APPDIR/usr/bin/"
|
||||||
|
|
||||||
|
# Create desktop entry
|
||||||
|
echo "Creating desktop entry..."
|
||||||
|
cat > "$APPDIR/usr/share/applications/notepad.desktop" << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=NotePad
|
||||||
|
Comment=Simple text editor
|
||||||
|
Exec=NotePad
|
||||||
|
Icon=notepad
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;TextEditor;
|
||||||
|
Terminal=false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create a simple icon (using text as placeholder - replace with actual icon)
|
||||||
|
echo "Creating icon placeholder..."
|
||||||
|
# This creates a simple SVG icon - you should replace with an actual icon
|
||||||
|
cat > "$APPDIR/usr/share/icons/hicolor/256x256/apps/notepad.svg" << EOF
|
||||||
|
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="256" height="256" fill="#4a90e2"/>
|
||||||
|
<text x="128" y="140" font-size="120" fill="white" text-anchor="middle" font-family="sans-serif">N</text>
|
||||||
|
</svg>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create AppRun script
|
||||||
|
echo "Creating AppRun script..."
|
||||||
|
cat > "$APPDIR/AppRun" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
SELF=$(readlink -f "$0")
|
||||||
|
HERE=${SELF%/*}
|
||||||
|
export PATH="${HERE}/usr/bin:${PATH}"
|
||||||
|
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
|
||||||
|
exec "${HERE}/usr/bin/NotePad" "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$APPDIR/AppRun"
|
||||||
|
|
||||||
|
# Copy desktop file and icon to root of AppDir
|
||||||
|
cp "$APPDIR/usr/share/applications/notepad.desktop" "$APPDIR/"
|
||||||
|
cp "$APPDIR/usr/share/icons/hicolor/256x256/apps/notepad.svg" "$APPDIR/notepad.svg"
|
||||||
|
|
||||||
|
# Check for appimagetool
|
||||||
|
if ! command -v appimagetool &> /dev/null; then
|
||||||
|
echo "⚠️ WARNING: appimagetool not found."
|
||||||
|
echo " Download from: https://github.com/AppImage/AppImageKit/releases"
|
||||||
|
echo " Or run: wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||||
|
echo " chmod +x appimagetool-x86_64.AppImage"
|
||||||
|
echo ""
|
||||||
|
echo " AppDir created at: $APPDIR"
|
||||||
|
echo " You can manually run: appimagetool $APPDIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# Build AppImage
|
||||||
|
echo "Building AppImage..."
|
||||||
|
ARCH=$ARCH appimagetool "$APPDIR" "$OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
echo "Cleaning up..."
|
||||||
|
rm -rf "$APPDIR"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "✅ AppImage created successfully!"
|
||||||
|
echo "📦 Output: $OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "To run: chmod +x $OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
|
||||||
|
echo " ./$OUTPUT_DIR/$APP_NAME-$VERSION-$ARCH.AppImage"
|
||||||
73
NotePad/install.sh
Executable file
73
NotePad/install.sh
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Installation script for NotePad
|
||||||
|
# This script installs NotePad to /opt/notepad and creates a symlink in /usr/local/bin
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_NAME="NotePad"
|
||||||
|
INSTALL_DIR="/opt/notepad"
|
||||||
|
BIN_LINK="/usr/local/bin/notepad"
|
||||||
|
DESKTOP_FILE="/usr/share/applications/notepad.desktop"
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "$APP_NAME Installer"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "❌ This script must be run as root (use sudo)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
echo "Installing $APP_NAME..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
echo "→ Creating installation directory: $INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# Copy files
|
||||||
|
echo "→ Copying application files..."
|
||||||
|
cp -r "$SCRIPT_DIR"/* "$INSTALL_DIR/"
|
||||||
|
|
||||||
|
# Make executable
|
||||||
|
chmod +x "$INSTALL_DIR/NotePad"
|
||||||
|
|
||||||
|
# Create symlink
|
||||||
|
echo "→ Creating symlink: $BIN_LINK"
|
||||||
|
ln -sf "$INSTALL_DIR/NotePad" "$BIN_LINK"
|
||||||
|
|
||||||
|
# Create desktop entry
|
||||||
|
echo "→ Creating desktop entry..."
|
||||||
|
cat > "$DESKTOP_FILE" << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=NotePad
|
||||||
|
Comment=Simple text editor
|
||||||
|
Exec=notepad %F
|
||||||
|
Icon=text-editor
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;TextEditor;
|
||||||
|
Terminal=false
|
||||||
|
MimeType=text/plain;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Update desktop database if available
|
||||||
|
if command -v update-desktop-database &> /dev/null; then
|
||||||
|
echo "→ Updating desktop database..."
|
||||||
|
update-desktop-database /usr/share/applications
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "✅ Installation complete!"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
echo "You can now run $APP_NAME by typing: notepad"
|
||||||
|
echo "Or find it in your applications menu."
|
||||||
|
echo ""
|
||||||
|
echo "To uninstall, run: sudo $INSTALL_DIR/uninstall.sh"
|
||||||
|
echo ""
|
||||||
248
NotePad/publish.py
Normal file
248
NotePad/publish.py
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build script for NotePad application
|
||||||
|
Creates distributable binaries for Linux and Windows
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tarfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, description):
|
||||||
|
"""Run a shell command and print status"""
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"{description}")
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
print(f"Command: {' '.join(cmd)}\n")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=False, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"\n❌ ERROR: {description} failed!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\n✅ {description} completed successfully!")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def copy_to_publish_dir(runtime_id, platform):
|
||||||
|
"""Copy published files to centralized publish directory"""
|
||||||
|
source_dir = Path(f"bin/Release/net8.0/{runtime_id}/publish")
|
||||||
|
dest_dir = Path(f"../publish/{runtime_id}")
|
||||||
|
|
||||||
|
if dest_dir.exists():
|
||||||
|
shutil.rmtree(dest_dir)
|
||||||
|
|
||||||
|
shutil.copytree(source_dir, dest_dir)
|
||||||
|
print(f"\n📦 Copied {platform.upper()} build to: {dest_dir.absolute()}")
|
||||||
|
|
||||||
|
return dest_dir
|
||||||
|
|
||||||
|
|
||||||
|
def build_windows_installer():
|
||||||
|
"""Build Windows installer using Inno Setup"""
|
||||||
|
iscc_path = shutil.which("iscc")
|
||||||
|
if not iscc_path:
|
||||||
|
print("\n⚠️ WARNING: Inno Setup not found. Skipping installer creation.")
|
||||||
|
print(" Install from: https://jrsoftware.org/isinfo.php")
|
||||||
|
print(" Or install via winget: winget install JRSoftware.InnoSetup")
|
||||||
|
return
|
||||||
|
|
||||||
|
setup_script = Path("setup.iss")
|
||||||
|
if not setup_script.exists():
|
||||||
|
print(f"\n⚠️ WARNING: {setup_script} not found. Skipping installer creation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = [iscc_path, str(setup_script)]
|
||||||
|
try:
|
||||||
|
run_command(cmd, "Creating Windows installer")
|
||||||
|
installer_dir = Path("../publish/installers")
|
||||||
|
if installer_dir.exists():
|
||||||
|
installers = list(installer_dir.glob("*.exe"))
|
||||||
|
if installers:
|
||||||
|
print(f"\n📦 Installer created: {installers[0].absolute()}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n⚠️ WARNING: Failed to create installer: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_linux_tarball():
|
||||||
|
"""Create tar.gz archive with install script"""
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print("Creating Linux tar.gz archive")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
source_dir = Path("../publish/linux-x64")
|
||||||
|
if not source_dir.exists():
|
||||||
|
print(f"⚠️ WARNING: {source_dir} not found. Skipping tar.gz creation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create tarball directory
|
||||||
|
tarball_dir = Path("../publish/tarball")
|
||||||
|
tarball_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Create temporary directory for tarball contents
|
||||||
|
temp_dir = tarball_dir / "notepad-temp"
|
||||||
|
if temp_dir.exists():
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
temp_dir.mkdir()
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
shutil.copytree(source_dir, temp_dir / "notepad")
|
||||||
|
|
||||||
|
# Copy install scripts
|
||||||
|
for script in ["install.sh", "uninstall.sh"]:
|
||||||
|
script_path = Path(script)
|
||||||
|
if script_path.exists():
|
||||||
|
shutil.copy(script_path, temp_dir / "notepad" / script)
|
||||||
|
os.chmod(temp_dir / "notepad" / script, 0o755)
|
||||||
|
|
||||||
|
# Create tar.gz
|
||||||
|
tarball_path = tarball_dir / "notepad-0.1.0-linux-x64.tar.gz"
|
||||||
|
with tarfile.open(tarball_path, "w:gz") as tar:
|
||||||
|
tar.add(temp_dir / "notepad", arcname="notepad")
|
||||||
|
|
||||||
|
# Clean up temp directory
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
print(f"\n✅ Tar.gz archive created successfully!")
|
||||||
|
print(f"📦 Output: {tarball_path.absolute()}")
|
||||||
|
print(f"\nTo install:")
|
||||||
|
print(f" tar -xzf {tarball_path.name}")
|
||||||
|
print(f" cd notepad")
|
||||||
|
print(f" sudo ./install.sh")
|
||||||
|
|
||||||
|
|
||||||
|
def build_linux_appimage():
|
||||||
|
"""Build Linux AppImage"""
|
||||||
|
build_script = Path("build-appimage.sh")
|
||||||
|
if not build_script.exists():
|
||||||
|
print(f"\n⚠️ WARNING: {build_script} not found. Skipping AppImage creation.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["bash", str(build_script)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
print(result.stdout)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(result.stderr)
|
||||||
|
print("\n⚠️ WARNING: AppImage creation failed.")
|
||||||
|
else:
|
||||||
|
print("\n✅ AppImage created successfully!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n⚠️ WARNING: Failed to create AppImage: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def publish_platform(platform, linux_package='both'):
|
||||||
|
"""Publish for a specific platform"""
|
||||||
|
runtime_id = f"{platform}-x64"
|
||||||
|
|
||||||
|
# Find dotnet executable
|
||||||
|
dotnet_path = shutil.which("dotnet")
|
||||||
|
if not dotnet_path:
|
||||||
|
# Try common locations
|
||||||
|
home = Path.home()
|
||||||
|
common_paths = [
|
||||||
|
home / ".dotnet" / "dotnet",
|
||||||
|
Path("/usr/bin/dotnet"),
|
||||||
|
Path("/usr/local/bin/dotnet")
|
||||||
|
]
|
||||||
|
for path in common_paths:
|
||||||
|
if path.exists():
|
||||||
|
dotnet_path = str(path)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not dotnet_path:
|
||||||
|
print("❌ ERROR: dotnet not found. Please install .NET SDK.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
dotnet_path, "publish",
|
||||||
|
"-c", "Release",
|
||||||
|
"-r", runtime_id,
|
||||||
|
"--self-contained", "true",
|
||||||
|
"-p:PublishSingleFile=true",
|
||||||
|
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
||||||
|
"-p:PublishTrimmed=false" # Avalonia doesn't work well with trimming
|
||||||
|
]
|
||||||
|
|
||||||
|
run_command(cmd, f"Building for {platform.upper()}")
|
||||||
|
|
||||||
|
# Copy to centralized publish directory
|
||||||
|
dest_dir = copy_to_publish_dir(runtime_id, platform)
|
||||||
|
|
||||||
|
# Show the output files
|
||||||
|
if dest_dir.exists():
|
||||||
|
files = list(dest_dir.glob("*"))
|
||||||
|
for file in files:
|
||||||
|
size_mb = file.stat().st_size / (1024 * 1024)
|
||||||
|
print(f" - {file.name} ({size_mb:.2f} MB)")
|
||||||
|
|
||||||
|
# Build installer for Windows
|
||||||
|
if platform == 'win':
|
||||||
|
build_windows_installer()
|
||||||
|
|
||||||
|
# Build Linux packages
|
||||||
|
if platform == 'linux':
|
||||||
|
if linux_package in ['appimage', 'both']:
|
||||||
|
build_linux_appimage()
|
||||||
|
if linux_package in ['tarball', 'both']:
|
||||||
|
build_linux_tarball()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point"""
|
||||||
|
# Parse arguments
|
||||||
|
target = 'both'
|
||||||
|
linux_package = 'both'
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
target = sys.argv[1].lower()
|
||||||
|
if target not in ['linux', 'windows', 'both', 'all']:
|
||||||
|
print("Usage: python3 publish.py [linux|windows|both] [appimage|tarball|both]")
|
||||||
|
print(" Platform: linux, windows, or both (default: both)")
|
||||||
|
print(" Linux package: appimage, tarball, or both (default: both)")
|
||||||
|
print("")
|
||||||
|
print("Examples:")
|
||||||
|
print(" python3 publish.py # Build everything")
|
||||||
|
print(" python3 publish.py linux # Build Linux with both packages")
|
||||||
|
print(" python3 publish.py linux appimage # Build Linux AppImage only")
|
||||||
|
print(" python3 publish.py linux tarball # Build Linux tarball only")
|
||||||
|
print(" python3 publish.py windows # Build Windows installer")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
linux_package = sys.argv[2].lower()
|
||||||
|
if linux_package not in ['appimage', 'tarball', 'both']:
|
||||||
|
print("Error: Linux package must be 'appimage', 'tarball', or 'both'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Change to script directory
|
||||||
|
script_dir = Path(__file__).parent
|
||||||
|
os.chdir(script_dir)
|
||||||
|
|
||||||
|
print(f"🚀 NotePad Build Script")
|
||||||
|
print(f"Target platform(s): {target.upper()}")
|
||||||
|
if target in ['linux', 'both', 'all']:
|
||||||
|
print(f"Linux package(s): {linux_package.upper()}")
|
||||||
|
|
||||||
|
# Build for requested platform(s)
|
||||||
|
if target in ['linux', 'both', 'all']:
|
||||||
|
publish_platform('linux', linux_package)
|
||||||
|
|
||||||
|
if target in ['windows', 'both', 'all']:
|
||||||
|
publish_platform('win')
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print("🎉 All builds completed successfully!")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
54
NotePad/setup.iss
Normal file
54
NotePad/setup.iss
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
; Inno Setup Script for NotePad
|
||||||
|
; Download Inno Setup from: https://jrsoftware.org/isinfo.php
|
||||||
|
|
||||||
|
#define MyAppName "NotePad"
|
||||||
|
#define MyAppVersion "0.1.0"
|
||||||
|
#define MyAppPublisher "Greg Gauthier"
|
||||||
|
#define MyAppExeName "NotePad.exe"
|
||||||
|
#define MyAppAssocName "Text File"
|
||||||
|
#define MyAppAssocExt ".txt"
|
||||||
|
#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||||
|
AppId={{A8F9C3E1-5B2D-4A7C-9E8F-1D3C5B7A9E2F}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
DefaultDirName={autopf}\{#MyAppName}
|
||||||
|
DefaultGroupName={#MyAppName}
|
||||||
|
AllowNoIcons=yes
|
||||||
|
LicenseFile=
|
||||||
|
OutputDir=..\publish\installers
|
||||||
|
OutputBaseFilename=NotePad-Setup-{#MyAppVersion}
|
||||||
|
Compression=lzma
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
ArchitecturesAllowed=x64
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||||
|
Name: "associate"; Description: "Associate {#MyAppAssocExt} files with {#MyAppName}"; GroupDescription: "File associations:"; Flags: unchecked
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "..\publish\win-x64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
|
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
|
||||||
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associate
|
||||||
|
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey; Tasks: associate
|
||||||
|
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"; Tasks: associate
|
||||||
|
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Tasks: associate
|
||||||
|
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".txt"; ValueData: ""; Tasks: associate
|
||||||
53
NotePad/uninstall.sh
Executable file
53
NotePad/uninstall.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Uninstallation script for NotePad
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_NAME="NotePad"
|
||||||
|
INSTALL_DIR="/opt/notepad"
|
||||||
|
BIN_LINK="/usr/local/bin/notepad"
|
||||||
|
DESKTOP_FILE="/usr/share/applications/notepad.desktop"
|
||||||
|
|
||||||
|
echo "============================================================"
|
||||||
|
echo "$APP_NAME Uninstaller"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "❌ This script must be run as root (use sudo)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Uninstalling $APP_NAME..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Remove symlink
|
||||||
|
if [ -L "$BIN_LINK" ]; then
|
||||||
|
echo "→ Removing symlink: $BIN_LINK"
|
||||||
|
rm -f "$BIN_LINK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove desktop entry
|
||||||
|
if [ -f "$DESKTOP_FILE" ]; then
|
||||||
|
echo "→ Removing desktop entry: $DESKTOP_FILE"
|
||||||
|
rm -f "$DESKTOP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update desktop database if available
|
||||||
|
if command -v update-desktop-database &> /dev/null; then
|
||||||
|
echo "→ Updating desktop database..."
|
||||||
|
update-desktop-database /usr/share/applications
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove installation directory
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "→ Removing installation directory: $INSTALL_DIR"
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "✅ Uninstallation complete!"
|
||||||
|
echo "============================================================"
|
||||||
|
echo ""
|
||||||
85
publish.py
Executable file
85
publish.py
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build script for NotePad application
|
||||||
|
Creates distributable binaries for Linux and Windows
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def run_command(cmd, description):
|
||||||
|
"""Run a shell command and print status"""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"{description}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"Command: {' '.join(cmd)}\n")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=False, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"\n❌ ERROR: {description} failed!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\n✅ {description} completed successfully!")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def publish_platform(platform):
|
||||||
|
"""Publish for a specific platform"""
|
||||||
|
runtime_id = f"{platform}-x64"
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"dotnet", "publish",
|
||||||
|
"-c", "Release",
|
||||||
|
"-r", runtime_id,
|
||||||
|
"--self-contained", "true",
|
||||||
|
"-p:PublishSingleFile=true",
|
||||||
|
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
||||||
|
"-p:PublishTrimmed=false" # Avalonia doesn't work well with trimming
|
||||||
|
]
|
||||||
|
|
||||||
|
run_command(cmd, f"Building for {platform.upper()}")
|
||||||
|
|
||||||
|
# Show the output location
|
||||||
|
output_dir = Path(f"bin/Release/net8.0/{runtime_id}/publish")
|
||||||
|
if output_dir.exists():
|
||||||
|
files = list(output_dir.glob("*"))
|
||||||
|
print(f"\n📦 Output files in: {output_dir.absolute()}")
|
||||||
|
for file in files:
|
||||||
|
size_mb = file.stat().st_size / (1024 * 1024)
|
||||||
|
print(f" - {file.name} ({size_mb:.2f} MB)")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point"""
|
||||||
|
# Parse arguments
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
target = sys.argv[1].lower()
|
||||||
|
if target not in ['linux', 'windows', 'both', 'all']:
|
||||||
|
print("Usage: python3 publish.py [linux|windows|both]")
|
||||||
|
print(" Default: both")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
target = 'both'
|
||||||
|
|
||||||
|
# Change to script directory
|
||||||
|
script_dir = Path(__file__).parent
|
||||||
|
os.chdir(script_dir)
|
||||||
|
|
||||||
|
print(f"🚀 NotePad Build Script")
|
||||||
|
print(f"Target platform(s): {target.upper()}")
|
||||||
|
|
||||||
|
# Build for requested platform(s)
|
||||||
|
if target in ['linux', 'both', 'all']:
|
||||||
|
publish_platform('linux')
|
||||||
|
|
||||||
|
if target in ['windows', 'both', 'all']:
|
||||||
|
publish_platform('win')
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("🎉 All builds completed successfully!")
|
||||||
|
print(f"{'='*60}\n")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "notepad"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Python dependencies for NotePad project"
|
||||||
|
authors = ["Greg Gauthier <gmgauthier@protonmail.com>"]
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.12"
|
||||||
Loading…
Reference in New Issue
Block a user