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