💄 Create "master" view (#2)

## "master" view
The view for an individual album.

## Basic Navigation
To make sure that we could do it, and to allow for more sophisticated manual testing, we also implemented basic navigation.

Co-authored-by: Alexis DRAI <alexis.drai@etu.uca.fr>
Reviewed-on: #2
main
Alexis Drai 2 years ago
parent 552a7745b7
commit 49b422d3d0

@ -0,0 +1,8 @@

namespace AMC.Model.Models
{
public class Library
{
public List<Album> Albums { get; set; }
}
}

@ -1,4 +1,5 @@
namespace AMC.Model.Models { namespace AMC.Model.Models
{
public class Song public class Song
{ {
public int Id { get; set; } public int Id { get; set; }

@ -49,7 +49,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Resources\Images\macroblank.svg" /> <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" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -82,6 +89,16 @@
<MauiXaml Update="Views\AlbumPage.xaml"> <MauiXaml Update="Views\AlbumPage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</MauiXaml> </MauiXaml>
<MauiXaml Update="Controls\LibraryCategoryItem.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\LibraryPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\Icons\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -1,6 +1,4 @@
using System.Globalization; namespace AMC.View;
namespace AMC.View;
public partial class App : Application public partial class App : Application
{ {

@ -3,12 +3,12 @@
x:Class="AMC.View.AppShell" x:Class="AMC.View.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:AMC.View" xmlns:local="clr-namespace:AMC.View.Views"
Shell.FlyoutBehavior="Disabled"> Shell.FlyoutBehavior="Disabled">
<ShellContent <ShellContent
Title="Home" Title="Home"
ContentTemplate="{DataTemplate local:MainPage}" ContentTemplate="{DataTemplate local:LibraryPage}"
Route="MainPage" /> Route="LibraryPage" />
</Shell> </Shell>

@ -0,0 +1,25 @@
<?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.LibraryCategoryItem">
<StackLayout Orientation="Horizontal"
Padding="8"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<Image x:Name="IconImage"
WidthRequest="24"
HeightRequest="24"
Margin="0, 0, 8, 0"
HorizontalOptions="Start" />
<Label x:Name="CategoryLabel"
FontSize="16"
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"
HorizontalOptions="End" />
</StackLayout>
</ContentView>

@ -0,0 +1,50 @@

namespace AMC.View.Controls
{
public partial class LibraryCategoryItem : ContentView
{
public static readonly BindableProperty CategoryTextProperty = BindableProperty.Create(
propertyName: nameof(CategoryText),
returnType: typeof(string),
declaringType: typeof(LibraryCategoryItem),
defaultValue: "",
propertyChanged: CategoryTextChanged);
public static readonly BindableProperty IconSourceProperty = BindableProperty.Create(
propertyName: nameof(IconSource),
returnType: typeof(ImageSource),
declaringType: typeof(LibraryCategoryItem),
defaultValue: null,
propertyChanged: IconSourceChanged);
public LibraryCategoryItem()
{
InitializeComponent();
}
public string CategoryText
{
get => (string)GetValue(CategoryTextProperty);
set => SetValue(CategoryTextProperty, value);
}
public ImageSource IconSource
{
get => (ImageSource)GetValue(IconSourceProperty);
set => SetValue(IconSourceProperty, value);
}
private static void CategoryTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (LibraryCategoryItem)bindable;
control.CategoryLabel.Text = (string)newValue;
}
private static void IconSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (LibraryCategoryItem)bindable;
control.IconImage.Source = (ImageSource)newValue;
}
}
}

@ -8,8 +8,8 @@ namespace AMC.View.Converters
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
var (songCount, totalDuration) = (ValueTuple<int, int>)value; var (songCount, totalDuration) = (ValueTuple<int, int>)value;
var songLabel = songCount == 1 ? Strings.SongsLabelSingular : Strings.SongsLabelPlural; var songLabel = int.Abs(songCount) < 2 ? Strings.SongsLabelSingular : Strings.SongsLabelPlural;
var minutesLabel = totalDuration == 1 ? Strings.MinutesLabelSingular : Strings.MinutesLabelPlural; var minutesLabel = int.Abs(totalDuration) < 2 ? Strings.MinutesLabelSingular : Strings.MinutesLabelPlural;
return string.Format( return string.Format(
"{0} {1}, {2} {3}", "{0} {1}, {2} {3}",
songCount, songCount,

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AMC.View.MainPage">
<Button
x:Name="RdmBtn"
Text="DO IT"
Clicked="OnRdmBtnClicked"
/>
</ContentPage>

@ -1,20 +0,0 @@
using AMC.View.Views;
using AMC.ViewModel.ViewModels;
namespace AMC.View;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
// TODO use commands (or navigation methods?) in VMApp instead
private void OnRdmBtnClicked(object sender, EventArgs e)
{
Navigation.PushAsync(new AlbumPage(new AlbumViewModel(null)));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

@ -60,6 +60,33 @@ namespace AMC.View.Resources.Strings {
} }
} }
/// <summary>
/// Looks up a localized string similar to Albums.
/// </summary>
public static string AlbumsCategory {
get {
return ResourceManager.GetString("AlbumsCategory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Artists.
/// </summary>
public static string ArtistsCategory {
get {
return ResourceManager.GetString("ArtistsCategory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Genres.
/// </summary>
public static string GenresCategory {
get {
return ResourceManager.GetString("GenresCategory", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Library. /// Looks up a localized string similar to Library.
/// </summary> /// </summary>
@ -96,6 +123,24 @@ namespace AMC.View.Resources.Strings {
} }
} }
/// <summary>
/// Looks up a localized string similar to Playlists.
/// </summary>
public static string PlaylistsCategory {
get {
return ResourceManager.GetString("PlaylistsCategory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Recently Added.
/// </summary>
public static string RecentlyAddedHeader {
get {
return ResourceManager.GetString("RecentlyAddedHeader", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Shuffle. /// Looks up a localized string similar to Shuffle.
/// </summary> /// </summary>
@ -105,6 +150,15 @@ namespace AMC.View.Resources.Strings {
} }
} }
/// <summary>
/// Looks up a localized string similar to Songs.
/// </summary>
public static string SongsCategory {
get {
return ResourceManager.GetString("SongsCategory", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to songs. /// Looks up a localized string similar to songs.
/// </summary> /// </summary>

@ -138,4 +138,22 @@
<data name="MinutesLabelSingular" xml:space="preserve"> <data name="MinutesLabelSingular" xml:space="preserve">
<value>minute</value> <value>minute</value>
</data> </data>
<data name="RecentlyAddedHeader" xml:space="preserve">
<value>Ajouts récents</value>
</data>
<data name="PlaylistsCategory" xml:space="preserve">
<value>Playlists</value>
</data>
<data name="ArtistsCategory" xml:space="preserve">
<value>Artistes</value>
</data>
<data name="AlbumsCategory" xml:space="preserve">
<value>Albums</value>
</data>
<data name="SongsCategory" xml:space="preserve">
<value>Morceaux</value>
</data>
<data name="GenresCategory" xml:space="preserve">
<value>Genres</value>
</data>
</root> </root>

@ -138,4 +138,22 @@
<data name="MinutesLabelSingular" xml:space="preserve"> <data name="MinutesLabelSingular" xml:space="preserve">
<value>minute</value> <value>minute</value>
</data> </data>
<data name="RecentlyAddedHeader" xml:space="preserve">
<value>Recently Added</value>
</data>
<data name="PlaylistsCategory" xml:space="preserve">
<value>Playlists</value>
</data>
<data name="ArtistsCategory" xml:space="preserve">
<value>Artists</value>
</data>
<data name="AlbumsCategory" xml:space="preserve">
<value>Albums</value>
</data>
<data name="SongsCategory" xml:space="preserve">
<value>Songs</value>
</data>
<data name="GenresCategory" xml:space="preserve">
<value>Genres</value>
</data>
</root> </root>

@ -4,6 +4,13 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="BoxView" x:Key="GraySeparator">
<Setter Property="HeightRequest" Value="1"/>
<Setter Property="Color" Value="{StaticResource Gray200}"/>
<Setter Property="HorizontalOptions" Value="FillAndExpand"/>
<Setter Property="Margin" Value="32,0,8,0"/>
</Style>
<Style TargetType="Label" x:Key="FooterLabel"> <Style TargetType="Label" x:Key="FooterLabel">
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="12" />
<Setter Property="TextColor" Value="{StaticResource Gray}" /> <Setter Property="TextColor" Value="{StaticResource Gray}" />

@ -16,7 +16,9 @@
</ContentPage.Resources> </ContentPage.Resources>
<ScrollView BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}"> <ScrollView
Padding="0, 0, 0, 128"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<Grid Margin="10"> <Grid Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="300"/> <RowDefinition Height="300"/>
@ -30,11 +32,15 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Image Source="{Binding CoverImage}" <Border
Aspect="AspectFill" Grid.Row="0"
BackgroundColor="{StaticResource Background}" Margin="32,0,32,0">
Margin="32,0,32,0" <Border.StrokeShape>
Grid.Row="0"/> <RoundRectangle CornerRadius="8,8,8,8" />
</Border.StrokeShape>
<Image Source="{Binding CoverImage}"
Aspect="AspectFill"/>
</Border>
<StackLayout Padding="8" <StackLayout Padding="8"
Grid.Row="1"> Grid.Row="1">

@ -6,11 +6,11 @@ namespace AMC.View.Views
{ {
private readonly AlbumViewModel viewModel; private readonly AlbumViewModel viewModel;
public AlbumPage(AlbumViewModel? albumViewModel) public AlbumPage(AlbumViewModel albumViewModel)
{ {
InitializeComponent(); InitializeComponent();
viewModel = albumViewModel ?? new AlbumViewModel(null); viewModel = albumViewModel;
BindingContext = viewModel; BindingContext = viewModel;
} }
} }

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
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.LibraryPage"
x:DataType="vm:LibraryViewModel">
<ScrollView
Padding="0, 0, 0, 128"
BackgroundColor="{AppThemeBinding Light={StaticResource Background}, Dark={StaticResource BackgroundDark}}">
<StackLayout Spacing="8">
<Label Text="{x:Static strings:Strings.LibraryTitle}"
FontSize="32"
FontAttributes="Bold"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
Margin="8" />
<StackLayout Orientation="Vertical" Spacing="0">
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.PlaylistsCategory}"
IconSource="icon_playlists.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.ArtistsCategory}"
IconSource="icon_artists.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.AlbumsCategory}"
IconSource="icon_albums.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.SongsCategory}"
IconSource="icon_songs.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
<ctl:LibraryCategoryItem
CategoryText="{x:Static strings:Strings.GenresCategory}"
IconSource="icon_genres.png" />
<BoxView Style="{StaticResource GraySeparator}"/>
</StackLayout>
<Label Text="{x:Static strings:Strings.RecentlyAddedHeader}"
FontSize="16"
FontAttributes="Bold"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
Margin="8, 16, 8, 8" />
<CollectionView
ItemsSource="{Binding Albums}"
ItemsLayout="VerticalGrid, 2"
SelectionMode="Single"
SelectionChanged="OnAlbumSelected">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:AlbumViewModel">
<StackLayout Margin="16">
<Border>
<Border.StrokeShape>
<RoundRectangle CornerRadius="8,8,8,8" />
</Border.StrokeShape>
<Image
Source="{Binding CoverImage}"
Aspect="AspectFill" />
</Border>
<Label
Text="{Binding Title}"
TextColor="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}"
LineBreakMode="TailTruncation" />
<Label
Text="{Binding Artist}"
TextColor="{StaticResource Gray}"
LineBreakMode="TailTruncation" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ScrollView>
</ContentPage>

@ -0,0 +1,32 @@
using AMC.ViewModel.ViewModels;
namespace AMC.View.Views
{
public partial class LibraryPage : ContentPage
{
private readonly LibraryViewModel viewModel;
public LibraryPage() : this(null)
{ }
public LibraryPage(LibraryViewModel? libraryViewModel = null)
{
InitializeComponent();
viewModel = libraryViewModel ?? new LibraryViewModel(null);
BindingContext = viewModel;
}
private void OnAlbumSelected(object sender, SelectionChangedEventArgs e)
{
var collectionView = (CollectionView)sender;
var selectedAlbum = (AlbumViewModel)e.CurrentSelection.FirstOrDefault();
if (selectedAlbum != null)
{
Navigation.PushAsync(new AlbumPage(selectedAlbum));
}
collectionView.SelectedItem = null;
}
}
}

@ -8,7 +8,7 @@ namespace AMC.ViewModel.ViewModels
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private readonly Album album = new(); private readonly Album album;
public int Id => album.Id; public int Id => album.Id;
@ -33,37 +33,9 @@ namespace AMC.ViewModel.ViewModels
private readonly ObservableCollection<SongViewModel> songs; private readonly ObservableCollection<SongViewModel> songs;
public AlbumViewModel(Album? album) public AlbumViewModel(Album album)
{ {
// Mocked data for testing this.album = album;
this.album = album ?? new Album
{
Id = 1,
Title = "Test Album",
Artist = "Test Artist",
CoverImage = "macroblank.png",
Genre = "Test genre",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),
CopyrightYear = 1996,
ProducerBlurb = "Test Records Ltd",
Songs = new List<Song>
{
new Song { Id = 1, Title = "Test Song 1", Duration = 210 },
new Song { Id = 2, Title = "Test Song 2", Duration = 260 },
new Song { Id = 3, Title = "Test Soooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong 3", Duration = 817 },
new Song { Id = 4, Title = "Test Song 4", Duration = 654 },
new Song { Id = 5, Title = "Test Song 5", Duration = 768 },
new Song { Id = 6, Title = "Test Song 6", Duration = 435 },
new Song { Id = 7, Title = "Test Song 7", Duration = 864 },
new Song { Id = 8, Title = "Test Song 8", Duration = 456 },
new Song { Id = 9, Title = "Test Song 9", Duration = 83 },
new Song { Id = 10, Title = "Test Song 10", Duration = 4533 },
new Song { Id = 11, Title = "Test Song 11", Duration = 785 },
new Song { Id = 12, Title = "Test Song 12", Duration = 712 },
new Song { Id = 13, Title = "Test Song 13", Duration = 523 },
}
};
int index = 1; int index = 1;
foreach (var song in this.album.Songs) foreach (var song in this.album.Songs)

@ -0,0 +1,89 @@
using AMC.Model.Models;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
namespace AMC.ViewModel.ViewModels
{
public class LibraryViewModel : INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler? CollectionChanged;
private readonly Library library;
public ReadOnlyObservableCollection<AlbumViewModel> Albums => new(albums);
private readonly ObservableCollection<AlbumViewModel> albums;
public LibraryViewModel(Library? library)
{
this.library = library ?? new Library
{
Albums = new List<Album> {
new Album
{
Id = 1,
Title = "Test Album 1",
Artist = "Test Artist 1",
CoverImage = "macroblank_1.png",
Genre = "Test genre 1",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),
CopyrightYear = 1996,
ProducerBlurb = "Test Records Ltd",
Songs = new List<Song>
{
new Song { Id = 1, Title = "Test Song 1", Duration = 210 },
new Song { Id = 2, Title = "Test Song 2", Duration = 260 },
new Song { Id = 3, Title = "Test Soooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong 3", Duration = 817 },
new Song { Id = 4, Title = "Test Song 4", Duration = 654 },
new Song { Id = 5, Title = "Test Song 5", Duration = 768 },
new Song { Id = 6, Title = "Test Song 6", Duration = 435 },
new Song { Id = 7, Title = "Test Song 7", Duration = 785 },
new Song { Id = 8, Title = "Test Song 8", Duration = 712 },
new Song { Id = 9, Title = "Test Song 9", Duration = 523 },
}
},
new Album
{
Id = 2,
Title = "Test Albuuuuuuuuuuuuuuuuuuum 2",
Artist = "Test Artist 2",
CoverImage = "macroblank_2.png",
Genre = "Test genre 2",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),
CopyrightYear = 1996,
ProducerBlurb = "Test Records Ltd",
Songs = new List<Song>
{
new Song { Id = 10, Title = "Test Song 10", Duration = 456 },
new Song { Id = 11, Title = "Test Song 11", Duration = 83 },
new Song { Id = 12, Title = "Test Song 12", Duration = 4533 },
new Song { Id = 13, Title = "Test Song 13", Duration = 785 },
new Song { Id = 14, Title = "Test Song 14", Duration = 712 },
new Song { Id = 15, Title = "Test Song 15", Duration = 523 },
}
},
new Album
{
Id = 3,
Title = "Test Album 3",
Artist = "Test Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaartist 3",
CoverImage = "macroblank_3.png",
Genre = "Test genre 3",
Year = 1970,
ReleaseDate = new DateTime(1970, 01, 01),
CopyrightYear = 1996,
ProducerBlurb = "Test Records Ltd",
Songs = new List<Song>
{
new Song { Id = 16, Title = "Test Song 16", Duration = 34 },
}
},
}
};
albums = new ObservableCollection<AlbumViewModel>(this.library.Albums.Select(album => new AlbumViewModel(album)));
}
}
}
Loading…
Cancel
Save