Skip to content
Permalink
Browse files
search function implemented
once users are logged in, they can search for tracks, albums, and artists through the search function in the app
  • Loading branch information
Chronos authored and Chronos committed Mar 23, 2023
1 parent da82ba7 commit d90c5b663150927179b566e9924aea954c537376
Show file tree
Hide file tree
Showing 146 changed files with 459 additions and 248 deletions.
@@ -12,11 +12,11 @@
Route="LoginPage" />

<TabBar Route="Main">
<ShellContent Title="Home"
Icon="icon.png"
Route="Home"
ContentTemplate="{DataTemplate views:HomeView}"/>
<ShellContent
Title="Home"
Icon="hone.png"
ContentTemplate="{DataTemplate views:HomeView}"
Route="Home" />
</TabBar>

</Shell>

@@ -0,0 +1,44 @@
using System;
using System.Runtime.CompilerServices;

namespace app.Behaviors {
public class FocusBehavior : Behavior<View> {
private View currentView;

public static BindableProperty IsFocusedProperty = BindableProperty.Create(nameof(IsFocused), typeof(bool), typeof(FocusBehavior));

public bool IsFocused {
get => (bool)GetValue(IsFocusedProperty);
set => SetValue(IsFocusedProperty, value);
}

public FocusBehavior() {
}

protected override void OnAttachedTo(View bindable) {
base.OnAttachedTo(bindable);

currentView = bindable;
currentView.Unfocused += CurrentView_Unfocused;
}

private void CurrentView_Unfocused(object sender, FocusEventArgs e) {
IsFocused = false;
}

protected override void OnDetachingFrom(View bindable) {
base.OnDetachingFrom(bindable);

currentView.Unfocused -= CurrentView_Unfocused;
currentView = null;
}

protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) {
base.OnPropertyChanged(propertyName);

if (propertyName == nameof(IsFocused) && IsFocused && currentView != null) {
currentView.Focus();
}
}
}
}
@@ -85,9 +85,6 @@ public class Artist {

[JsonPropertyName("images")]
public List<Image> Images { get; set; }

[JsonPropertyName("followers")]
public Followers Followers { get; set; }
}

