í·‚️ Separate concerns and improve views (#3)

* Add bottom bar façade
* Apply good practices re. separation of concerns
* Generally clean things up

Co-authored-by: Alexis DRAI <alexis.drai@etu.uca.fr>
Reviewed-on: #3
Alexis Drai 2 years ago committed by Alexis DRAI
parent 49b422d3d0
commit 4753f987bf

@ -49,14 +49,31 @@
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Images\icon_albums.png" />
<None Remove="Resources\Images\icon_artists.png" />
<None Remove="Resources\Images\icon_genres.png" />
<None Remove="Resources\Images\icon_playlists.png" />
<None Remove="Resources\Images\icon_songs.png" />
<None Remove="Resources\Images\macroblank_1.png" />
<None Remove="Resources\Images\macroblank_2.png" />
<None Remove="Resources\Images\macroblank_3.png" />
<None Remove="Resources\Images\album_macroblank_1.png" />
<None Remove="Resources\Images\album_macroblank_2.png" />
<None Remove="Resources\Images\album_macroblank_3.png" />
<None Remove="Resources\Images\icon_bottom_browse_gray.png" />
<None Remove="Resources\Images\icon_bottom_browse_red.png" />
<None Remove="Resources\Images\icon_bottom_library_gray.png" />
<None Remove="Resources\Images\icon_bottom_library_red.png" />
<None Remove="Resources\Images\icon_bottom_play_gray.png" />
<None Remove="Resources\Images\icon_bottom_play_red.png" />
<None Remove="Resources\Images\icon_bottom_radio_gray.png" />
<None Remove="Resources\Images\icon_bottom_radio_red.png" />
<None Remove="Resources\Images\icon_bottom_search_gray.png" />
<None Remove="Resources\Images\icon_bottom_search_red.png" />
<None Remove="Resources\Images\icon_categories_albums.png" />
<None Remove="Resources\Images\icon_categories_artists.png" />
<None Remove="Resources\Images\icon_categories_genres.png" />
<None Remove="Resources\Images\icon_categories_playlists.png" />
<None Remove="Resources\Images\icon_categories_songs.png" />
<None Remove="Resources\Images\icon_default_song.png" />
<None Remove="Resources\Images\icon_next.png" />
<None Remove="Resources\Images\icon_next_dark.png" />
<None Remove="Resources\Images\icon_play.png" />
<None Remove="Resources\Images\icon_play_dark.png" />
<None Remove="Resources\Images\icon_wide_button_play.png" />
<None Remove="Resources\Images\icon_wide_button_shuffle.png" />
</ItemGroup>
<ItemGroup>
@ -68,6 +85,9 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Controls\BottomBar.xaml.cs">
<DependentUpon>BottomBar.xaml</DependentUpon>
</Compile>
<Compile Update="Resources\Strings\Strings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -86,6 +106,18 @@
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Controls\BottomBar.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Controls\IconLabelButton.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Controls\IconLabelButtonWide.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Resources\Styles\Values.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\AlbumPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
@ -97,8 +129,4 @@
</MauiXaml>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\Icons\" />
</ItemGroup>
</Project>

@ -8,6 +8,7 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles/Values.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

@ -8,14 +8,4 @@ public partial class App : Application
MainPage = new AppShell();
}
protected override void OnStart()
{
base.OnStart();
// Uncomment to set the culture to French
// CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
// CultureInfo.CurrentUICulture = new CultureInfo("fr-FR");
}
}

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:strings="clr-namespace:AMC.View.Resources.Strings"
xmlns:ctl="clr-namespace:AMC.View.Controls"
x:Class="AMC.View.Controls.BottomBar">
<StackLayout Orientation="Vertical"
BackgroundColor="{AppThemeBinding Light={StaticResource Gray100}, Dark={StaticResource Gray900}}">
<StackLayout Orientation="Horizontal"
HeightRequest="{StaticResource SpaceXL}">
<Frame Margin="{StaticResource HSpaceLittleVSpaceVeryLittle}"
BackgroundColor="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray750}}"
BorderColor="Transparent"
HorizontalOptions="Start"
VerticalOptions="Center">
<Image Source="icon_default_song.png"
Aspect="AspectFill"/>
</Frame>
<Label Text="{x:Static strings:Strings.DefaultPlayingSongLabel}"
HorizontalOptions="StartAndExpand"
VerticalOptions="Center"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<ImageButton Source="{AppThemeBinding Light='icon_play.png', Dark='icon_play_dark.png'}"
Margin="{StaticResource SpaceS}"
HorizontalOptions="End"
VerticalOptions="Center"
HeightRequest="{StaticResource SpaceML}"
WidthRequest="{StaticResource SpaceML}" />
<ImageButton Source="{AppThemeBinding Light='icon_next.png', Dark='icon_next_dark.png'}"
Margin="{StaticResource SpaceS}"
HorizontalOptions="End"
VerticalOptions="Center"
HeightRequest="{StaticResource SpaceML}"
WidthRequest="{StaticResource SpaceML}" />
</StackLayout>
<BoxView Style="{StaticResource BottomBarGraySeparator}" />
<FlexLayout Direction="Row"
HeightRequest="{StaticResource SpaceXL}"
JustifyContent="SpaceAround"
AlignItems="Center">
<ctl:IconLabelButton ButtonSource="icon_bottom_play_gray.png"
ButtonLabelText="{x:Static strings:Strings.ListenNowTitle}"
LabelTextColor="{StaticResource Gray}"/>
<ctl:IconLabelButton ButtonSource="icon_bottom_browse_gray.png"
ButtonLabelText="{x:Static strings:Strings.BrowseTitle}"
LabelTextColor="{StaticResource Gray}"/>
<ctl:IconLabelButton ButtonSource="icon_bottom_radio_gray.png"
ButtonLabelText="{x:Static strings:Strings.RadioTitle}"
LabelTextColor="{StaticResource Gray}"/>
<ctl:IconLabelButton ButtonSource="icon_bottom_library_red.png"
ButtonLabelText="{x:Static strings:Strings.LibraryTitle}"
LabelTextColor="{StaticResource Secondary}"/>
<ctl:IconLabelButton ButtonSource="icon_bottom_search_gray.png"
ButtonLabelText="{x:Static strings:Strings.SearchTitle}"
LabelTextColor="{StaticResource Gray}"/>
</FlexLayout>
</StackLayout>
</ContentView>

