Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/qodana_code_quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Qodana
on:
workflow_dispatch:
pull_request:
push:
branches: # Specify your branches here
- main # The 'main' branch
- 'releases/*' # The release branches

jobs:
qodana:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
fetch-depth: 0 # a full history is required for pull request analysis
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2025.1
with:
pr-mode: false
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1557167003 }}
QODANA_ENDPOINT: 'https://qodana.cloud'
2 changes: 1 addition & 1 deletion Meio.app/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Meio.app.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

<Application.Styles>
<FluentTheme />
Expand Down
37 changes: 36 additions & 1 deletion Meio.app/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using PrettyLogging.Console;

namespace Meio.app;

public partial class App : Application
public class App : Application
{
private static ILoggerFactory? LoggerFactory { get; set; }

public static ILogger<App>? Logger { get; private set; }

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
// Create and configure PrettyLogger
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
{
builder.ClearProviders();
builder.AddPrettyConsole(opt =>
{
opt.ShowLogLevel = true;
opt.ShowEventId = false;
opt.ShowManagedThreadId = false;
opt.SingleLine = true;
opt.IncludeScopes = true;
opt.ShowTimestamp = true;
opt.LogLevelCase = LogLevelCase.Upper;
opt.CategoryMode = LoggerCategoryMode.Short;
opt.ColorBehavior = LoggerColorBehavior.Enabled;
opt.UseUtcTimestamp = false;
});

#if DEBUG
builder.SetMinimumLevel(LogLevel.Trace);
#else
builder.SetMinimumLevel(LogLevel.Information);
#endif
});

Logger = LoggerFactory.CreateLogger<App>();
Logger.LogInformation("Meio Application started.");

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
Expand Down
24 changes: 21 additions & 3 deletions Meio.app/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Meio.app.MainWindow"
Title="Meio.app">
Welcome to Avalonia!
</Window>
Title="Meio">
<Grid ColumnDefinitions="Auto,Auto,*" HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center">
<Slider Name="VolumeSlider" Orientation="Vertical" Height="100" Maximum="30"
ValueChanged="VolumeSlider_OnValueChanged" />
<TextBlock Name="VolumeText" HorizontalAlignment="Center" Text="Volume" />
</StackPanel>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Width="350">
<TextBlock HorizontalAlignment="Center">Currently reading</TextBlock>
<Image Name="AlbumArtImage" Width="200" Height="200" />
<Border Margin="5" CornerRadius="10" Background="LightCoral">
<TextBlock Name="CurrentMusicText" Margin="5" HorizontalAlignment="Center" FontSize="24" Text="Nothing" />
</Border>
<Button Name="UploadButton" HorizontalAlignment="Center" Click="UploadButton_OnClick" Content="Upload song" />
<Button Name="PlayButton" HorizontalAlignment="Center" Click="Button_OnClick" Content="Play" />

</StackPanel>

</Grid>

</Window>
83 changes: 83 additions & 0 deletions Meio.app/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,94 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Meio.app.Services;
using Microsoft.Extensions.Logging;

namespace Meio.app;

public partial class MainWindow : Window
{
private readonly AudioPlayerService _audioPlayerService;
private string? _author = "unknown"; // this is a cheap fix, but it is because this Window is just for testing, so it's alright.
private bool _debounce;
private string? _filePath;
private CancellationTokenSource? _volumeDebounceToken;

// DIS WHOLE CODE IS HORIRBLE AAAAAAAAAA

public MainWindow()
{
InitializeComponent();
_audioPlayerService = new AudioPlayerService();
}

private void Button_OnClick(object? sender, RoutedEventArgs e)
{
if (!_debounce)
{
_debounce = true;

if (_filePath == null) return;

var metadata = AudioMetadataService.LoadMetadata(_filePath);
if (metadata == null) return;

_audioPlayerService.Play(_filePath);

PlayButton.Content = "Stop";
_author = metadata.Artists is { Length: 0 } ? "unknown" : metadata.Artists?[0];

CurrentMusicText.Text = $"{metadata.Title} - {_author}";
AlbumArtImage.Source = metadata.AlbumArt != null ? ImageHelper.LoadBitmapFromBytes(metadata.AlbumArt) : null;
}
else
{
PlayButton.Content = "Play";
CurrentMusicText.Text = "Nothing";
AlbumArtImage.Source = null;

_audioPlayerService.Stop();
_debounce = false;
}
}

private void VolumeSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
_volumeDebounceToken?.Cancel();
_volumeDebounceToken = new CancellationTokenSource();

var token = _volumeDebounceToken.Token;
var newVolume = (int)e.NewValue;
VolumeText.Text = $"{newVolume * 100 / 30}%";

Task.Run(async () =>
{
try
{
await Task.Delay(100, token); // 100ms debounce
if (!token.IsCancellationRequested) _audioPlayerService.ChangeVolume(newVolume);
}
catch (TaskCanceledException)
{
// Ignore
}
},
token);
}

