Finished UWP Example using protocol registration - Docs TODO

This commit is contained in:
Jonas Dellinger 2020-05-18 11:39:01 +02:00
parent d095a5a870
commit 24bb3d345f
22 changed files with 521 additions and 241 deletions

View File

@ -80,30 +80,6 @@ namespace SpotifyAPI.Web.Auth
return Task.CompletedTask; return Task.CompletedTask;
} }
public Uri BuildLoginUri(LoginRequest request)
{
Ensure.ArgumentNotNull(request, nameof(request));
StringBuilder builder = new StringBuilder(SpotifyUrls.Authorize.ToString());
builder.Append($"?client_id={request.ClientId}");
builder.Append($"&response_type={request.ResponseTypeParam.ToString().ToLower()}");
builder.Append($"&redirect_uri={HttpUtility.UrlEncode(BaseUri.ToString())}");
if (!string.IsNullOrEmpty(request.State))
{
builder.Append($"&state={HttpUtility.UrlEncode(request.State)}");
}
if (request.Scope != null)
{
builder.Append($"&scope={HttpUtility.UrlEncode(string.Join(" ", request.Scope))}");
}
if (request.ShowDialog != null)
{
builder.Append($"&show_dialog={request.ShowDialog.Value}");
}
return new Uri(builder.ToString());
}
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);

View File

@ -12,8 +12,6 @@ namespace SpotifyAPI.Web.Auth
Task Start(); Task Start();
Task Stop(); Task Stop();
Uri BuildLoginUri(LoginRequest request);
Uri BaseUri { get; } Uri BaseUri { get; }
} }
} }

View File

@ -1,26 +0,0 @@
using System.Collections.Generic;
namespace SpotifyAPI.Web.Auth
{
public class LoginRequest
{
public LoginRequest(string clientId, ResponseType responseType)
{
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
ClientId = clientId;
ResponseTypeParam = responseType;
}
public ResponseType ResponseTypeParam { get; }
public string ClientId { get; }
public string State { get; set; }
public ICollection<string> Scope { get; set; }
public bool? ShowDialog { get; set; }
public enum ResponseType
{
Code,
Token
}
}
}

View File

@ -26,19 +26,19 @@ namespace Example.CLI.CustomHTML
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived; _server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
var request = new LoginRequest(clientId, LoginRequest.ResponseType.Code) var request = new LoginRequest(_server.BaseUri, clientId, LoginRequest.ResponseType.Code)
{ {
Scope = new List<string> { UserReadEmail } Scope = new List<string> { UserReadEmail }
}; };
Uri url = _server.BuildLoginUri(request); Uri uri = request.ToUri();
try try
{ {
BrowserUtil.Open(url); BrowserUtil.Open(uri);
} }
catch (Exception) catch (Exception)
{ {
Console.WriteLine("Unable to open URL, manually open: {0}", url); Console.WriteLine("Unable to open URL, manually open: {0}", uri);
} }
Console.ReadKey(); Console.ReadKey();

View File

@ -65,19 +65,19 @@ namespace Example.CLI.PersistentConfig
await _server.Start(); await _server.Start();
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived; _server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
var request = new LoginRequest(clientId, LoginRequest.ResponseType.Code) var request = new LoginRequest(_server.BaseUri, clientId, LoginRequest.ResponseType.Code)
{ {
Scope = new List<string> { UserReadEmail, UserReadPrivate, PlaylistReadPrivate } Scope = new List<string> { UserReadEmail, UserReadPrivate, PlaylistReadPrivate }
}; };
Uri url = _server.BuildLoginUri(request); Uri uri = request.ToUri();
try try
{ {
BrowserUtil.Open(url); BrowserUtil.Open(uri);
} }
catch (Exception) catch (Exception)
{ {
Console.WriteLine("Unable to open URL, manually open: {0}", url); Console.WriteLine("Unable to open URL, manually open: {0}", uri);
} }
} }

View File