@ -0,0 +1,10 @@
namespace AMC.View.Controls
{
public partial class BottomBar : ContentView
{
public BottomBar()
{
InitializeComponent();
}
}
}

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AMC.View.Controls.IconLabelButton">
<StackLayout Orientation="Vertical"
Margin="{StaticResource SpaceXS}">
<ImageButton x:Name="Button"
HeightRequest="{StaticResource SpaceML}"
WidthRequest="{StaticResource SpaceML}" />
<Label x:Name="ButtonLabel"
FontSize="{StaticResource TinyFontSize}"
Margin="{StaticResource SpaceXXS}"/>
</StackLayout>
</ContentView>

@ -0,0 +1,58 @@
namespace AMC.View.Controls
{
public partial class IconLabelButton : ContentView
{
public static readonly BindableProperty ButtonSourceProperty = BindableProperty.Create(
nameof(ButtonSource),
typeof(string),
typeof(IconLabelButton),
default(string),
propertyChanged: (bindable, oldValue, newValue) =>
{
((IconLabelButton)bindable).Button.Source = (string)newValue;
});
public static readonly BindableProperty ButtonLabelTextProperty = BindableProperty.Create(
nameof(ButtonLabelText),
typeof(string),
typeof(IconLabelButton),
default(string),
propertyChanged: (bindable, oldValue, newValue) =>
{
((IconLabelButton)bindable).ButtonLabel.Text = (string)newValue;
});
public static readonly BindableProperty LabelTextColorProperty = BindableProperty.Create(
nameof(LabelTextColor),
typeof(Color),
typeof(IconLabelButton),
default(Color),
propertyChanged: (bindable, oldValue, newValue) =>
{
((IconLabelButton)bindable).ButtonLabel.TextColor = (Color)newValue;
});
public IconLabelButton()
{
InitializeComponent();
}
public string ButtonSource
{
get => (string)GetValue(ButtonSourceProperty);
set => SetValue(ButtonSourceProperty, value);
}
public string ButtonLabelText
{
get => (string)GetValue(ButtonLabelTextProperty);
set => SetValue(ButtonLabelTextProperty, value);
}
public Color LabelTextColor
{
get => (Color)GetValue(LabelTextColorProperty);
set => SetValue(LabelTextColorProperty, value);
}
}
}

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AMC.View.Controls.IconLabelButtonWide">
<Frame HeightRequest="{StaticResource SpaceLXL}"
WidthRequest="{StaticResource SpaceXXXL}"
HorizontalOptions="Center"
BackgroundColor="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray750}}"
Margin="{StaticResource SpaceXS}"
Padding="{StaticResource SpaceXS}"
CornerRadius="{StaticResource SlightlyRoundedCorners}">
<StackLayout Orientation="Horizontal"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"
Spacing="{StaticResource SpaceXXS}">
<ImageButton x:Name="Button"
HeightRequest="{StaticResource SpaceML}"
WidthRequest="{StaticResource SpaceML}" />
<Label x:Name="ButtonLabel"
FontSize="{StaticResource SubSubtitleFontSize}"
HorizontalOptions="Center"
VerticalOptions="Center"
TextColor="{StaticResource Secondary}"/>
</StackLayout>
</Frame>
</ContentView>

