├── 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 | --------------------------------------------------------------------------------