@ -1,7 +1,9 @@
<Application <local:ExampleApp x:Class="Example.UWP.App"
x:Class="UWP.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP"> xmlns:local="using:Example.UWP"
RequestedTheme="Dark">
</Application> <Application.Resources>
<x:String x:Key="WelcomeText">Hello World!</x:String>
</Application.Resources>
</local:ExampleApp>

View File

@ -1,26 +1,24 @@
using System; using System;
using System.Collections.Generic; using MvvmCross;
using System.IO; using MvvmCross.Platforms.Uap.Core;
using System.Linq; using MvvmCross.Platforms.Uap.Views;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel; using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Activation;
using Windows.Foundation; using Windows.UI.Popups;
using Windows.Foundation.Collections;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation; using Windows.UI.Xaml.Navigation;
namespace Example.UWP namespace Example.UWP
{ {
public abstract class ExampleApp : MvxApplication<MvxWindowsSetup<CoreApp>, CoreApp>
{
}
/// <summary> /// <summary>
/// Provides application-specific behavior to supplement the default Application class. /// Provides application-specific behavior to supplement the default Application class.
/// </summary> /// </summary>
sealed partial class App : Application public sealed partial class App
{ {
/// <summary> /// <summary>
/// Initializes the singleton application object. This is the first line of authored code /// Initializes the singleton application object. This is the first line of authored code
@ -28,73 +26,17 @@ namespace Example.UWP
/// </summary> /// </summary>
public App() public App()
{ {
this.InitializeComponent(); InitializeComponent();
this.Suspending += OnSuspending;
} }
/// <summary> protected override void OnActivated(IActivatedEventArgs args)
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used such as when the application is launched to open a specific file.
/// </summary>
/// <param name="e">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs e)
{ {
Frame rootFrame = Window.Current.Content as Frame; if (args.Kind == ActivationKind.Protocol)
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{ {
// Create a Frame to act as the navigation context and navigate to the first page ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs;
rootFrame = new Frame(); var publisher = Mvx.IoCProvider.Resolve<ITokenPublisherService>();
publisher.ReceiveToken(eventArgs.Uri);
rootFrame.NavigationFailed += OnNavigationFailed; }
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
}
}
/// <summary>
/// Invoked when Navigation to a certain page fails
/// </summary>
/// <param name="sender">The Frame which failed navigation</param>
/// <param name="e">Details about the navigation failure</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// Invoked when application execution is being suspended. Application state is saved
/// without knowing whether the application will be terminated or resumed with the contents
/// of memory still intact.
/// </summary>
/// <param name="sender">The source of the suspend request.</param>
/// <param name="e">Details about the suspend request.</param>
private void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
//TODO: Save application state and stop any background activity
deferral.Complete();
} }
} }
} }

View File

@ -0,0 +1,19 @@
using Example.UWP.ViewModels;
using MvvmCross.IoC;
using MvvmCross.ViewModels;
namespace Example.UWP
{
public class CoreApp : MvxApplication
{
public override void Initialize()
{
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
RegisterAppStart<LoginViewModel>();
}
}
}

View File

@ -4,11 +4,11 @@
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform> <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectGuid>{8968B88F-3A6B-4012-8449-B502C7CD658A}</ProjectGuid> <ProjectGuid>{70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}</ProjectGuid>
<OutputType>AppContainerExe</OutputType> <OutputType>AppContainerExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>UWP</RootNamespace> <RootNamespace>Example.UWP</RootNamespace>
<AssemblyName>UWP</AssemblyName> <AssemblyName>Example.UWP</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage> <DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier> <TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.18362.0</TargetPlatformVersion> <TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.18362.0</TargetPlatformVersion>
@ -119,10 +119,17 @@
<Compile Include="App.xaml.cs"> <Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon> <DependentUpon>App.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="MainPage.xaml.cs"> <Compile Include="CoreApp.cs" />
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TokenPublisherService.cs" />
<Compile Include="ViewModels\LoginViewModel.cs" />
<Compile Include="ViewModels\PlaylistsListViewModel.cs" />
<Compile Include="Views\LoginView.xaml.cs">
<DependentUpon>LoginView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PlaylistsListView.xaml.cs">
<DependentUpon>PlaylistsListView.xaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AppxManifest Include="Package.appxmanifest"> <AppxManifest Include="Package.appxmanifest">
@ -144,15 +151,30 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</ApplicationDefinition> </ApplicationDefinition>
<Page Include="MainPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform"> <PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.9</Version> <Version>6.2.9</Version>
</PackageReference> </PackageReference>
<PackageReference Include="MvvmCross">
<Version>6.4.2</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Page Include="Views\LoginView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\PlaylistsListView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\SpotifyAPI.Web\SpotifyAPI.Web.csproj">
<Project>{ec8a93ba-27c4-4642-b7a0-11b809c35be4}</Project>
<Name>SpotifyAPI.Web</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' "> <PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion> <VisualStudioVersion>14.0</VisualStudioVersion>