@ -0,0 +1,43 @@
namespace AMC.View.Controls
{
public partial class IconLabelButtonWide : ContentView
{
public static readonly BindableProperty ButtonSourceProperty = BindableProperty.Create(
nameof(ButtonSource),
typeof(string),
typeof(IconLabelButtonWide),
default(string),
propertyChanged: (bindable, oldValue, newValue) =>
{
((IconLabelButtonWide)bindable).Button.Source = (string)newValue;
});
public static readonly BindableProperty ButtonLabelTextProperty = BindableProperty.Create(
nameof(ButtonLabelText),
typeof(string),
typeof(IconLabelButtonWide),
default(string),
propertyChanged: (bindable, oldValue, newValue) =>
{
((IconLabelButtonWide)bindable).ButtonLabel.Text = (string)newValue;
});
public IconLabelButtonWide()
{
InitializeComponent();
}
public string ButtonSource
{
get => (string)GetValue(ButtonSourceProperty);
set => SetValue(ButtonSourceProperty, value);
}
public string ButtonLabelText
{
get => (string)GetValue(ButtonLabelTextProperty);
set => SetValue(ButtonLabelTextProperty, value);
}
}
}

@ -4,21 +4,21 @@
x:Class="AMC.View.Controls.LibraryCategoryItem">
<StackLayout Orientation="Horizontal"
Padding="8"
Padding="{StaticResource SpaceXS}"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<Image x:Name="IconImage"
WidthRequest="24"
HeightRequest="24"
Margin="0, 0, 8, 0"
WidthRequest="{StaticResource SpaceM}"
HeightRequest="{StaticResource SpaceM}"
Margin="{StaticResource RightSpaceLittle}"
HorizontalOptions="Start" />
<Label x:Name="CategoryLabel"
FontSize="16"
FontSize="{StaticResource SubSubtitleFontSize}"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
HorizontalOptions="StartAndExpand" />
<Image Source="icon_chevron_right.png"
WidthRequest="24"
HeightRequest="24"
Margin="8, 0, 0, 0"
WidthRequest="{StaticResource SpaceM}"
HeightRequest="{StaticResource SpaceM}"
Margin="{StaticResource LeftSpaceLittle}"
HorizontalOptions="End" />
</StackLayout>

@ -1,5 +1,4 @@

