├── XpoMusic
├── Stylesheets
│ ├── DarkTheme
│ │ └── dark.scss
│ ├── Common
│ │ ├── searchPage.scss
│ │ ├── windowTitle.scss
│ │ ├── themedScrollbar.scss
│ │ ├── Animation
│ │ │ ├── searchPage.scss
│ │ │ ├── tracklistEntrance.scss
│ │ │ ├── pivotEntrance.scss
│ │ │ ├── navbarStartup.scss
│ │ │ ├── nowPlayingStartup.scss
│ │ │ ├── pageEntrance.scss
│ │ │ └── artistPage.scss
│ │ ├── adsContainer.scss
│ │ ├── contextMenu.scss
│ │ ├── backButton.scss
│ │ ├── common.scss
│ │ ├── artistPage.scss
│ │ ├── trackList.scss
│ │ ├── fontStyle.scss
│ │ ├── navBar.scss
│ │ ├── nowPlayingBar.scss
│ │ └── pivotTabStyle.scss
│ └── LightTheme
│ │ ├── windowTitle.scss
│ │ ├── contextMenu.scss
│ │ ├── progressBar.scss
│ │ ├── trackList.scss
│ │ ├── pivotTabStyle.scss
│ │ ├── podcastEpisodeDetails.scss
│ │ ├── searchPage.scss
│ │ ├── nowPlayingBar.scss
│ │ ├── navBar.scss
│ │ ├── light.scss
│ │ └── artistPage.scss
├── Assets
│ ├── xpotify.png
│ ├── Media
│ │ └── silent.wav
│ ├── xpotify-white.png
│ ├── TransparentSquare.png
│ ├── Logo
│ │ ├── SplashScreen.scale-100.png
│ │ ├── SplashScreen.scale-125.png
│ │ ├── SplashScreen.scale-150.png
│ │ ├── SplashScreen.scale-200.png
│ │ ├── SplashScreen.scale-400.png
│ │ ├── contrast-black
│ │ │ ├── LargeTile.scale-100.png
│ │ │ ├── LargeTile.scale-125.png
│ │ │ ├── LargeTile.scale-150.png
│ │ │ ├── LargeTile.scale-200.png
│ │ │ ├── LargeTile.scale-400.png
│ │ │ ├── SmallTile.scale-100.png
│ │ │ ├── SmallTile.scale-125.png
│ │ │ ├── SmallTile.scale-150.png
│ │ │ ├── SmallTile.scale-200.png
│ │ │ ├── SmallTile.scale-400.png
│ │ │ ├── StoreLogo.scale-100.png
│ │ │ ├── StoreLogo.scale-125.png
│ │ │ ├── StoreLogo.scale-150.png
│ │ │ ├── StoreLogo.scale-200.png
│ │ │ ├── StoreLogo.scale-400.png
│ │ │ ├── Square44x44Logo.scale-100.png
│ │ │ ├── Square44x44Logo.scale-125.png
│ │ │ ├── Square44x44Logo.scale-150.png
│ │ │ ├── Square44x44Logo.scale-200.png
│ │ │ ├── Square44x44Logo.scale-400.png
│ │ │ ├── Wide310x150Logo.scale-100.png
│ │ │ ├── Wide310x150Logo.scale-125.png
│ │ │ ├── Wide310x150Logo.scale-150.png
│ │ │ ├── Wide310x150Logo.scale-200.png
│ │ │ ├── Wide310x150Logo.scale-400.png
│ │ │ ├── Square150x150Logo.scale-100.png
│ │ │ ├── Square150x150Logo.scale-125.png
│ │ │ ├── Square150x150Logo.scale-150.png
│ │ │ ├── Square150x150Logo.scale-200.png
│ │ │ ├── Square150x150Logo.scale-400.png
│ │ │ ├── Square44x44Logo.targetsize-16.png
│ │ │ ├── Square44x44Logo.targetsize-24.png
│ │ │ ├── Square44x44Logo.targetsize-256.png
│ │ │ ├── Square44x44Logo.targetsize-32.png
│ │ │ ├── Square44x44Logo.targetsize-48.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-24.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-48.png
│ │ │ └── Square44x44Logo.altform-unplated_targetsize-256.png
│ │ ├── contrast-white
│ │ │ ├── LargeTile.scale-100.png
│ │ │ ├── LargeTile.scale-125.png
│ │ │ ├── LargeTile.scale-150.png
│ │ │ ├── LargeTile.scale-200.png
│ │ │ ├── LargeTile.scale-400.png
│ │ │ ├── SmallTile.scale-100.png
│ │ │ ├── SmallTile.scale-125.png
│ │ │ ├── SmallTile.scale-150.png
│ │ │ ├── SmallTile.scale-200.png
│ │ │ ├── SmallTile.scale-400.png
│ │ │ ├── StoreLogo.scale-100.png
│ │ │ ├── StoreLogo.scale-125.png
│ │ │ ├── StoreLogo.scale-150.png
│ │ │ ├── StoreLogo.scale-200.png
│ │ │ ├── StoreLogo.scale-400.png
│ │ │ ├── Square44x44Logo.scale-100.png
│ │ │ ├── Square44x44Logo.scale-125.png
│ │ │ ├── Square44x44Logo.scale-150.png
│ │ │ ├── Square44x44Logo.scale-200.png
│ │ │ ├── Square44x44Logo.scale-400.png
│ │ │ ├── Wide310x150Logo.scale-100.png
│ │ │ ├── Wide310x150Logo.scale-125.png
│ │ │ ├── Wide310x150Logo.scale-150.png
│ │ │ ├── Wide310x150Logo.scale-200.png
│ │ │ ├── Wide310x150Logo.scale-400.png
│ │ │ ├── Square150x150Logo.scale-100.png
│ │ │ ├── Square150x150Logo.scale-125.png
│ │ │ ├── Square150x150Logo.scale-150.png
│ │ │ ├── Square150x150Logo.scale-200.png
│ │ │ ├── Square150x150Logo.scale-400.png
│ │ │ ├── Square44x44Logo.targetsize-16.png
│ │ │ ├── Square44x44Logo.targetsize-24.png
│ │ │ ├── Square44x44Logo.targetsize-256.png
│ │ │ ├── Square44x44Logo.targetsize-32.png
│ │ │ ├── Square44x44Logo.targetsize-48.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-24.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-48.png
│ │ │ └── Square44x44Logo.altform-unplated_targetsize-256.png
│ │ └── contrast-standard
│ │ │ ├── LargeTile.scale-100.png
│ │ │ ├── LargeTile.scale-125.png
│ │ │ ├── LargeTile.scale-150.png
│ │ │ ├── LargeTile.scale-200.png
│ │ │ ├── LargeTile.scale-400.png
│ │ │ ├── SmallTile.scale-100.png
│ │ │ ├── SmallTile.scale-125.png
│ │ │ ├── SmallTile.scale-150.png
│ │ │ ├── SmallTile.scale-200.png
│ │ │ ├── SmallTile.scale-400.png
│ │ │ ├── StoreLogo.scale-100.png
│ │ │ ├── StoreLogo.scale-125.png
│ │ │ ├── StoreLogo.scale-150.png
│ │ │ ├── StoreLogo.scale-200.png
│ │ │ ├── StoreLogo.scale-400.png
│ │ │ ├── Square44x44Logo.scale-100.png
│ │ │ ├── Square44x44Logo.scale-125.png
│ │ │ ├── Square44x44Logo.scale-150.png
│ │ │ ├── Square44x44Logo.scale-200.png
│ │ │ ├── Square44x44Logo.scale-400.png
│ │ │ ├── Wide310x150Logo.scale-100.png
│ │ │ ├── Wide310x150Logo.scale-125.png
│ │ │ ├── Wide310x150Logo.scale-150.png
│ │ │ ├── Wide310x150Logo.scale-200.png
│ │ │ ├── Wide310x150Logo.scale-400.png
│ │ │ ├── Square150x150Logo.scale-100.png
│ │ │ ├── Square150x150Logo.scale-125.png
│ │ │ ├── Square150x150Logo.scale-150.png
│ │ │ ├── Square150x150Logo.scale-200.png
│ │ │ ├── Square150x150Logo.scale-400.png
│ │ │ ├── Square44x44Logo.targetsize-16.png
│ │ │ ├── Square44x44Logo.targetsize-24.png
│ │ │ ├── Square44x44Logo.targetsize-256.png
│ │ │ ├── Square44x44Logo.targetsize-32.png
│ │ │ ├── Square44x44Logo.targetsize-48.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-24.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-256.png
│ │ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png
│ │ │ └── Square44x44Logo.altform-unplated_targetsize-48.png
│ └── TileTemplates
│ │ ├── LiveTileAlbumArtOnly.xml
│ │ ├── LiveTileArtistArtOnly.xml
│ │ └── LiveTileAlbumAndArtistArt.xml
├── InjectedAssets
│ ├── isLoggedInCheck.js
│ ├── clickOnFacebookLogin.js
│ └── clearPlaybackLocalStorage.js
├── SpotifyApi
│ ├── Model
│ │ ├── Image.cs
│ │ ├── ArtistSimplified.cs
│ │ ├── Artist.cs
│ │ ├── Track.cs
│ │ ├── Devices.cs
│ │ ├── AlbumSimplified.cs
│ │ ├── FollowedArtistsResponse.cs
│ │ ├── SavedAlbum.cs
│ │ ├── CurrentlyPlayingContext.cs
│ │ ├── Playlist.cs
│ │ ├── Device.cs
│ │ └── Paging.cs
│ ├── Artist.cs
│ ├── Album.cs
│ ├── Playlist.cs
│ └── Authorization.cs
├── Flyouts
│ ├── IFlyout.cs
│ ├── SettingsFlyout.xaml
│ ├── SettingsFlyout.xaml.cs
│ ├── DeveloperMessageFlyout.xaml
│ └── WhatsNewFlyout.xaml.cs
├── Classes
│ ├── Model
│ │ ├── Theme.cs
│ │ ├── LocalStoragePlayback.cs
│ │ ├── AutoPlayAction.cs
│ │ ├── SongExtraInfo.cs
│ │ ├── WebResourceModifications
│ │ │ ├── WebResourceModificationRuleType.cs
│ │ │ ├── WebResourceStringModificationRule.cs
│ │ │ └── WebResourceModificationRule.cs
│ │ ├── Language.cs
│ │ └── LyricsViewer
│ │ │ └── CurrentPlayingSongInfo.cs
│ ├── Cache
│ │ ├── GlobalCache.cs
│ │ ├── ArtistStore.cs
│ │ ├── AlbumStore.cs
│ │ ├── PlaylistStore.cs
│ │ ├── SongExtraInfoStore.cs
│ │ └── CacheStore.cs
│ ├── Converters
│ │ ├── ThemeToStringConverter.cs
│ │ ├── LanguageToStringConverter.cs
│ │ ├── MillisecondsToMinSecConverter.cs
│ │ ├── BooleanToVisibilityConverter.cs
│ │ └── LiveTileDesignToStringConverter.cs
│ └── SemaphoreQueue.cs
├── Scripts
│ ├── Common
│ │ ├── browserHistory.ts
│ │ ├── web-player-backup.ts
│ │ ├── dragDrop.ts
│ │ ├── mouseWheelListener.ts
│ │ ├── resize.ts
│ │ ├── uiInjector.ts
│ │ ├── uriHelper.ts
│ │ ├── startupAnimation.ts
│ │ ├── requestIntercepter.ts
│ │ ├── playbackStuckHelper.ts
│ │ ├── pageTitleFinder.ts
│ │ └── indeterminateProgressBarHandler.ts
│ ├── DarkTheme
│ │ ├── tsconfig.json
│ │ └── initScript-dark.ts
│ ├── LightTheme
│ │ ├── tsconfig.json
│ │ ├── pageOverlay.ts
│ │ └── initScript-light.ts
│ └── SpotifyApi
│ │ ├── player.ts
│ │ ├── apiBase.ts
│ │ └── library.ts
├── compilerconfig.json
├── XpotifyApi
│ ├── Model
│ │ ├── DeveloperMessageCollection.cs
│ │ ├── AssetPackageInfo.cs
│ │ ├── AssetPackage.cs
│ │ └── DeveloperMessage.cs
│ ├── DeveloperMessageApi.cs
│ └── AppConstantsApi.cs
├── ViewModels
│ ├── ViewModelBase.cs
│ ├── MainPageViewModel.cs
│ └── TopBarViewModel.cs
├── Helpers
│ ├── StoryboardExtensions.cs
│ ├── UnauthorizedHelper.cs
│ ├── JumpListHelper.cs
│ ├── DeviceInfoHelper.cs
│ ├── ThemeHelper.cs
│ ├── DeveloperMessageHelper.cs
│ ├── ClipboardHelper.cs
│ ├── PackageHelper.cs
│ ├── PlaybackActionHelper.cs
│ ├── SongImageProvider.cs
│ ├── ToastHelper.cs
│ └── WhatsNewHelper.cs
├── NLog.config
├── App.xaml
├── Pages
│ ├── SettingsPage.xaml.cs
│ ├── HelpPage.xaml
│ ├── DonatePage.xaml.cs
│ └── DonatePage.xaml
├── Properties
│ ├── AssemblyInfo.cs
│ └── Default.rd.xml
├── compilerconfig.json.defaults
├── Controls
│ ├── ProxyConfiguration.xaml.cs
│ └── SplashScreen.xaml.cs
├── Package.appxmanifest
└── Package.debug.appxmanifest
├── XpoMusicInstaller
├── Resources
│ └── XpoMusic.cer
├── XpoMusicInstallerLogo.ico
├── Assets
│ └── XpoMusicInstallerLogo.png
├── App.config
├── Properties
│ ├── Settings.settings
│ ├── Settings.Designer.cs
│ └── AssemblyInfo.cs
├── App.xaml
└── App.xaml.cs
├── XpoMusicWebAgent
├── Model
│ ├── InitFailedEventArgs.cs
│ ├── LogMessageReceivedEventArgs.cs
│ ├── StatusReportReceivedEventArgs.cs
│ ├── WebAppStatus.cs
│ ├── ActionRequestedEventArgs.cs
│ ├── ProgressBarCommandEventArgs.cs
│ └── NowPlayingData.cs
└── Properties
│ └── AssemblyInfo.cs
├── Preview.md
├── README.md
└── .gitattributes
/XpoMusic/Stylesheets/DarkTheme/dark.scss:
--------------------------------------------------------------------------------
1 | @import "../Common/common.scss";
2 |
--------------------------------------------------------------------------------
/XpoMusic/Assets/xpotify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/xpotify.png
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/searchPage.scss:
--------------------------------------------------------------------------------
1 |
2 | #searchPage h2 {
3 | font-weight: 100;
4 | }
5 |
6 |
7 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/windowTitle.scss:
--------------------------------------------------------------------------------
1 |
2 | .xpotifyWindowTitle {
3 | font-weight: 300;
4 | }
5 |
--------------------------------------------------------------------------------
/XpoMusic/Assets/Media/silent.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Media/silent.wav
--------------------------------------------------------------------------------
/XpoMusic/Assets/xpotify-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/xpotify-white.png
--------------------------------------------------------------------------------
/XpoMusic/InjectedAssets/isLoggedInCheck.js:
--------------------------------------------------------------------------------
1 | (document.querySelectorAll('[data-testid=signup-bar]').length === 0) ? "1" : "0"
--------------------------------------------------------------------------------
/XpoMusic/Assets/TransparentSquare.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/TransparentSquare.png
--------------------------------------------------------------------------------
/XpoMusicInstaller/Resources/XpoMusic.cer:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusicInstaller/Resources/XpoMusic.cer
--------------------------------------------------------------------------------
/XpoMusicInstaller/XpoMusicInstallerLogo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusicInstaller/XpoMusicInstallerLogo.ico
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/SplashScreen.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/SplashScreen.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/SplashScreen.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/SplashScreen.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/SplashScreen.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/SplashScreen.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/SplashScreen.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/SplashScreen.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/SplashScreen.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/SplashScreen.scale-400.png
--------------------------------------------------------------------------------
/XpoMusicInstaller/Assets/XpoMusicInstallerLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusicInstaller/Assets/XpoMusicInstallerLogo.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/LargeTile.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/SmallTile.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/StoreLogo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/LargeTile.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/SmallTile.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/StoreLogo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/LargeTile.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/SmallTile.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/StoreLogo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Wide310x150Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Wide310x150Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square150x150Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Wide310x150Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square150x150Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-16.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-24.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-256.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-32.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.targetsize-48.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-100.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-125.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-150.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square150x150Logo.scale-400.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-16.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-24.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-256.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-32.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.targetsize-48.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-16.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-24.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-256.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-32.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.targetsize-48.png
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Image.cs:
--------------------------------------------------------------------------------
1 | namespace XpoMusic.SpotifyApi.Model
2 | {
3 | public class Image
4 | {
5 | public string url;
6 | public int? height;
7 | public int? width;
8 | }
9 | }
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-16.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-24.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-32.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-48.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-16.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-24.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-32.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-48.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-black/Square44x44Logo.altform-unplated_targetsize-256.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-white/Square44x44Logo.altform-unplated_targetsize-256.png
--------------------------------------------------------------------------------
/XpoMusicInstaller/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-16.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-24.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-256.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-32.png
--------------------------------------------------------------------------------
/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MahdiGhiasi/XpoMusic/HEAD/XpoMusic/Assets/Logo/contrast-standard/Square44x44Logo.altform-unplated_targetsize-48.png
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/contextMenu.scss:
--------------------------------------------------------------------------------
1 | .react-contextmenu.react-contextmenu--visible {
2 | /* Context menu shadow should be dark in light mode as well */
3 | box-shadow: 0 0 28px 4px rgba(255,255,255,0.2) !important;
4 | }
5 |
--------------------------------------------------------------------------------
/XpoMusicInstaller/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/ArtistSimplified.cs:
--------------------------------------------------------------------------------
1 | namespace XpoMusic.SpotifyApi.Model
2 | {
3 | public class ArtistSimplified
4 | {
5 | public string href;
6 | public string id;
7 | public string name;
8 | public string uri;
9 | }
10 | }
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/progressBar.scss:
--------------------------------------------------------------------------------
1 | .progress-bar .progress-bar__fg {
2 | transition-duration: 0s;
3 | background-color: #b3b3b3;
4 | }
5 |
6 | .progress-bar--is-active .progress-bar__fg {
7 | background-color: #1db954;
8 | filter: invert(1);
9 | }
10 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Artist.cs:
--------------------------------------------------------------------------------
1 | namespace XpoMusic.SpotifyApi.Model
2 | {
3 | public class Artist
4 | {
5 | public string href;
6 | public string id;
7 | public string name;
8 | public string uri;
9 | public Image[] images;
10 | }
11 | }
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/trackList.scss:
--------------------------------------------------------------------------------
1 |
2 | .tracklistSongExistsInLibrary .trackListRemoveSongButton {
3 | color: #E246AB;
4 | }
5 |
6 | .tracklist-row .more {
7 | filter: invert(1);
8 | }
9 |
10 | .TrackListHeader__actions > button {
11 | filter: invert(1);
12 | }
13 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/pivotTabStyle.scss:
--------------------------------------------------------------------------------
1 | .Root__main-view nav ul li a[aria-current=page], .Root__top-bar nav ul li a[aria-current=page] {
2 | border-bottom-color: #E246AB;
3 | }
4 |
5 | .Root__main-view .artist nav ul li a[aria-current=page] {
6 | border-bottom-color: #1DB954;
7 | }
8 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/podcastEpisodeDetails.scss:
--------------------------------------------------------------------------------
1 | .ad6f7074db563f98f848cc3ffdbd8190\.scss img, .ad6f7074db563f98f848cc3ffdbd8190\.scss button {
2 | filter: invert(1);
3 | }
4 |
5 | .ad6f7074db563f98f848cc3ffdbd8190\.scss button._81a88e0d61622b328f181589e4074b12\.scss {
6 | filter: invert(0);
7 | }
8 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Track.cs:
--------------------------------------------------------------------------------
1 | namespace XpoMusic.SpotifyApi.Model
2 | {
3 | public class Track
4 | {
5 | public int duration_ms;
6 | public string id;
7 | public string name;
8 | public ArtistSimplified[] artists;
9 | public AlbumSimplified album;
10 | }
11 | }
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Devices.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class Devices
10 | {
11 | public Device[] devices;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/Flyouts/IFlyout.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Flyouts
8 | {
9 | internal interface IFlyout
10 | {
11 | event EventHandler FlyoutCloseRequest;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/Theme.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model
8 | {
9 | public enum Theme
10 | {
11 | Dark,
12 | Light,
13 | System,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/LocalStoragePlayback.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model
8 | {
9 | public class LocalStoragePlayback
10 | {
11 | public double volume;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/windowTitle.scss:
--------------------------------------------------------------------------------
1 | /* Window title */
2 | .xpotifyWindowTitle {
3 | position: fixed;
4 | font-family: Segoe UI;
5 | font-weight: 400;
6 | padding-top: 5px;
7 | padding-left: 54px;
8 | color: white;
9 | font-size: 12px;
10 | pointer-events: none;
11 | z-index: 100;
12 | }
13 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/searchPage.scss:
--------------------------------------------------------------------------------
1 |
2 | #searchPage div[role=search] {
3 | filter: invert(1);
4 | }
5 |
6 | #searchPage img {
7 | filter: invert(1);
8 | }
9 |
10 | #searchPage a[href^="/genre"] {
11 | filter: invert(1);
12 | }
13 |
14 | #searchPage a[href^="/genre"] img {
15 | filter: invert(0);
16 | }
17 |
--------------------------------------------------------------------------------
/XpoMusic/InjectedAssets/clickOnFacebookLogin.js:
--------------------------------------------------------------------------------
1 |
2 | function pushFacebookButton() {
3 | var facebookButton = document.querySelectorAll("a.btn-facebook");
4 |
5 | if (facebookButton.length > 0) {
6 | facebookButton[0].click();
7 | return "1";
8 | }
9 |
10 | return "0";
11 | }
12 |
13 | pushFacebookButton();
14 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/AutoPlayAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model
8 | {
9 | public enum AutoPlayAction
10 | {
11 | None,
12 | Track,
13 | Playlist,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/browserHistory.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.BrowserHistory {
2 | export function canGoBack() {
3 | return window.location.hash !== "#xpotifyInitialPage";
4 | }
5 |
6 | export function goBack() {
7 | if (canGoBack()) {
8 | window.history.go(-1);
9 | }
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/AlbumSimplified.cs:
--------------------------------------------------------------------------------
1 | namespace XpoMusic.SpotifyApi.Model
2 | {
3 | public class AlbumSimplified
4 | {
5 | public ArtistSimplified[] artists;
6 | public string href;
7 | public string id;
8 | public Image[] images;
9 | public string name;
10 | public string uri;
11 | }
12 | }
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/InitFailedEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class InitFailedEventArgs
10 | {
11 | public string Errors { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/compilerconfig.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "outputFile": "InjectedAssets/style-light.css",
4 | "inputFile": "Stylesheets/LightTheme/light.scss",
5 | "includeInProject": true
6 | },
7 | {
8 | "outputFile": "InjectedAssets/style-dark.css",
9 | "inputFile": "Stylesheets/DarkTheme/dark.scss",
10 | "includeInProject": true
11 | }
12 | ]
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/FollowedArtistsResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class FollowedArtistsResponse
10 | {
11 | public Paging artists { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/nowPlayingBar.scss:
--------------------------------------------------------------------------------
1 | .now-playing-bar-container {
2 | background-color: rgba(30, 33, 40, 0.65);
3 | }
4 |
5 | .connect-device-list-item--active .connect-device-list-item__icon,
6 | .connect-device-list-item--active .connect-device-list-item__info,
7 | .connect-device-list-content .btn-blue {
8 | filter: invert(1);
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/LogMessageReceivedEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class LogMessageReceivedEventArgs
10 | {
11 | public string Message { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/navBar.scss:
--------------------------------------------------------------------------------
1 | .Root__nav-bar {
2 | background-color: rgba(40, 40, 40, 0.6) !important;
3 | backdrop-filter: blur(20px);
4 | -webkit-backdrop-filter: blur(20px);
5 | }
6 |
7 | .RootlistItem--is-active:before {
8 | background-color: #E246AB;
9 | }
10 |
11 | .navBar-link--active:before {
12 | background-color: #E246AB;
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/XpotifyApi/Model/DeveloperMessageCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.XpotifyApi.Model
8 | {
9 | public class DeveloperMessageCollection
10 | {
11 | public DeveloperMessage[] messages { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/StatusReportReceivedEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class StatusReportReceivedEventArgs
10 | {
11 | public WebAppStatus Status { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/SongExtraInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model
8 | {
9 | public class SongExtraInfo
10 | {
11 | public string SongId { get; set; }
12 | public bool IsSavedToLibrary { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/SavedAlbum.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class SavedAlbum
10 | {
11 | public string added_at { get; set; }
12 | public AlbumSimplified album { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/XpoMusic/XpotifyApi/Model/AssetPackageInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.XpotifyApi.Model
8 | {
9 | public class AssetPackageInfo
10 | {
11 | public int version { get; set; }
12 | public string downloadUri { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/WebAppStatus.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class WebAppStatus
10 | {
11 | public bool BackButtonEnabled { get; set; }
12 | public NowPlayingData NowPlaying { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/WebResourceModifications/WebResourceModificationRuleType.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model.WebResourceModifications
8 | {
9 | public enum WebResourceModificationRuleType
10 | {
11 | ReplaceWholeFile = 1,
12 | ModifyString = 2,
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/light.scss:
--------------------------------------------------------------------------------
1 | @import "../Common/common.scss";
2 | @import "general.scss";
3 | @import "artistPage.scss";
4 | @import "contextMenu.scss";
5 | @import "navBar.scss";
6 | @import "nowPlayingBar.scss";
7 | @import "progressBar.scss";
8 | @import "windowTitle.scss";
9 | @import "podcastEpisodeDetails.scss";
10 | @import "searchPage.scss";
11 | @import "trackList.scss";
12 | @import "pivotTabStyle.scss";
13 |
--------------------------------------------------------------------------------
/XpoMusic/XpotifyApi/Model/AssetPackage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.Storage;
7 |
8 | namespace XpoMusic.XpotifyApi.Model
9 | {
10 | public class AssetPackage
11 | {
12 | public AssetPackageInfo AssetPackageInfo { get; set; }
13 | public StorageFile File { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/XpoMusicInstaller/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/XpoMusicInstaller/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 |
9 | namespace XpoMusicInstaller
10 | {
11 | ///
12 | /// Interaction logic for App.xaml
13 | ///
14 | public partial class App : Application
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/DarkTheme/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "noImplicitAny": false,
5 | "noEmitOnError": true,
6 | "removeComments": false,
7 | "sourceMap": true,
8 | "module": "system",
9 | "outFile": "../../InjectedAssets/init-dark.js",
10 | "target": "es6"
11 | },
12 | "include": [
13 | "*.ts",
14 | "../Common/*.ts",
15 | "../Lib/*.ts",
16 | "../SpotifyApi/*.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/LightTheme/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": true,
3 | "compilerOptions": {
4 | "noImplicitAny": false,
5 | "noEmitOnError": true,
6 | "removeComments": false,
7 | "sourceMap": true,
8 | "module": "system",
9 | "outFile": "../../InjectedAssets/init-light.js",
10 | "target": "es6"
11 | },
12 | "include": [
13 | "*.ts",
14 | "../Common/*.ts",
15 | "../Lib/*.ts",
16 | "../SpotifyApi/*.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/web-player-backup.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.WebPlayerBackup {
2 |
3 | declare var XpoMusic: any;
4 |
5 | export function runWebPlayerBackup() {
6 | if (document.querySelectorAll("#main").length > 0) {
7 | XpoMusic.log("runWebPlayerBackup requested but #main already exists.");
8 | return;
9 | }
10 | XpoMusic.log("web-player-backup is not present in the current script version.");
11 | }
12 | }
--------------------------------------------------------------------------------
/XpoMusic/Scripts/SpotifyApi/player.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace XpoMusicScript.SpotifyApi {
4 | export class Player extends ApiBase {
5 | public async getCurrentlyPlaying(): Promise {
6 | var url = 'https://api.spotify.com/v1/me/player';
7 | var result = await this.sendJsonRequestWithToken(url, 'get');
8 |
9 | var data = await result.json();
10 | return data;
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/CurrentlyPlayingContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class CurrentlyPlayingContext
10 | {
11 | public int progress_ms;
12 | public bool is_playing;
13 | public string currently_playing_type;
14 | public Track item;
15 | public Device device;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/XpoMusic/InjectedAssets/clearPlaybackLocalStorage.js:
--------------------------------------------------------------------------------
1 |
2 | // This script works around a bug of Spotify PWA in Microsoft Edge,
3 | // where volume is reset to the initial value in LocalStorage every few
4 | // minutes, by clearing the LocalStorage.playback before opening the PWA
5 |
6 | function clearPlaybackLocalStorage() {
7 | var value = window.localStorage.playback;
8 | window.localStorage.removeItem('playback');
9 |
10 | return value;
11 | }
12 |
13 |
14 | clearPlaybackLocalStorage();
15 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Cache/GlobalCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Cache
8 | {
9 | public static class GlobalCache
10 | {
11 | public static ArtistStore Artist { get; } = new ArtistStore();
12 | public static AlbumStore Album { get; } = new AlbumStore();
13 | public static PlaylistStore Playlist { get; } = new PlaylistStore();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/themedScrollbar.scss:
--------------------------------------------------------------------------------
1 |
2 | .xpo-scroll-container {
3 | overflow-y: auto;
4 | overflow-x: hidden;
5 | -ms-overflow-style: none;
6 | }
7 |
8 | .xpo-extra-scrollbar-fixed {
9 | position: absolute;
10 | top: 0px;
11 | right: 0px;
12 | bottom: 0px;
13 | width: 20px;
14 | /*-ms-overflow-style: -ms-autohiding-scrollbar;*/
15 | -ms-overflow-style: scrollbar;
16 | filter: invert(1) /*brightness(1.25)*/;
17 | transform: translate3d(0, 0, 0);
18 | }
19 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Playlist.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class Playlist
10 | {
11 | public bool collaborative;
12 | public string description;
13 | public string href;
14 | public string id;
15 | public Image[] images;
16 | public string name;
17 | public string uri;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/searchPage.scss:
--------------------------------------------------------------------------------
1 |
2 | @for $i from 1 through 5 {
3 | #searchPage section:nth-child(#{$i}) {
4 | opacity: 0;
5 | animation: animation-entrance-search-section 0.3s ease #{($i - 1) * 0.1}s forwards;
6 | }
7 | }
8 |
9 | @keyframes animation-entrance-search-section {
10 | 0% {
11 | opacity: 0;
12 | transform: translateY(32px);
13 | }
14 |
15 | 100% {
16 | opacity: 1;
17 | transform: translateY(0);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/DarkTheme/initScript-dark.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace XpoMusicScript {
4 |
5 | document.getElementsByTagName('body')[0].setAttribute('data-xpotifyTheme', 'dark');
6 |
7 | var errors = "";
8 |
9 | errors += Common.init();
10 |
11 | if (errors.length > 0) {
12 | try {
13 | // @ts-ignore
14 | XpoMusic.initFailed(errors);
15 | }
16 | catch (ex) { }
17 |
18 | throw errors;
19 | }
20 | }
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Device.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class Device
10 | {
11 | public string id;
12 | public bool is_active;
13 | public bool is_private_session;
14 | public bool is_restricted;
15 | public string name;
16 | public string type;
17 | public int? volume_percent;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Cache/ArtistStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Cache
8 | {
9 | public class ArtistStore : CacheStore
10 | {
11 | protected override Task RetrieveItem(string key)
12 | {
13 | var artist = new SpotifyApi.Artist();
14 | return artist.GetArtist(key);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Cache/AlbumStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Cache
8 | {
9 | public class AlbumStore : CacheStore
10 | {
11 | protected override Task RetrieveItem(string key)
12 | {
13 | var album = new SpotifyApi.Album();
14 | return album.GetAlbum(key);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Cache/PlaylistStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Cache
8 | {
9 | public class PlaylistStore : CacheStore
10 | {
11 | protected override Task RetrieveItem(string key)
12 | {
13 | var playlist = new SpotifyApi.Playlist();
14 | return playlist.GetPlaylist(key);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/LightTheme/pageOverlay.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Light.PageOverlay {
2 |
3 | export function createPageOverlay() {
4 | try {
5 | var body = document.getElementsByTagName('body')[0];
6 | var overlayDiv = document.createElement('div');
7 | overlayDiv.classList.add("whole-page-overlay");
8 | body.appendChild(overlayDiv);
9 | }
10 | catch (ex) {
11 | return "injectOverlayFailed,";
12 | }
13 | return "";
14 | }
15 | }
--------------------------------------------------------------------------------
/XpoMusic/XpotifyApi/Model/DeveloperMessage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.XpotifyApi.Model
8 | {
9 | public class DeveloperMessage
10 | {
11 | public string id { get; set; }
12 | public string title { get; set; }
13 | public string content { get; set; }
14 | public long timestamp { get; set; }
15 | public DateTime messageDate => DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime.ToLocalTime();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/ActionRequestedEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class ActionRequestedEventArgs
10 | {
11 | public Action Action { get; set; }
12 | }
13 |
14 | public enum Action
15 | {
16 | PinToStart,
17 | OpenSettings,
18 | OpenDonate,
19 | OpenAbout,
20 | OpenMiniView,
21 | OpenNowPlaying,
22 | NavigateToClipboardUri,
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/dragDrop.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace XpoMusicScript.Common.DragDrop {
4 |
5 | export function allowDrop(event) {
6 | event.preventDefault();
7 | }
8 |
9 | export function drop(event) {
10 | var data = event.dataTransfer.getData("Text");
11 | var uri = UriHelper.getPwaUri(data);
12 |
13 | if (uri === undefined || uri.length === 0) {
14 | return;
15 | }
16 |
17 | event.preventDefault();
18 |
19 | // Navigate to page
20 | Action.navigateToPage(uri);
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/XpoMusic/ViewModels/ViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace XpoMusic.ViewModels
9 | {
10 | public abstract class ViewModelBase : INotifyPropertyChanged
11 | {
12 | public event PropertyChangedEventHandler PropertyChanged;
13 |
14 | protected void FirePropertyChangedEvent(string propertyName)
15 | {
16 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/ProgressBarCommandEventArgs.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class ProgressBarCommandEventArgs
10 | {
11 | public ProgressBarCommand Command { get; set; }
12 | public double Left { get; set; }
13 | public double Top { get; set; }
14 | public double Width { get; set; }
15 | }
16 |
17 | public enum ProgressBarCommand
18 | {
19 | Show,
20 | Hide,
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/WebResourceModifications/WebResourceStringModificationRule.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 | using System.Threading.Tasks;
7 |
8 | namespace XpoMusic.Classes.Model.WebResourceModifications
9 | {
10 | public class WebResourceStringModificationRule
11 | {
12 | public string RegexMatch { get; set; }
13 | public string ReplaceTo { get; set; }
14 |
15 | public string Apply(string responseString) => Regex.Replace(responseString, RegexMatch, ReplaceTo);
16 | }
17 | }
--------------------------------------------------------------------------------
/XpoMusic/Scripts/LightTheme/initScript-light.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | namespace XpoMusicScript {
5 |
6 | document.getElementsByTagName('body')[0].setAttribute('data-xpotifyTheme', 'light');
7 |
8 | var errors = "";
9 |
10 | errors += Common.init();
11 | errors += Light.PageOverlay.createPageOverlay();
12 |
13 | if (errors.length > 0) {
14 | try {
15 | // @ts-ignore
16 | XpoMusic.initFailed(errors);
17 | }
18 | catch (ex) { }
19 |
20 | throw errors;
21 | }
22 | }
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/adsContainer.scss:
--------------------------------------------------------------------------------
1 |
2 | .Root__main-view--has-ads > div:nth-child(3) {
3 | z-index: 10000;
4 | }
5 |
6 | .Root__main-view--has-ads > div:nth-child(3) > div {
7 | background-color: rgba(24, 24, 24, 0.6);
8 | backdrop-filter: blur(30px);
9 | -webkit-backdrop-filter: blur(30px);
10 | border-top: 0px;
11 | }
12 |
13 | .AdsContainer {
14 | background-color: transparent;
15 | overflow: hidden;
16 | }
17 |
18 | .now-playing-bar__right {
19 | padding-right: 8px;
20 | }
21 |
22 | @media (min-width: 721px) {
23 | .now-playing-bar__right__inner {
24 | width: 200px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/contextMenu.scss:
--------------------------------------------------------------------------------
1 | /* Context menu */
2 | .react-contextmenu {
3 | border: 0;
4 | border: 1px solid #505050;
5 | border-radius: 0;
6 | background-color: rgba(30, 30, 30, 0.7);
7 | }
8 |
9 | .react-contextmenu.react-contextmenu--visible {
10 | backdrop-filter: blur(20px);
11 | -webkit-backdrop-filter: blur(20px);
12 | box-shadow: 0 0 20px 4px rgba(0,0,0,0.25);
13 | }
14 |
15 | .react-contextmenu-item {
16 | color: #eeeeee;
17 | opacity: 0;
18 | transition: opacity ease-out .15s;
19 | }
20 |
21 | .react-contextmenu.react-contextmenu--visible .react-contextmenu-item {
22 | opacity: 1;
23 | }
24 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Model/Paging.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.SpotifyApi.Model
8 | {
9 | public class Paging
10 | {
11 | public string href { get; set; }
12 | public T[] items { get; set; }
13 | public int limit { get; set; }
14 | public int offset { get; set; }
15 | public string next { get; set; }
16 | public string previous { get; set; }
17 | public int total { get; set; }
18 | public bool hasNext => next != null;
19 | public bool hasPrev => previous != null;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/backButton.scss:
--------------------------------------------------------------------------------
1 |
2 | .backButtonContainer {
3 | position: fixed;
4 | left: 0;
5 | top: 0;
6 | z-index: 10;
7 | width: 46px;
8 | height: 32px;
9 | text-align: center;
10 | cursor: default;
11 | }
12 |
13 | .backButtonContainer:hover {
14 | background: rgba(70, 70, 70, 0.5);
15 | }
16 |
17 | .backButtonContainer-disabled {
18 | opacity: 0.4;
19 | pointer-events: none;
20 | cursor: default;
21 | }
22 |
23 | .backButtonContainer-disabled:hover {
24 | background: unset;
25 | }
26 |
27 | .backButtonContainer span {
28 | font-family: Segoe MDL2 Assets;
29 | font-size: 14px;
30 | padding-top: 6px;
31 | display: inline-block;
32 | }
33 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Cache/SongExtraInfoStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using XpoMusic.Classes.Model;
7 | using XpoMusic.SpotifyApi;
8 |
9 | namespace XpoMusic.Classes.Cache
10 | {
11 | public class SongExtraInfoStore : CacheStore
12 | {
13 | protected override async Task RetrieveItem(string key)
14 | {
15 | var library = new Library();
16 | var info = new SongExtraInfo
17 | {
18 | SongId = key,
19 | IsSavedToLibrary = await library.IsTrackSaved(key),
20 | };
21 | return info;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/WebResourceModifications/WebResourceModificationRule.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 | using System.Threading.Tasks;
7 |
8 | namespace XpoMusic.Classes.Model.WebResourceModifications
9 | {
10 | public class WebResourceModificationRule
11 | {
12 | public string UriRegexMatch { get; set; }
13 | public WebResourceModificationRuleType Type { get; set; }
14 | public string AlternativeFileUri { get; set; }
15 | public WebResourceStringModificationRule[] StringModificationRules { get; set; }
16 |
17 | public bool Match(string requestUri) => Regex.IsMatch(requestUri, UriRegexMatch);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/common.scss:
--------------------------------------------------------------------------------
1 | @import "general.scss";
2 | @import "adsContainer.scss";
3 | @import "backButton.scss";
4 | @import "contextMenu.scss";
5 | @import "fontStyle.scss";
6 | @import "navBar.scss";
7 | @import "nowPlayingBar.scss";
8 | @import "pivotTabStyle.scss";
9 | @import "smallWidthWindow.scss";
10 | @import "windowTitle.scss";
11 | @import "trackList.scss";
12 | @import "searchPage.scss";
13 | @import "artistPage.scss";
14 | @import "themedScrollbar.scss";
15 | @import "Animation/pageEntrance.scss";
16 | @import "Animation/pivotEntrance.scss";
17 | @import "Animation/tracklistEntrance.scss";
18 | @import "Animation/nowPlayingStartup.scss";
19 | @import "Animation/navBarStartup.scss";
20 | @import "Animation/searchPage.scss";
21 | @import "Animation/artistPage.scss";
22 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/StoryboardExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.UI.Xaml.Media.Animation;
7 |
8 | namespace XpoMusic.Helpers
9 | {
10 | public static class StoryboardExtensions
11 | {
12 | public static async Task RunAsync(this Storyboard storyboard)
13 | {
14 | TaskCompletionSource tcs;
15 | tcs = new TaskCompletionSource();
16 |
17 | void handler(object s, object e) => tcs.TrySetResult(true);
18 |
19 | storyboard.Completed += handler;
20 |
21 | storyboard.Begin();
22 | await tcs.Task;
23 |
24 | storyboard.Completed -= handler;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/tracklistEntrance.scss:
--------------------------------------------------------------------------------
1 | /* Tracklist item animation */
2 | @for $i from 1 through 11 {
3 | .tracklist > div:nth-child(#{$i}) .tracklist-row {
4 | opacity: 0;
5 | animation: animation-entrance-tracklist-item 0.4s ease #{($i - 1) * 0.03}s forwards;
6 | }
7 | }
8 | @for $i from 12 through 30 {
9 | .tracklist > div:nth-child(#{$i}) .tracklist-row {
10 | opacity: 0;
11 | animation: animation-entrance-tracklist-item 0.4s ease #{0.30 + ($i - 11) * 0.02}s forwards;
12 | }
13 | }
14 |
15 | /* Keyframes */
16 | @keyframes animation-entrance-tracklist-item {
17 | 0% {
18 | opacity: 0;
19 | transform: translateX(-24px);
20 | }
21 |
22 | 100% {
23 | opacity: 1;
24 | transform: translateY(0);
25 | }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/pivotEntrance.scss:
--------------------------------------------------------------------------------
1 | /* Pivot item animation */
2 |
3 | .Root__main-view nav ul li {
4 | opacity: 0;
5 | }
6 |
7 | @for $i from 1 through 12 {
8 | .Root__main-view nav ul li:nth-child(#{$i}) {
9 | animation: animation-entrance-pivot-item 0.4s ease #{($i - 1) * 0.05}s forwards;
10 | }
11 | }
12 |
13 | @for $i from 1 through 12 {
14 | .Root__main-view .artist nav ul li:nth-child(#{$i}) {
15 | animation: animation-entrance-pivot-item 0.4s ease #{($i - 1) * 0.05 + 0.2}s forwards;
16 | }
17 | }
18 |
19 |
20 | /* Keyframes */
21 | @keyframes animation-entrance-pivot-item {
22 | 0% {
23 | opacity: 0;
24 | transform: translateX(-16px);
25 | }
26 |
27 | 100% {
28 | opacity: 1;
29 | transform: translateY(0);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Converters/ThemeToStringConverter.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes.Model;
2 | using XpoMusic.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Windows.UI.Xaml.Data;
9 |
10 | namespace XpoMusic.Classes.Converters
11 | {
12 | public class ThemeToStringConverter : IValueConverter
13 | {
14 | public object Convert(object value, Type targetType, object parameter, string language)
15 | {
16 | var lang = (Theme)value;
17 |
18 | return ThemeHelper.GetThemeName(lang);
19 | }
20 |
21 | public object ConvertBack(object value, Type targetType, object parameter, string language)
22 | {
23 | throw new NotImplementedException();
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Converters/LanguageToStringConverter.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes.Model;
2 | using XpoMusic.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Windows.UI.Xaml.Data;
9 |
10 | namespace XpoMusic.Classes.Converters
11 | {
12 | public class LanguageToStringConverter : IValueConverter
13 | {
14 | public object Convert(object value, Type targetType, object parameter, string language)
15 | {
16 | var lang = (Language)value;
17 |
18 | return LanguageHelper.GetLanguageName(lang);
19 | }
20 |
21 | public object ConvertBack(object value, Type targetType, object parameter, string language)
22 | {
23 | throw new NotImplementedException();
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/Language.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model
8 | {
9 | public enum Language
10 | {
11 | Default = 0,
12 | English = 1,
13 | Arabic = 2,
14 | Hungarian = 4,
15 | Czech = 5,
16 | German = 6,
17 | Spanish = 7,
18 | Finnish = 8,
19 | French = 9,
20 | CanadianFrench = 10,
21 | Greek = 11,
22 | Indonesian = 12,
23 | Italian = 13,
24 | Japanese = 14,
25 | Malay = 15,
26 | Dutch = 16,
27 | Polish = 17,
28 | BrazillianPortuguese = 18,
29 | Swedish = 19,
30 | Thai = 20,
31 | Turkish = 21,
32 | Vietnamese = 22,
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/XpoMusic/NLog.config:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Converters/MillisecondsToMinSecConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.UI.Xaml.Data;
7 |
8 | namespace XpoMusic.Classes.Converters
9 | {
10 | public class MillisecondsToMinSecConverter : IValueConverter
11 | {
12 | public object Convert(object value, Type targetType, object parameter, string language)
13 | {
14 | var ms = (double)value;
15 | var t = TimeSpan.FromMilliseconds(ms);
16 |
17 | return $"{(int)Math.Floor(t.TotalMinutes)}:{t.Seconds.ToString("00")}";
18 | }
19 |
20 | public object ConvertBack(object value, Type targetType, object parameter, string language)
21 | {
22 | throw new NotImplementedException();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Converters/BooleanToVisibilityConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.UI.Xaml;
7 | using Windows.UI.Xaml.Data;
8 |
9 | namespace XpoMusic.Classes.Converters
10 | {
11 | public class BooleanToVisibilityConverter : IValueConverter
12 | {
13 | public object Convert(object value, Type targetType, object parameter, string language) =>
14 | (bool)value ^ (parameter as string ?? string.Empty).Equals("Reverse") ?
15 | Visibility.Visible : Visibility.Collapsed;
16 |
17 | public object ConvertBack(object value, Type targetType, object parameter, string language) =>
18 | (Visibility)value == Visibility.Visible ^ (parameter as string ?? string.Empty).Equals("Reverse");
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Artist.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using Newtonsoft.Json;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace XpoMusic.SpotifyApi
11 | {
12 | public class Artist : ApiBase
13 | {
14 | public async Task GetArtist(string artistId)
15 | {
16 | var result = await SendRequestWithTokenAsync($"https://api.spotify.com/v1/artists/{artistId}", HttpMethod.Get);
17 | var resultString = await result.Content.ReadAsStringAsync();
18 |
19 | if (result.IsSuccessStatusCode == false)
20 | AnalyticsHelper.Log("api", "getartist::" + result.StatusCode.ToString());
21 |
22 | return JsonConvert.DeserializeObject(resultString);
23 | }
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Album.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using Newtonsoft.Json;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace XpoMusic.SpotifyApi
11 | {
12 | public class Album : ApiBase
13 | {
14 | public async Task GetAlbum(string albumId)
15 | {
16 | var result = await SendRequestWithTokenAsync($"https://api.spotify.com/v1/albums/{albumId}", HttpMethod.Get);
17 | var resultString = await result.Content.ReadAsStringAsync();
18 |
19 | if (result.IsSuccessStatusCode == false)
20 | AnalyticsHelper.Log("api", "getalbum::" + result.StatusCode.ToString());
21 |
22 | return JsonConvert.DeserializeObject(resultString);
23 | }
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Playlist.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using Newtonsoft.Json;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace XpoMusic.SpotifyApi
11 | {
12 | public class Playlist : ApiBase
13 | {
14 | public async Task GetPlaylist(string playlistId)
15 | {
16 | var result = await SendRequestWithTokenAsync($"https://api.spotify.com/v1/playlists/{playlistId}", HttpMethod.Get);
17 | var resultString = await result.Content.ReadAsStringAsync();
18 |
19 | if (result.IsSuccessStatusCode == false)
20 | AnalyticsHelper.Log("api", "getplaylist::" + result.StatusCode.ToString());
21 |
22 | return JsonConvert.DeserializeObject(resultString);
23 | }
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/LightTheme/artistPage.scss:
--------------------------------------------------------------------------------
1 |
2 | .content.artist > div:nth-child(1) {
3 | filter: invert(1);
4 | }
5 |
6 | .content.artist button[aria-label=Play], .content.artist button[title=Play] {
7 | filter: invert(1);
8 | }
9 |
10 | .content.artist img {
11 | filter: invert(0);
12 | }
13 |
14 | .Root__main-view .react-contextmenu-wrapper button {
15 | filter: invert(0);
16 | }
17 |
18 | .content.artist header > div {
19 | filter: invert(1);
20 | }
21 |
22 | .content.artist header::before {
23 | /* background: -webkit-gradient(linear,left top,left bottom,color-stop(-30%,transparent),to(#121212)); */
24 | background: linear-gradient(transparent 40%,#f2f2f2);
25 | }
26 |
27 | .content.artist header h1, .content.artist header .contentSpacing > div {
28 | filter: invert(1);
29 | }
30 |
31 | .Root__main-view .artist .contentSpacing > span {
32 | /* Monthly listeners */
33 | filter: invert(1);
34 | }
35 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/navbarStartup.scss:
--------------------------------------------------------------------------------
1 | @for $i from 1 through 3 {
2 | .Root__nav-bar .navBar ul li:nth-child(#{$i}) {
3 | opacity: 0;
4 | animation: animation-entrance-nav-bar-item 0.4s ease #{0.5 + ($i - 1) * 0.05}s forwards;
5 | }
6 | }
7 |
8 | .Root__nav-bar .navBar .recently-played ul li {
9 | opacity: 1;
10 | animation: none !important;
11 | }
12 |
13 | .Root__nav-bar .navBar .recently-played,
14 | .Root__nav-bar .navBar .Rootlist {
15 | opacity: 0;
16 | animation: animation-entrance-nav-bar-item 0.4s ease 0.75s forwards;
17 | }
18 |
19 | .NavBarFooter .navBar-item {
20 | opacity: 0;
21 | animation: animation-entrance-nav-bar-item 0.4s ease 0.85s forwards;
22 | }
23 |
24 | /* Keyframes */
25 | @keyframes animation-entrance-nav-bar-item {
26 | 0% {
27 | opacity: 0;
28 | transform: translateY(-12px);
29 | }
30 |
31 | 100% {
32 | opacity: 1;
33 | transform: translateY(0);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Model/NowPlayingData.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusicWebAgent.Model
8 | {
9 | public sealed class NowPlayingData
10 | {
11 | public string TrackName { get; set; }
12 | public string TrackId { get; set; }
13 | public string AlbumId { get; set; }
14 | public string ArtistName { get; set; }
15 | public string ArtistId { get; set; }
16 | public string TrackFingerprint { get; set; }
17 | public double Volume { get; set; }
18 | public int ElapsedTime { get; set; }
19 | public int TotalTime { get; set; }
20 | public bool IsPrevTrackAvailable { get; set; }
21 | public bool IsNextTrackAvailable { get; set; }
22 | public bool IsPlaying { get; set; }
23 | public bool IsTrackSavedToLibrary { get; set; }
24 | public bool Success { get; set; }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/artistPage.scss:
--------------------------------------------------------------------------------
1 |
2 | .content.artist header::before {
3 | /* background: -webkit-gradient(linear,left top,left bottom,color-stop(-30%,transparent),to(#121212)); */
4 | background: linear-gradient(transparent 40%, #121212);
5 | }
6 |
7 | .content.artist header h1 {
8 | max-width: 100% !important;
9 | font-weight: bolder !important;
10 | text-shadow: 0 0 10px #000, 0 0 20px #000, 0 0 30px #000, 0 0 40px #000, 0 0 50px #000, 0 0 60px #000, 0 0 70px #000;
11 | }
12 |
13 | .Root__main-view .artist .contentSpacing > span {
14 | /* Monthly listeners */
15 | text-align: center;
16 | color: white;
17 | text-shadow: 0 0 10px #000, 0 0 20px #000, 0 0 30px #000, 0 0 40px #000, 0 0 50px #000, 0 0 60px #000, 0 0 70px #000;
18 | font-weight: 500;
19 | }
20 |
21 | .content.artist header h1, .content.artist header .contentSpacing > div {
22 | text-align: center;
23 | }
24 |
25 | .Root__main-view .artist .contentSpacing {
26 | margin-bottom: 0 !important;
27 | padding-top: 0 !important;
28 | }
29 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/mouseWheelListener.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | namespace XpoMusicScript.Common.MouseWheelListener {
5 |
6 | function volumeMouseWheelHandler(e) {
7 | var { deltaY } = e;
8 | var currentVolume = StatusReport.getVolume();
9 | var newVolume = Math.max(Math.min(currentVolume - (deltaY / 2000), 1), 0);
10 |
11 | Action.seekVolume(newVolume);
12 | }
13 |
14 | function setVolumeBarListener() {
15 | var volumeBar = document.querySelector(".Root__top-container .Root__now-playing-bar .now-playing-bar__right__inner .volume-bar > div");
16 |
17 | if (volumeBar === null) {
18 | // Volume bar not present yet. Will try again later.
19 | setTimeout(setVolumeBarListener, 1000);
20 | return;
21 | }
22 |
23 | volumeBar.addEventListener("mousewheel", (e) => { volumeMouseWheelHandler(e) });
24 | }
25 |
26 | export function init() {
27 | setVolumeBarListener();
28 | }
29 | }
--------------------------------------------------------------------------------
/XpoMusic/App.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/UnauthorizedHelper.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.SpotifyApi;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using Windows.UI.Popups;
8 |
9 | namespace XpoMusic.Helpers
10 | {
11 | public static class UnauthorizedHelper
12 | {
13 | static readonly TimeSpan minimumNotifyDelay = TimeSpan.FromMinutes(3);
14 |
15 | static DateTime lastNotifyToUser = DateTime.MinValue;
16 |
17 | public static async void OnUnauthorizedError()
18 | {
19 | if (DateTime.UtcNow - lastNotifyToUser < minimumNotifyDelay)
20 | return;
21 |
22 | lastNotifyToUser = DateTime.UtcNow;
23 |
24 | TokenHelper.ClearTokens();
25 |
26 | var md = new MessageDialog("Please close and reopen Xpo Music; you may be asked to enter your credentials " +
27 | "again. In the meantime, some features might not work correctly.", "Authorization Error");
28 | await md.ShowAsync();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/trackList.scss:
--------------------------------------------------------------------------------
1 |
2 | .tracklist-top-align {
3 | /* No top align, will always be middle aligned. */
4 | position: relative;
5 | top: 50%;
6 | -webkit-transform: translateY(-50%);
7 | transform: translateY(-50%);
8 | margin-top: 0;
9 | }
10 |
11 | .tracklist-col.more > div:nth-child(1) {
12 | float: left;
13 | }
14 |
15 | .trackListAddRemoveSongButton {
16 | float: right;
17 | padding: 8px;
18 | margin-top: -2px;
19 | animation: animation-tracklist-addremove-entrance 0.3s ease-out;
20 | display: none;
21 | }
22 |
23 | .trackListAddRemoveSongButton:hover {
24 | opacity: 0.8;
25 | }
26 |
27 | .tracklistSongNotExistsInLibrary .trackListAddSongButton {
28 | display: block;
29 | }
30 |
31 | .tracklistSongExistsInLibrary .trackListRemoveSongButton {
32 | display: block;
33 | color: #1DB954;
34 | }
35 |
36 |
37 | /* Keyframes */
38 | @keyframes animation-tracklist-addremove-entrance {
39 | 0% {
40 | opacity: 0;
41 | }
42 |
43 | 100% {
44 | opacity: 1;
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Cache/CacheStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Cache
8 | {
9 | public abstract class CacheStore
10 | {
11 | private Dictionary data = new Dictionary();
12 | private SemaphoreQueue semaphore = new SemaphoreQueue(1, 1);
13 |
14 | public void Clear()
15 | {
16 | data.Clear();
17 | }
18 |
19 | protected abstract Task RetrieveItem(string key);
20 |
21 | public async Task GetItem(string key)
22 | {
23 | try
24 | {
25 | await semaphore.WaitAsync();
26 | if (!data.ContainsKey(key))
27 | {
28 | data[key] = await RetrieveItem(key);
29 | }
30 |
31 | return data[key];
32 | }
33 | finally
34 | {
35 | semaphore.Release();
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/XpoMusic/Pages/SettingsPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices.WindowsRuntime;
7 | using Windows.Foundation;
8 | using Windows.Foundation.Collections;
9 | using Windows.UI.Xaml;
10 | using Windows.UI.Xaml.Controls;
11 | using Windows.UI.Xaml.Controls.Primitives;
12 | using Windows.UI.Xaml.Data;
13 | using Windows.UI.Xaml.Input;
14 | using Windows.UI.Xaml.Media;
15 | using Windows.UI.Xaml.Navigation;
16 |
17 | namespace XpoMusic.Pages
18 | {
19 | public sealed partial class SettingsPage : Page
20 | {
21 | public SettingsPage()
22 | {
23 | this.InitializeComponent();
24 | }
25 |
26 | private void RestartApp_Click(object sender, RoutedEventArgs e)
27 | {
28 | PackageHelper.RestartApp();
29 | }
30 |
31 | private async void PinTileToStart_Click(object sender, RoutedEventArgs e)
32 | {
33 | await LiveTileHelper.PinToStart();
34 | ViewModel.CheckPrimaryTileStatus();
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/JumpListHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.UI.StartScreen;
7 |
8 | namespace XpoMusic.Helpers
9 | {
10 | public static class JumpListHelper
11 | {
12 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
13 |
14 | ///
15 | /// Entries get added to the jump list when you play music via Cortana.
16 | /// This function removes them.
17 | ///
18 | public static async void DeleteRecentJumplistEntries()
19 | {
20 | try
21 | {
22 | var jumpList = await JumpList.LoadCurrentAsync();
23 |
24 | jumpList.SystemGroupKind = JumpListSystemGroupKind.None;
25 | jumpList.Items.Clear();
26 |
27 | await jumpList.SaveAsync();
28 | }
29 | catch (Exception ex)
30 | {
31 | logger.Warn("DeleteRecentJumplistEntries failed: " + ex.ToString());
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/XpoMusic/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("Xpotify")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("Xpotify")]
13 | [assembly: AssemblyCopyright("Copyright © 2019")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Version information for an assembly consists of the following four values:
18 | //
19 | // Major Version
20 | // Minor Version
21 | // Build Number
22 | // Revision
23 | //
24 | // You can specify all the values or you can default the Build and Revision Numbers
25 | // by using the '*' as shown below:
26 | // [assembly: AssemblyVersion("1.0.*")]
27 | [assembly: AssemblyVersion("1.0.0.0")]
28 | [assembly: AssemblyFileVersion("1.0.0.0")]
29 | [assembly: ComVisible(false)]
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/nowPlayingStartup.scss:
--------------------------------------------------------------------------------
1 | .now-playing-bar__left {
2 | opacity: 0;
3 | animation: animation-entrance-now-playing-item 0.4s ease forwards;
4 | }
5 |
6 | .playback-bar {
7 | opacity: 0;
8 | animation: animation-entrance-now-playing-item 0.4s ease 0.7s forwards;
9 | }
10 |
11 | @for $i from 1 through 5 {
12 | .player-controls__buttons > .control-button:nth-child(#{$i}),
13 | .player-controls__buttons > .react-contextmenu-wrapper:nth-child(#{$i}) {
14 | opacity: 0;
15 | animation: animation-entrance-now-playing-item 0.4s ease #{0.6 + ($i - 1) * 0.05}s forwards;
16 | }
17 | }
18 |
19 | @for $i from 1 through 4 {
20 | .now-playing-bar__right .ExtraControls > div:nth-child(#{$i}) {
21 | opacity: 0;
22 | animation: animation-entrance-now-playing-item 0.4s ease #{0.9 + ($i - 1) * 0.05}s forwards;
23 | }
24 | }
25 |
26 | /* Keyframes */
27 | @keyframes animation-entrance-now-playing-item {
28 | 0% {
29 | opacity: 0;
30 | transform: translateX(-12px);
31 | }
32 |
33 | 100% {
34 | opacity: 1;
35 | transform: translateY(0);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/DeviceInfoHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.Networking.Connectivity;
7 |
8 | namespace XpoMusic.Helpers
9 | {
10 | public class DeviceInfoHelper
11 | {
12 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
13 |
14 | public static string GetDeviceName()
15 | {
16 | try
17 | {
18 | var hostNames = NetworkInformation.GetHostNames();
19 | var localName = hostNames.FirstOrDefault(name => name.DisplayName.Contains(".local"));
20 | var computerName = localName.DisplayName.Replace(".local", "");
21 |
22 | return computerName;
23 | }
24 | catch (Exception ex)
25 | {
26 | logger.Info("GetDeviceName via HostName failed: " + ex.ToString() + "\n Will try using EasClientDeviceInformation instead.");
27 | return (new Windows.Security.ExchangeActiveSyncProvisioning.EasClientDeviceInformation()).FriendlyName;
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/XpoMusicWebAgent/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("XpotifyWebAgent")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("XpotifyWebAgent")]
13 | [assembly: AssemblyCopyright("Copyright © 2019")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Version information for an assembly consists of the following four values:
18 | //
19 | // Major Version
20 | // Minor Version
21 | // Build Number
22 | // Revision
23 | //
24 | // You can specify all the values or you can default the Build and Revision Numbers
25 | // by using the '*' as shown below:
26 | // [assembly: AssemblyVersion("1.0.*")]
27 | [assembly: AssemblyVersion("1.0.0.0")]
28 | [assembly: AssemblyFileVersion("1.0.0.0")]
29 | [assembly: ComVisible(false)]
--------------------------------------------------------------------------------
/XpoMusicInstaller/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace XpoMusicInstaller.Properties
12 | {
13 |
14 |
15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
18 | {
19 |
20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
21 |
22 | public static Settings Default
23 | {
24 | get
25 | {
26 | return defaultInstance;
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/pageEntrance.scss:
--------------------------------------------------------------------------------
1 | /* Tab/page change entrance animation */
2 |
3 | .HeaderedGrid__header,
4 | .container-fluid,
5 | .search-history {
6 | animation: animation-entrance-content 0.4s ease;
7 | }
8 |
9 | .asideButton {
10 | animation: animation-entrance-fade-in 0.4s ease;
11 | }
12 |
13 | .TrackListHeader__entity-long-description {
14 | animation: animation-entrance-fade-in-60 0.4s ease forwards;
15 | }
16 |
17 | @for $i from 1 through 32 {
18 | .Root__main-view section > div > div:nth-child(#{$i}) {
19 | opacity: 0;
20 | animation: animation-entrance-content 0.4s ease #{($i - 1) * 0.03}s forwards;
21 | }
22 | }
23 |
24 |
25 | /* Keyframes */
26 | @keyframes animation-entrance-fade-in {
27 | 0% {
28 | opacity: 0;
29 | }
30 |
31 | 100% {
32 | opacity: 1;
33 | }
34 | }
35 |
36 | @keyframes animation-entrance-fade-in-60 {
37 | 0% {
38 | opacity: 0;
39 | }
40 |
41 | 100% {
42 | opacity: .6;
43 | }
44 | }
45 |
46 | @keyframes animation-entrance-content {
47 | 0%, 25% {
48 | opacity: 0;
49 | transform: translateY(32px);
50 | }
51 |
52 | 100% {
53 | opacity: 1;
54 | transform: translateY(0);
55 | }
56 | }
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/resize.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.Resize {
2 | export function onResize() {
3 | try {
4 | /*
5 | * I couldn't fix the width of the new track list for Edge (Works fine in Chrome but not in Edge),
6 | * so I use a javascript workaround for that.
7 | */
8 | var contentDiv = document.querySelectorAll(".main-view-container__content");
9 | if (contentDiv.length === 0) {
10 | contentDiv = document.querySelectorAll(".main-view-container__scroll-node");
11 | }
12 |
13 | // 230px is added because it's added in css as well, for acrylic behind artist page.
14 | (contentDiv[0]).style.width = 230 + (window.innerWidth - (document.querySelectorAll(".Root__nav-bar")[0]).offsetWidth) + "px";
15 |
16 |
17 | var adContainerDiv = document.querySelectorAll(".AdsContainer");
18 | if (adContainerDiv.length > 0)
19 | (adContainerDiv[0]).style.width = (window.innerWidth - (document.querySelectorAll(".Root__nav-bar")[0]).offsetWidth) + "px";
20 | }
21 | catch (ex) {
22 | console.log("resize event failed");
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/fontStyle.scss:
--------------------------------------------------------------------------------
1 | /* Font style changes */
2 |
3 | body {
4 | --glue-font-weight-normal: 400;
5 | --glue-font-weight-bold: 500;
6 | --glue-font-weight-black: 700;
7 | -webkit-font-smoothing: antialiased;
8 | font-family: Segoe UI,spotify-circular,spotify-circular-cyrillic,spotify-circular-arabic,spotify-circular-hebrew,Helvetica Neue,Helvetica,Arial,Hiragino Kaku Gothic Pro,Meiryo,MS Gothic,sans-serif;
9 | }
10 |
11 | h1 {
12 | font-family: Segoe UI !important;
13 | font-weight: 100 !important;
14 | font-size: 40px !important;
15 | line-height: 60px !important;
16 | }
17 |
18 | .navBar-item {
19 | font-weight: var(--glue-font-weight-black);
20 | }
21 |
22 | .ResponsiveViewMoreButton {
23 | margin-top: 10px;
24 | }
25 |
26 | .artist-header h1.large {
27 | font-size: 72px !important;
28 | font-weight: var(--glue-font-weight-black) !important;
29 | }
30 |
31 | .artist-header h1.medium {
32 | font-size: 48px !important;
33 | font-weight: var(--glue-font-weight-black) !important;
34 | }
35 |
36 | .view-header__header-title--xxxl {
37 | font-size: 48px !important;
38 | font-weight: var(--glue-font-weight-black) !important;
39 | }
40 |
41 | .TrackListHeader__entity-name h2 {
42 | font-weight: 300;
43 | }
44 |
--------------------------------------------------------------------------------
/XpoMusic/Properties/Default.rd.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Converters/LiveTileDesignToStringConverter.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes.Model;
2 | using XpoMusic.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Windows.UI.Xaml.Data;
9 | using static XpoMusic.Helpers.LiveTileHelper;
10 |
11 | namespace XpoMusic.Classes.Converters
12 | {
13 | public class LiveTileDesignToStringConverter : IValueConverter
14 | {
15 | public object Convert(object value, Type targetType, object parameter, string language)
16 | {
17 | var design = (LiveTileDesign)value;
18 |
19 | switch (design)
20 | {
21 | case LiveTileDesign.AlbumArtOnly:
22 | return "Album art only";
23 | case LiveTileDesign.ArtistArtOnly:
24 | return "Artist art only";
25 | case LiveTileDesign.Disabled:
26 | return "Disabled";
27 | case LiveTileDesign.AlbumAndArtistArt:
28 | return "Album art and artist art";
29 | default:
30 | return "???";
31 | }
32 | }
33 |
34 | public object ConvertBack(object value, Type targetType, object parameter, string language)
35 | {
36 | throw new NotImplementedException();
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/Animation/artistPage.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | .artist h1 {
5 | opacity: 0;
6 | animation: animation-entrance-artist-section-header-item 0.4s ease-out 0s forwards;
7 | }
8 |
9 | .artist header {
10 | opacity: 0;
11 | animation: animation-entrance-artist-header 0.5s ease-out 0.2s forwards;
12 | }
13 |
14 | .artist header h1 {
15 | opacity: 0;
16 | animation: animation-entrance-artist-header-content 0.5s ease-out 0.4s forwards;
17 | }
18 |
19 | .artist header button {
20 | opacity: 0;
21 | animation: animation-entrance-artist-header-content 0.5s ease-out 0.7s forwards;
22 | }
23 |
24 | /* Keyframes */
25 | @keyframes animation-entrance-artist-section-header-item {
26 | 0% {
27 | opacity: 0;
28 | transform: translateX(-24px);
29 | }
30 |
31 | 100% {
32 | opacity: 1;
33 | transform: translateX(0);
34 | }
35 | }
36 |
37 |
38 | @keyframes animation-entrance-artist-header {
39 | 0% {
40 | opacity: 0;
41 | transform: translateY(-100px);
42 | }
43 |
44 | 100% {
45 | opacity: 1;
46 | transform: translateY(0);
47 | }
48 | }
49 |
50 |
51 | @keyframes animation-entrance-artist-header-content {
52 | 0% {
53 | opacity: 0;
54 | transform: translateY(-24px);
55 | }
56 |
57 | 100% {
58 | opacity: 1;
59 | transform: translateY(0);
60 | }
61 | }
62 |
63 |
64 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/navBar.scss:
--------------------------------------------------------------------------------
1 |
2 | .Root__nav-bar {
3 | background-color: rgba(45, 45, 45, 0.45) !important;
4 | backdrop-filter: blur(20px);
5 | -webkit-backdrop-filter: blur(20px);
6 | position: relative;
7 | overflow: hidden;
8 | z-index: 1;
9 | }
10 |
11 | .navBar {
12 | position: absolute;
13 | bottom: 0;
14 | top: 0;
15 | padding-top: 36px;
16 | width: 100%;
17 | }
18 |
19 | .main-view-container__content, .main-view-container__scroll-node {
20 | /* Acrylic behind navbar in artist page */
21 | margin-left: -230px;
22 | padding-left: 230px;
23 | }
24 |
25 | .navBar-header {
26 | margin-bottom: 12px;
27 | }
28 |
29 | @media (max-width: 720px) {
30 | .Root__nav-bar .sessionInfo .UserWidget__user-link {
31 | display: none;
32 | }
33 | }
34 |
35 | .navBar-logo--size-large {
36 | display: none;
37 | }
38 |
39 | .navBar-logo--size-small {
40 | display: none;
41 | }
42 |
43 | .NavBarFooter {
44 | margin-top: 16px;
45 | }
46 |
47 | @media (max-width: 720px) {
48 | .navBar ul {
49 | margin-bottom: 40px;
50 | }
51 | }
52 |
53 | @media (max-height: 560px) {
54 | .navBar ul {
55 | margin-bottom: 10px;
56 | }
57 | }
58 |
59 | li.navBar-item span {
60 | font-weight: 500 !important;
61 | }
62 |
63 | .Rootlist__playlists-header {
64 | font-weight: 700 !important;
65 | font-size: 16px !important;
66 | letter-spacing: 0 !important;
67 | }
68 |
--------------------------------------------------------------------------------
/XpoMusic/ViewModels/MainPageViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.UI.ViewManagement;
7 |
8 | namespace XpoMusic.ViewModels
9 | {
10 | public class MainPageViewModel : ViewModelBase
11 | {
12 | private double topBarButtonWidth;
13 | public double TopBarButtonWidth
14 | {
15 | get => topBarButtonWidth;
16 | set
17 | {
18 | topBarButtonWidth = value;
19 | FirePropertyChangedEvent(nameof(TopBarButtonWidth));
20 | FirePropertyChangedEvent(nameof(TopBarBehindSystemControlsAreaWidth));
21 | }
22 | }
23 |
24 | private double topBarButtonHeight;
25 | public double TopBarButtonHeight
26 | {
27 | get => topBarButtonHeight;
28 | set
29 | {
30 | topBarButtonHeight = value;
31 | FirePropertyChangedEvent(nameof(TopBarButtonHeight));
32 | }
33 | }
34 |
35 | public double TopBarBehindSystemControlsAreaWidth
36 | {
37 | get
38 | {
39 | var isTabletMode = (UIViewSettings.GetForCurrentView().UserInteractionMode == UserInteractionMode.Touch);
40 |
41 | return TopBarButtonWidth * (isTabletMode ? 1.0 : 3.0);
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/nowPlayingBar.scss:
--------------------------------------------------------------------------------
1 |
2 | .Root__now-playing-bar {
3 | background-color: transparent;
4 | z-index: 10;
5 | }
6 |
7 | .now-playing-bar-container {
8 | border: 0;
9 | z-index: 10;
10 | }
11 |
12 | .now-playing-bar {
13 | background-color: rgba(40, 40, 40, 0.6);
14 | transition: background-color ease-in-out 2s;
15 | z-index: 10;
16 | backdrop-filter: blur(30px);
17 | -webkit-backdrop-filter: blur(30px);
18 | }
19 |
20 | .now-playing-bar__right__inner .CompactOverlayButton {
21 | margin-top: -1px;
22 | }
23 |
24 | .now-playing-bar__right__inner .CompactOverlayButton a {
25 | transition: opacity .3s ease-out;
26 | }
27 |
28 | .now-playing-bar__right__inner .CompactOverlayButton-disabled a {
29 | opacity: 0.35;
30 | }
31 |
32 | .now-playing-bar__right__inner .CompactOverlayButton-disabled .control-button:hover {
33 | color: #b3b3b3;
34 | }
35 |
36 | /* Fluent blur behind now playing bar */
37 | .main-view-container {
38 | margin-bottom: -90px;
39 | }
40 |
41 | .main-view-container__scroll-node,
42 | .main-view-container__scroll-node-child {
43 | padding-bottom: 90px;
44 | }
45 |
46 | .Root__main-view--has-ads .main-view-container {
47 | margin-bottom: -190px;
48 | }
49 |
50 | .Root__main-view--has-ads .main-view-container__scroll-node,
51 | .Root__main-view--has-ads .main-view-container__scroll-node-child {
52 | padding-bottom: 190px;
53 | }
54 | /* END Fluent blur behind now playing bar */
55 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/uiInjector.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.UiInjector {
2 | export function injectNavbarDownButton(button) {
3 | var sessionInfo = document.querySelectorAll(".Root__nav-bar nav > div:last-child");
4 | if (sessionInfo.length === 0) {
5 | setTimeout(function () {
6 | injectNavbarDownButton(button);
7 | }, 500);
8 | } else {
9 | var navbar = sessionInfo[0].parentElement;
10 | navbar.insertBefore(button, sessionInfo[0]);
11 |
12 | ((sessionInfo[0])).style.display = 'none';
13 | }
14 | }
15 |
16 | export function injectNowPlayingRightButton(button) {
17 | var extraControlsBar = document.querySelectorAll('.Root__now-playing-bar .ExtraControls');
18 | if (extraControlsBar.length === 0) {
19 | setTimeout(function () {
20 | injectNowPlayingRightButton(button);
21 | }, 500);
22 | } else {
23 | extraControlsBar[0].prepend(button);
24 | }
25 | }
26 |
27 | export function injectNowPlayingNavBarButton(button) {
28 | var extraControlsBar = document.querySelectorAll('.Root__top-container .Root__nav-bar nav ul');
29 | if (extraControlsBar.length === 0) {
30 | setTimeout(function () {
31 | injectNowPlayingNavBarButton(button);
32 | }, 500);
33 | } else {
34 | extraControlsBar[0].append(button);
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/XpoMusic/Stylesheets/Common/pivotTabStyle.scss:
--------------------------------------------------------------------------------
1 | /* Pivot tab style */
2 |
3 | .Root__main-view nav ul li a, .Root__top-bar nav ul li a {
4 | /* text-transform: lowercase !important; */
5 | font-family: Segoe UI !important;
6 | font-weight: 400 !important;
7 | font-size: 16px !important;
8 | letter-spacing: normal !important;
9 | margin: 10px 12px !important;
10 | background: transparent !important;
11 | border-bottom: 2px transparent solid;
12 | border-radius: 0;
13 | padding: 0 !important;
14 | width: auto !important;
15 | color: white;
16 | line-height: 28px !important;
17 | }
18 |
19 | /*
20 | .Root__main-view nav ul li a::first-letter {
21 | text-transform: uppercase !important;
22 | }
23 | */
24 |
25 | .Root__main-view nav ul li a[aria-current=page], .Root__top-bar nav ul li a[aria-current=page] {
26 | border-bottom-color: #1DB954;
27 | }
28 |
29 | .Root__main-view nav ul, .Root__top-bar nav ul {
30 | text-align: left !important;
31 | }
32 |
33 | .Root__main-view .Search__content nav ul, .Root__top-bar .Search__content nav ul {
34 | margin-left: 16px;
35 | }
36 |
37 | .Root__main-view .artist-header nav ul, .Root__top-bar .artist-header nav ul {
38 | margin-left: -12px;
39 | }
40 |
41 | @media (min-width: 1100px) {
42 | .asideButton {
43 | position: absolute;
44 | right: 0;
45 | top: 8px;
46 | }
47 |
48 | .Root__main-view nav ul, .Root__top-bar nav ul {
49 | margin-right: 150px !important;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/SpotifyApi/apiBase.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.SpotifyApi {
2 |
3 | declare var XpoMusic: any;
4 | var accessToken = "{{SPOTIFYACCESSTOKEN}}";
5 |
6 | export abstract class ApiBase {
7 |
8 | protected async sendJsonRequestWithToken(uri, method, body = undefined): Promise {
9 | if (accessToken.length === 0) {
10 | accessToken = await XpoMusic.getNewAccessTokenAsync();
11 | }
12 |
13 | return await this.sendJsonRequestWithTokenInternal(uri, method, body, true);
14 | }
15 |
16 | private async sendJsonRequestWithTokenInternal(uri, method, body, allowRefreshingToken): Promise {
17 | var response = await fetch(uri, {
18 | method: method,
19 | body: JSON.stringify(body),
20 | headers: {
21 | 'Authorization': 'Bearer ' + accessToken,
22 | },
23 | });
24 |
25 | XpoMusic.log("SpotifyApi: " + uri + " (" + method + ") -> result status = " + response.status);
26 |
27 | if (response.status == 401 && allowRefreshingToken) {
28 | // Refresh access token and retry
29 | XpoMusic.log("Will ask for new token.");
30 | accessToken = await XpoMusic.getNewAccessTokenAsync();
31 | return await this.sendJsonRequestWithTokenInternal(uri, method, body, false);
32 | }
33 |
34 | return response;
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/XpoMusic/Classes/SemaphoreQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace XpoMusic.Classes
10 | {
11 | public class SemaphoreQueue
12 | {
13 | private SemaphoreSlim semaphore;
14 | private ConcurrentQueue> queue = new ConcurrentQueue>();
15 |
16 | public int WaitingCount => queue.Count;
17 |
18 | public SemaphoreQueue(int initialCount)
19 | {
20 | semaphore = new SemaphoreSlim(initialCount);
21 | }
22 |
23 | public SemaphoreQueue(int initialCount, int maxCount)
24 | {
25 | semaphore = new SemaphoreSlim(initialCount, maxCount);
26 | }
27 |
28 | public void Wait()
29 | {
30 | WaitAsync().Wait();
31 | }
32 |
33 | public Task WaitAsync()
34 | {
35 | var tcs = new TaskCompletionSource();
36 | queue.Enqueue(tcs);
37 | semaphore.WaitAsync().ContinueWith(t =>
38 | {
39 | TaskCompletionSource popped;
40 | if (queue.TryDequeue(out popped))
41 | popped.SetResult(true);
42 | });
43 | return tcs.Task;
44 | }
45 |
46 | public void Release()
47 | {
48 | semaphore.Release();
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/XpoMusic/Classes/Model/LyricsViewer/CurrentPlayingSongInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace XpoMusic.Classes.Model.LyricsViewer
8 | {
9 | public class CurrentPlayingSongInfo
10 | {
11 | public string SongName { get; set; }
12 | public string ArtistName { get; set; }
13 | public string AlbumName { get; set; }
14 | public string AlbumArtUri { get; set; }
15 |
16 | public override bool Equals(object obj)
17 | {
18 | if (obj is CurrentPlayingSongInfo cpsi)
19 | {
20 | return SongName == cpsi.SongName
21 | && ArtistName == cpsi.ArtistName
22 | && AlbumArtUri == cpsi.AlbumArtUri
23 | && AlbumName == cpsi.AlbumName;
24 | }
25 |
26 | return false;
27 | }
28 |
29 | public override int GetHashCode()
30 | {
31 | var hashCode = 2090074316;
32 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SongName);
33 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ArtistName);
34 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AlbumName);
35 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AlbumArtUri);
36 | return hashCode;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/XpoMusic/compilerconfig.json.defaults:
--------------------------------------------------------------------------------
1 | {
2 | "compilers": {
3 | "less": {
4 | "autoPrefix": "",
5 | "cssComb": "none",
6 | "ieCompat": true,
7 | "strictMath": false,
8 | "strictUnits": false,
9 | "relativeUrls": true,
10 | "rootPath": "",
11 | "sourceMapRoot": "",
12 | "sourceMapBasePath": "",
13 | "sourceMap": false
14 | },
15 | "sass": {
16 | "autoPrefix": "",
17 | "includePath": "",
18 | "indentType": "space",
19 | "indentWidth": 2,
20 | "outputStyle": "nested",
21 | "Precision": 5,
22 | "relativeUrls": true,
23 | "sourceMapRoot": "",
24 | "lineFeed": "",
25 | "sourceMap": false
26 | },
27 | "stylus": {
28 | "sourceMap": false
29 | },
30 | "babel": {
31 | "sourceMap": false
32 | },
33 | "coffeescript": {
34 | "bare": false,
35 | "runtimeMode": "node",
36 | "sourceMap": false
37 | },
38 | "handlebars": {
39 | "root": "",
40 | "noBOM": false,
41 | "name": "",
42 | "namespace": "",
43 | "knownHelpersOnly": false,
44 | "forcePartial": false,
45 | "knownHelpers": [],
46 | "commonjs": "",
47 | "amd": false,
48 | "sourceMap": false
49 | }
50 | },
51 | "minifiers": {
52 | "css": {
53 | "enabled": true,
54 | "termSemicolons": true,
55 | "gzip": false
56 | },
57 | "javascript": {
58 | "enabled": true,
59 | "termSemicolons": true,
60 | "gzip": false
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/XpoMusic/Helpers/ThemeHelper.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes;
2 | using XpoMusic.Classes.Model;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using Windows.UI.Xaml;
9 |
10 | namespace XpoMusic.Helpers
11 | {
12 | public static class ThemeHelper
13 | {
14 | public static List GetThemes()
15 | {
16 | var output = new List();
17 |
18 | var items = Enum.GetValues(typeof(Theme));
19 | foreach (var item in items)
20 | {
21 | output.Add((Theme)item);
22 | }
23 |
24 | output = output.OrderBy(x => (int)x).ToList();
25 |
26 | return output;
27 | }
28 |
29 | public static string GetThemeName(Theme theme)
30 | {
31 | switch (theme)
32 | {
33 | case Theme.Dark:
34 | return "Dark";
35 | case Theme.Light:
36 | return "Light";
37 | case Theme.System:
38 | return "Follow Windows app theme";
39 | default:
40 | return "???";
41 | }
42 | }
43 |
44 | internal static Theme GetCurrentTheme()
45 | {
46 | if (LocalConfiguration.Theme != Theme.System)
47 | return LocalConfiguration.Theme;
48 |
49 | return (Application.Current.RequestedTheme == ApplicationTheme.Dark) ? Theme.Dark : Theme.Light;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/uriHelper.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.UriHelper {
2 |
3 | export function getPwaUri(uri) {
4 | if (uri === undefined || uri.trim() === "") {
5 | return "";
6 | }
7 |
8 | uri = uri.replace('http://', 'https://');
9 | var uriLowerCase = uri.toLowerCase();
10 |
11 | if (uriLowerCase.startsWith("https://open.spotify.com"))
12 | return uri;
13 |
14 | if (uriLowerCase.startsWith("spotify:")) {
15 | if (uriLowerCase.indexOf("spotify:artist:") >= 0) {
16 | var idx = uriLowerCase.indexOf("spotify:artist:") + "spotify:artist:".length;
17 | return "https://open.spotify.com/artist/" + uri.substring(idx);
18 | }
19 | else if (uriLowerCase.indexOf("spotify:album:") >= 0) {
20 | var idx = uriLowerCase.indexOf("spotify:album:") + "spotify:album:".length;
21 | return "https://open.spotify.com/album/" + uri.substring(idx);
22 | }
23 | else if (uriLowerCase.indexOf("spotify:playlist:") >= 0) {
24 | var idx = uriLowerCase.indexOf("spotify:playlist:") + "spotify:playlist:".length;
25 | return "https://open.spotify.com/playlist/" + uri.substring(idx);
26 | }
27 | else if (uriLowerCase.indexOf("spotify:track:") >= 0) {
28 | var idx = uriLowerCase.indexOf("spotify:track:") + "spotify:track:".length;
29 | return "https://open.spotify.com/track/" + uri.substring(idx);
30 | }
31 | }
32 |
33 | return "";
34 | }
35 | }
--------------------------------------------------------------------------------
/XpoMusic/Helpers/DeveloperMessageHelper.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes;
2 | using XpoMusic.XpotifyApi;
3 | using XpoMusic.XpotifyApi.Model;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace XpoMusic.Helpers
11 | {
12 | public static class DeveloperMessageHelper
13 | {
14 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
15 |
16 | public static async Task GetNextDeveloperMessage()
17 | {
18 | try
19 | {
20 | var messages = await DeveloperMessageApi.GetDeveloperMessages();
21 |
22 | if (messages == null || messages.messages == null)
23 | return null;
24 |
25 | var readMessageIds = LocalConfiguration.DeveloperMessageShownIds;
26 | var unreadMessages = messages.messages.Where(x => !readMessageIds.Contains(x.id)).ToList();
27 |
28 | var firstUnreadMessage = unreadMessages.FirstOrDefault();
29 |
30 | if (firstUnreadMessage == null)
31 | return null;
32 |
33 | LocalConfiguration.AddIdToDeveloperMessageShownIds(firstUnreadMessage.id);
34 | return firstUnreadMessage;
35 | }
36 | catch (Exception ex)
37 | {
38 | logger.Warn("GetNextDeveloperMessage failed: " + ex.ToString());
39 | AnalyticsHelper.Log("exception-GetNextDeveloperMessage", ex.Message, ex.ToString());
40 | return null;
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/startupAnimation.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.StartupAnimation {
2 | export function init() {
3 | try {
4 | var pivotItems = document.querySelectorAll(".Root__main-view nav ul li");
5 | var entranceItems = document.querySelectorAll(".container-fluid");
6 |
7 | for (var i = 0; i < pivotItems.length; i++) {
8 | (pivotItems[i]).style.animation = 'none';
9 | }
10 | for (var i = 0; i < entranceItems.length; i++) {
11 | (entranceItems[i]).style.animation = 'none';
12 | (entranceItems[i]).style.opacity = '0';
13 | }
14 |
15 | setTimeout(function () {
16 | try {
17 | for (var i = 0; i < pivotItems.length; i++) {
18 | (pivotItems[i]).style.animation = '';
19 | }
20 | }
21 | catch (ex2) {
22 | console.log(ex2);
23 | }
24 | }, 400);
25 |
26 | setTimeout(function () {
27 | try {
28 | for (var i = 0; i < entranceItems.length; i++) {
29 | (entranceItems[i]).style.animation = '';
30 | (entranceItems[i]).style.opacity = '1';
31 | }
32 | }
33 | catch (ex2) {
34 | console.log(ex2);
35 | }
36 | }, 400);
37 | }
38 | catch (ex) {
39 | console.log(ex);
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/XpoMusic/Assets/TileTemplates/LiveTileAlbumArtOnly.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {songName}
8 | {artistName}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {songName}
18 | {artistName}
19 | {albumName}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {songName}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {artistName}
36 | {albumName}
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/SpotifyApi/library.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace XpoMusicScript.SpotifyApi {
4 | export class Library extends ApiBase {
5 | public async isTrackSaved(trackId: string): Promise {
6 | return (await this.isTracksSaved([trackId]))[0];
7 | }
8 |
9 | public async isTracksSaved(trackIds: string[]): Promise {
10 | var output = [];
11 | for (var i = 0; i < trackIds.length; i += 50) {
12 | var slice = trackIds.slice(i, i + 50);
13 | output = output.concat(await this.isTracksSavedInternal(slice));
14 | }
15 | return output;
16 | }
17 |
18 | private async isTracksSavedInternal(trackIds: string[]): Promise {
19 | var url = 'https://api.spotify.com/v1/me/tracks/contains?ids=' + trackIds.join(',');
20 | var result = await this.sendJsonRequestWithToken(url, 'get');
21 |
22 | var data = await result.json();
23 | return data;
24 | }
25 |
26 | public async saveTrack(trackId: string): Promise {
27 | var url = "https://api.spotify.com/v1/me/tracks?ids=" + trackId;
28 | var result = await this.sendJsonRequestWithToken(url, 'put');
29 |
30 | return result.status >= 200 && result.status <= 299;
31 | }
32 |
33 | public async removeTrack(trackId: string): Promise {
34 | var url = "https://api.spotify.com/v1/me/tracks?ids=" + trackId;
35 | var result = await this.sendJsonRequestWithToken(url, 'delete');
36 |
37 | return result.status >= 200 && result.status <= 299;
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/XpoMusic/Helpers/ClipboardHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.ApplicationModel.DataTransfer;
7 |
8 | namespace XpoMusic.Helpers
9 | {
10 | public static class ClipboardHelper
11 | {
12 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
13 |
14 | public static async Task IsSpotifyUriPresent()
15 | {
16 | try
17 | {
18 | var content = Clipboard.GetContent();
19 |
20 | if (!content.Contains(StandardDataFormats.Text))
21 | return false;
22 |
23 | var data = await content.GetTextAsync();
24 | var uri = SpotifyShareUriHelper.GetPwaUri(data);
25 |
26 | return !string.IsNullOrWhiteSpace(uri);
27 | }
28 | catch (Exception ex)
29 | {
30 | logger.Error(ex.ToString());
31 | return false;
32 | }
33 | }
34 |
35 | public static async Task GetSpotifyUri()
36 | {
37 | try
38 | {
39 | var content = Clipboard.GetContent();
40 |
41 | if (!content.Contains(StandardDataFormats.Text))
42 | return "";
43 |
44 | var data = await content.GetTextAsync();
45 | var uri = SpotifyShareUriHelper.GetPwaUri(data);
46 |
47 | return uri;
48 | }
49 | catch (Exception ex)
50 | {
51 | logger.Error(ex.ToString());
52 | return "";
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/requestIntercepter.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace XpoMusicScript.Common.RequestIntercepter {
4 |
5 | export function startInterceptingFetch() {
6 |
7 | const fetch = window.fetch;
8 | window.fetch = (...args) => (async (args) => {
9 |
10 | // Request interception
11 | if (args[0].toString().endsWith('/state')) {
12 | console.log(args);
13 | var reqJson = JSON.stringify(args);
14 | if (reqJson.indexOf("before_track_load") >= 0) {
15 | IndeterminateProgressBarHandler.onTrackLoadBegin();
16 | }
17 | } else if (args[0].toString().endsWith('/v1/devices')) {
18 | console.log(args);
19 | console.log(args[1].body);
20 | var appName = Common.getAppName();
21 | var spotifyConnectName = Common.getDeviceName() + ' (' + appName + ')';
22 |
23 | // On 1809+, we try to change 'Web Player([browser])' to 'Xpo Music' in web-player.xxx.js. But it might fail
24 | // in case of code change, and also this feature is not supported on 1803 and below, so we replace both
25 | // 'Web Player (Microsoft Edge)' and 'Xpo Music' to the desired name.
26 | args[1].body = args[1].body.toString().replace('Web Player (Microsoft Edge)', spotifyConnectName).replace('Xpo Music', spotifyConnectName);
27 | }
28 |
29 | // Sending the real request
30 | var result = await fetch(...args);
31 |
32 | // Response interception
33 |
34 | // Returning response
35 | return result;
36 | })(args);
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/Preview.md:
--------------------------------------------------------------------------------
1 | # Xpo Music Preview
2 |
3 | Xpo Music v3 is currently in development. It utilizes [WinUI 3](https://docs.microsoft.com/en-us/windows/apps/winui/winui3/) and brings numerous improvements, notably a new rendering engine (Edge Chromium) which will result in a lot of performance and reliability improvements.
4 |
5 |
6 | ### When will this get released?
7 |
8 | Xpo Music v3.0 will replace the current version when WinUI 3 is stable enough. Currently WinUI 3 itself is in preview, and is not stable enough for production usage. The goal is to make Xpo Music v3 ready before WinUI 3 launch.
9 |
10 | ### How can I get Xpo Music 3 Preview?
11 |
12 | Here are the steps to get the Xpo Music Preview:
13 |
14 | 1. Make sure you have installed new Edge browser (Beta, Dev or Canary version) on your PC.
15 |
16 | 2. Install test certificate [from here](https://xpomusic.com/vNext/get/XpoMusic_3.0.1.0_Debug_Test/XpoMusic_3.0.1.0_x86_Debug.cer); make sure to install it on Local Machine, inside Trusted Root Certificate Authorities.
17 |
18 | 3. Get the app by clicking on "Get the app" button on [this page](https://xpomusic.com/vNext/get/).
19 |
20 | ### Is the preview stable enough for daily use?
21 |
22 | The core playback functionality is working and is already more performant than v2, but some crashes are to be expected.
23 |
24 | Also, certain Windows integration features like Live Tiles and Pin To Start are not yet available in v3 Preview.
25 |
26 | ### How can I send feedback for the preview version?
27 |
28 | Simply open an issue in this repository, make sure to mention you're talking about the preview version, and not the release version.
29 |
30 | ### Can I contribute?
31 |
32 | Of course! read [here](https://github.com/MahdiGhiasi/XpoMusic#contributing) for more information.
33 |
--------------------------------------------------------------------------------
/XpoMusic/Assets/TileTemplates/LiveTileArtistArtOnly.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {songName}
9 | {artistName}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {songName}
19 | {artistName}
20 | {albumName}
21 |
22 |
23 |
24 |
25 |
26 |
27 | {songName}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {artistName}
36 | {albumName}
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/PackageHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.ApplicationModel;
7 | using Windows.ApplicationModel.Core;
8 | using Windows.UI.Xaml;
9 |
10 | namespace XpoMusic.Helpers
11 | {
12 | public static class PackageHelper
13 | {
14 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
15 |
16 | private static readonly string proPackageName = "36835MahdiGhiasi.XpotifyPro";
17 | internal static readonly Uri ProStoreUri = new Uri("ms-windows-store://pdp/?productid=9PC9VV8KTXPL");
18 |
19 | public static string GetAppVersionString()
20 | {
21 | Package package = Package.Current;
22 | PackageId packageId = package.Id;
23 | PackageVersion version = packageId.Version;
24 |
25 | return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
26 | }
27 |
28 | public static string GetAppNameString()
29 | {
30 | return Package.Current.DisplayName;
31 | }
32 |
33 | public static Version GetAppVersion()
34 | {
35 | return Version.Parse(GetAppVersionString());
36 | }
37 |
38 | public static void RestartApp()
39 | {
40 | ToastHelper.SendReopenAppToast();
41 |
42 | LiveTileHelper.ClearLiveTile();
43 | logger.Info("Cleared live tile on RestartApp().");
44 |
45 | logger.Info("RestartApp() will close the app now.");
46 | Application.Current.Exit();
47 | }
48 |
49 | public static bool IsProVersion => Package.Current.Id.FamilyName.Contains(proPackageName);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/XpoMusic/Flyouts/SettingsFlyout.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
19 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/XpoMusic/XpotifyApi/DeveloperMessageApi.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using XpoMusic.XpotifyApi.Model;
3 | using Newtonsoft.Json;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Net.Http;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 |
11 | namespace XpoMusic.XpotifyApi
12 | {
13 | public static class DeveloperMessageApi
14 | {
15 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
16 |
17 | private static string DevMessagesUri
18 | {
19 | get
20 | {
21 | var endpoint = "https://xpomusic.com/devmessages";
22 | var version = PackageHelper.GetAppVersion().ToString(3);
23 | var fileName = "messages.json";
24 | var query = "?date=" + DateTime.Now.ToString("yyyyMMddHHmmss");
25 | var proSuffix = PackageHelper.IsProVersion ? "pro" : "";
26 |
27 | return $"{endpoint}/{version}{proSuffix}/{fileName}{query}";
28 | }
29 | }
30 |
31 | public static async Task GetDeveloperMessages()
32 | {
33 | try
34 | {
35 | using (var client = new HttpClient())
36 | {
37 | var response = await client.GetAsync(DevMessagesUri);
38 | if (response.IsSuccessStatusCode == false)
39 | {
40 | logger.Warn($"GetDeveloperMessages failed because request failed with status code {response.StatusCode}.");
41 | return null;
42 | }
43 |
44 | var result = await response.Content.ReadAsStringAsync();
45 | return JsonConvert.DeserializeObject(result);
46 | }
47 | }
48 | catch (Exception ex)
49 | {
50 | logger.Warn("GetDeveloperMessages failed: " + ex.ToString());
51 | return null;
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/XpoMusic/Assets/TileTemplates/LiveTileAlbumAndArtistArt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {songName}
9 | {artistName}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {songName}
20 | {artistName}
21 | {albumName}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {songName}
44 | {artistName}
45 | {albumName}
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/XpoMusic/Pages/HelpPage.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Keyboard shortcuts
18 |
19 |
20 |
21 |
22 | Enable local proxy for Xpo Music
23 |
24 |
25 |
26 | Want to report a problem, request a feature or ask a question?
27 |
28 |
29 |
30 | Come join us at
31 | UWP Community discord server
32 | ,
33 |
34 |
35 |
36 | create an issue on GitHub
37 | ,
38 |
39 |
40 | or contact me via email.
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/playbackStuckHelper.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.PlaybackStuckHelper
2 | {
3 | async function resolveStart() {
4 | var initialElapsedTime = StatusReport.getElapsedTime();
5 |
6 | if (Action.nextTrack() == '0')
7 | return;
8 |
9 | while (!StatusReport.getIsPlaying() || StatusReport.getElapsedTime() === 0 || StatusReport.getElapsedTime() === initialElapsedTime)
10 | await Common.sleep(500);
11 |
12 | if (Action.prevTrack() == '0')
13 | return;
14 |
15 | do {
16 | await Common.sleep(500);
17 | } while (!StatusReport.getIsPlaying())
18 |
19 | if (Action.prevTrack() == '0')
20 | return;
21 |
22 | var newInitialElapsedTime = StatusReport.getElapsedTime();
23 | while (!StatusReport.getIsPlaying() || StatusReport.getElapsedTime() === 0 || StatusReport.getElapsedTime() === newInitialElapsedTime)
24 | await Common.sleep(100);
25 | }
26 |
27 | export async function tryResolveStart() {
28 | // @ts-ignore
29 | if (window.xpotifyStuckStartResolvingInProgress === true)
30 | return;
31 |
32 | // @ts-ignore
33 | window.xpotifyStuckStartResolvingInProgress = true;
34 |
35 | var volume = StatusReport.getVolume();
36 | try {
37 | Action.seekVolume(0);
38 |
39 | await resolveStart();
40 | } finally {
41 | Action.seekVolume(volume);
42 |
43 | // @ts-ignore
44 | window.xpotifyStuckStartResolvingInProgress = false;
45 | }
46 | }
47 |
48 |
49 | export async function tryResolveMiddle() {
50 | var elapsedTime = StatusReport.getElapsedTime();
51 | var totalTime = StatusReport.getTotalTime();
52 |
53 | var changeTime = Math.max(1000, totalTime / 200);
54 |
55 | var newElapsedTime = elapsedTime - changeTime;
56 | if (newElapsedTime <= 0)
57 | newElapsedTime = elapsedTime + changeTime;
58 |
59 | Action.seekPlayback(newElapsedTime / totalTime);
60 |
61 | await Common.sleep(1000);
62 | Action.play();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Xpo Music (formerly Xpotify)
2 | A modern Spotify experience for Windows 10
3 |
4 | **Get it from [XpoMusic.com](https://xpomusic.com/)**
5 |
6 | ## Building from source
7 |
8 | #### Requirements
9 |
10 | You need to have these components installed in order to build Xpo Music:
11 |
12 | * Visual Studio 2019 (or 2017)
13 | * Windows 10 SDK 18362
14 | * TypeScript
15 | * [Web Compiler extension for Visual Studio](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.WebCompiler)
16 |
17 | #### Configuration
18 |
19 | Create a file named `Secrets.cs` in the `XpoMusic/` directory, and put the following content into it:
20 |
21 | namespace XpoMusic
22 | {
23 | internal static class Secrets
24 | {
25 | internal static readonly string SpotifyClientId = "";
26 | internal static readonly string SpotifyClientSecret = "";
27 | internal static readonly string GoogleAnalyticsTrackerId = "";
28 | internal static readonly string AppCenterId = "";
29 | }
30 | }
31 |
32 | Sign up on [Spotify Developer website](https://developer.spotify.com/) and get an API key for yourself. Put Id and Secret that you get from Spotify into the `SpotifyClientId` and `SpotifyClientSecret` fields. You can leave the `GoogleAnalyticsTrackerId` empty.
33 |
34 | Also, you will need to add `https://xpomusic.ghiasi.net/login/redirect` as the redirect URI on Spotify developer dashboard for the app entry you created. Alternatively, you can choose a different redirect URI and then modify [this line of code](https://github.com/MahdiGhiasi/Xpotify/blob/7e003b9879104a5b8b771f48475feca92155de8a/Xpotify/SpotifyApi/Authorization.cs#L18) accordingly.
35 |
36 | ## Contributing
37 |
38 | If you want to work on a bug or an enhancement that is already present and approved in the Issues, please leave a comment under that issue stating that you're going to work on it (so we can avoid doing duplicate work).
39 |
40 | Also, if you want to work on a new feature you have in mind for Xpo Music, please create an issue first so we can discuss it.
41 |
42 | ## License
43 |
44 | Xpo Music is available under [GNU General Public License v3.0](https://github.com/MahdiGhiasi/Xpotify/blob/master/LICENSE.md).
45 |
--------------------------------------------------------------------------------
/XpoMusic/ViewModels/TopBarViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.UI.ViewManagement;
7 | using Windows.UI.Xaml;
8 |
9 | namespace XpoMusic.ViewModels
10 | {
11 | public class TopBarViewModel : ViewModelBase
12 | {
13 | private double topBarButtonWidth;
14 | public double TopBarButtonWidth
15 | {
16 | get => topBarButtonWidth;
17 | set
18 | {
19 | topBarButtonWidth = value;
20 | FirePropertyChangedEvent(nameof(TopBarButtonWidth));
21 | FirePropertyChangedEvent(nameof(TopBarBehindSystemControlsAreaWidth));
22 | }
23 | }
24 |
25 | private double topBarButtonHeight;
26 | public double TopBarButtonHeight
27 | {
28 | get => topBarButtonHeight;
29 | set
30 | {
31 | topBarButtonHeight = value;
32 | FirePropertyChangedEvent(nameof(TopBarButtonHeight));
33 | }
34 | }
35 |
36 | public double TopBarBehindSystemControlsAreaWidth
37 | {
38 | get
39 | {
40 | var isTabletMode = (UIViewSettings.GetForCurrentView().UserInteractionMode == UserInteractionMode.Touch);
41 |
42 | return TopBarButtonWidth * (isTabletMode ? 1.0 : 3.0);
43 | }
44 | }
45 |
46 | private Visibility openLinkFromClipboardVisibility;
47 | public Visibility OpenLinkFromClipboardVisibility
48 | {
49 | get
50 | {
51 | if (!ShowExtraButtons)
52 | return Visibility.Collapsed;
53 |
54 | return openLinkFromClipboardVisibility;
55 | }
56 | set
57 | {
58 | openLinkFromClipboardVisibility = value;
59 | FirePropertyChangedEvent(nameof(OpenLinkFromClipboardVisibility));
60 | }
61 | }
62 |
63 | private bool showExtraButtons;
64 | public bool ShowExtraButtons
65 | {
66 | get => showExtraButtons;
67 | set
68 | {
69 | showExtraButtons = value;
70 | FirePropertyChangedEvent(nameof(ShowExtraButtons));
71 | FirePropertyChangedEvent(nameof(OpenLinkFromClipboardVisibility));
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/XpoMusic/Pages/DonatePage.xaml.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices.WindowsRuntime;
7 | using System.Threading.Tasks;
8 | using Windows.ApplicationModel.DataTransfer;
9 | using Windows.Foundation;
10 | using Windows.Foundation.Collections;
11 | using Windows.System;
12 | using Windows.UI.Xaml;
13 | using Windows.UI.Xaml.Controls;
14 | using Windows.UI.Xaml.Controls.Primitives;
15 | using Windows.UI.Xaml.Data;
16 | using Windows.UI.Xaml.Input;
17 | using Windows.UI.Xaml.Media;
18 | using Windows.UI.Xaml.Navigation;
19 |
20 | namespace XpoMusic.Pages
21 | {
22 | public sealed partial class DonatePage : Page
23 | {
24 | private readonly string bitcoinWalletAddress = "38TmLnUjix9NiPpiFoKV7qAjNeqaSi1EJH";
25 | private readonly string ethereumWalletAddress = "0x8c8d6a7f0c4e2da49bf9e249fdb349ffde884a00";
26 |
27 | public DonatePage()
28 | {
29 | this.InitializeComponent();
30 |
31 | bitcoinWallet.Text = bitcoinWalletAddress;
32 | ethereumWallet.Text = ethereumWalletAddress;
33 |
34 | runningProVersion.Visibility = PackageHelper.IsProVersion ? Visibility.Visible : Visibility.Collapsed;
35 | }
36 |
37 | private async void CopyBitcoinAddressToClipboard_Click(object sender, RoutedEventArgs e)
38 | {
39 | CopyTextToClipboard(bitcoinWalletAddress);
40 |
41 | AnalyticsHelper.Log("donate", "copyToClipboard", "bitcoin");
42 | bitcoinAddressCopiedCheckMark.Visibility = Visibility.Visible;
43 | await Task.Delay(TimeSpan.FromSeconds(3));
44 | bitcoinAddressCopiedCheckMark.Visibility = Visibility.Collapsed;
45 | }
46 |
47 | private async void CopyEthereumAddressToClipboard_Click(object sender, RoutedEventArgs e)
48 | {
49 | CopyTextToClipboard(ethereumWalletAddress);
50 |
51 | AnalyticsHelper.Log("donate", "copyToClipboard", "ethereum");
52 | ethereumAddressCopiedCheckMark.Visibility = Visibility.Visible;
53 | await Task.Delay(TimeSpan.FromSeconds(3));
54 | ethereumAddressCopiedCheckMark.Visibility = Visibility.Collapsed;
55 | }
56 |
57 | private void CopyTextToClipboard(string text)
58 | {
59 | var dataPackage = new DataPackage();
60 | dataPackage.SetText(text);
61 | Clipboard.SetContent(dataPackage);
62 | Clipboard.Flush();
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/PlaybackActionHelper.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes;
2 | using XpoMusic.SpotifyApi;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace XpoMusic.Helpers
10 | {
11 | public static class PlaybackActionHelper
12 | {
13 | private static WebViewController _controller;
14 |
15 | public static void SetController(WebViewController controller)
16 | {
17 | _controller = controller;
18 | }
19 |
20 | public static async Task Play()
21 | {
22 | if (await _controller.Play())
23 | {
24 | await Task.Delay(100);
25 | return true;
26 | }
27 |
28 | return await (new Player()).ResumePlaying();
29 | }
30 |
31 | public static async Task Pause()
32 | {
33 | if (await _controller.Pause())
34 | {
35 | await Task.Delay(100);
36 | return true;
37 | }
38 |
39 | return await (new Player()).Pause();
40 | }
41 |
42 | public static async Task NextTrack()
43 | {
44 | // Progress bar on CompactOverlay should jump *immediately* to 0,
45 | // so the user get the feeling that their command was received.
46 | PlayStatusTracker.LastPlayStatus.ProgressedMilliseconds = 0;
47 |
48 | if (await _controller.NextTrack())
49 | {
50 | await Task.Delay(100);
51 | return true;
52 | }
53 |
54 | return await (new Player()).NextTrack();
55 | }
56 |
57 | public static async Task PreviousTrack(bool canGoToBeginningOfCurrentSong = true)
58 | {
59 | // Progress bar on CompactOverlay should jump *immediately* to 0,
60 | // so the user get the feeling that their command was received.
61 | PlayStatusTracker.LastPlayStatus.ProgressedMilliseconds = 0;
62 |
63 | if (!canGoToBeginningOfCurrentSong && await (new Player()).PreviousTrack())
64 | {
65 | // Prefer API call when not going to the beginning of the current song
66 | return true;
67 | }
68 |
69 | if (await _controller.PreviousTrack(canGoToBeginningOfCurrentSong))
70 | {
71 | await Task.Delay(100);
72 | return true;
73 | }
74 |
75 | return await (new Player()).PreviousTrack();
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/XpoMusicInstaller/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("XpoMusicInstaller")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("XpoMusicInstaller")]
15 | [assembly: AssemblyCopyright("Copyright © 2020")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("1.0.0.0")]
55 | [assembly: AssemblyFileVersion("1.0.0.0")]
56 |
--------------------------------------------------------------------------------
/XpoMusic/XpotifyApi/AppConstantsApi.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Helpers;
2 | using Newtonsoft.Json;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Net.Http;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace XpoMusic.XpotifyApi
11 | {
12 | public static class AppConstantsApi
13 | {
14 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
15 |
16 | private static string UpdateUri
17 | {
18 | get
19 | {
20 | var endpoint = "https://xpomusic.com/constants";
21 | var version = PackageHelper.GetAppVersion().ToString(3);
22 | var fileName = "constants.json";
23 | var query = "?date=" + DateTime.Now.ToString("yyyyMMddHHmmss");
24 | var proSuffix = PackageHelper.IsProVersion ? "pro" : "";
25 |
26 | return $"{endpoint}/{version}{proSuffix}/{fileName}{query}";
27 | }
28 | }
29 |
30 | public static async Task GetAppConstantsString()
31 | {
32 | try
33 | {
34 | using (var client = new HttpClient())
35 | {
36 | var response = await client.GetAsync(UpdateUri);
37 | if (response.IsSuccessStatusCode == false)
38 | {
39 | if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
40 | {
41 | // If this happens, first session will contain the previous updated values since it's cached.
42 | // But the next sessions will have the values defined in code.
43 | logger.Info("GetAppConstantsString could not find a constants file on the server. Will return an empty json.");
44 | return "{}";
45 | }
46 | else
47 | {
48 | logger.Warn($"GetAppConstantsString failed because request failed with status code {response.StatusCode}.");
49 | return null;
50 | }
51 | }
52 |
53 | var result = await response.Content.ReadAsStringAsync();
54 | return result;
55 | }
56 | }
57 | catch (Exception ex)
58 | {
59 | logger.Warn("GetAppConstantsString failed: " + ex.ToString());
60 | return null;
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/XpoMusic/Controls/ProxyConfiguration.xaml.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes;
2 | using XpoMusic.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Runtime.InteropServices.WindowsRuntime;
8 | using System.Threading.Tasks;
9 | using Windows.ApplicationModel.Core;
10 | using Windows.Foundation;
11 | using Windows.Foundation.Collections;
12 | using Windows.UI.Xaml;
13 | using Windows.UI.Xaml.Controls;
14 | using Windows.UI.Xaml.Controls.Primitives;
15 | using Windows.UI.Xaml.Data;
16 | using Windows.UI.Xaml.Input;
17 | using Windows.UI.Xaml.Media;
18 | using Windows.UI.Xaml.Navigation;
19 | using Windows.System;
20 |
21 | namespace XpoMusic.Controls
22 | {
23 | public sealed partial class ProxyConfiguration : UserControl
24 | {
25 | private readonly Uri localProxyHelpPage = new Uri("https://xpomusic.com/loopback");
26 |
27 | public ProxyConfiguration()
28 | {
29 | this.InitializeComponent();
30 | }
31 |
32 | private void PortTextBox_BeforeTextChanging(TextBox sender, TextBoxBeforeTextChangingEventArgs args)
33 | {
34 | args.Cancel = args.NewText.Any(c => !char.IsDigit(c));
35 | }
36 |
37 | private async void Button_Click(object sender, RoutedEventArgs e)
38 | {
39 | // Save config
40 | LocalConfiguration.IsCustomProxyEnabled = true;
41 | LocalConfiguration.CustomProxyType = ViewModel.ProxyType;
42 | LocalConfiguration.CustomProxyAddress = ViewModel.ProxyAddress;
43 | LocalConfiguration.CustomProxyPort = ViewModel.ProxyPort;
44 |
45 | ContentDialog cd = new ContentDialog
46 | {
47 | Title = "Restart Xpo Music",
48 | Content = "We need to restart Xpo Music in order to apply proxy settings.",
49 | RequestedTheme = ElementTheme.Dark,
50 | IsPrimaryButtonEnabled = true,
51 | PrimaryButtonText = "Restart Now",
52 | IsSecondaryButtonEnabled = true,
53 | SecondaryButtonText = "Restart Later",
54 | };
55 | cd.PrimaryButtonClick += (s, args) =>
56 | {
57 | PackageHelper.RestartApp();
58 | };
59 |
60 | await cd.ShowAsync();
61 | }
62 |
63 | private void RestartApp_Click(object sender, RoutedEventArgs e)
64 | {
65 | PackageHelper.RestartApp();
66 | }
67 |
68 | private async void LocalProxyHelp_Click(object sender, RoutedEventArgs e)
69 | {
70 | await Launcher.LaunchUriAsync(localProxyHelpPage);
71 | AnalyticsHelper.Log("helpLink", "localProxy");
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/SongImageProvider.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.SpotifyApi;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using XpoMusic.Classes.Cache;
9 |
10 | namespace XpoMusic.Helpers
11 | {
12 | public static class SongImageProvider
13 | {
14 | private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
15 |
16 | static Dictionary artistImages = new Dictionary();
17 | static Dictionary albumImages = new Dictionary();
18 | static Dictionary playlistImages = new Dictionary();
19 |
20 | public static async Task GetArtistArt(string artistId)
21 | {
22 | if (string.IsNullOrEmpty(artistId))
23 | return "";
24 |
25 | try
26 | {
27 | var artist = await GlobalCache.Artist.GetItem(artistId);
28 |
29 | artistImages[artistId] = artist.images.OrderBy(x => x.width).Last().url;
30 | return artistImages[artistId];
31 | }
32 | catch (Exception ex)
33 | {
34 | logger.Info($"Fetching artist art for {artistId} failed: {ex}");
35 | return "";
36 | }
37 | }
38 |
39 | public static async Task GetAlbumArt(string albumId)
40 | {
41 | if (string.IsNullOrEmpty(albumId))
42 | return "";
43 |
44 | try
45 | {
46 | var album = await GlobalCache.Album.GetItem(albumId);
47 |
48 | albumImages[albumId] = album.images.OrderBy(x => x.width).Last().url;
49 | return albumImages[albumId];
50 | }
51 | catch (Exception ex)
52 | {
53 | logger.Info($"Fetching album art for {albumId} failed: {ex}");
54 | return "";
55 | }
56 | }
57 |
58 | public static async Task GetPlaylistArt(string playlistId)
59 | {
60 | if (string.IsNullOrEmpty(playlistId))
61 | return "";
62 |
63 | try
64 | {
65 | var playlist = await GlobalCache.Playlist.GetItem(playlistId);
66 |
67 | playlistImages[playlistId] = playlist.images.OrderBy(x => x.width).Last().url;
68 | return playlistImages[playlistId];
69 | }
70 | catch (Exception ex)
71 | {
72 | logger.Info($"Fetching playlist art for {playlistId} failed: {ex}");
73 | return "";
74 | }
75 | }
76 | }
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/ToastHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace XpoMusic.Helpers
9 | {
10 | public static class ToastHelper
11 | {
12 | public static void SendDebugToast(string title, string text)
13 | {
14 | #if DEBUG
15 | // template to load for showing Toast Notification
16 | var xmlToastTemplate = "" +
17 | "" +
18 | "" +
19 | "" + WebUtility.HtmlEncode(title) + "" +
20 | "" +
21 | WebUtility.HtmlEncode(text) +
22 | "" +
23 | "" +
24 | "" +
25 | "";
26 |
27 | // load the template as XML document
28 | var xmlDocument = new Windows.Data.Xml.Dom.XmlDocument();
29 | xmlDocument.LoadXml(xmlToastTemplate);
30 |
31 | // create the toast notification and show to user
32 | var toastNotification = new Windows.UI.Notifications.ToastNotification(xmlDocument);
33 | var notification = Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier();
34 | notification.Show(toastNotification);
35 | #endif
36 | }
37 |
38 | public static void SendReopenAppToast()
39 | {
40 | // template to load for showing Toast Notification
41 | var xmlToastTemplate = "" +
42 | "" +
43 | "" +
44 | "Tap here to open Xpo Music again" +
45 | "" +
46 | "" +
47 | "";
48 |
49 | // load the template as XML document
50 | var xmlDocument = new Windows.Data.Xml.Dom.XmlDocument();
51 | xmlDocument.LoadXml(xmlToastTemplate);
52 |
53 | // create the toast notification and show to user
54 | var toastNotification = new Windows.UI.Notifications.ToastNotification(xmlDocument);
55 | var notification = Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier();
56 | notification.Show(toastNotification);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/XpoMusic/Helpers/WhatsNewHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Windows.Storage;
7 |
8 | namespace XpoMusic.Helpers
9 | {
10 | public static class WhatsNewHelper
11 | {
12 | const string latestWhatsNewVersionKey = "LatestWhatsNewVersion";
13 | public static readonly bool testMode = false;
14 |
15 | public static bool ShouldShowWhatsNew()
16 | {
17 | if (testMode)
18 | return true;
19 |
20 | if (!ApplicationData.Current.LocalSettings.Values.ContainsKey(latestWhatsNewVersionKey))
21 | {
22 | MarkThisWhatsNewAsRead();
23 | return false;
24 | }
25 |
26 | if (Version.TryParse(ApplicationData.Current.LocalSettings.Values[latestWhatsNewVersionKey].ToString(), out Version v))
27 | {
28 | if (v < Version.Parse(PackageHelper.GetAppVersionString()))
29 | {
30 | if (GetWhatsNewContentId().Count > 0)
31 | {
32 | return true;
33 | }
34 | }
35 | }
36 |
37 | return false;
38 | }
39 |
40 | public static List GetWhatsNewContentIdAndMarkAsRead()
41 | {
42 | if (!ShouldShowWhatsNew())
43 | return new List();
44 |
45 | List output = GetWhatsNewContentId();
46 |
47 | MarkThisWhatsNewAsRead();
48 |
49 | return output;
50 | }
51 |
52 | private static List GetWhatsNewContentId()
53 | {
54 | Version prevVersion = new Version(0, 0, 0, 0);
55 |
56 | if (!testMode)
57 | if (ApplicationData.Current.LocalSettings.Values.ContainsKey(latestWhatsNewVersionKey))
58 | Version.TryParse(ApplicationData.Current.LocalSettings.Values[latestWhatsNewVersionKey].ToString(), out prevVersion);
59 |
60 | List output = new List();
61 |
62 | if (prevVersion < new Version("2.1.0.0"))
63 | output.Add("5");
64 |
65 | if (prevVersion < new Version("1.6.0.0"))
66 | output.Add("4");
67 |
68 | if (prevVersion < new Version("1.5.0.0"))
69 | output.Add("3");
70 |
71 | if (prevVersion < new Version("1.4.2.0"))
72 | output.Add("2");
73 |
74 | if (prevVersion < new Version("1.1.0.0"))
75 | output.Add("1");
76 |
77 | return output;
78 | }
79 |
80 | private static void MarkThisWhatsNewAsRead()
81 | {
82 | ApplicationData.Current.LocalSettings.Values[latestWhatsNewVersionKey] = PackageHelper.GetAppVersionString();
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/XpoMusic/Controls/SplashScreen.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices.WindowsRuntime;
6 | using Windows.Foundation;
7 | using Windows.Foundation.Collections;
8 | using Windows.UI.Xaml;
9 | using Windows.UI.Xaml.Controls;
10 | using Windows.UI.Xaml.Controls.Primitives;
11 | using Windows.UI.Xaml.Data;
12 | using Windows.UI.Xaml.Input;
13 | using Windows.UI.Xaml.Media;
14 | using Windows.UI.Xaml.Navigation;
15 | using XpoMusic.Classes.Model;
16 | using XpoMusic.Helpers;
17 |
18 | namespace XpoMusic.Controls
19 | {
20 | public sealed partial class SplashScreen : UserControl
21 | {
22 | public enum SplashScreenShowState
23 | {
24 | Visible,
25 | Closed,
26 | ClosedQuick,
27 | }
28 |
29 | #region Custom Properties
30 | public static readonly DependencyProperty SplashStateProperty = DependencyProperty.Register(
31 | "SplashState", typeof(SplashScreenShowState), typeof(SplashScreen), new PropertyMetadata(defaultValue: SplashScreenShowState.Visible,
32 | propertyChangedCallback: new PropertyChangedCallback(OnSplashStatePropertyChanged)));
33 |
34 | public SplashScreenShowState SplashState
35 | {
36 | get => (SplashScreenShowState)GetValue(SplashStateProperty);
37 | set
38 | {
39 | if (SplashState != value)
40 | SetValue(SplashStateProperty, value);
41 |
42 | switch (value)
43 | {
44 | case SplashScreenShowState.Visible:
45 | VisualStateManager.GoToState(this, nameof(SplashScreenVisibleVisualState), true);
46 | break;
47 | case SplashScreenShowState.Closed:
48 | VisualStateManager.GoToState(this, nameof(SplashScreenClosedVisualState), true);
49 | break;
50 | case SplashScreenShowState.ClosedQuick:
51 | default:
52 | VisualStateManager.GoToState(this, nameof(SplashScreenClosedQuickVisualState), true);
53 | break;
54 | }
55 | }
56 | }
57 |
58 | private static void OnSplashStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
59 | {
60 | (d as SplashScreen).SplashState = (SplashScreenShowState)e.NewValue;
61 | }
62 | #endregion
63 |
64 | public SplashScreen()
65 | {
66 | this.InitializeComponent();
67 | }
68 |
69 | private void UserControl_Loaded(object sender, RoutedEventArgs e)
70 | {
71 | if (ThemeHelper.GetCurrentTheme() == Theme.Light)
72 | splashScreenToLightStoryboard.Begin();
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/XpoMusic/Pages/DonatePage.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
20 |
24 |
28 |
33 |
34 |
40 |
44 |
45 |
49 |
54 |
55 |
61 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/pageTitleFinder.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.PageTitleFinder {
2 |
3 | function titleCase(str) {
4 | var splitStr = str.toLowerCase().split(' ');
5 | for (var i = 0; i < splitStr.length; i++) {
6 | // You do not need to check if i is larger than splitStr length, as your for does that for you
7 | // Assign it back to the array
8 | splitStr[i] = splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1);
9 | }
10 | // Directly return the joined string
11 | return splitStr.join(' ');
12 | }
13 |
14 | function searchForNavBarSelectedItem() {
15 | var candidates = document.querySelectorAll(".Root__main-view nav a");
16 |
17 | var selIndex = -1;
18 |
19 | for (var i = 0; i < candidates.length; i++) {
20 | if (candidates[i].classList.length == 1) {
21 | continue;
22 | } else if (candidates[i].classList.length == 2) {
23 | if (selIndex == -1) {
24 | selIndex = i;
25 | } else {
26 | return '';
27 | }
28 | } else {
29 | return '';
30 | }
31 | }
32 |
33 | if (selIndex == -1) {
34 | return '';
35 | } else {
36 | return titleCase((candidates[selIndex]).innerText);
37 | }
38 | }
39 |
40 | function searchForHTag(selector) {
41 | var candidates = document.querySelectorAll(".Root__main-view " + selector);
42 | for (var i = 0; i < candidates.length; i++) {
43 | var s = (candidates[i]).innerText;
44 | if (s.length > 0 && s.length < 80) {
45 | return s;
46 | }
47 | }
48 | return '';
49 | }
50 |
51 | export function getTitle() {
52 | var h1AndNav = document.querySelectorAll('.Root__main-view h1, .Root__main-view nav');
53 | var result;
54 |
55 | if (h1AndNav.length > 0) {
56 |
57 | // Between h1 and nav tags, prioritise the one that comes first
58 | if (h1AndNav[0].tagName.toLowerCase() == "nav") {
59 | result = searchForNavBarSelectedItem();
60 | if (result != '')
61 | return result;
62 |
63 | result = searchForHTag('h1');
64 | if (result != '')
65 | return result;
66 | } else {
67 | result = searchForHTag('h1');
68 | if (result != '')
69 | return result;
70 |
71 | result = searchForNavBarSelectedItem();
72 | if (result != '')
73 | return result;
74 | }
75 | }
76 |
77 | result = searchForHTag('h2');
78 | if (result != '')
79 | return result;
80 |
81 | return window.location.href.substring(window.location.href.lastIndexOf('/') + 1).replace(/-/g, ' ');
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/XpoMusic/Flyouts/SettingsFlyout.xaml.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes;
2 | using XpoMusic.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Runtime.InteropServices.WindowsRuntime;
8 | using System.Runtime.Serialization;
9 | using Windows.ApplicationModel.Email;
10 | using Windows.Foundation;
11 | using Windows.Foundation.Collections;
12 | using Windows.Storage;
13 | using Windows.Storage.Streams;
14 | using Windows.System;
15 | using Windows.UI.Xaml;
16 | using Windows.UI.Xaml.Controls;
17 | using Windows.UI.Xaml.Controls.Primitives;
18 | using Windows.UI.Xaml.Data;
19 | using Windows.UI.Xaml.Input;
20 | using Windows.UI.Xaml.Media;
21 | using Windows.UI.Xaml.Media.Animation;
22 | using Windows.UI.Xaml.Navigation;
23 | using XpoMusic.Pages;
24 |
25 | namespace XpoMusic.Flyouts
26 | {
27 | public sealed partial class SettingsFlyout : UserControl, IFlyout
28 | {
29 | public event EventHandler FlyoutCloseRequest;
30 |
31 | public SettingsFlyout()
32 | {
33 | this.InitializeComponent();
34 |
35 | if (ThemeHelper.GetCurrentTheme() == Classes.Model.Theme.Dark)
36 | RequestedTheme = ElementTheme.Dark;
37 | else
38 | RequestedTheme = ElementTheme.Light;
39 | }
40 |
41 | public void InitFlyout(int tabId)
42 | {
43 | navigationView.SelectedItem = navigationView.MenuItems[tabId];
44 | }
45 |
46 | private void NavigationView_BackRequested(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewBackRequestedEventArgs args)
47 | {
48 | FlyoutCloseRequest?.Invoke(this, new EventArgs());
49 | }
50 |
51 | private void NavigationView_SelectionChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
52 | {
53 | if (args.SelectedItem == settingsMenuItem)
54 | {
55 | contentFrame.Navigate(typeof(SettingsPage), null, new EntranceNavigationTransitionInfo());
56 | AnalyticsHelper.PageView("SettingsPage");
57 | }
58 | else if (args.SelectedItem == helpMenuItem)
59 | {
60 | contentFrame.Navigate(typeof(HelpPage), null, new EntranceNavigationTransitionInfo());
61 | AnalyticsHelper.PageView("HelpPage");
62 | }
63 | else if (args.SelectedItem == aboutMenuItem)
64 | {
65 | contentFrame.Navigate(typeof(AboutPage), null, new EntranceNavigationTransitionInfo());
66 | AnalyticsHelper.PageView("AboutPage");
67 | }
68 | else if (args.SelectedItem == donateMenuItem)
69 | {
70 | contentFrame.Navigate(typeof(DonatePage), null, new EntranceNavigationTransitionInfo());
71 | AnalyticsHelper.PageView("DonatePage");
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/XpoMusic/Flyouts/DeveloperMessageFlyout.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
39 |
45 |
46 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/XpoMusic/Package.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Xpo Music
7 | Mahdi Ghiasi
8 | Assets\Logo\StoreLogo.png
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Assets\Logo\Square44x44Logo.png
32 | Xpo Music
33 |
34 |
35 |
36 |
37 | Assets\Logo\Square44x44Logo.png
38 | Xpo Music
39 |
40 |
41 |
42 |
43 | Assets\Logo\Square44x44Logo.png
44 | Xpo Music
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/XpoMusic/Package.debug.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Xpo Music Alpha
7 | Mahdi Ghiasi
8 | Assets\Logo\StoreLogo.png
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Assets\Logo\Square44x44Logo.png
32 | Xpo Music
33 |
34 |
35 |
36 |
37 | Assets\Logo\Square44x44Logo.png
38 | Xpo Music
39 |
40 |
41 |
42 |
43 | Assets\Logo\Square44x44Logo.png
44 | Xpo Music
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/XpoMusic/Scripts/Common/indeterminateProgressBarHandler.ts:
--------------------------------------------------------------------------------
1 | namespace XpoMusicScript.Common.IndeterminateProgressBarHandler {
2 |
3 | declare var XpoMusic: any;
4 |
5 | function trackLoadCheck(initialProgress) {
6 | try {
7 | var playbackBar = (document.querySelectorAll(".Root__now-playing-bar .playback-bar")[0]);
8 | var progressBarProgress = ((playbackBar.querySelectorAll(".progress-bar__fg")[0])).style.transform;
9 |
10 | var isPlaying = true;
11 | try {
12 | isPlaying = StatusReport.getIsPlaying();
13 | } catch (ex) { }
14 |
15 | if (progressBarProgress != initialProgress || isPlaying === false) {
16 | var progressBarBg = (playbackBar.querySelectorAll(".progress-bar__bg")[0]);
17 | var times = playbackBar.querySelectorAll(".playback-bar__progress-time");
18 |
19 | progressBarBg.style.opacity = '1';
20 | ((times[0])).style.opacity = '1';
21 | ((times[1])).style.opacity = '1';
22 |
23 | XpoMusic.hideProgressBar();
24 | } else {
25 | setTimeout(function () {
26 | trackLoadCheck(initialProgress);
27 | }, 250);
28 | }
29 | } catch (ex) {
30 | console.log("trackLoadCheck failed.", ex);
31 | }
32 | }
33 |
34 | export function onTrackLoadBegin() {
35 | try {
36 | var playbackBar = (document.querySelectorAll(".Root__now-playing-bar .playback-bar")[0]);
37 | var progressBarBg = (playbackBar.querySelectorAll(".progress-bar__bg")[0]);
38 | var times = playbackBar.querySelectorAll(".playback-bar__progress-time");
39 |
40 | progressBarBg.style.opacity = '0';
41 | ((times[0])).style.opacity = '0';
42 | ((times[1])).style.opacity = '0';
43 |
44 | var rect = progressBarBg.getBoundingClientRect();
45 |
46 | XpoMusic.showProgressBar(rect.left / window.innerWidth, rect.top / window.innerHeight, rect.width / window.innerWidth);
47 |
48 | setTimeout(function () {
49 | var progressBarProgress = ((playbackBar.querySelectorAll(".progress-bar__fg")[0])).style.transform;
50 |
51 | if (progressBarProgress.indexOf("translate") == -1) {
52 | // Something's wrong. Will ignore progress bar
53 | progressBarBg.style.opacity = '1';
54 | ((times[0])).style.opacity = '1';
55 | ((times[1])).style.opacity = '1';
56 | console.log("onTrackLoadBegin failed, and reverted changes.")
57 | return;
58 | }
59 |
60 | setTimeout(function () {
61 | trackLoadCheck(progressBarProgress);
62 | }, 250);
63 | }, 250);
64 | } catch (ex) {
65 | console.log("onTrackLoadBegin failed.", ex);
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/XpoMusic/SpotifyApi/Authorization.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Http;
7 | using System.Net.Http.Headers;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using XpoMusic.Classes;
11 |
12 | namespace XpoMusic.SpotifyApi
13 | {
14 | public static class Authorization
15 | {
16 | internal static readonly string Scopes = "user-read-recently-played playlist-read-private playlist-read-collaborative playlist-modify-private playlist-modify-public user-read-email user-library-read user-library-modify user-read-playback-state user-modify-playback-state user-read-private user-read-currently-playing user-follow-read user-follow-modify streaming user-top-read app-remote-control";
17 | internal static readonly string SpotifyLoginUri = "https://accounts.spotify.com/";
18 | internal static readonly string FacebookLoginFinishRedirectUri = "https://accounts.spotify.com/api/facebook/oauth/access_token";
19 | internal static readonly string RedirectUri = "https://xpomusic.ghiasi.net/login/redirect";
20 | internal static readonly string FacebookLoginUri = "https://www.facebook.com/login.php";
21 |
22 | public static string GetAuthorizationUrl(string state)
23 | {
24 | return "https://accounts.spotify.com/authorize?"
25 | + $"client_id={WebUtility.UrlEncode(Secrets.SpotifyClientId)}&"
26 | + $"response_type=code&"
27 | + $"redirect_uri={WebUtility.UrlEncode(RedirectUri)}&"
28 | + $"state={WebUtility.UrlEncode(state)}&"
29 | + $"scope={WebUtility.UrlEncode(Scopes)}&"
30 | + $"show_dialog=false";
31 | }
32 |
33 | public static async Task RetrieveAndSaveTokensFromAuthCode(string code)
34 | {
35 | var httpClient = new HttpClient();
36 |
37 | HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Post, "https://accounts.spotify.com/api/token");
38 | msg.Headers.Clear();
39 | msg.Content = new FormUrlEncodedContent(new KeyValuePair[]
40 | {
41 | new KeyValuePair("grant_type", "authorization_code"),
42 | new KeyValuePair("code", code),
43 | new KeyValuePair("redirect_uri", RedirectUri),
44 | new KeyValuePair("client_id", Secrets.SpotifyClientId),
45 | new KeyValuePair("client_secret", Secrets.SpotifyClientSecret),
46 | });
47 |
48 | var response = await httpClient.SendAsync(msg);
49 | var responseString = await response.Content.ReadAsStringAsync();
50 | var responseData = JsonConvert.DeserializeObject>(responseString);
51 |
52 | var accessToken = responseData["access_token"].ToString();
53 | var refreshToken = responseData["refresh_token"].ToString();
54 |
55 | TokenHelper.SaveTokens(accessToken, refreshToken);
56 |
57 | LocalConfiguration.ApiTokenVersion = 2;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/XpoMusic/Flyouts/WhatsNewFlyout.xaml.cs:
--------------------------------------------------------------------------------
1 | using XpoMusic.Classes;
2 | using XpoMusic.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Runtime.InteropServices.WindowsRuntime;
8 | using Windows.Foundation;
9 | using Windows.Foundation.Collections;
10 | using Windows.System;
11 | using Windows.UI.Xaml;
12 | using Windows.UI.Xaml.Controls;
13 | using Windows.UI.Xaml.Controls.Primitives;
14 | using Windows.UI.Xaml.Data;
15 | using Windows.UI.Xaml.Input;
16 | using Windows.UI.Xaml.Media;
17 | using Windows.UI.Xaml.Navigation;
18 |
19 | namespace XpoMusic.Flyouts
20 | {
21 | public sealed partial class WhatsNewFlyout : UserControl, IFlyout
22 | {
23 | public event EventHandler FlyoutCloseRequest;
24 |
25 | public WhatsNewFlyout()
26 | {
27 | this.InitializeComponent();
28 |
29 | if (ThemeHelper.GetCurrentTheme() == Classes.Model.Theme.Dark)
30 | RequestedTheme = ElementTheme.Dark;
31 | else
32 | RequestedTheme = ElementTheme.Light;
33 |
34 | foreach (var item in Content.Children)
35 | {
36 | var sp = (item as StackPanel);
37 | if (sp != null)
38 | sp.Visibility = Visibility.Collapsed;
39 | }
40 |
41 | if (WhatsNewHelper.testMode)
42 | VersionText.Text = "Next";
43 | else
44 | VersionText.Text = PackageHelper.GetAppVersionString();
45 |
46 | navigationView.SelectedItem = navigationView.MenuItems.First();
47 | }
48 |
49 | private void NavigationView_BackRequested(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewBackRequestedEventArgs args)
50 | {
51 | FlyoutCloseRequest?.Invoke(this, new EventArgs());
52 | }
53 |
54 | public void InitFlyout()
55 | {
56 | bool changelogPresent = false;
57 | var ids = WhatsNewHelper.GetWhatsNewContentIdAndMarkAsRead();
58 |
59 | foreach (var item in Content.Children)
60 | {
61 | var sp = item as StackPanel;
62 | if (sp == null)
63 | continue;
64 |
65 | if (ids.Contains(sp.Tag.ToString()))
66 | {
67 | changelogPresent = true;
68 | sp.Visibility = Visibility.Visible;
69 | }
70 | else
71 | {
72 | sp.Visibility = Visibility.Collapsed;
73 | }
74 | }
75 |
76 | if (!changelogPresent)
77 | FlyoutCloseRequest?.Invoke(this, new EventArgs());
78 | #if !DEBUG
79 | //App.Tracker.Send(HitBuilder.CreateCustomEvent("What's new", "Show", DeviceInfo.ApplicationVersionString).Build());
80 | #endif
81 | }
82 |
83 | private async void GetXpotifyPro_Click(object sender, RoutedEventArgs e)
84 | {
85 | await Launcher.LaunchUriAsync(PackageHelper.ProStoreUri);
86 | AnalyticsHelper.Log("whatsNew", "getXpotifyPro");
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------