View File

@ -1,14 +0,0 @@
<Page
x:Class="UWP.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWP"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
</Grid>
</Page>

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace Example.UWP
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
}
}

View File

@ -7,14 +7,14 @@
IgnorableNamespaces="uap mp"> IgnorableNamespaces="uap mp">
<Identity <Identity
Name="ca23f8da-8ef6-406e-8234-ba506f52edc2" Name="ad405b02-e8d1-4972-9f4c-d0741ac5f355"
Publisher="CN=jonas" Publisher="CN=jonas"
Version="1.0.0.0" /> Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="ca23f8da-8ef6-406e-8234-ba506f52edc2" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="ad405b02-e8d1-4972-9f4c-d0741ac5f355" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties> <Properties>
<DisplayName>UWP</DisplayName> <DisplayName>Example.UWP</DisplayName>
<PublisherDisplayName>jonas</PublisherDisplayName> <PublisherDisplayName>jonas</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo> <Logo>Assets\StoreLogo.png</Logo>
</Properties> </Properties>
@ -35,11 +35,19 @@
DisplayName="Example.UWP" DisplayName="Example.UWP"
Square150x150Logo="Assets\Square150x150Logo.png" Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png" Square44x44Logo="Assets\Square44x44Logo.png"
Description="UWP" Description="Example.UWP"
BackgroundColor="transparent"> BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/> <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" /> <uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements> </uap:VisualElements>
<Extensions>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="spotifyapi.web.oauth">
<uap:Logo>Assets\StoreLogo.png</uap:Logo>
<uap:DisplayName>SpotifyAPI.Example.UWP</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application> </Application>
</Applications> </Applications>

View File