public class Artists {
@@ -1,10 +1,8 @@
using System;
namespace app.Services;

public interface ISecureStorageService {

Task Save(string key, string value);
Task<bool> Contains(string key);
Task<string> Get(string key);

namespace app.Services {
public interface ISecureStorageService {
Task Save(string key, string value);
Task<bool> Contains(string key);
Task<string> Get(string key);
}
}
@@ -2,6 +2,7 @@

public interface ISpotifyService {
Task<bool> Initialize(string authCode);
Task<bool> IsSignedIn();
Task<SearchResult> Search(string searchText, string types);
}

@@ -43,11 +43,21 @@ public class SpotifyService : ISpotifyService {
return response.IsSuccessStatusCode;
}

public async Task<bool> IsSignedIn() {
var hasToken = await secureStorageService.Contains(nameof(AuthResult.AccessToken));

if (hasToken) {
accessToken = await secureStorageService.Get(nameof(AuthResult.AccessToken));
}

return hasToken;
}

public async Task<SearchResult> Search(string searchText, string types) {
var client = await GetClient();
var response = await client.GetAsync($"search?q={searchText}&type={types}");

var content = await response.Content.ReadAsStreamAsync();
var content = await response.Content.ReadAsStringAsync();

//throw exception in case of unsuccessful response
response.EnsureSuccessStatusCode();
@@ -1,17 +1,102 @@
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;

namespace app.ViewModels;

public partial class HomeViewModel : ViewModel {
public HomeViewModel() {
private readonly ISpotifyService spotifyService;

public HomeViewModel(ISpotifyService spotifyService) {
this.spotifyService = spotifyService;
}

//using ObservableProperty in order for a property to generate
[ObservableProperty]
private bool isSearching;

[ObservableProperty]
private string searchText;

[ObservableProperty]
private bool hasResult;

[ObservableProperty]
private ObservableCollection<SearchItemViewModel> artists;

[ObservableProperty]
private ObservableCollection<SearchItemViewModel> albums;

[ObservableProperty]
private ObservableCollection<SearchItemViewModel> tracks;


[RelayCommand]
private void StartSearch() {
IsSearching = true;
}
}

[RelayCommand]
private async Task Search() {
try {
IsBusy = true;
var types = "artist,album,track";
var result = await spotifyService.Search(SearchText, types);

var artists = result.Artists.Items.Select(x => new SearchItemViewModel() {
Title = x.Name,
ImageUrl = x.Images.Any() ? x.Images.First().Url : null,
TapCommand = NavigateToArtistCommand,
}).ToList();

Artists = new ObservableCollection<SearchItemViewModel>(artists);

var albums = result.Albums.Items.Select(x => new SearchItemViewModel() {
Title = x.Name,
ImageUrl = x.Images.Any() ? x.Images.First().Url : null,
TapCommand = NavigateToAlbumCommand,
});

Albums = new ObservableCollection<SearchItemViewModel>(albums);

var tracks = result.Tracks.Items.Select(x => new SearchItemViewModel() {
Title = x.Name,
SubTitle = x.Artists.Any() ? x.Artists.First().Name : null,
ImageUrl = x.Album.Images.Any() ? x.Album.Images.First().Url : null,
TapCommand = NavigateToTrackCommand,
});

Tracks = new ObservableCollection<SearchItemViewModel>(tracks);

HasResult = true;

}
catch (Exception ex) {
await HandleException(ex);
}
IsBusy = false;
}

[RelayCommand]
private void NavigateToArtist() {

}

[RelayCommand]
private void NavigateToAlbum() {

}

[RelayCommand]
private void NavigateToTrack() {

}

}

public class SearchItemViewModel : ViewModel {
public string Title { get; set; }
public string SubTitle { get; set; }
public string ImageUrl { get; set; }
public ICommand TapCommand { get; set; }
}
@@ -3,4 +3,10 @@
public abstract class ViewModel : TinyViewModel {
public ViewModel() {
}

protected Task HandleException(Exception ex) {
Console.Write(ex);
return Task.CompletedTask;
}

}
@@ -1,27 +1,86 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<mvvm:TinyView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:app.ViewModels"
xmlns:mvvm="clr-namespace:TinyMvvm;assembly=TinyMvvm.Maui"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:vm="clr-namespace:app.ViewModels"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:behaviors="clr-namespace:app.Behaviors"
x:Class="app.Views.HomeView"
x:DataType="vm:HomeViewModel"
Title="HomeView">
<ContentPage.Resources>
<toolkit:InvertedBoolConverter x:Key="InvertBool"/>
</ContentPage.Resources>
Title="HomeView" x:DataType="vm:HomeViewModel"
x:Name="ThisPage">
<mvvm:TinyView.Resources>
<toolkit:InvertedBoolConverter x:Key="InvertBool" />
</mvvm:TinyView.Resources>
<Shell.TitleView>
<SearchBar IsVisible="{Binding IsSearching}"
TextColor="{StaticResource AccentTextColor}"/>
</Shell.TitleView>
<VerticalStackLayout>
<!-- using MAUI community's Event-To-Command behavior
to bind search bar to "Focus" event, instead of command-->
<SearchBar IsVisible="{Binding IsSearching, Converter={StaticResource InvertBool}}">
Text="{Binding SearchText}"
SearchCommand="{Binding SearchCommand}"
TextColor="{StaticResource AccentTextColor}">
<SearchBar.Behaviors>
<toolkit:EventToCommandBehavior EventName="Focused"
Command="{Binding StartSearchCommand}"/>
<behaviors:FocusBehavior BindingContext="{Binding Source={x:Reference ThisPage}, Path=BindingContext}"
IsFocused="{Binding IsSearching}" />
</SearchBar.Behaviors>
</SearchBar>
</VerticalStackLayout>
</mvvm:TinyView>
</Shell.TitleView>
<Grid Padding="10">
<ActivityIndicator IsRunning="{Binding IsBusy}" HorizontalOptions="Center" VerticalOptions="Center" />
<ScrollView IsVisible="{Binding IsNotBusy}">
<VerticalStackLayout>
<SearchBar IsVisible="{Binding IsSearching, Converter={StaticResource InvertBool}}"
Text="{Binding SearchText}" SearchCommand="{Binding SearchCommand}">
<SearchBar.Behaviors>
<toolkit:EventToCommandBehavior EventName="Focused" Command="{Binding StartSearchCommand}" />
</SearchBar.Behaviors>
</SearchBar>
<VerticalStackLayout IsVisible="{Binding HasResult}" Spacing="10">
<Label Text="Artists" FontSize="Title" />
<CollectionView ItemsSource="{Binding Artists}" ItemsLayout="HorizontalList">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:SearchItemViewModel">
<VerticalStackLayout Padding="0,0,10,0" WidthRequest="150">
<Image Source="{Binding ImageUrl, Mode=OneTime}"
WidthRequest="150"
HeightRequest="150"
Aspect="AspectFill"/>
<Label Text="{Binding Title}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label Text="Albums" FontSize="Title" />
<CollectionView ItemsSource="{Binding Albums}" ItemsLayout="HorizontalList">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:SearchItemViewModel">
<VerticalStackLayout Padding="0,0,10,0" WidthRequest="150">
<Image Source="{Binding ImageUrl, Mode=OneTime}"
WidthRequest="150"
HeightRequest="150"
Aspect="AspectFill"/>
<Label Text="{Binding Title}" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label Text="Tracks" FontSize="Title" />
<CollectionView ItemsSource="{Binding Tracks}">

<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:SearchItemViewModel">
<HorizontalStackLayout Spacing="10" Padding="0,0,0,10">
<Image Source="{Binding ImageUrl, Mode=OneTime}"
WidthRequest="30"
HeightRequest="30"
Aspect="AspectFill"/>
<VerticalStackLayout>
<Label Text="{Binding Title}" FontSize="Header" VerticalOptions="Center" />
<Label Text="{Binding Title}" FontSize="Caption" VerticalOptions="Center" />
</VerticalStackLayout>
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</VerticalStackLayout>
</ScrollView>
</Grid>
</mvvm:TinyView>
@@ -66,12 +66,14 @@
<None Remove="Models\" />
<None Remove="Resources\Images\login_background.png" />
<None Remove="Resources\Images\icon.png" />
<None Remove="Behaviors\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\" />
<Folder Include="ViewModels\" />
<Folder Include="Services\" />
<Folder Include="Models\" />
<Folder Include="Behaviors\" />
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\Images\login_background.png" />
BIN +27.5 KB (100%) bin/Debug/net7.0-android/app.dll
Binary file not shown.
BIN +10.3 KB (110%) bin/Debug/net7.0-android/app.pdb
Binary file not shown.

0 comments on commit d90c5b6

Please sign in to comment.