private async void UploadButton_OnClick(object? sender, RoutedEventArgs e)
{
try
{
var url = await FileHelper.GetFilePathDialog(GetTopLevel(this));
if (url != null) _filePath = Uri.UnescapeDataString(url.AbsolutePath);
}
catch (Exception exception)
{
App.Logger!.LogError("There was an error trying to parse the URI unescape data. {exception}", exception.Message);
}
}
}
43 changes: 24 additions & 19 deletions Meio.app/Meio.app.csproj
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.5" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.5">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.5"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.5"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.5"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.5"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.5">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="LibVLCSharp" Version="3.9.4"/>
<PackageReference Include="PrettyLogging.Console" Version="1.0.3"/>
<PackageReference Include="TagLibSharp" Version="2.3.0"/>
<PackageReference Include="VideoLAN.LibVLC.Mac" Version="3.1.3.1"/>
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21"/>
</ItemGroup>
</Project>
19 changes: 12 additions & 7 deletions Meio.app/Program.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
using Avalonia;
using System;
using System;
using Avalonia;

namespace Meio.app;

class Program
internal 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);
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>()
{
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
}
}
64 changes: 64 additions & 0 deletions Meio.app/Services/AudioMetadataService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.Extensions.Logging;
using TagLib;

namespace Meio.app.Services;

public class MetadataInfo
{
public string? Title { get; set; }

public string[]? Artists { get; set; }

public string? Album { get; set; }

public uint Year { get; set; }

public string[]? Genres { get; set; }

public byte[]? AlbumArt { get; set; }
}

public static class AudioMetadataService
{
/// <summary>
/// Loads the metadata info of a given audio file.
/// </summary>
/// <param name="filePath">Path of the audio file to get the metadata from.</param>
/// <returns>The metadata info of the audio file.</returns>
public static MetadataInfo? LoadMetadata(string filePath)
{
try
{
var file = File.Create(filePath);
var tag = file.Tag;

App.Logger?.LogDebug("Asked for the metadata of {filePath}.", filePath);
App.Logger?.LogTrace("Title: {tagTitle}", tag.Title);
App.Logger?.LogTrace("Artists: {tagArtists}", string.Join(", ", tag.AlbumArtists));
App.Logger?.LogTrace("Album: {tagAlbum}", tag.Album);
App.Logger?.LogTrace("Year: {tagYear}", tag.Year);
App.Logger?.LogTrace("Genre: {tagGenres}", string.Join(", ", tag.Genres));
App.Logger?.LogTrace(tag.Pictures.Length > 0 ? "Got an album art." : "No album art was found.");

return new MetadataInfo
{
Title = tag.Title,
Artists = tag.AlbumArtists,
Album = tag.Album,
Genres = tag.Genres,
Year = tag.Year,
AlbumArt = tag.Pictures.Length > 0 ? tag.Pictures[0].Data.Data : null
};
}
catch (CorruptFileException corruptFileException)
{
App.Logger?.LogError(corruptFileException, "Failed to load metadata. File is corrupted.");
return null;
}
catch (UnsupportedFormatException unsupportedFormatException)
{
App.Logger?.LogError(unsupportedFormatException, "Failed to load metadata. File format is unsupported.");
return null;
}
}
}
Loading
Loading