@ -5,11 +5,11 @@ using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following // General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information // set of attributes. Change these attribute values to modify the information
// associated with an assembly. // associated with an assembly.
[assembly: AssemblyTitle("UWP")] [assembly: AssemblyTitle("Example.UWP")]
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("UWP")] [assembly: AssemblyProduct("Example.UWP")]
[assembly: AssemblyCopyright("Copyright © 2020")] [assembly: AssemblyCopyright("Copyright © 2020")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Example.UWP
{
public interface ITokenPublisherService
{
event EventHandler<string> TokenReceived;
void ReceiveToken(Uri uri);
}
public class TokenPublisherService : ITokenPublisherService
{
public event EventHandler<string> TokenReceived;
public void ReceiveToken(Uri uri)
{
if(string.IsNullOrEmpty(uri.Fragment))
{
throw new Exception($"Received weird URI: {uri}");
}
var arguments = uri.Fragment.Substring(1).Split("&")
.Select(param => param.Split("="))
.ToDictionary(param => param[0], param => param[1]);
if(arguments["access_token"] == null)
{
throw new Exception($"No access token found in URI: {uri}");
}
TokenReceived?.Invoke(this, arguments["access_token"]);
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Threading.Tasks;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using SpotifyAPI.Web;
namespace Example.UWP.ViewModels
{
public class LoginViewModel : MvxViewModel, IDisposable
{
public LoginViewModel(ITokenPublisherService tokenPublisher, IMvxNavigationService navigation)
{
_tokenPublisher = tokenPublisher;
_navigation = navigation;
}
public override void ViewAppeared()
{
base.ViewAppeared();
_tokenPublisher.TokenReceived += TokenPublisher_TokenReceived;
}
public override void ViewDisappeared()
{
base.ViewDisappeared();
_tokenPublisher.TokenReceived -= TokenPublisher_TokenReceived;
}
public void Dispose()
{
_tokenPublisher.TokenReceived -= TokenPublisher_TokenReceived;
}
private readonly ITokenPublisherService _tokenPublisher;
private readonly IMvxNavigationService _navigation;
private string _titleText = "Spotify OAuth2 Login";
public string TitleText
{
get => _titleText;
set => SetProperty(ref _titleText, value);
}
private Uri _redirectUri = new Uri("spotifyapi.web.oauth://token");
public Uri RedirectUri
{
get => _redirectUri;
set => SetProperty(ref _redirectUri, value);
}
private string _clientId = "";
public string ClientId
{
get => _clientId;
set {
if(SetProperty(ref _clientId, value))
{
OpenAuthenticationPage.RaiseCanExecuteChanged();
}
}
}
private MvxCommand _openAuthenticationPage;
public MvxCommand OpenAuthenticationPage => _openAuthenticationPage ?? (_openAuthenticationPage =
new MvxCommand(
async () => await DoOpenAuthenticationPage(),
() => !string.IsNullOrEmpty(ClientId)
));
public async Task DoOpenAuthenticationPage()
{
var request = new LoginRequest(new Uri("spotifyapi.web.oauth://token"), ClientId, LoginRequest.ResponseType.Token);
await Windows.System.Launcher.LaunchUriAsync(request.ToUri());
}
private void TokenPublisher_TokenReceived(object sender, string token)
{
_navigation.Navigate<PlaylistsListViewModel, string>(token);
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MvvmCross.ViewModels;
using SpotifyAPI.Web;
namespace Example.UWP.ViewModels
{
/// <summary>
/// Spotify Token is the parameter
/// </summary>
public class PlaylistsListViewModel : MvxViewModel<string>
{
private SpotifyClient _spotify;
private List<SimplePlaylist> _playlists;
public List<SimplePlaylist> Playlists {
get => _playlists ?? (_playlists = new List<SimplePlaylist>());
set => SetProperty(ref _playlists, value);
}
public override void Prepare(string token)
{
_spotify = new SpotifyClient(SpotifyClientConfig.CreateDefault(token));
}
public override async Task Initialize()
{
await base.Initialize();
Playlists = await _spotify.Paginate(() => _spotify.Playlists.CurrentUsers());
}
}
}

View File

@ -0,0 +1,71 @@
<app:LoginViewPage x:Class="Example.UWP.Views.LoginView"
d:DataContext="{d:DesignInstance Type=models:LoginViewModel, IsDesignTimeCreatable=True}"
xmlns:app="using:Example.UWP.Views"
xmlns:models="using:Example.UWP.ViewModels"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="#353b48">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Margin="0,50"
Grid.Row="0"
TextAlignment="Center"
Text="{Binding TitleText}"
FontSize="45" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0"
Grid.Row="0"
Text="Redirect URI:"
VerticalAlignment="Top"
Margin="50,20"
FontSize="25"
HorizontalAlignment="Center" />
<TextBox Grid.Column="1"
Grid.Row="0"
Margin="20"
Text="{Binding RedirectUri, Mode=OneWay}"
IsReadOnly="True"
FontSize="25"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<TextBlock Grid.Column="0"
Grid.Row="1"
Text="Spotify Client ID:"
VerticalAlignment="Top"
Margin="50,20"
FontSize="25"
HorizontalAlignment="Center" />
<TextBox Grid.Column="1"
Grid.Row="1"
Margin="20"
FontSize="25"
Text="{Binding ClientId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
</Grid>
<Button Grid.Row="2"
Command="{Binding OpenAuthenticationPage}"
Background="#1db954"
FontSize="25"
Padding="50,20"
HorizontalAlignment="Center">
Login
</Button>
</Grid>
</app:LoginViewPage>

View File

@ -0,0 +1,26 @@
using Example.UWP.ViewModels;
using MvvmCross.Platforms.Uap.Presenters.Attributes;
using MvvmCross.Platforms.Uap.Views;
using MvvmCross.ViewModels;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238
namespace Example.UWP.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
[MvxViewFor(typeof(LoginViewModel))]
[MvxPagePresentation]
public sealed partial class LoginView : LoginViewPage
{
public LoginView()
{
InitializeComponent();
}
}
public abstract class LoginViewPage : MvxWindowsPage<LoginViewModel>
{
}
}

View File

@ -0,0 +1,37 @@
<app:PlaylistsListViewPage x:Class="Example.UWP.Views.PlaylistsListView"
d:DataContext="{d:DesignInstance Type=models:PlaylistsListViewModel, IsDesignTimeCreatable=False}"
xmlns:app="using:Example.UWP.Views"
xmlns:models="using:Example.UWP.ViewModels"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ListView x:Name="FruitsList"
Background="#353b48"
ItemsSource="{Binding Playlists}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Margin="20">
<Image.Source>
<BitmapImage UriSource="{Binding Images[0].Url}" />
</Image.Source>
</Image>
<TextBlock Text="{Binding Name}"
Margin="20"
FontSize="30"
Grid.Column="1"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</app:PlaylistsListViewPage>

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Example.UWP.ViewModels;
using MvvmCross.Platforms.Uap.Presenters.Attributes;
using MvvmCross.Platforms.Uap.Views;
using MvvmCross.ViewModels;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238
namespace Example.UWP.Views
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
[MvxViewFor(typeof(PlaylistsListViewModel))]
[MvxPagePresentation]
public sealed partial class PlaylistsListView : PlaylistsListViewPage
{
public PlaylistsListView()
{
InitializeComponent();
}
}
public abstract class PlaylistsListViewPage : MvxWindowsPage<PlaylistsListViewModel>
{
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
namespace SpotifyAPI.Web
{
public class LoginRequest
{
public LoginRequest(Uri redirectUri, string clientId, ResponseType responseType)
{
Ensure.ArgumentNotNull(redirectUri, nameof(redirectUri));
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
RedirectUri = redirectUri;
ClientId = clientId;
ResponseTypeParam = responseType;
}
public Uri RedirectUri { get; }
public ResponseType ResponseTypeParam { get; }
public string ClientId { get; }
public string State { get; set; }
public ICollection<string> Scope { get; set; }
public bool? ShowDialog { get; set; }
public Uri ToUri()
{
StringBuilder builder = new StringBuilder(SpotifyUrls.Authorize.ToString());
builder.Append($"?client_id={ClientId}");
builder.Append($"&response_type={ResponseTypeParam.ToString().ToLower()}");
builder.Append($"&redirect_uri={HttpUtility.UrlEncode(RedirectUri.ToString())}");
if (!string.IsNullOrEmpty(State))
{
builder.Append($"&state={HttpUtility.UrlEncode(State)}");
}
if (Scope != null)
{
builder.Append($"&scope={HttpUtility.UrlEncode(string.Join(" ", Scope))}");
}
if (ShowDialog != null)
{
builder.Append($"&show_dialog={ShowDialog.Value}");
}
return new Uri(builder.ToString());
}
public enum ResponseType
{
Code,
Token
}
}
}

View File

@ -20,7 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig .editorconfig = .editorconfig
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.UWP", "SpotifyAPI.Web.Examples\Example.UWP\Example.UWP.csproj", "{8968B88F-3A6B-4012-8449-B502C7CD658A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.UWP", "SpotifyAPI.Web.Examples\Example.UWP\Example.UWP.csproj", "{70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -136,32 +136,32 @@ Global
{941AB88D-B3A9-407F-BF88-BFE14B401687}.Release|x64.Build.0 = Release|Any CPU {941AB88D-B3A9-407F-BF88-BFE14B401687}.Release|x64.Build.0 = Release|Any CPU
{941AB88D-B3A9-407F-BF88-BFE14B401687}.Release|x86.ActiveCfg = Release|Any CPU {941AB88D-B3A9-407F-BF88-BFE14B401687}.Release|x86.ActiveCfg = Release|Any CPU
{941AB88D-B3A9-407F-BF88-BFE14B401687}.Release|x86.Build.0 = Release|Any CPU {941AB88D-B3A9-407F-BF88-BFE14B401687}.Release|x86.Build.0 = Release|Any CPU
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|Any CPU.ActiveCfg = Debug|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|Any CPU.ActiveCfg = Debug|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|ARM.ActiveCfg = Debug|ARM {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|ARM.ActiveCfg = Debug|ARM
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|ARM.Build.0 = Debug|ARM {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|ARM.Build.0 = Debug|ARM
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|ARM.Deploy.0 = Debug|ARM {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|ARM.Deploy.0 = Debug|ARM
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|ARM64.ActiveCfg = Debug|ARM64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|ARM64.ActiveCfg = Debug|ARM64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|ARM64.Build.0 = Debug|ARM64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|ARM64.Build.0 = Debug|ARM64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|ARM64.Deploy.0 = Debug|ARM64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|ARM64.Deploy.0 = Debug|ARM64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|x64.ActiveCfg = Debug|x64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|x64.ActiveCfg = Debug|x64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|x64.Build.0 = Debug|x64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|x64.Build.0 = Debug|x64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|x64.Deploy.0 = Debug|x64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|x64.Deploy.0 = Debug|x64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|x86.ActiveCfg = Debug|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|x86.ActiveCfg = Debug|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|x86.Build.0 = Debug|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|x86.Build.0 = Debug|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Debug|x86.Deploy.0 = Debug|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Debug|x86.Deploy.0 = Debug|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|Any CPU.ActiveCfg = Release|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|Any CPU.ActiveCfg = Release|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|ARM.ActiveCfg = Release|ARM {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|ARM.ActiveCfg = Release|ARM
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|ARM.Build.0 = Release|ARM {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|ARM.Build.0 = Release|ARM
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|ARM.Deploy.0 = Release|ARM {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|ARM.Deploy.0 = Release|ARM
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|ARM64.ActiveCfg = Release|ARM64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|ARM64.ActiveCfg = Release|ARM64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|ARM64.Build.0 = Release|ARM64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|ARM64.Build.0 = Release|ARM64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|ARM64.Deploy.0 = Release|ARM64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|ARM64.Deploy.0 = Release|ARM64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|x64.ActiveCfg = Release|x64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|x64.ActiveCfg = Release|x64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|x64.Build.0 = Release|x64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|x64.Build.0 = Release|x64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|x64.Deploy.0 = Release|x64 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|x64.Deploy.0 = Release|x64
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|x86.ActiveCfg = Release|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|x86.ActiveCfg = Release|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|x86.Build.0 = Release|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|x86.Build.0 = Release|x86
{8968B88F-3A6B-4012-8449-B502C7CD658A}.Release|x86.Deploy.0 = Release|x86 {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -169,7 +169,7 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{F4ECE937-99F2-4C4F-9F5C-4AB875D9538A} = {48A7DE65-29BB-409C-AC45-77F6586C0B15} {F4ECE937-99F2-4C4F-9F5C-4AB875D9538A} = {48A7DE65-29BB-409C-AC45-77F6586C0B15}
{941AB88D-B3A9-407F-BF88-BFE14B401687} = {48A7DE65-29BB-409C-AC45-77F6586C0B15} {941AB88D-B3A9-407F-BF88-BFE14B401687} = {48A7DE65-29BB-409C-AC45-77F6586C0B15}
{8968B88F-3A6B-4012-8449-B502C7CD658A} = {48A7DE65-29BB-409C-AC45-77F6586C0B15} {70D529B5-CEC0-4D3D-B50D-8ECF921DEC70} = {48A7DE65-29BB-409C-AC45-77F6586C0B15}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {097062B8-0E87-43C8-BD98-61843A68BE6D} SolutionGuid = {097062B8-0E87-43C8-BD98-61843A68BE6D}