namespace AMC.View.Controls
namespace AMC.View.Controls
{
public partial class LibraryCategoryItem : ContentView
{
@ -8,14 +7,16 @@ namespace AMC.View.Controls
returnType: typeof(string),
declaringType: typeof(LibraryCategoryItem),
defaultValue: "",
propertyChanged: CategoryTextChanged);
propertyChanged: CategoryTextChanged
);
public static readonly BindableProperty IconSourceProperty = BindableProperty.Create(
propertyName: nameof(IconSource),
returnType: typeof(ImageSource),
declaringType: typeof(LibraryCategoryItem),
defaultValue: null,
propertyChanged: IconSourceChanged);
propertyChanged: IconSourceChanged
);
public LibraryCategoryItem()
{

@ -1,22 +0,0 @@
using System.Globalization;
namespace AMC.View.Converters
{
public class AlbumDetailsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var (genre, year) = (ValueTuple<string, int>)value;
return string.Format(
"{0} · {1}",
genre,
year
);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

@ -1,22 +0,0 @@
using System.Globalization;
namespace AMC.View.Converters
{
public class CopyrightInfoConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var (copyrightYear, producerBlurb) = (ValueTuple<int, string>)value;
return string.Format(
"℗ {0} {1}",
copyrightYear,
producerBlurb
);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

@ -78,6 +78,24 @@ namespace AMC.View.Resources.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to Browse.
/// </summary>
public static string BrowseTitle {
get {
return ResourceManager.GetString("BrowseTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Not Playing.
/// </summary>
public static string DefaultPlayingSongLabel {
get {
return ResourceManager.GetString("DefaultPlayingSongLabel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Genres.
/// </summary>
@ -87,6 +105,15 @@ namespace AMC.View.Resources.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to ···.
/// </summary>
public static string HThreeDotsMenu {
get {
return ResourceManager.GetString("HThreeDotsMenu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Library.
/// </summary>
@ -96,6 +123,15 @@ namespace AMC.View.Resources.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to Listen Now.
/// </summary>
public static string ListenNowTitle {
get {
return ResourceManager.GetString("ListenNowTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to minutes.
/// </summary>
@ -132,6 +168,15 @@ namespace AMC.View.Resources.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to Radio.
/// </summary>
public static string RadioTitle {
get {
return ResourceManager.GetString("RadioTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Recently Added.
/// </summary>
@ -141,6 +186,15 @@ namespace AMC.View.Resources.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to Search.
/// </summary>
public static string SearchTitle {
get {
return ResourceManager.GetString("SearchTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shuffle.
/// </summary>

@ -156,4 +156,19 @@
<data name="GenresCategory" xml:space="preserve">
<value>Genres</value>
</data>
<data name="DefaultPlayingSongLabel" xml:space="preserve">
<value>Rien en lecture</value>
</data>
<data name="ListenNowTitle" xml:space="preserve">
<value>Ecouter</value>
</data>
<data name="RadioTitle" xml:space="preserve">
<value>Radio</value>
</data>
<data name="BrowseTitle" xml:space="preserve">
<value>Éxplorer</value>
</data>
<data name="SearchTitle" xml:space="preserve">
<value>Recherche</value>
</data>
</root>

@ -156,4 +156,23 @@
<data name="GenresCategory" xml:space="preserve">
<value>Genres</value>
</data>
<data name="HThreeDotsMenu" xml:space="preserve">
<value>···</value>
<comment>@Invariant</comment>
</data>
<data name="DefaultPlayingSongLabel" xml:space="preserve">
<value>Not Playing</value>
</data>
<data name="ListenNowTitle" xml:space="preserve">
<value>Listen Now</value>
</data>
<data name="RadioTitle" xml:space="preserve">
<value>Radio</value>
</data>
<data name="BrowseTitle" xml:space="preserve">
<value>Browse</value>
</data>
<data name="SearchTitle" xml:space="preserve">
<value>Search</value>
</data>
</root>

@ -27,6 +27,7 @@
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray750">#303030</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>

@ -6,9 +6,22 @@
<Style TargetType="BoxView" x:Key="GraySeparator">
<Setter Property="HeightRequest" Value="1"/>
<Setter Property="Color" Value="{StaticResource Gray200}"/>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray900}}"/>
<Setter Property="HorizontalOptions" Value="FillAndExpand"/>
<Setter Property="Margin" Value="32, 0, 8, 0"/>
</Style>
<Style TargetType="BoxView" x:Key="HeadGraySeparator">
<Setter Property="HeightRequest" Value="1"/>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray900}}"/>
<Setter Property="HorizontalOptions" Value="FillAndExpand"/>
<Setter Property="Margin" Value="8, 8, 8, 8"/>
</Style>
<Style TargetType="BoxView" x:Key="BottomBarGraySeparator">
<Setter Property="HeightRequest" Value="1"/>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray600}}"/>
<Setter Property="HorizontalOptions" Value="FillAndExpand"/>
<Setter Property="Margin" Value="32,0,8,0"/>
</Style>
<Style TargetType="Label" x:Key="FooterLabel">
@ -38,7 +51,7 @@
<Style TargetType="Button">
<Setter Property="TextColor" Value="{StaticResource Secondary}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray750}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="CornerRadius" Value="8"/>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<x:Double x:Key="SpaceXXS">4</x:Double>
<x:Double x:Key="SpaceXS">8</x:Double>
<x:Double x:Key="SpaceS">12</x:Double>
<x:Double x:Key="SpaceM">16</x:Double>
<x:Double x:Key="SpaceML">24</x:Double>
<x:Double x:Key="SpaceL">32</x:Double>
<x:Double x:Key="SpaceLXL">48</x:Double>
<x:Double x:Key="SpaceXL">64</x:Double>
<x:Double x:Key="SpaceXXL">128</x:Double>
<x:Double x:Key="SpaceXXXL">144</x:Double>
<x:Double x:Key="AlbumPageCoverHeight">320</x:Double>
<Thickness x:Key="BottomBarSpace">0, 0, 0, 128</Thickness>
<Thickness x:Key="TopSpaceSome">0, 24, 0, 0</Thickness>
<Thickness x:Key="TopBottomSpaceLittle">0, 8, 0, 8</Thickness>
<Thickness x:Key="RightSpaceLittle">0, 0, 8, 0</Thickness>
<Thickness x:Key="LeftSpaceLittle">8, 0, 0, 0</Thickness>
<Thickness x:Key="BottomSpaceLittle">0, 0, 0, 8</Thickness>
<Thickness x:Key="LeftRightSpaceLarge">32, 0, 32, 0</Thickness>
<Thickness x:Key="WideButtonLeft">16, 16, 8, 16</Thickness>
<Thickness x:Key="WideButtonRight">8, 16, 16, 16</Thickness>
<Thickness x:Key="HSpaceLittleVSpaceVeryLittle">8, 4, 8, 4</Thickness>
<CornerRadius x:Key="SlightlyRoundedCorners">8</CornerRadius>
<x:Double x:Key="TitleFontSize">32</x:Double>
<x:Double x:Key="SubtitleFontSize">24</x:Double>
<x:Double x:Key="SubSubtitleFontSize">16</x:Double>
<x:Double x:Key="DetailsFontSize">12</x:Double>
<x:Double x:Key="TinyFontSize">8</x:Double>
</ResourceDictionary>

@ -4,143 +4,141 @@
xmlns:local="clr-namespace:AMC.View"
xmlns:strings="clr-namespace:AMC.View.Resources.Strings"
xmlns:conv="clr-namespace:AMC.View.Converters"
xmlns:ctl="clr-namespace:AMC.View.Controls"
xmlns:vm="clr-namespace:AMC.ViewModel.ViewModels;assembly=AMC.ViewModel"
x:Class="AMC.View.Views.AlbumPage"
x:DataType="vm:AlbumViewModel">
<ContentPage.Resources>
<ResourceDictionary>
<conv:SongsInfoConverter x:Key="SongsInfo" />
<conv:CopyrightInfoConverter x:Key="CopyrightInfo" />
<conv:AlbumDetailsConverter x:Key="AlbumDetails" />
</ResourceDictionary>
</ContentPage.Resources>
<ScrollView
Padding="0, 0, 0, 128"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<Grid Margin="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="300"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollView Grid.Row="0"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<Grid Margin="{StaticResource SpaceXS}">
<Grid.RowDefinitions>
<RowDefinition Height="{StaticResource AlbumPageCoverHeight}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border
Grid.Row="0"
Margin="32,0,32,0">
Margin="{StaticResource LeftRightSpaceLarge}">
<Border.StrokeShape>
<RoundRectangle CornerRadius="8,8,8,8" />
<RoundRectangle CornerRadius="{StaticResource SlightlyRoundedCorners}" />
</Border.StrokeShape>
<Image Source="{Binding CoverImage}"
Aspect="AspectFill"/>
</Border>
<StackLayout Padding="8"
<StackLayout Padding="{StaticResource SpaceXS}"
Grid.Row="1">
<Label Text="{Binding Title}"
FontSize="24"
FontSize="{StaticResource SubtitleFontSize}"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
HorizontalTextAlignment="Center"/>
<Label Text="{Binding Artist}"
FontSize="16"
FontSize="{StaticResource SubSubtitleFontSize}"
TextColor="{StaticResource Secondary}"
HorizontalTextAlignment="Center" />
</StackLayout>
<StackLayout Grid.Row="2">
<Label Text="{Binding Details, Converter={StaticResource AlbumDetails}}"
FontSize="12"
<Label Grid.Row="2"
Text="{Binding Details, StringFormat='\{0\} · \{1\}'}"
FontSize="{StaticResource DetailsFontSize}"
TextColor="{StaticResource Gray}"
HorizontalTextAlignment="Center"/>
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<FlexLayout Grid.Row="3"
Direction="Row"
Margin="{StaticResource SpaceXS}"
JustifyContent="SpaceAround"
AlignItems="Center">
<Button Text="{x:Static strings:Strings.PlayButton}"
Margin="16,16,8,16"
Grid.Column="0"/>
<ctl:IconLabelButtonWide ButtonSource="icon_wide_button_play.png"
ButtonLabelText="{x:Static strings:Strings.PlayButton}"
Margin="{StaticResource WideButtonLeft}" />
<Button Text="{x:Static strings:Strings.ShuffleButton}"
Margin="8,16,16,16"
Grid.Column="1"/>
<ctl:IconLabelButtonWide ButtonSource="icon_wide_button_shuffle.png"
ButtonLabelText="{x:Static strings:Strings.ShuffleButton}"
Margin="{StaticResource WideButtonRight}" />
</Grid>
</FlexLayout>
</StackLayout>
<BoxView HeightRequest="1"
Color="{StaticResource Gray300}"
HorizontalOptions="FillAndExpand"
Margin="8"
Grid.Row="3" />
<BoxView Style="{StaticResource HeadGraySeparator}"
Grid.Row="4" />
<CollectionView ItemsSource="{Binding Songs}"
Margin="0,0,0,24"
Grid.Row="4">
Grid.Row="5">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:SongViewModel">
<StackLayout>
<Grid Margin="0,8,0,8">
<Grid Margin="{StaticResource TopBottomSpaceLittle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="{StaticResource SpaceL}"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Text="{Binding Index}"
FontSize="16"
FontSize="{StaticResource SubSubtitleFontSize}"
TextColor="{StaticResource Gray}"
Margin="0,0,8,0"
Margin="{StaticResource RightSpaceLittle}"
HorizontalTextAlignment="Center"
HorizontalOptions="Center"
Grid.Column="0"/>
<Label Text="{Binding Title}"
FontSize="16"
FontSize="{StaticResource SubSubtitleFontSize}"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
LineBreakMode="TailTruncation"
Grid.Column="1"/>
<Label Text="···"
FontSize="16"
<Label Text="{x:Static strings:Strings.HThreeDotsMenu}"
FontSize="{StaticResource SubSubtitleFontSize}"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
FontAttributes="Bold"
HorizontalOptions="End"
Margin="0,0,8,0"
Margin="{StaticResource RightSpaceLittle}"
Grid.Column="2"/>
</Grid>
<BoxView HeightRequest="1"
Color="{StaticResource Gray300}"
HorizontalOptions="FillAndExpand"
Margin="32,0,8,0"/>
<BoxView Style="{StaticResource GraySeparator}"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<StackLayout Margin="{StaticResource TopSpaceSome}"
Grid.Row="6">
<Label Text="{Binding ReleaseDate, StringFormat='{0:d MMMM yyyy}'}"
Style="{StaticResource FooterLabel}"
Grid.Row="5" />
Style="{StaticResource FooterLabel}" />
<Label Text="{Binding SongsInfo, Converter={StaticResource SongsInfo}}"
Style="{StaticResource FooterLabel}"
Grid.Row="6" />
Style="{StaticResource FooterLabel}" />
<Label Text="{Binding CopyrightInfo, StringFormat='℗ \{0\} \{1\}'}"
Style="{StaticResource FooterLabel}" />
</StackLayout>
<Label Text="{Binding CopyrightInfo, Converter={StaticResource CopyrightInfo}}"
Style="{StaticResource FooterLabel}"
Grid.Row="7" />
</Grid>
</ScrollView>
<!-- TODO Insert this in a main layout of some sort...-->
<ctl:BottomBar Grid.Row="1" />
</Grid>
</ContentPage>

@ -9,73 +9,69 @@
x:Class="AMC.View.Views.LibraryPage"
x:DataType="vm:LibraryViewModel">
<ScrollView
Padding="0, 0, 0, 128"
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollView Grid.Row="0"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<StackLayout Spacing="8">
<StackLayout Spacing="{StaticResource SpaceXS}"
Margin="{StaticResource BottomSpaceLittle}">
<Label Text="{x:Static strings:Strings.LibraryTitle}"
FontSize="32"
FontSize="{StaticResource TitleFontSize}"
FontAttributes="Bold"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
Margin="8" />
Margin="{StaticResource SpaceXS}" />
<StackLayout Orientation="Vertical" Spacing="0">
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.PlaylistsCategory}"
IconSource="icon_playlists.png" />
<ctl:LibraryCategoryItem CategoryText="{x:Static strings:Strings.PlaylistsCategory}"
IconSource="icon_categories_playlists.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.ArtistsCategory}"
IconSource="icon_artists.png" />
<ctl:LibraryCategoryItem CategoryText="{x:Static strings:Strings.ArtistsCategory}"
IconSource="icon_categories_artists.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.AlbumsCategory}"
IconSource="icon_albums.png" />
<ctl:LibraryCategoryItem CategoryText="{x:Static strings:Strings.AlbumsCategory}"
IconSource="icon_categories_albums.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.SongsCategory}"
IconSource="icon_songs.png" />
<ctl:LibraryCategoryItem CategoryText="{x:Static strings:Strings.SongsCategory}"
IconSource="icon_categories_songs.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.GenresCategory}"
IconSource="icon_genres.png" />
<ctl:LibraryCategoryItem CategoryText="{x:Static strings:Strings.GenresCategory}"
IconSource="icon_categories_genres.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
</StackLayout>
<Label Text="{x:Static strings:Strings.RecentlyAddedHeader}"
FontSize="16"
FontSize="{StaticResource SubSubtitleFontSize}"
FontAttributes="Bold"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
Margin="8, 16, 8, 8" />
Margin="{StaticResource SpaceXS}" />
<CollectionView
ItemsSource="{Binding Albums}"
<CollectionView ItemsSource="{Binding Albums}"
ItemsLayout="VerticalGrid, 2"
SelectionMode="Single"
SelectionChanged="OnAlbumSelected">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:AlbumViewModel">
<StackLayout Margin="16">
<StackLayout Margin="{StaticResource SpaceS}">
<Border>
<Border.StrokeShape>
<RoundRectangle CornerRadius="8,8,8,8" />
<RoundRectangle CornerRadius="{StaticResource SlightlyRoundedCorners}" />
</Border.StrokeShape>
<Image
Source="{Binding CoverImage}"
<Image Source="{Binding CoverImage}"
Aspect="AspectFill" />
</Border>
<Label
Text="{Binding Title}"
<Label Text="{Binding Title}"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
LineBreakMode="TailTruncation" />
<Label
Text="{Binding Artist}"
<Label Text="{Binding Artist}"
TextColor="{StaticResource Gray}"
LineBreakMode="TailTruncation" />
</StackLayout>
@ -84,4 +80,7 @@
</CollectionView>
</StackLayout>
</ScrollView>
<!-- TODO Insert this in a main layout of some sort...-->
<ctl:BottomBar Grid.Row="1" />
</Grid>
</ContentPage>

@ -8,6 +8,8 @@ namespace AMC.View.Views
public LibraryPage() : this(null)
{ }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Blocker Code Smell", "S3427:Method overloads with default parameter values should not overlap ", Justification = "The parameterless ctor is needed by MAUI")]
public LibraryPage(LibraryViewModel? libraryViewModel = null)
{
InitializeComponent();
@ -19,9 +21,8 @@ namespace AMC.View.Views
private void OnAlbumSelected(object sender, SelectionChangedEventArgs e)
{
var collectionView = (CollectionView)sender;
var selectedAlbum = (AlbumViewModel)e.CurrentSelection.FirstOrDefault();
if (selectedAlbum != null)
if (e.CurrentSelection.FirstOrDefault() is AlbumViewModel selectedAlbum)
{
Navigation.PushAsync(new AlbumPage(selectedAlbum));
}

@ -24,7 +24,7 @@ namespace AMC.ViewModel.ViewModels
Id = 1,
Title = "Test Album 1",
Artist = "Test Artist 1",
CoverImage = "macroblank_1.png",
CoverImage = "album_macroblank_1.png",
Genre = "Test genre 1",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),
@ -48,7 +48,7 @@ namespace AMC.ViewModel.ViewModels
Id = 2,
Title = "Test Albuuuuuuuuuuuuuuuuuuum 2",
Artist = "Test Artist 2",
CoverImage = "macroblank_2.png",
CoverImage = "album_macroblank_2.png",
Genre = "Test genre 2",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),
@ -69,7 +69,7 @@ namespace AMC.ViewModel.ViewModels
Id = 3,
Title = "Test Album 3",
Artist = "Test Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaartist 3",
CoverImage = "macroblank_3.png",
CoverImage = "album_macroblank_3.png",
Genre = "Test genre 3",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),

@ -1,7 +1,94 @@
# AD_MAUI
# Apple Music Clone
A MAUI Apple Music mini-clone based on [these instructions](https://codefirst.iut.uca.fr/git/mchSamples_.NET/MAUI_TP1_2023)
## Project Overview
## README stuff
Apple Music allows users to listen to music, browse playlists, and interact with a
sophisticated UI to control their playback.
It goes here...
This clone does not.
Apple Music Clone is an application built on the .NET MAUI (Multi-platform App UI)
framework. It focuses on the `Library` "master-detail". As of the 20th of May 2023,
it is a _façade_: only the View part of the project is serviceable.
## The View
* ### Library ("master")
Users can browse a `Library` of `Album` and select any album to inspect.
* ### Album ("detail")
Users can browse an `Album` of `Songs`.
* ### Dark/Light theme
This clone replicates the original dark/light themes by Apple music.
To test this, you can change your device's (or your emulator's) display
setting to dark/light theme.
* ### `i18n`: `en`, `fr`
This clone supports two locales: English (by default), and French.
To test this, you can change your device's (or your emulator's) primary language
settings to English/French.
* ### Bottom bar
In Apple Music, a consistent, stylish, slightly transparent bottom bar allows
navigation between different views, and paying songs.
In this clone, the bar is just eye-candy, and not even a little bit transparent.
* ### Top bar
In Apple Music, a stylish, slightly transparent top bar contains certain menu
options, and displays the name of the current
section once the user has scrolled past the corresponding header.
In this clone, the top bar is left at the OS default.
## Installation
To run the Apple Music Clone, you must first install .NET MAUI. You can follow
[the official guide](https://learn.microsoft.com/en-us/dotnet/maui/get-started/installation)
for the same.
Once MAUI is installed, clone this repository and open the `AD_MAUI.sln` Solution in
*Visual Studio*.
If you don't have an Android emulator installed for *Visual Studio* already, open the
*Android Device Manager* to take care of that. For reference, this project was tested on a
`Pixel 5 - API 33 (Android 13.0 - API 33)`.
When you're ready to run the project, please make sure you launch the `AMC.View` project as a
`Single startup project`, if *Visual Studio* hasn't configured it that way automatically.
## Some known limitations and shortcomings
Concerning the View part of this project:
* the bottom bar is inserted once in the `Library` view, and once in the `Album` view
* instead, it should be incorporated in a main layout.
* the bottom bar's "top" part, AKA the player, has some repeated code.
```csharp
<ImageButton Source="{AppThemeBinding Light='icon_play.png', Dark='icon_play_dark.png'}"
Margin="{StaticResource SpaceS}"
HorizontalOptions="End"
VerticalOptions="Center"
HeightRequest="{StaticResource SpaceML}"
WidthRequest="{StaticResource SpaceML}" />
<ImageButton Source="{AppThemeBinding Light='icon_next.png', Dark='icon_next_dark.png'}"
Margin="{StaticResource SpaceS}"
HorizontalOptions="End"
VerticalOptions="Center"
HeightRequest="{StaticResource SpaceML}"
WidthRequest="{StaticResource SpaceML}" />
```
* instead, it should have been extracted into another reusable component, but there were
difficulties in doing that -- having to do with the dark/light themes.
* the bottom bar is not as stylish as the original.
* the top bar was left to the OS default.
* and many others will join this list, no doubt.
Loading…
Cancel
Save