├── .dockerignore ├── .github └── workflows │ ├── codeql-analysis.yml │ ├── podcast-api.yml │ ├── podcast-dotnet-maui-blazor-cd.yml │ ├── podcast-dotnet-maui-blazor-ci.yml │ ├── podcast-dotnet-maui-cd.yml │ ├── podcast-dotnet-maui-ci.yml │ ├── podcast-hub.yml │ ├── podcast-loadtest.yml │ ├── podcast-web.yml │ ├── template-api.yml │ ├── template-dotnet-maui-cd.yml │ ├── template-dotnet-maui-ci.yml │ ├── template-hub.yml │ └── template-web.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── NetPodcast.Services.sln ├── NetPodcast.sln ├── NuGet.config ├── Podcast.Web.sln ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── Test ├── ARMTemplate │ ├── parameters.json │ └── template.json ├── DemoPodcastTest.jmx ├── Shows.csv ├── config.yml └── jmeter-plugins-functions-2.2.jar ├── deploy ├── Android │ └── android-keystore-test.jks.gpg ├── Images │ ├── Covers │ │ ├── .NET Rocks!.jpg │ │ ├── .future.png │ │ ├── Adventures in .NET – Devchat.tv.jpg │ │ ├── Adventures in .NET.jpg │ │ ├── Asp.Net Monsters.jpg │ │ ├── Away From The Keyboard.png │ │ ├── CodeNewbie.png │ │ ├── Gone Mobile.jpg │ │ ├── Hanselminutes with Scott Hanselman.jpg │ │ ├── Hello World (Audio) - Channel 9.jpg │ │ ├── Last Week in .NET.jpg │ │ ├── MS Dev Show.png │ │ ├── Merge Conflict.jpg │ │ ├── Microsoft 365 Developer Podcast.jpg │ │ ├── Microsoft Mechanics Podcast.png │ │ ├── Null Pointers.jpg │ │ ├── Paths Uncovered.jpg │ │ ├── RunAs Radio.jpg │ │ ├── The .NET Core Podcast.jpg │ │ ├── The .NET MAUI Podcast.jpg │ │ ├── The Unhandled Exception Podcast.jpeg │ │ ├── Upwards.png │ │ ├── intrazone.png │ │ └── no dogma podcast.jpg │ ├── Deploy-Images.ps1 │ └── Readme.md ├── Services │ ├── acr.bicep │ ├── api.bicep │ └── hub.bicep └── Web │ ├── web.bicep │ └── web.json ├── docker-compose.arm64.yml ├── docker-compose.dcproj ├── docker-compose.override.yml ├── docker-compose.yml ├── docs ├── demos │ ├── aspnetcore-blazor │ │ └── README.md │ ├── authentication │ │ ├── README.md │ │ └── images │ │ │ ├── add-swagger-authorization.png │ │ │ ├── authorize-swagger.png │ │ │ ├── create-app-role.png │ │ │ ├── delete-feed.png │ │ │ ├── generated-token.png │ │ │ ├── get-feeds.png │ │ │ ├── provide-application-name.png │ │ │ ├── select-active-directory.png │ │ │ ├── select-app-registrations.png │ │ │ ├── select-app-roles.png │ │ │ ├── select-create-app-role.png │ │ │ └── select-new-registration.png │ ├── azure-load-testing │ │ └── README.md │ ├── azure-monitor │ │ └── README.md │ ├── azurecontainerapps │ │ ├── MessageCount.ps1 │ │ ├── Readme.md │ │ ├── Simulate-Feed-Requests.ps1 │ │ ├── feeds.json │ │ ├── scale-out-demo-0.png │ │ ├── scale-out-demo-1.png │ │ └── scale-out-demo-2.png │ ├── blazor-hybrid │ │ └── README.md │ ├── dotnet-maui │ │ └── README.md │ ├── playwright-tests │ │ └── README.md │ └── powerapps │ │ ├── README.md │ │ └── assets │ │ ├── customconnector.jpg │ │ ├── editpowerapp.jpg │ │ ├── onstart.jpg │ │ ├── powerapp.jpg │ │ └── refreshbutton.jpg ├── deploy-websites-services.md └── images │ ├── Enable-Workflow.png │ ├── all-workflow-runs.png │ ├── arch_diagram_podcast.png │ ├── azure-secret-add.png │ ├── docker-app-config.png │ ├── docker-services-config.png │ ├── gh-configured-secrets.png │ ├── github-repo-settings.png │ ├── net-podcasts.png │ ├── podcast-api-ci-run.png │ └── select-secrets.png └── src ├── Lib └── SharedMauiLib │ ├── INativeAudioService.cs │ ├── Platforms │ ├── Android │ │ ├── CurrentActivity │ │ │ ├── ActivityEvent.cs │ │ │ ├── ActivityEventArgs.cs │ │ │ ├── CrossCurrentActivity.cs │ │ │ ├── CurrentActivityImplementation.cs │ │ │ └── ICurrentActivity.cs │ │ ├── EventHandlers.cs │ │ ├── IAudioActivity.cs │ │ ├── MediaPlayerService.cs │ │ ├── MediaPlayerServiceConnection.cs │ │ ├── NativeAudioService.cs │ │ ├── NotificationHelper.cs │ │ └── RemoteControlBroadcastReceiver.cs │ ├── MacCatalyst │ │ └── NativeAudioService.cs │ ├── Windows │ │ └── NativeAudioService.cs │ └── iOS │ │ └── NativeAudioService.cs │ ├── Resources │ └── Images │ │ └── player_play.svg │ └── SharedMauiLib.csproj ├── Mobile ├── .editorconfig ├── App.xaml ├── App.xaml.cs ├── Blazor │ └── ListenTogetherComponent.razor ├── Config.cs ├── Controls │ ├── HeaderControl.xaml │ ├── HeaderControl.xaml.cs │ ├── Player.xaml │ └── Player.xaml.cs ├── Converters │ ├── DurationConverter.cs │ ├── IsNullConverter.cs │ └── TextToTypeTextConverter.cs ├── GlobalUsings.cs ├── Helpers │ ├── Settings.cs │ └── TheTheme.cs ├── MauiProgram.cs ├── MauiWindow.cs ├── Messaging │ ├── ChangeThemeNotification.cs │ └── LeaveRoomNotification.cs ├── Microsoft.NetConf2021.Maui.csproj ├── Models │ ├── AppSection.cs │ ├── Category.cs │ ├── Episode.cs │ ├── Responses │ │ ├── CategoryResponse.cs │ │ ├── EpisodeResponse.cs │ │ └── ShowResponse.cs │ ├── Show.cs │ └── ShowGroup.cs ├── Pages │ ├── CategoriesPage.xaml │ ├── CategoriesPage.xaml.cs │ ├── CategoryPage.xaml │ ├── CategoryPage.xaml.cs │ ├── DesktopShell.xaml │ ├── DesktopShell.xaml.cs │ ├── DiscoverPage.xaml │ ├── DiscoverPage.xaml.cs │ ├── EpisodeDetailPage.xaml │ ├── EpisodeDetailPage.xaml.cs │ ├── ListenLaterPage.xaml │ ├── ListenLaterPage.xaml.cs │ ├── ListenTogetherPage.xaml │ ├── ListenTogetherPage.xaml.cs │ ├── MobileShell.xaml │ ├── MobileShell.xaml.cs │ ├── PagesExtensions.cs │ ├── SettingsPage.xaml │ ├── SettingsPage.xaml.cs │ ├── ShowDetailPage.xaml │ ├── ShowDetailPage.xaml.cs │ ├── SubscriptionsPage.xaml │ └── SubscriptionsPage.xaml.cs ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ ├── MainApplication.cs │ │ └── Resources │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── ConnectivityService.cs │ │ ├── Info.plist │ │ └── Program.cs │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Package.appxmanifest │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Entitlements.plist │ │ ├── Info.plist │ │ └── Program.cs ├── Podcasts.DotnetMaui.sln ├── Properties │ └── launchSettings.json ├── README.md ├── Resources │ ├── Fonts │ │ ├── Segoe-Ui-Bold.ttf │ │ ├── Segoe-Ui-Regular.ttf │ │ ├── Segoe-Ui-Semibold.ttf │ │ └── Segoe-Ui-Semilight.ttf │ ├── Images │ │ ├── clock.svg │ │ ├── clock_dark.svg │ │ ├── clockpink.svg │ │ ├── default_podcast_image.svg │ │ ├── discover.svg │ │ ├── discover_dark.svg │ │ ├── dotnet_bot.svg │ │ ├── empty_collection.png │ │ ├── listenlaterfilled.svg │ │ ├── listentogether.svg │ │ ├── listentogether_dark.svg │ │ ├── lists.svg │ │ ├── logo_color_horizontal_darkmode.svg │ │ ├── logo_header_horizontal.svg │ │ ├── player_pause.svg │ │ ├── sare_button.svg │ │ ├── search.svg │ │ ├── settings.svg │ │ ├── settings_dark.svg │ │ ├── subscriptions.svg │ │ ├── subscriptions_dark.svg │ │ ├── suscribed.svg │ │ ├── tab_home.svg │ │ └── tab_home_on.svg │ ├── Strings │ │ ├── AppResource.Designer.cs │ │ └── AppResource.resx │ ├── Styles │ │ ├── DefaultTheme.xaml │ │ └── DefaultTheme.xaml.cs │ ├── appicon.svg │ ├── appiconfg.svg │ └── splash.svg ├── Services │ ├── ImageProcessingService.cs │ ├── ListenLaterService.cs │ ├── PlayerService.cs │ ├── ServicesExtensions.cs │ ├── ServicesProvider.cs │ ├── ShowsService.cs │ ├── SubscriptionsService.cs │ └── WifiOptionsService.cs ├── ViewModels │ ├── CategoriesViewModel.cs │ ├── CategoryViewModel.cs │ ├── DiscoverViewModel.cs │ ├── EpisodeDetailViewModel.cs │ ├── EpisodeViewModel.cs │ ├── ListenLaterViewModel.cs │ ├── SettingsViewModel.cs │ ├── ShellViewModel.cs │ ├── ShowDetailViewModel.cs │ ├── ShowViewModel.cs │ ├── SubscriptionsViewModel.cs │ ├── ViewModelBase.cs │ └── ViewModelExtensions.cs ├── Views │ ├── EpisodeItemView.xaml │ ├── EpisodeItemView.xaml.cs │ ├── ShowItemView.xaml │ └── ShowItemView.xaml.cs ├── _Imports.razor └── wwwroot │ └── index.html ├── MobileBlazor ├── Podcast.DotnetMaui.Blazor.sln ├── README.md └── mauiapp │ ├── App.xaml │ ├── App.xaml.cs │ ├── Main.razor │ ├── MainPage.xaml │ ├── MainPage.xaml.cs │ ├── MauiProgram.cs │ ├── NetPodsMauiBlazor.csproj │ ├── Pages │ └── Index.razor │ ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ ├── MainApplication.cs │ │ └── Resources │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Package.appxmanifest │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ ├── Program.cs │ │ └── Resources │ │ └── LaunchScreen.xib │ ├── Properties │ └── launchSettings.json │ ├── Resources │ ├── Images │ │ └── dotnet_bot.svg │ ├── appicon.svg │ ├── appiconfg.svg │ └── splash.svg │ ├── Services │ └── AudioInteropService.cs │ ├── _Imports.razor │ └── wwwroot │ ├── appsettings.Development.json │ ├── appsettings.json │ └── index.html ├── PowerApps ├── README.md ├── Solution.zip └── assets │ ├── devapi.jpg │ ├── devresources.jpg │ └── powerapp.jpg ├── Services ├── ListenTogether │ ├── ListenTogether.Application │ │ ├── Grains │ │ │ └── RoomGrain.cs │ │ ├── Interfaces │ │ │ ├── IApplicationDbContext.cs │ │ │ ├── IEpisodesClient.cs │ │ │ ├── IRequest.cs │ │ │ ├── IRequestHandler.cs │ │ │ └── IRoomGrain.cs │ │ ├── ListenTogether.Application.csproj │ │ ├── Rooms │ │ │ ├── GetRoomsQueryHandler.cs │ │ │ ├── GetUserRoomQueryHandler.cs │ │ │ ├── JoinRoomRequestHandler.cs │ │ │ ├── LeaveRoomRequestHandler.cs │ │ │ ├── OpenRoomRequestHandler.cs │ │ │ └── UpdatePlayerRequestHandler.cs │ │ └── ServiceCollectionsExtensions.cs │ ├── ListenTogether.Domain │ │ ├── Episode.cs │ │ ├── ListenTogether.Domain.csproj │ │ ├── PlayerState.cs │ │ ├── Room.cs │ │ ├── Show.cs │ │ └── User.cs │ ├── ListenTogether.Hub │ │ ├── Dockerfile │ │ ├── GlobalUsings.cs │ │ ├── Hubs │ │ │ └── ListenTogetherHub.cs │ │ ├── ListenTogether.Hub.csproj │ │ ├── OrleansExtensions.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ServiceCollectionsExtensions.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── ListenTogether.Infrastructure │ │ ├── Data │ │ ├── ListenTogetherDbContext.cs │ │ └── Migrations │ │ │ ├── 20211111150042_InitialCreate.Designer.cs │ │ │ ├── 20211111150042_InitialCreate.cs │ │ │ └── ListenTogetherDbContextModelSnapshot.cs │ │ ├── Http │ │ └── EpisodesHttpClient.cs │ │ ├── ListenTogether.Infrastructure.csproj │ │ └── ServiceCollectionsExtensions.cs └── Podcasts │ ├── Podcast.API │ ├── Dockerfile │ ├── GlobalUsings.cs │ ├── Podcast.API.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── README.md │ ├── Routes │ │ ├── CategoriesApi.cs │ │ ├── EpisodesApi.cs │ │ ├── FeedsApi.cs │ │ └── ShowsApi.cs │ ├── appsettings.Development.json │ └── appsettings.json │ ├── Podcast.Infrastructure │ ├── Data │ │ ├── DTOs │ │ │ ├── CategoryDto.cs │ │ │ ├── EpisodeDto.cs │ │ │ ├── ShowDto.cs │ │ │ └── UserSubmittedFeedDto.cs │ │ ├── Migrations │ │ │ ├── 20211123085635_InitialCreate.Designer.cs │ │ │ ├── 20211123085635_InitialCreate.cs │ │ │ ├── 20220324201931_feed-approval.Designer.cs │ │ │ ├── 20220324201931_feed-approval.cs │ │ │ └── PodcastDbContextModelSnapshot.cs │ │ ├── Models │ │ │ ├── Category.cs │ │ │ ├── Episode.cs │ │ │ ├── Feed.cs │ │ │ ├── FeedCategory.cs │ │ │ ├── Show.cs │ │ │ └── UserSubmittedFeed.cs │ │ ├── PodcastDbContext.cs │ │ └── Seed.cs │ ├── Http │ │ ├── Feeds │ │ │ ├── FeedClient.cs │ │ │ ├── IFeedClient.cs │ │ │ ├── Mapper.cs │ │ │ ├── Rss.cs │ │ │ └── RssHelper.cs │ │ └── ShowClient.cs │ ├── Podcast.Infrastructure.csproj │ └── Queues │ │ └── Messages │ │ └── NewFeedRequested.cs │ ├── Podcast.Ingestion.Worker │ ├── Dockerfile │ ├── GlobalUsings.cs │ ├── Podcast.Ingestion.Worker.csproj │ ├── PodcastIngestionHandler.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Worker.cs │ ├── appsettings.Development.json │ └── appsettings.json │ └── Podcast.Updater.Worker │ ├── Dockerfile │ ├── GlobalUsings.cs │ ├── Podcast.Updater.Worker.csproj │ ├── PodcastUpdateHandler.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Worker.cs │ ├── appsettings.Development.json │ └── appsettings.json └── Web ├── Client ├── App.razor ├── Podcast.Client.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── _Imports.razor └── wwwroot │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── images │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ └── icon-512x512.png │ ├── manifest.json │ ├── service-worker.js │ └── service-worker.published.js ├── Components ├── AudioInterop.cs ├── ClipboardInterop.cs ├── Enums │ └── Theme.cs ├── Events │ └── EventHandlers.cs ├── Extensions │ └── TimeSpanExtensions.cs ├── IAudioInterop.cs ├── ListenTogether │ ├── Emojis │ │ ├── EmojiPanel.razor │ │ ├── EmojiPanel.razor.css │ │ ├── EmojiPanelButton.razor │ │ └── EmojiPanelButton.razor.css │ ├── ListenTogether.razor │ ├── ListenTogether.razor.css │ ├── ListenTogetherEmptyRoom.razor │ ├── ListenTogetherEmptyRoom.razor.css │ ├── ListenTogetherEpisode.razor │ ├── ListenTogetherEpisode.razor.css │ ├── ListenTogetherInvite.razor │ ├── ListenTogetherInvite.razor.css │ ├── ListenTogetherJoinRoom.razor │ ├── ListenTogetherJoinRoom.razor.css │ ├── ListenTogetherRoom.razor │ ├── ListenTogetherRoom.razor.css │ ├── UsernameForm.razor │ └── UsernameForm.razor.css ├── ListenTogetherHubClient.cs ├── LocalStorageInterop.cs ├── Models │ ├── PlayerState.cs │ ├── Room.cs │ ├── RoomEpisode.cs │ ├── RoomShow.cs │ └── User.cs ├── Player │ ├── FloatingPlayer.razor │ └── FloatingPlayer.razor.css ├── Podcast.Components.csproj ├── Shared │ ├── Emojis │ │ ├── ClappingHandsEmoji.razor │ │ ├── CryingFaceEmoji.razor │ │ ├── FaceTearsOfJoyEmoji.razor │ │ ├── HeartEmoji.razor │ │ ├── ThumbsDownEmoji.razor │ │ └── ThumbsUpEmoji.razor │ ├── Grid │ │ ├── Card.razor │ │ ├── Card.razor.css │ │ ├── Grid.razor │ │ └── Grid.razor.css │ ├── Modal.razor │ ├── Modal.razor.css │ ├── NoResults.razor │ ├── NoResults.razor.css │ ├── Picture.razor │ ├── Picture.razor.css │ ├── PlayerBars.razor │ ├── PlayerBars.razor.css │ ├── Spinner.razor │ ├── Spinner.razor.css │ ├── SubtitlePage.razor │ ├── SubtitlePage.razor.css │ ├── Tags.razor │ ├── Tags.razor.css │ ├── TitlePage.razor │ ├── TitlePage.razor.css │ ├── ToggleSwitch.razor │ ├── ToggleSwitch.razor.css │ ├── Tooltip.razor │ └── Tooltip.razor.css ├── ThemeInterop.cs ├── _Imports.razor └── wwwroot │ ├── audioJsInterop.js │ ├── css │ ├── button.css │ ├── fonts │ │ ├── SegoeUI.woff │ │ ├── SegoeUIBold.woff │ │ ├── SegoeUILight.woff │ │ ├── SegoeUISemibold.woff │ │ └── SegoeUISemilight.woff │ ├── icons │ │ ├── fonts │ │ │ ├── podcast-icons.svg │ │ │ ├── podcast-icons.ttf │ │ │ └── podcast-icons.woff │ │ └── style.css │ ├── input.css │ ├── reset.css │ ├── site.css │ ├── splash.css │ └── styles.css │ ├── images │ ├── bg-splash.jpg │ ├── bot-wait.svg │ ├── empty-results.png │ └── logo-icon.svg │ └── themeJsInterop.js ├── E2E ├── package-lock.json ├── package.json ├── playwright.config.ts └── tests │ ├── discover.spec.ts │ ├── listen-later.spec.ts │ ├── listen-together.spec.ts │ ├── login.spec.ts │ ├── podcast-perspective.spec.ts │ ├── settings.spec.ts │ └── subscriptions.spec.ts ├── Pages ├── Data │ ├── ListenLaterService.cs │ ├── PlayerService.cs │ └── SubscriptionsService.cs ├── Events │ ├── DurationChangeEventArgs.cs │ ├── EventHandlers.cs │ ├── PlaybackRateChangeEventArgs.cs │ └── TimeUpdateEventArgs.cs ├── Models │ ├── EpisodeInfo.cs │ └── ShowInfo.cs ├── Pages │ ├── CategoriesPage.razor │ ├── CategoriesPage.razor.css │ ├── CategoryPage.razor │ ├── DiscoverPage.razor │ ├── DiscoverPage.razor.css │ ├── ListenLaterPage.razor │ ├── ListenTogetherPage.razor │ ├── SettingsPage.razor │ ├── SettingsPage.razor.css │ ├── ShowPage.razor │ ├── ShowPage.razor.css │ └── SubscriptionsPage.razor ├── Podcast.Pages.csproj ├── Shared │ ├── Cards │ │ └── ShowCard.razor │ ├── Header.razor │ ├── Header.razor.css │ ├── Lists │ │ ├── EpisodeListItem.razor │ │ └── EpisodeListItem.razor.css │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ ├── NavMenu.razor.css │ ├── Player │ │ ├── Audio.razor │ │ └── PlayerBar.razor │ ├── SearchBar.razor │ ├── SearchBar.razor.css │ ├── ShowResume.razor │ └── ShowResume.razor.css ├── _Imports.razor └── wwwroot │ ├── images │ ├── logo-flat.svg │ ├── no-listen-later.png │ └── no-subscriptions.png │ └── js │ ├── customEvents.js │ └── theme.js ├── README.md ├── Server ├── Dockerfile ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ ├── Landing.cshtml │ ├── Landing.cshtml.cs │ ├── _Host.cshtml │ └── _Layout.cshtml ├── Podcast.Server.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── css │ ├── layout.css │ ├── reset.css │ └── styles.css │ ├── favicon.png │ ├── fonts │ ├── SegoeUI.woff │ ├── SegoeUIBold.woff │ ├── SegoeUILight.woff │ ├── SegoeUISemibold.woff │ └── SegoeUISemilight.woff │ ├── images │ ├── available-android.svg │ ├── available-apple.svg │ ├── available-web.svg │ ├── available-windows.svg │ ├── device_Android.png │ ├── device_Screen_home.png │ ├── device_iPhone.png │ ├── download-app_store.png │ ├── download-google_play.png │ ├── download-microsoft.png │ ├── features-01.png │ ├── features-02.png │ ├── features-03.png │ ├── features-04.png │ ├── features-05.png │ ├── hero_landing_image.jpg │ ├── logo-color.svg │ ├── logo-white-flat.svg │ ├── logo-white.svg │ ├── microsoft-logo.svg │ ├── waves1.png │ ├── waves2.png │ ├── waves3.png │ └── waves4.png │ └── js │ └── scripts.js ├── Shared ├── Category.cs ├── Episode.cs ├── Podcast.Shared.csproj ├── PodcastService.cs └── Show.cs └── docs └── screens ├── desktop ├── discover-dark.jpeg ├── discover.jpeg ├── full-landing.jpeg ├── listen-later.jpeg ├── listen-together-room.jpeg ├── listen-together.jpeg ├── podcast-detail.jpeg ├── settings.jpeg └── subscriptions.jpeg └── mobile ├── discover.jpeg ├── listen-together-room.jpeg ├── listen-together.jpeg ├── podcast-detail.jpeg └── settings.jpeg /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '15 21 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'csharp', 'javascript' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v1 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v1 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v1 39 | -------------------------------------------------------------------------------- /.github/workflows/podcast-api.yml: -------------------------------------------------------------------------------- 1 | name: Podcast API CICD 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "src/Services/Podcasts/**" 8 | - "deploy/Services/api.bicep" 9 | - ".github/workflows/podcast-api.yml" 10 | pull_request: 11 | branches: [main] 12 | paths: 13 | - "src/Services/Podcasts/**" 14 | - ".github/workflows/podcast-api.yml" 15 | 16 | workflow_dispatch: 17 | 18 | jobs: 19 | buildPushDeployStaging: 20 | name: "staging" 21 | if: "!contains(github.ref, 'refs/heads/main')" 22 | uses: ./.github/workflows/template-api.yml 23 | concurrency: 24 | group: staging-api 25 | cancel-in-progress: true 26 | secrets: inherit 27 | with: 28 | environment: staging 29 | 30 | buildPushDeployProd: 31 | name: "prod" 32 | if: contains(github.ref, 'refs/heads/main') 33 | uses: ./.github/workflows/template-api.yml 34 | concurrency: 35 | group: prod-api 36 | cancel-in-progress: true 37 | secrets: inherit 38 | with: 39 | environment: prod 40 | -------------------------------------------------------------------------------- /.github/workflows/podcast-dotnet-maui-blazor-cd.yml: -------------------------------------------------------------------------------- 1 | name: Podcast .NET MAUI Blazor CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/MobileBlazor/**' 8 | - '**/*build.props' 9 | - '.github/workflows/podcast-dotnet-maui-blazor-cd.yml' 10 | - '.github/workflows/template-dotnet-maui-cd.yml' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build-mobile: 17 | uses: ./.github/workflows/template-dotnet-maui-cd.yml 18 | with: 19 | csproj: src/MobileBlazor/mauiapp/NetPodsMauiBlazor.csproj 20 | root_path: src/MobileBlazor/mauiapp/ 21 | encrypted_keystore_path: deploy/Android/android-keystore-test.jks.gpg 22 | keystore_path: android-keystore-test.keystore 23 | secrets: 24 | android_keystore_gpg_pass: ${{secrets.ANDROID_KEYSTORE_GPG_PASSWORD}} 25 | android_signing_store_pass: ${{secrets.ANDROID_SIGNING_STORE_PASS}} 26 | android_signing_key_alias: ${{secrets.ANDROID_SIGNING_KEY_ALIAS}} 27 | android_signing_key_pass: ${{secrets.ANDROID_SIGNING_KEY_PASS}} 28 | -------------------------------------------------------------------------------- /.github/workflows/podcast-dotnet-maui-blazor-ci.yml: -------------------------------------------------------------------------------- 1 | name: Podcast .NET MAUI Blazor CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/MobileBlazor/**' 8 | - 'src/Web/Components/**' 9 | - '**/*build.props' 10 | - '.github/workflows/podcast-dotnet-maui-blazor-ci.yml' 11 | - '.github/workflows/template-dotnet-maui-ci.yml' 12 | pull_request: 13 | branches: 14 | - '**' 15 | paths: 16 | - 'src/MobileBlazor/**' 17 | - 'src/Web/Components/**' 18 | - '**/*build.props' 19 | - '.github/workflows/podcast-dotnet-maui-blazor-ci.yml' 20 | - '.github/workflows/template-dotnet-maui-ci.yml' 21 | 22 | # Allows you to run this workflow manually from the Actions tab 23 | workflow_dispatch: 24 | 25 | jobs: 26 | build-mobile: 27 | uses: ./.github/workflows/template-dotnet-maui-ci.yml 28 | with: 29 | csproj: src/MobileBlazor/mauiapp/NetPodsMauiBlazor.csproj -------------------------------------------------------------------------------- /.github/workflows/podcast-dotnet-maui-cd.yml: -------------------------------------------------------------------------------- 1 | name: Podcast .NET MAUI CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/Mobile/**' 8 | - '**/*build.props' 9 | - '.github/workflows/podcast-dotnet-maui-cd.yml' 10 | - '.github/workflows/template-dotnet-maui-cd.yml' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build-mobile: 17 | uses: ./.github/workflows/template-dotnet-maui-cd.yml 18 | with: 19 | csproj: src/Mobile/Microsoft.NetConf2021.Maui.csproj 20 | root_path: src/Mobile/ 21 | encrypted_keystore_path: deploy/Android/android-keystore-test.jks.gpg 22 | keystore_path: android-keystore-test.keystore 23 | secrets: 24 | android_keystore_gpg_pass: ${{secrets.ANDROID_KEYSTORE_GPG_PASSWORD}} 25 | android_signing_store_pass: ${{secrets.ANDROID_SIGNING_STORE_PASS}} 26 | android_signing_key_alias: ${{secrets.ANDROID_SIGNING_KEY_ALIAS}} 27 | android_signing_key_pass: ${{secrets.ANDROID_SIGNING_KEY_PASS}} -------------------------------------------------------------------------------- /.github/workflows/podcast-dotnet-maui-ci.yml: -------------------------------------------------------------------------------- 1 | name: Podcast .NET MAUI CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/Mobile/**' 8 | - 'src/Web/Components/**' 9 | - '**/*build.props' 10 | - '.github/workflows/podcast-dotnet-maui-ci.yml' 11 | - '.github/workflows/template-dotnet-maui-ci.yml' 12 | pull_request: 13 | branches: 14 | - '**' 15 | paths: 16 | - 'src/Mobile/**' 17 | - 'src/Web/Components/**' 18 | - '**/*build.props' 19 | - '.github/workflows/podcast-dotnet-maui-ci.yml' 20 | - '.github/workflows/template-dotnet-maui-ci.yml' 21 | 22 | # Allows you to run this workflow manually from the Actions tab 23 | workflow_dispatch: 24 | 25 | jobs: 26 | build-mobile: 27 | uses: ./.github/workflows/template-dotnet-maui-ci.yml 28 | with: 29 | csproj: src/Mobile/Microsoft.NetConf2021.Maui.csproj -------------------------------------------------------------------------------- /.github/workflows/template-dotnet-maui-ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | csproj: 5 | required: true 6 | type: string 7 | 8 | jobs: 9 | build-mobile: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v2 17 | with: 18 | dotnet-version: 7.0.x 19 | include-prerelease: true 20 | 21 | - name: Install MAUI Workloads 22 | run: | 23 | dotnet workload install maui --source https://api.nuget.org/v3/index.json 24 | 25 | - name: Build MAUI Mobile app 26 | shell: pwsh 27 | run: | 28 | dotnet build ${{inputs.csproj}} -bl:mobile.binlog 29 | 30 | - name: Archive build log 31 | uses: actions/upload-artifact@v2 32 | with: 33 | name: logs 34 | path: '*.binlog' 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | ## Microsoft Support Policy 10 | 11 | Support for this sample app is limited to the resources listed above. 12 | -------------------------------------------------------------------------------- /Test/ARMTemplate/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "name": { 6 | "value": "" 7 | }, 8 | "location": { 9 | "value": "" 10 | }, 11 | "tags": { 12 | "value": {} 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Test/ARMTemplate/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "name": { 6 | "type": "String" 7 | }, 8 | "location": { 9 | "type": "String" 10 | }, 11 | "tags": { 12 | "type": "Object" 13 | } 14 | }, 15 | "resources": [ 16 | { 17 | "type": "Microsoft.LoadTestService/loadtests", 18 | "apiVersion": "2021-12-01-preview", 19 | "name": "[parameters('name')]", 20 | "location": "[parameters('location')]", 21 | "tags": "[parameters('tags')]", 22 | "identity": { 23 | "type": "SystemAssigned" 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /Test/Shows.csv: -------------------------------------------------------------------------------- 1 | Merge,facd94eb-8ef9-414a-e72a-08dae7f770f7 2 | Rocks,f3bfb1cf-395a-4c15-e724-08dae7f770f7 3 | CodeNewbie,191e2d48-6722-4f75-e728-08dae7f770f7 4 | Hanselminutes,e5a6f6c9-feec-4883-e722-08dae7f770f7 5 | Mobile,4d812eab-e7c6-4ed9-e71c-08dae7f770f7 6 | Adventures,9c4751d2-d102-4947-e71f-08dae7f770f7 7 | future,01c23abf-8211-422a-e72b-08dae7f770f7 8 | MAUI,8617def2-5021-49e7-e726-08dae7f770f7 9 | Monsters,0befb23c-5991-4e2c-e727-08dae7f770f7 10 | Microsoft,e0f61f7f-79c4-47a6-e723-08dae7f770f7 -------------------------------------------------------------------------------- /Test/config.yml: -------------------------------------------------------------------------------- 1 | testId: MyGitHubPodcastTest 2 | displayName: My GitHub podcast test 3 | testPlan: DemoPodcastTest.jmx 4 | description: '' 5 | engineInstances: 5 6 | configurationFiles: 7 | - jmeter-plugins-functions-2.2.jar 8 | - Shows.csv 9 | splitAllCSVs: True 10 | failureCriteria: 11 | - Podcast page: p90(response_time_ms) > 10000 12 | - Homepage: p90(response_time_ms) > 60000 13 | - percentage(error) > 10 14 | -------------------------------------------------------------------------------- /Test/jmeter-plugins-functions-2.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/Test/jmeter-plugins-functions-2.2.jar -------------------------------------------------------------------------------- /deploy/Android/android-keystore-test.jks.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Android/android-keystore-test.jks.gpg -------------------------------------------------------------------------------- /deploy/Images/Covers/.NET Rocks!.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/.NET Rocks!.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/.future.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/.future.png -------------------------------------------------------------------------------- /deploy/Images/Covers/Adventures in .NET – Devchat.tv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Adventures in .NET – Devchat.tv.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Adventures in .NET.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Adventures in .NET.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Asp.Net Monsters.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Asp.Net Monsters.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Away From The Keyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Away From The Keyboard.png -------------------------------------------------------------------------------- /deploy/Images/Covers/CodeNewbie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/CodeNewbie.png -------------------------------------------------------------------------------- /deploy/Images/Covers/Gone Mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Gone Mobile.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Hanselminutes with Scott Hanselman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Hanselminutes with Scott Hanselman.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Hello World (Audio) - Channel 9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Hello World (Audio) - Channel 9.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Last Week in .NET.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Last Week in .NET.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/MS Dev Show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/MS Dev Show.png -------------------------------------------------------------------------------- /deploy/Images/Covers/Merge Conflict.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Merge Conflict.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Microsoft 365 Developer Podcast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Microsoft 365 Developer Podcast.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Microsoft Mechanics Podcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Microsoft Mechanics Podcast.png -------------------------------------------------------------------------------- /deploy/Images/Covers/Null Pointers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Null Pointers.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/Paths Uncovered.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Paths Uncovered.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/RunAs Radio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/RunAs Radio.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/The .NET Core Podcast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/The .NET Core Podcast.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/The .NET MAUI Podcast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/The .NET MAUI Podcast.jpg -------------------------------------------------------------------------------- /deploy/Images/Covers/The Unhandled Exception Podcast.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/The Unhandled Exception Podcast.jpeg -------------------------------------------------------------------------------- /deploy/Images/Covers/Upwards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/Upwards.png -------------------------------------------------------------------------------- /deploy/Images/Covers/intrazone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/intrazone.png -------------------------------------------------------------------------------- /deploy/Images/Covers/no dogma podcast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/deploy/Images/Covers/no dogma podcast.jpg -------------------------------------------------------------------------------- /deploy/Images/Readme.md: -------------------------------------------------------------------------------- 1 | # Deploy Images 2 | 3 | The `Covers` folder contains all shows pictures resized to 225x255. Images of this folder are deployed to an Azure Blob Storage container with the script [`/Deploy-Images.ps1`](/deploy/Images/Deploy-Images.ps1) and executed in [`podcast-api.yml`](/.github/workflows/podcast-api.yml) workflow. 4 | -------------------------------------------------------------------------------- /deploy/Services/acr.bicep: -------------------------------------------------------------------------------- 1 | @description('Name of the azure container registry (must be globally unique)') 2 | @minLength(5) 3 | @maxLength(50) 4 | param acrName string 5 | 6 | @description('Enable an admin user that has push/pull permission to the registry.') 7 | param acrAdminUserEnabled bool = true 8 | 9 | @description('Location for all resources.') 10 | param location string = resourceGroup().location 11 | 12 | @description('Tier of your Azure Container Registry.') 13 | @allowed([ 14 | 'Basic' 15 | 'Standard' 16 | 'Premium' 17 | ]) 18 | param acrSku string = 'Basic' 19 | 20 | resource acr 'Microsoft.ContainerRegistry/registries@2021-09-01' = { 21 | name: acrName 22 | location: location 23 | tags: { 24 | displayName: 'Container Registry' 25 | 'container.registry': acrName 26 | } 27 | sku: { 28 | name: acrSku 29 | } 30 | properties: { 31 | adminUserEnabled: acrAdminUserEnabled 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | 00e1d6c6-1f3e-44d4-8ea6-b33f5e3aabf6 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort}/swagger 9 | podcast.api 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/demos/authentication/images/add-swagger-authorization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/add-swagger-authorization.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/authorize-swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/authorize-swagger.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/create-app-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/create-app-role.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/delete-feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/delete-feed.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/generated-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/generated-token.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/get-feeds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/get-feeds.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/provide-application-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/provide-application-name.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/select-active-directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/select-active-directory.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/select-app-registrations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/select-app-registrations.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/select-app-roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/select-app-roles.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/select-create-app-role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/select-create-app-role.png -------------------------------------------------------------------------------- /docs/demos/authentication/images/select-new-registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/authentication/images/select-new-registration.png -------------------------------------------------------------------------------- /docs/demos/azurecontainerapps/Simulate-Feed-Requests.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [parameter(Mandatory=$false)][string]$baseUrl = "" 3 | ) 4 | 5 | class FeedRequest 6 | { 7 | [string] $Title 8 | [string] $Url 9 | [System.Collections.Generic.List[String]] $Categories 10 | } 11 | 12 | function SimulateRequest { 13 | Param( 14 | [int]$sleep = 100, 15 | [string]$baseUrl 16 | ) 17 | 18 | $feeds = [System.Collections.Generic.List[FeedRequest]](Get-Content './feeds.json' | Out-String | ConvertFrom-Json) 19 | 20 | $feeds | ForEach-Object -ThrottleLimit 20 -Parallel { 21 | Write-Host "> Requesting feed" $_.Title 22 | 23 | Invoke-WebRequest -Method POST -Uri $using:baseUrl"v1/feeds" ` 24 | -Body ($_|ConvertTo-Json) ` 25 | -ContentType application/json 26 | 27 | [System.Threading.Thread]::Sleep($sleep) 28 | } 29 | } 30 | 31 | SimulateRequest -baseUrl $baseUrl 32 | -------------------------------------------------------------------------------- /docs/demos/azurecontainerapps/scale-out-demo-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/azurecontainerapps/scale-out-demo-0.png -------------------------------------------------------------------------------- /docs/demos/azurecontainerapps/scale-out-demo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/azurecontainerapps/scale-out-demo-1.png -------------------------------------------------------------------------------- /docs/demos/azurecontainerapps/scale-out-demo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/azurecontainerapps/scale-out-demo-2.png -------------------------------------------------------------------------------- /docs/demos/blazor-hybrid/README.md: -------------------------------------------------------------------------------- 1 | ### Blazor Hybrid + .NET MAUI Setup 2 | 3 | The .NET MAUI & Blazor Hybrid apps are setup to run locally against the APIs running in Docker. If you are unable to deploy locally, you can use these pre-deployed services: 4 | 5 | Open `src/MobileBlazor/mauiapp/MauiProgram.cs` and enter 6 | 7 | ```csharp 8 | public static string BaseWeb = $"https://dotnetpodcasts.azurewebsites.net/"; 9 | public static string APIUrl = $"https://podcastapica.ashyhill-df3dfdf5.eastus.azurecontainerapps.io"; 10 | public static string ListenTogetherUrl = $"https://dotnetpodcasts-listentogether-hub.azurewebsites.net/listentogether"; 11 | ``` 12 | 13 | Watch the .NET Conf 2021 demo of Blaozry Hybrid & .NET MAUI here: https://youtu.be/gYQxBHjRNr0?t=3999 14 | -------------------------------------------------------------------------------- /docs/demos/dotnet-maui/README.md: -------------------------------------------------------------------------------- 1 | ## .NET MAUI Configuration 2 | 3 | The .NET MAUI apps are setup to run locally against the APIs running in Docker. If you are unable to deploy locally, you can use these pre-deployed services: 4 | 5 | Open `src/Mobile/Config.cs` and enter 6 | 7 | ```csharp 8 | public static string BaseWeb = $"https://dotnetpodcasts.azurewebsites.net/"; 9 | public static string APIUrl = $"https://podcastapica.ashyhill-df3dfdf5.eastus.azurecontainerapps.io/"; 10 | public static string ListenTogetherUrl = $"https://dotnetpodcasts-listentogether-hub.azurewebsites.net/listentogether"; 11 | ``` 12 | 13 | Watch the .NET Conf 2021 demo of .NET MAUI here: https://youtu.be/gYQxBHjRNr0?t=3357 14 | -------------------------------------------------------------------------------- /docs/demos/powerapps/assets/customconnector.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/powerapps/assets/customconnector.jpg -------------------------------------------------------------------------------- /docs/demos/powerapps/assets/editpowerapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/powerapps/assets/editpowerapp.jpg -------------------------------------------------------------------------------- /docs/demos/powerapps/assets/onstart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/powerapps/assets/onstart.jpg -------------------------------------------------------------------------------- /docs/demos/powerapps/assets/powerapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/powerapps/assets/powerapp.jpg -------------------------------------------------------------------------------- /docs/demos/powerapps/assets/refreshbutton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/demos/powerapps/assets/refreshbutton.jpg -------------------------------------------------------------------------------- /docs/images/Enable-Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/Enable-Workflow.png -------------------------------------------------------------------------------- /docs/images/all-workflow-runs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/all-workflow-runs.png -------------------------------------------------------------------------------- /docs/images/arch_diagram_podcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/arch_diagram_podcast.png -------------------------------------------------------------------------------- /docs/images/azure-secret-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/azure-secret-add.png -------------------------------------------------------------------------------- /docs/images/docker-app-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/docker-app-config.png -------------------------------------------------------------------------------- /docs/images/docker-services-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/docker-services-config.png -------------------------------------------------------------------------------- /docs/images/gh-configured-secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/gh-configured-secrets.png -------------------------------------------------------------------------------- /docs/images/github-repo-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/github-repo-settings.png -------------------------------------------------------------------------------- /docs/images/net-podcasts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/net-podcasts.png -------------------------------------------------------------------------------- /docs/images/podcast-api-ci-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/podcast-api-ci-run.png -------------------------------------------------------------------------------- /docs/images/select-secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/docs/images/select-secrets.png -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/INativeAudioService.cs: -------------------------------------------------------------------------------- 1 | namespace SharedMauiLib; 2 | 3 | public interface INativeAudioService 4 | { 5 | Task InitializeAsync(string audioURI); 6 | 7 | Task PlayAsync(double position = 0); 8 | 9 | Task PauseAsync(); 10 | 11 | Task SetMuted(bool value); 12 | 13 | Task SetVolume(int value); 14 | 15 | Task SetCurrentTime(double value); 16 | 17 | ValueTask DisposeAsync(); 18 | 19 | bool IsPlaying { get; } 20 | 21 | double CurrentPosition { get; } 22 | 23 | event EventHandler IsPlayingChanged; 24 | } -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEvent.cs: -------------------------------------------------------------------------------- 1 | namespace SharedMauiLib.Platforms.Android.CurrentActivity 2 | { 3 | public enum ActivityEvent 4 | { 5 | Created, 6 | Resumed, 7 | Paused, 8 | Destroyed, 9 | SaveInstanceState, 10 | Started, 11 | Stopped 12 | } 13 | } -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/Platforms/Android/CurrentActivity/ActivityEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | 3 | namespace SharedMauiLib.Platforms.Android.CurrentActivity 4 | { 5 | public class ActivityEventArgs : EventArgs 6 | { 7 | internal ActivityEventArgs(Activity activity, ActivityEvent ev) 8 | { 9 | Event = ev; 10 | Activity = activity; 11 | } 12 | 13 | public ActivityEvent Event { get; } 14 | public Activity Activity { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/Platforms/Android/EventHandlers.cs: -------------------------------------------------------------------------------- 1 | namespace SharedMauiLib.Platforms.Android; 2 | 3 | public delegate void StatusChangedEventHandler(object sender, EventArgs e); 4 | 5 | public delegate void BufferingEventHandler(object sender, EventArgs e); 6 | 7 | public delegate void CoverReloadedEventHandler(object sender, EventArgs e); 8 | 9 | public delegate void PlayingEventHandler(object sender, EventArgs e); 10 | 11 | public delegate void PlayingChangedEventHandler(object sender, bool e); -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/Platforms/Android/IAudioActivity.cs: -------------------------------------------------------------------------------- 1 | namespace SharedMauiLib.Platforms.Android 2 | { 3 | public interface IAudioActivity 4 | { 5 | public MediaPlayerServiceBinder Binder { get; set; } 6 | 7 | public event StatusChangedEventHandler StatusChanged; 8 | 9 | public event CoverReloadedEventHandler CoverReloaded; 10 | 11 | public event PlayingEventHandler Playing; 12 | 13 | public event BufferingEventHandler Buffering; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/Resources/Images/player_play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Lib/SharedMauiLib/SharedMauiLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0;net7.0-android;net7.0-ios;net7.0-maccatalyst 5 | $(TargetFrameworks);net7.0-windows10.0.19041.0 6 | true 7 | true 8 | enable 9 | 10 | 14.2 11 | 14.0 12 | 21.0 13 | 10.0.17763.0 14 | 10.0.17763.0 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Mobile/App.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Mobile/Config.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui; 2 | 3 | public static class Config 4 | { 5 | public static bool ListenTogetherIsVisible => true; 6 | 7 | public static bool Desktop 8 | { 9 | get 10 | { 11 | #if WINDOWS || MACCATALYST 12 | return true; 13 | #else 14 | return false; 15 | #endif 16 | } 17 | } 18 | 19 | public static string BaseWeb = $"{Base}:5002/"; 20 | public static string Base = DeviceInfo.Platform == DevicePlatform.Android ? "http://10.0.2.2" : "http://localhost"; 21 | public static string APIUrl = $"{Base}:5003/"; 22 | public static string ListenTogetherUrl = $"{Base}:5001/listentogether"; 23 | } 24 | -------------------------------------------------------------------------------- /src/Mobile/Converters/DurationConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Microsoft.NetConf2021.Maui.Converters 4 | { 5 | class DurationConverter : IValueConverter 6 | { 7 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 8 | { 9 | if (value == null) 10 | return value; 11 | var result = string.Empty; 12 | 13 | if (value is string stringValue){ 14 | try 15 | { 16 | var duration = TimeSpan.Parse(stringValue); 17 | result = $"{duration.TotalMinutes.ToString("N0")} min"; 18 | } 19 | finally 20 | { 21 | } 22 | } 23 | 24 | return result; 25 | } 26 | 27 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 28 | { 29 | throw new NotImplementedException(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mobile/Converters/IsNullConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Microsoft.NetConf2021.Maui.Converters 4 | { 5 | class IsNullConverter : IValueConverter 6 | { 7 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 8 | { 9 | if(parameter == null) 10 | { 11 | return value == null; 12 | } 13 | else 14 | { 15 | return value != null; 16 | } 17 | } 18 | 19 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 20 | { 21 | throw new NotImplementedException(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mobile/Converters/TextToTypeTextConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Microsoft.NetConf2021.Maui.Converters; 5 | 6 | class TextToTypeTextConverter : IValueConverter 7 | { 8 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 9 | { 10 | var isHtml = ContainsHTML(value as string); 11 | 12 | return isHtml 13 | ? TextType.Html 14 | : TextType.Text; 15 | } 16 | 17 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | 22 | private bool ContainsHTML(string text) 23 | { 24 | return !string.IsNullOrWhiteSpace(text) && Regex.IsMatch(text, "<(.|\n)*?>"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mobile/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using CommunityToolkit.Mvvm.ComponentModel; 2 | global using CommunityToolkit.Mvvm.Input; 3 | global using Microsoft.Extensions.DependencyInjection.Extensions; 4 | global using Microsoft.NetConf2021.Maui.Models; 5 | global using Microsoft.NetConf2021.Maui.Pages; 6 | global using Microsoft.NetConf2021.Maui.Services; 7 | global using Microsoft.NetConf2021.Maui.ViewModels; 8 | global using System.Windows.Input; 9 | global using System.Collections.ObjectModel; 10 | global using Microsoft.NetConf2021.Maui.Helpers; 11 | 12 | -------------------------------------------------------------------------------- /src/Mobile/Helpers/TheTheme.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.Messaging; 2 | using Microsoft.NetConf2021.Maui.Messaging; 3 | 4 | namespace Microsoft.NetConf2021.Maui.Helpers; 5 | 6 | public static class TheTheme 7 | { 8 | public static void SetTheme() 9 | { 10 | switch (Settings.Theme) 11 | { 12 | default: 13 | case AppTheme.Light: 14 | App.Current.UserAppTheme = AppTheme.Light; 15 | break; 16 | case AppTheme.Dark: 17 | App.Current.UserAppTheme = AppTheme.Dark; 18 | break; 19 | 20 | } 21 | 22 | WeakReferenceMessenger.Default.Send(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mobile/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using MonkeyCache.FileStore; 2 | 3 | namespace Microsoft.NetConf2021.Maui; 4 | 5 | public static class MauiProgram 6 | { 7 | public static MauiApp CreateMauiApp() 8 | { 9 | var builder = MauiApp.CreateBuilder(); 10 | builder 11 | .UseMauiApp() 12 | .ConfigureEssentials() 13 | .ConfigureServices() 14 | .ConfigurePages() 15 | .ConfigureViewModels() 16 | .ConfigureFonts(fonts => 17 | { 18 | fonts.AddFont("Segoe-Ui-Bold.ttf", "SegoeUiBold"); 19 | fonts.AddFont("Segoe-Ui-Regular.ttf", "SegoeUiRegular"); 20 | fonts.AddFont("Segoe-Ui-Semibold.ttf", "SegoeUiSemibold"); 21 | fonts.AddFont("Segoe-Ui-Semilight.ttf", "SegoeUiSemilight"); 22 | }); 23 | 24 | Barrel.ApplicationId = "dotnetpodcasts"; 25 | 26 | builder.Services.AddMauiBlazorWebView(); 27 | return builder.Build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mobile/Messaging/ChangeThemeNotification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Microsoft.NetConf2021.Maui.Messaging; 3 | 4 | public class ChangeThemeNotification 5 | { 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/Mobile/Messaging/LeaveRoomNotification.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Messaging; 2 | 3 | public class LeaveRoomNotification 4 | { 5 | } 6 | 7 | -------------------------------------------------------------------------------- /src/Mobile/Models/AppSection.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Models; 2 | 3 | public class AppSection 4 | { 5 | public string Title { get; set; } 6 | public string Icon { get; set; } 7 | public string IconDark { get; set; } 8 | public Type TargetType { get; set; } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/Mobile/Models/Category.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.NetConf2021.Maui.Models.Responses; 2 | 3 | namespace Microsoft.NetConf2021.Maui.Models; 4 | 5 | public class Category 6 | { 7 | public Category(CategoryResponse response) 8 | { 9 | Id = response.Id; 10 | Genre = response.Genre; 11 | } 12 | 13 | public Guid Id { get; set; } 14 | 15 | public string Genre { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Mobile/Models/Responses/CategoryResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Models.Responses; 2 | 3 | public class CategoryResponse 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Genre { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Mobile/Models/Responses/EpisodeResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Models.Responses; 2 | 3 | public class EpisodeResponse 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Title { get; set; } 8 | 9 | public string Description { get; set; } 10 | 11 | public DateTime Published { get; set; } 12 | 13 | public string Duration { get; set; } 14 | 15 | public Uri Url { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Mobile/Models/Responses/ShowResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Models.Responses; 2 | 3 | public class ShowResponse 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Title { get; set; } 8 | 9 | public string Author { get; set; } 10 | 11 | public string Description { get; set; } 12 | 13 | public Uri Image { get; set; } 14 | 15 | public DateTime Updated { get; set; } 16 | 17 | public Uri Link { get; set; } 18 | 19 | public string Email { get; set; } 20 | 21 | public string Language { get; set; } 22 | 23 | public bool IsFeatured { get; set; } 24 | 25 | public CategoryResponse[] Categories { get; set; } 26 | 27 | public EpisodeResponse[] Episodes { get; set; } 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Mobile/Models/ShowGroup.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Models; 2 | 3 | public class ShowGroup : List 4 | { 5 | public string Name { get; private set; } 6 | 7 | public ShowGroup(string name, List show) : base(show) 8 | { 9 | Name = name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Mobile/Pages/CategoriesPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class CategoriesPage : ContentPage 4 | { 5 | CategoriesViewModel vm => BindingContext as CategoriesViewModel; 6 | public CategoriesPage(CategoriesViewModel vm) 7 | { 8 | InitializeComponent(); 9 | BindingContext = vm; 10 | } 11 | 12 | protected override async void OnAppearing() 13 | { 14 | base.OnAppearing(); 15 | await vm.InitializeAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mobile/Pages/CategoryPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class CategoryPage : ContentPage 4 | { 5 | CategoryViewModel vm => BindingContext as CategoryViewModel; 6 | public CategoryPage(CategoryViewModel vm) 7 | { 8 | InitializeComponent(); 9 | BindingContext = vm; 10 | } 11 | 12 | protected override async void OnAppearing() 13 | { 14 | base.OnAppearing(); 15 | await vm.InitializeAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mobile/Pages/DesktopShell.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages 2 | { 3 | public partial class DesktopShell 4 | { 5 | public DesktopShell() 6 | { 7 | InitializeComponent(); 8 | 9 | BindingContext = new ShellViewModel(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Mobile/Pages/DiscoverPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class DiscoverPage : ContentPage 4 | { 5 | private DiscoverViewModel viewModel => BindingContext as DiscoverViewModel; 6 | 7 | public DiscoverPage(DiscoverViewModel vm) 8 | { 9 | InitializeComponent(); 10 | BindingContext = vm; 11 | } 12 | 13 | protected override async void OnAppearing() 14 | { 15 | base.OnAppearing(); 16 | player.OnAppearing(); 17 | await viewModel.InitializeAsync(); 18 | } 19 | 20 | 21 | protected override void OnDisappearing() 22 | { 23 | player.OnDisappearing(); 24 | base.OnDisappearing(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mobile/Pages/EpisodeDetailPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages 2 | { 3 | public partial class EpisodeDetailPage 4 | { 5 | private EpisodeDetailViewModel viewModel => BindingContext as EpisodeDetailViewModel; 6 | 7 | public EpisodeDetailPage(EpisodeDetailViewModel vm) 8 | { 9 | InitializeComponent(); 10 | BindingContext = vm; 11 | } 12 | 13 | protected override async void OnAppearing() 14 | { 15 | base.OnAppearing(); 16 | this.player.OnAppearing(); 17 | await viewModel.InitializeAsync(); 18 | } 19 | 20 | protected override void OnDisappearing() 21 | { 22 | this.player.OnDisappearing(); 23 | base.OnDisappearing(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mobile/Pages/ListenLaterPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class ListenLaterPage : ContentPage 4 | { 5 | ListenLaterViewModel viewModel => BindingContext as ListenLaterViewModel; 6 | 7 | public ListenLaterPage(ListenLaterViewModel vm) 8 | { 9 | InitializeComponent(); 10 | BindingContext = vm; 11 | } 12 | 13 | protected override async void OnAppearing() 14 | { 15 | base.OnAppearing(); 16 | player.OnAppearing(); 17 | await viewModel.InitializeAsync(); 18 | } 19 | 20 | protected override void OnDisappearing() 21 | { 22 | this.player.OnDisappearing(); 23 | base.OnDisappearing(); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/Mobile/Pages/ListenTogetherPage.xaml.cs: -------------------------------------------------------------------------------- 1 | 2 | using CommunityToolkit.Mvvm.Messaging; 3 | using Microsoft.NetConf2021.Maui.Messaging; 4 | 5 | namespace Microsoft.NetConf2021.Maui.Pages 6 | { 7 | public partial class ListenTogetherPage 8 | { 9 | public ListenTogetherPage() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | protected override void OnAppearing() 15 | { 16 | base.OnAppearing(); 17 | player.OnAppearing(); 18 | } 19 | 20 | protected override void OnDisappearing() 21 | { 22 | player.OnDisappearing(); 23 | 24 | WeakReferenceMessenger.Default.Send(); 25 | 26 | base.OnDisappearing(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mobile/Pages/MobileShell.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages 2 | { 3 | public partial class MobileShell 4 | { 5 | public MobileShell() 6 | { 7 | InitializeComponent(); 8 | 9 | BindingContext = new ShellViewModel(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Mobile/Pages/PagesExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public static class PagesExtensions 4 | { 5 | public static MauiAppBuilder ConfigurePages(this MauiAppBuilder builder) 6 | { 7 | // main tabs of the app 8 | builder.Services.AddSingleton(); 9 | builder.Services.AddSingleton(); 10 | builder.Services.AddSingleton(); 11 | builder.Services.AddSingleton(); 12 | builder.Services.AddSingleton(); 13 | 14 | // pages that are navigated to 15 | builder.Services.AddTransient(); 16 | builder.Services.AddTransient(); 17 | builder.Services.AddTransient(); 18 | builder.Services.AddTransient(); 19 | 20 | return builder; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mobile/Pages/SettingsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class SettingsPage 4 | { 5 | public SettingsPage(SettingsViewModel vm) 6 | { 7 | InitializeComponent(); 8 | BindingContext = vm; 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/Mobile/Pages/ShowDetailPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class ShowDetailPage : ContentPage 4 | { 5 | private ShowDetailViewModel viewModel => BindingContext as ShowDetailViewModel; 6 | 7 | public ShowDetailPage(ShowDetailViewModel vm) 8 | { 9 | InitializeComponent(); 10 | BindingContext = vm; 11 | } 12 | 13 | protected override async void OnAppearing() 14 | { 15 | base.OnAppearing(); 16 | this.player.OnAppearing(); 17 | await viewModel.InitializeAsync(); 18 | } 19 | 20 | protected override void OnDisappearing() 21 | { 22 | this.player.OnDisappearing(); 23 | base.OnDisappearing(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mobile/Pages/SubscriptionsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Pages; 2 | 3 | public partial class SubscriptionsPage: ContentPage 4 | { 5 | SubscriptionsViewModel viewModel => BindingContext as SubscriptionsViewModel; 6 | public SubscriptionsPage(SubscriptionsViewModel vm) 7 | { 8 | InitializeComponent(); 9 | BindingContext = vm; 10 | } 11 | 12 | protected override async void OnAppearing() 13 | { 14 | base.OnAppearing(); 15 | await viewModel.InitializeAsync(); 16 | this.player.OnAppearing(); 17 | } 18 | 19 | protected override void OnDisappearing() 20 | { 21 | this.player.OnDisappearing(); 22 | base.OnDisappearing(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace Microsoft.NetConf2021.Maui; 5 | 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #C00CC0 4 | #C00CC0 5 | #C00CC0 6 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Android/Resources/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | 6 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace Microsoft.NetConf2021.Maui; 4 | 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/MacCatalyst/ConnectivityService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.NetworkInformation; 2 | 3 | namespace Microsoft.NetConf2021.Maui.Platforms.MacCatalyst; 4 | 5 | public class ConnectivityService 6 | { 7 | public ConnectivityService() 8 | { 9 | } 10 | 11 | public async Task IsConnected() 12 | { 13 | bool result = false; 14 | try 15 | { 16 | var hostUrl = "www.google.com"; 17 | 18 | Ping ping = new Ping(); 19 | 20 | PingReply pingReply = await ping.SendPingAsync(hostUrl); 21 | result = pingReply.Status == IPStatus.Success; 22 | } 23 | catch 24 | { 25 | result = false; 26 | } 27 | return result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UIRequiredDeviceCapabilities 11 | 12 | arm64 13 | 14 | UISupportedInterfaceOrientations 15 | 16 | UIInterfaceOrientationPortrait 17 | UIInterfaceOrientationLandscapeLeft 18 | UIInterfaceOrientationLandscapeRight 19 | 20 | UISupportedInterfaceOrientations~ipad 21 | 22 | UIInterfaceOrientationPortrait 23 | UIInterfaceOrientationPortraitUpsideDown 24 | UIInterfaceOrientationLandscapeLeft 25 | UIInterfaceOrientationLandscapeRight 26 | 27 | XSAppIconAssets 28 | Assets.xcassets/appicon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace Microsoft.NetConf2021.Maui; 4 | 5 | public class Program 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, typeof(AppDelegate)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | // To learn more about WinUI, the WinUI project structure, 4 | // and more about our project templates, see: http://aka.ms/winui-project-info. 5 | 6 | namespace Microsoft.NetConf2021.Maui.WinUI; 7 | 8 | /// 9 | /// Provides application-specific behavior to supplement the default Application class. 10 | /// 11 | public partial class App : MauiWinUIApplication 12 | { 13 | /// 14 | /// Initializes the singleton application object. This is the first line of authored code 15 | /// executed, and as such is the logical equivalent of main() or WinMain(). 16 | /// 17 | public App() 18 | { 19 | this.InitializeComponent(); 20 | } 21 | 22 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 23 | } 24 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | using Microsoft.Maui.Hosting; 3 | 4 | namespace Microsoft.NetConf2021.Maui; 5 | 6 | [Register("AppDelegate")] 7 | public class AppDelegate : MauiUIApplicationDelegate 8 | { 9 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/iOS/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Mobile/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace Microsoft.NetConf2021.Maui; 4 | 5 | public class Program 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, typeof(AppDelegate)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Mobile/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Mobile/Resources/Fonts/Segoe-Ui-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Mobile/Resources/Fonts/Segoe-Ui-Bold.ttf -------------------------------------------------------------------------------- /src/Mobile/Resources/Fonts/Segoe-Ui-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Mobile/Resources/Fonts/Segoe-Ui-Regular.ttf -------------------------------------------------------------------------------- /src/Mobile/Resources/Fonts/Segoe-Ui-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Mobile/Resources/Fonts/Segoe-Ui-Semibold.ttf -------------------------------------------------------------------------------- /src/Mobile/Resources/Fonts/Segoe-Ui-Semilight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Mobile/Resources/Fonts/Segoe-Ui-Semilight.ttf -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/clock_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/clockpink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/discover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/discover_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/empty_collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Mobile/Resources/Images/empty_collection.png -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/listenlaterfilled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/lists.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/player_pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/sare_button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/subscriptions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/subscriptions_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Images/suscribed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Mobile/Resources/Styles/DefaultTheme.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Resources.Styles; 2 | 3 | public partial class DefaultTheme : ResourceDictionary 4 | { 5 | public DefaultTheme() 6 | { 7 | InitializeComponent(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Mobile/Services/ServicesProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.Services 2 | { 3 | public static class ServicesProvider 4 | { 5 | public static TService GetService() 6 | => Current.GetService(); 7 | 8 | public static IServiceProvider Current 9 | => 10 | #if WINDOWS10_0_17763_0_OR_GREATER 11 | MauiWinUIApplication.Current.Services; 12 | #elif ANDROID 13 | MauiApplication.Current.Services; 14 | #elif IOS || MACCATALYST 15 | MauiUIApplicationDelegate.Current.Services; 16 | #else 17 | null; 18 | #endif 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Mobile/ViewModels/CategoriesViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.ViewModels; 2 | 3 | public partial class CategoriesViewModel : ViewModelBase 4 | { 5 | private readonly ShowsService showsService; 6 | 7 | [ObservableProperty] 8 | List categories; 9 | 10 | public CategoriesViewModel(ShowsService shows) 11 | { 12 | showsService = shows; 13 | } 14 | 15 | public async Task InitializeAsync() 16 | { 17 | var categories = await showsService.GetAllCategories(); 18 | 19 | Categories = categories?.ToList(); 20 | } 21 | 22 | [RelayCommand] 23 | Task Selected(Category category) 24 | { 25 | return Shell.Current.GoToAsync($"{nameof(CategoryPage)}?Id={category.Id}"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mobile/ViewModels/EpisodeViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.ViewModels; 2 | 3 | public partial class EpisodeViewModel : ViewModelBase 4 | { 5 | readonly PlayerService playerService; 6 | 7 | [ObservableProperty] 8 | Episode episode; 9 | 10 | public bool IsInListenLater 11 | { 12 | get 13 | { 14 | return episode.IsInListenLater; 15 | } 16 | set 17 | { 18 | episode.IsInListenLater = value; 19 | OnPropertyChanged(); 20 | } 21 | } 22 | 23 | public Show Show { get; set; } 24 | 25 | public EpisodeViewModel( 26 | Episode episode, 27 | Show show, 28 | PlayerService player) 29 | { 30 | playerService = player; 31 | 32 | Episode = episode; 33 | Show = show; 34 | } 35 | 36 | [RelayCommand] 37 | Task PlayEpisode() => playerService.PlayAsync(Episode, Show); 38 | 39 | [RelayCommand] 40 | Task NavigateToDetail() => Shell.Current.GoToAsync($"{nameof(EpisodeDetailPage)}?Id={episode.Id}&ShowId={Show.Id}"); 41 | } 42 | -------------------------------------------------------------------------------- /src/Mobile/ViewModels/SettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.ViewModels; 2 | 3 | public partial class SettingsViewModel : ViewModelBase 4 | { 5 | [ObservableProperty] 6 | bool isDarkModeEnabled; 7 | 8 | [ObservableProperty] 9 | bool isWifiOnlyEnabled; 10 | 11 | partial void OnIsDarkModeEnabledChanged(bool value) => 12 | ChangeUserAppTheme(value); 13 | partial void OnIsWifiOnlyEnabledChanged(bool value) => 14 | Settings.IsWifiOnlyEnabled = value; 15 | 16 | public string AppVersion => AppInfo.VersionString; 17 | 18 | public SettingsViewModel() 19 | { 20 | isDarkModeEnabled = Settings.Theme == AppTheme.Dark; 21 | isWifiOnlyEnabled = Settings.IsWifiOnlyEnabled; 22 | } 23 | 24 | void ChangeUserAppTheme(bool activateDarkMode) 25 | { 26 | Settings.Theme = activateDarkMode 27 | ? AppTheme.Dark 28 | : AppTheme.Light; 29 | 30 | TheTheme.SetTheme(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mobile/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.ViewModels; 2 | 3 | public partial class ViewModelBase : ObservableObject 4 | { 5 | [ObservableProperty] 6 | string title; 7 | 8 | [ObservableProperty] 9 | string subtitle; 10 | 11 | [ObservableProperty] 12 | string icon; 13 | 14 | [ObservableProperty] 15 | [NotifyPropertyChangedFor(nameof(IsNotBusy))] 16 | bool isBusy; 17 | 18 | public bool IsNotBusy => !isBusy; 19 | 20 | [ObservableProperty] 21 | bool canLoadMore; 22 | 23 | [ObservableProperty] 24 | string header; 25 | 26 | [ObservableProperty] 27 | string footer; 28 | } 29 | -------------------------------------------------------------------------------- /src/Mobile/ViewModels/ViewModelExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.NetConf2021.Maui.ViewModels; 2 | 3 | public static class ViewModelExtensions 4 | { 5 | public static MauiAppBuilder ConfigureViewModels(this MauiAppBuilder builder) 6 | { 7 | builder.Services.AddSingleton(); 8 | builder.Services.AddTransient(); 9 | builder.Services.AddSingleton(); 10 | builder.Services.AddTransient(); 11 | builder.Services.AddSingleton(); 12 | builder.Services.AddSingleton(); 13 | builder.Services.AddSingleton(); 14 | builder.Services.AddSingleton(); 15 | builder.Services.AddTransient(); 16 | builder.Services.AddSingleton(); 17 | builder.Services.AddSingleton(); 18 | 19 | return builder; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mobile/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Components.Forms 3 | @using Microsoft.AspNetCore.Components.Routing 4 | @using Microsoft.AspNetCore.Components.Web 5 | @using Microsoft.AspNetCore.Components.Web.Virtualization 6 | @using Microsoft.JSInterop 7 | @using Microsoft.NetConf2021.Maui 8 | @using Microsoft.NetConf2021.Maui.Blazor 9 | @using Podcast.Components 10 | -------------------------------------------------------------------------------- /src/Mobile/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blazormauiapp 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | An unhandled error has occurred. 18 | Reload 19 | 🗙 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/MobileBlazor/README.md: -------------------------------------------------------------------------------- 1 | # .NET MAUI Blazor podcast app for Android, iOS, macOS, and Windows 2 | The *.sln* is located in [/src/MobileBlazor/Podcast.DotnetMaui.Blazor.sln](Podcast.DotnetMaui.Blazor.sln) 3 | The startup project is **NetPodsMauiBlazor** 4 | 5 | This solution has four projects: 6 | - **NetPodsMauiBlazor**: Maui App based on default template of .NET Maui Blazor App. 7 | - References to Blazor projects 8 | - **[/src/Web/Components/Podcast.Components.csproj](/src/Web/Components/Podcast.Components.csproj)**: Razor Class Library project with shared components. 9 | - **[/src/Web/Pages/Podcast.Pages.csproj](/src/Web/Pages/Podcast.Pages.csproj)**: Razor Class Library project with shared pages. 10 | - **[/src/Web/Shared/Podcast.Shared.csproj](/src/Web/Shared/Podcast.Shared.csproj)**: Shared class library project with services. -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/App.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | #512bdf 9 | White 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Application = Microsoft.Maui.Controls.Application; 2 | 3 | namespace NetPodsMauiBlazor; 4 | 5 | public partial class App : Application 6 | { 7 | public App() 8 | { 9 | InitializeComponent(); 10 | 11 | MainPage = new MainPage(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Main.razor: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Sorry, there's nothing at this address. 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace NetPodsMauiBlazor; 2 | 3 | public partial class MainPage : ContentPage 4 | { 5 | public MainPage() 6 | { 7 | InitializeComponent(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @inject NavigationManager NavigationManager 3 | 4 | @code { 5 | protected override void OnInitialized() 6 | { 7 | NavigationManager.NavigateTo("/discover"); 8 | } 9 | } -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace NetPodsMauiBlazor; 5 | 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } 16 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #C00CC0 4 | #C00CC0 5 | #C00CC0 6 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Android/Resources/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/MacCatalyst/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace NetPodsMauiBlazor; 4 | 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/MacCatalyst/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UIRequiredDeviceCapabilities 11 | 12 | arm64 13 | 14 | UISupportedInterfaceOrientations 15 | 16 | UIInterfaceOrientationPortrait 17 | UIInterfaceOrientationLandscapeLeft 18 | UIInterfaceOrientationLandscapeRight 19 | 20 | UISupportedInterfaceOrientations~ipad 21 | 22 | UIInterfaceOrientationPortrait 23 | UIInterfaceOrientationPortraitUpsideDown 24 | UIInterfaceOrientationLandscapeLeft 25 | UIInterfaceOrientationLandscapeRight 26 | 27 | XSAppIconAssets 28 | Assets.xcassets/appicon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/MacCatalyst/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace NetPodsMauiBlazor; 4 | 5 | public class Program 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, typeof(AppDelegate)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | // To learn more about WinUI, the WinUI project structure, 4 | // and more about our project templates, see: http://aka.ms/winui-project-info. 5 | 6 | namespace NetPodsMauiBlazor.WinUI; 7 | 8 | /// 9 | /// Provides application-specific behavior to supplement the default Application class. 10 | /// 11 | public partial class App : MauiWinUIApplication 12 | { 13 | /// 14 | /// Initializes the singleton application object. This is the first line of authored code 15 | /// executed, and as such is the logical equivalent of main() or WinMain(). 16 | /// 17 | public App() 18 | { 19 | this.InitializeComponent(); 20 | } 21 | 22 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 23 | 24 | protected override void OnLaunched(LaunchActivatedEventArgs args) 25 | { 26 | base.OnLaunched(args); 27 | 28 | Microsoft.Maui.ApplicationModel.Platform.OnLaunched(args); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace NetPodsMauiBlazor; 4 | 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace NetPodsMauiBlazor; 4 | 5 | public class Program 6 | { 7 | // This is the main entry point of the application. 8 | static void Main(string[] args) 9 | { 10 | // if you want to use a different Application Delegate class from "AppDelegate" 11 | // you can specify it here. 12 | UIApplication.Main(args, null, typeof(AppDelegate)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.JSInterop 8 | @using NetPodsMauiBlazor 9 | @using Podcast.Pages 10 | @using Podcast.Pages.Shared -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /src/MobileBlazor/mauiapp/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "PodcastApi": { 3 | "BaseAddress": "http://localhost:5003" 4 | }, 5 | "ListenTogetherHub": "http://localhost:5001/listentogether" 6 | } 7 | -------------------------------------------------------------------------------- /src/PowerApps/Solution.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/PowerApps/Solution.zip -------------------------------------------------------------------------------- /src/PowerApps/assets/devapi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/PowerApps/assets/devapi.jpg -------------------------------------------------------------------------------- /src/PowerApps/assets/devresources.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/PowerApps/assets/devresources.jpg -------------------------------------------------------------------------------- /src/PowerApps/assets/powerapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/PowerApps/assets/powerapp.jpg -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/Interfaces/IApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using ListenTogether.Domain; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace ListenTogether.Application.Interfaces; 5 | 6 | public interface IApplicationDbContext 7 | { 8 | DbSet Rooms { get; } 9 | Task SaveChangesAsync(CancellationToken cancellationToken); 10 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/Interfaces/IEpisodesClient.cs: -------------------------------------------------------------------------------- 1 | using ListenTogether.Domain; 2 | 3 | namespace ListenTogether.Application.Interfaces; 4 | 5 | public interface IEpisodesClient 6 | { 7 | Task GetEpisodeByIdAsync(Guid episodeId, CancellationToken cancellationToken); 8 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/Interfaces/IRequest.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Application.Interfaces; 2 | 3 | public interface IRequest 4 | { 5 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/Interfaces/IRequestHandler.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Application.Interfaces; 2 | 3 | public interface IRequestHandler 4 | { 5 | Task HandleAsync(TRequest request, CancellationToken cancellationToken); 6 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/Interfaces/IRoomGrain.cs: -------------------------------------------------------------------------------- 1 | using ListenTogether.Domain; 2 | using Orleans; 3 | 4 | namespace ListenTogether.Application.Interfaces; 5 | 6 | public interface IRoomGrain : IGrainWithStringKey 7 | { 8 | Task SetRoom(Room room); 9 | Task JoinRoom(string connectionId, string userName); 10 | Task LeaveRoom(string connectionId); 11 | Task UpdatePlayerState(TimeSpan seconds, PlayerState playerState); 12 | } 13 | -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/ListenTogether.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Application/Rooms/GetRoomsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using ListenTogether.Application.Interfaces; 2 | using ListenTogether.Domain; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace ListenTogether.Application.Rooms; 6 | 7 | public class GetRoomsRequest : IRequest> { } 8 | 9 | public class GetRoomsQueryQueryHandler : IRequestHandler> 10 | { 11 | private readonly IApplicationDbContext _dbContext; 12 | 13 | public GetRoomsQueryQueryHandler(IApplicationDbContext dbContext) 14 | { 15 | _dbContext = dbContext; 16 | } 17 | 18 | public async Task> HandleAsync(GetRoomsRequest request, CancellationToken cancellationToken) 19 | { 20 | var rooms = await _dbContext.Rooms 21 | .Include(room => room.Users) 22 | .Include(room => room.Episode.Show).ToListAsync(cancellationToken); 23 | return rooms; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Domain/Episode.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Domain; 2 | 3 | public class Episode 4 | { 5 | protected Episode() { } 6 | 7 | public Episode(Guid id, string title, string description, string url, DateTime published, TimeSpan? duration, Show show) 8 | { 9 | Id = Guid.NewGuid(); 10 | EpisodeId = id; 11 | Title = title; 12 | Description = description; 13 | Url = url; 14 | Published = published; 15 | Duration = duration; 16 | Show = show; 17 | } 18 | public Guid Id { get; private set; } 19 | public Guid EpisodeId { get; private set; } 20 | public string Title { get; private set; } 21 | public string Description { get; private set; } 22 | public string Url { get; private set; } 23 | public DateTime Published { get; private set; } 24 | public TimeSpan? Duration { get; private set; } 25 | public Show Show { get; private set; } 26 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Domain/ListenTogether.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Domain/PlayerState.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Domain; 2 | 3 | public enum PlayerState 4 | { 5 | Paused, 6 | Playing 7 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Domain/Show.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Domain; 2 | 3 | public class Show 4 | { 5 | protected Show() {} 6 | 7 | public Show(Guid id, string title, string author, string image) 8 | { 9 | Id = Guid.NewGuid(); 10 | ShowId = id; 11 | Title = title; 12 | Author = author; 13 | Image = image; 14 | } 15 | 16 | public Guid Id { get; private set; } 17 | public Guid ShowId { get; private set; } 18 | public string Title { get; private set; } 19 | public string Author { get; private set; } 20 | public string Image { get; private set; } 21 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Domain/User.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Domain; 2 | 3 | public class User 4 | { 5 | protected User() { } 6 | 7 | public User(string connectionId, string name) 8 | { 9 | ConnectionId = connectionId; 10 | Name = name; 11 | } 12 | 13 | public Guid Id { get; private set; } 14 | public string ConnectionId { get; private set; } 15 | public string Name { get; private set; } 16 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Hub/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using ListenTogether.Hub.Hubs; 2 | global using Microsoft.AspNetCore.SignalR; 3 | global using System; -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Hub/Program.cs: -------------------------------------------------------------------------------- 1 | using ListenTogether.Application; 2 | using ListenTogether.Hub; 3 | using ListenTogether.Infrastructure; 4 | using ListenTogether.Infrastructure.Data; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | builder.AddOrleans(); 9 | 10 | var serviceCollection = builder.Services; 11 | serviceCollection.AddHub(); 12 | serviceCollection.AddApplication(); 13 | serviceCollection.AddInfrastructure(builder.Configuration); 14 | 15 | var app = builder.Build(); 16 | await EnsureDbAsync(app.Services); 17 | 18 | app.UseCors(); 19 | app.MapHub("/listentogether"); 20 | app.MapGet("/", () => "Listen Together Hub"); 21 | app.MapOrleansDashboard(); 22 | app.Run(); 23 | 24 | static async Task EnsureDbAsync(IServiceProvider sp) 25 | { 26 | await using var db = sp.CreateScope().ServiceProvider.GetRequiredService(); 27 | await db.Database.MigrateAsync(); 28 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Hub/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52077", 7 | "sslPort": 44378 8 | } 9 | }, 10 | "profiles": { 11 | "ListenTogether.Hub": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "applicationUrl": "https://localhost:7237;http://localhost:5237", 18 | "dotnetRunMessages": true 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | }, 27 | "Docker": { 28 | "commandName": "Docker", 29 | "launchBrowser": true, 30 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 31 | "publishAllPorts": true, 32 | "useSSL": true 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Hub/ServiceCollectionsExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ListenTogether.Hub; 2 | 3 | public static class ServiceCollectionsExtensions 4 | { 5 | public static IServiceCollection AddHub(this IServiceCollection serviceCollection) 6 | { 7 | serviceCollection.AddCors(setup => 8 | { 9 | setup.AddDefaultPolicy(policy => 10 | policy.SetIsOriginAllowed(_ => true).AllowCredentials().AllowAnyHeader().AllowAnyMethod()); 11 | }); 12 | serviceCollection.AddSignalR(); 13 | 14 | return serviceCollection; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Hub/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Orleans.*": "Warning" 7 | } 8 | }, 9 | "NetPodcastApi": { 10 | "BaseAddress": "http://localhost:5003/" 11 | }, 12 | "ConnectionStrings": { 13 | "ListenTogetherDb": "Server=localhost, 5433;Database=ListenTogether;User Id=sa;Password=Pass@word;Encrypt=False", 14 | "OrleansStorage": "UseDevelopmentStorage=true;" 15 | } 16 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Hub/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Orleans.*": "Warning" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Infrastructure/Data/ListenTogetherDbContext.cs: -------------------------------------------------------------------------------- 1 | using ListenTogether.Application.Interfaces; 2 | using ListenTogether.Domain; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace ListenTogether.Infrastructure.Data; 6 | 7 | public class ListenTogetherDbContext : DbContext, IApplicationDbContext 8 | { 9 | public ListenTogetherDbContext(DbContextOptions options) : base(options) 10 | { 11 | 12 | } 13 | 14 | public DbSet Rooms => Set(); 15 | 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.Entity().HasKey(prop => prop.Code); 19 | modelBuilder.Entity().HasMany(room => room.Users).WithOne(); 20 | modelBuilder.Entity().HasKey(e => e.Id); 21 | modelBuilder.Entity().ToTable("Episodes").HasOne(e=> e.Show).WithMany(); 22 | modelBuilder.Entity().ToTable("Shows").HasKey(prop => prop.Id); 23 | modelBuilder.Entity().ToTable("Users").HasKey(prop => prop.Id); 24 | base.OnModelCreating(modelBuilder); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Infrastructure/Http/EpisodesHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using ListenTogether.Application.Interfaces; 3 | using ListenTogether.Domain; 4 | 5 | namespace ListenTogether.Infrastructure.Http; 6 | 7 | public class EpisodesHttpClient : IEpisodesClient 8 | { 9 | private readonly HttpClient _httpClient; 10 | 11 | public EpisodesHttpClient(HttpClient httpClient) 12 | { 13 | _httpClient = httpClient; 14 | } 15 | 16 | public async Task GetEpisodeByIdAsync(Guid episodeId, CancellationToken cancellationToken) 17 | { 18 | return (await _httpClient.GetFromJsonAsync($"episodes/{episodeId}", cancellationToken))!; 19 | } 20 | } -------------------------------------------------------------------------------- /src/Services/ListenTogether/ListenTogether.Infrastructure/ListenTogether.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 9 | WORKDIR /src 10 | COPY ["src/Services/Podcasts/Podcast.API/Podcast.API.csproj", "src/Services/Podcasts/Podcast.API/"] 11 | COPY ["src/Services/Podcasts/Podcast.Infrastructure/Podcast.Infrastructure.csproj", "src/Services/Podcasts/Podcast.Infrastructure/"] 12 | RUN dotnet restore "src/Services/Podcasts/Podcast.API/Podcast.API.csproj" 13 | COPY . . 14 | WORKDIR "/src/src/Services/Podcasts/Podcast.API" 15 | RUN dotnet build "Podcast.API.csproj" -c Release -o /app/build 16 | 17 | FROM build AS publish 18 | RUN dotnet publish "Podcast.API.csproj" -c Release -o /app/publish 19 | 20 | FROM base AS final 21 | WORKDIR /app 22 | COPY --from=publish /app/publish . 23 | ENTRYPOINT ["dotnet", "Podcast.API.dll"] -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Azure.Storage.Queues; 2 | global using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | global using Microsoft.AspNetCore.Http.HttpResults; 4 | global using Microsoft.EntityFrameworkCore; 5 | global using Microsoft.OpenApi.Models; 6 | global using Podcast.API.Models; 7 | global using Podcast.API.Routes; 8 | global using Podcast.Infrastructure.Data; 9 | global using Podcast.Infrastructure.Data.Models; 10 | global using Podcast.Infrastructure.Http.Feeds; 11 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56906", 7 | "sslPort": 44385 8 | } 9 | }, 10 | "profiles": { 11 | "Podcast.API": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "applicationUrl": "https://localhost:5001;http://localhost:5003", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "IIS Express": { 20 | "commandName": "IISExpress", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "environmentVariables": {} 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/Routes/CategoriesApi.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.API.Routes; 2 | 3 | public static class CategoriesApi 4 | { 5 | public static RouteGroupBuilder MapCategoriesApi(this RouteGroupBuilder group) 6 | { 7 | group.MapGet("/", GetAllCategories).WithName("GetCategories"); 8 | return group; 9 | } 10 | 11 | public static async ValueTask>> GetAllCategories(PodcastDbContext podcastDbContext, CancellationToken cancellationToken) 12 | { 13 | var categories = await podcastDbContext.Categories.Select(x => new CategoryDto(x.Id, x.Genre)) 14 | .ToListAsync(cancellationToken); 15 | return TypedResults.Ok(categories); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/Routes/EpisodesApi.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.API.Routes; 2 | 3 | public static class EpisodesApi 4 | { 5 | public static RouteGroupBuilder MapEpisodesApi(this RouteGroupBuilder group) 6 | { 7 | group.MapGet("/{id}", GetEpisodeById).WithName("GetEpisodeById"); 8 | return group; 9 | } 10 | 11 | public static async ValueTask> GetEpisodeById(PodcastDbContext podcastDbContext, CancellationToken cancellationToken, Guid id) 12 | { 13 | var episode = await podcastDbContext.Episodes.Include(episode => episode.Show) 14 | .Where(episode => episode.Id == id) 15 | .Select(episode => new EpisodeDto(episode)) 16 | .FirstAsync(cancellationToken); 17 | return TypedResults.Ok(episode); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "PodcastDb": "Server=localhost, 5433;Database=Podcast;User Id=sa;Password=Pass@word;Encrypt=False", 11 | "FeedQueue": "UseDevelopmentStorage=true" 12 | }, 13 | "Authentication": { 14 | "Schemes": { 15 | "Bearer": { 16 | "ValidAudiences": [ 17 | "http://localhost:56906", 18 | "https://localhost:44385", 19 | "https://localhost:5001", 20 | "http://localhost:5003", 21 | "1ba2c41d-3a54-414a-9700-1f9393cfafca" 22 | ], 23 | "ValidIssuer": "dotnet-user-jwts" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Features": { 11 | "FeedIngestion": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/DTOs/CategoryDto.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.API.Models; 2 | 3 | public record CategoryDto(Guid Id, string Genre); 4 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/DTOs/EpisodeDto.cs: -------------------------------------------------------------------------------- 1 | using Podcast.Infrastructure.Data.Models; 2 | 3 | namespace Podcast.API.Models; 4 | 5 | public record EpisodeDto 6 | { 7 | public EpisodeDto(Episode episode) 8 | { 9 | Id = episode.Id; 10 | Title = episode.Title; 11 | Published = episode.Published; 12 | Url = episode.Url; 13 | Show = new ShowDetailDto(episode.Show!.Id, episode.Show.Title, episode.Show.Author, 14 | episode.Show.Image); 15 | Description = episode.Description; 16 | Duration = episode.Duration?.ToString(); 17 | } 18 | 19 | public Guid Id { get; } 20 | public string Title { get; } 21 | public string Url { get; } 22 | public DateTime Published { get; } 23 | public string? Duration { get; set; } 24 | public string Description { get; set; } 25 | public ShowDetailDto Show { get; } 26 | 27 | public record ShowDetailDto(Guid Id, string Title, string Author, string Image); 28 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/DTOs/UserSubmittedFeedDto.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.API.Models; 2 | 3 | public record UserSubmittedFeedDto(Guid Id, string Title, string Url, List Categories); -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/Models/Category.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Infrastructure.Data.Models; 2 | 3 | public class Category 4 | { 5 | public Category(Guid id, string genre) 6 | { 7 | Id = id; 8 | Genre = genre; 9 | } 10 | 11 | public Guid Id { get; private set; } 12 | public string Genre { get; private set; } 13 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/Models/Episode.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Infrastructure.Data.Models; 2 | 3 | public class Episode 4 | { 5 | public Episode(string description, TimeSpan? duration, string @explicit, DateTime published, string title, string url) 6 | { 7 | Description = description; 8 | Duration = duration; 9 | Explicit = @explicit; 10 | Published = published; 11 | Title = title; 12 | Url = url; 13 | } 14 | 15 | public Guid Id { get; private set; } 16 | public string Title { get; private set; } 17 | public string Description { get; private set; } 18 | public string Explicit { get; private set; } 19 | public DateTime Published { get; private set; } 20 | public TimeSpan? Duration { get; private set; } 21 | public string Url { get; private set; } 22 | public Show Show { get; private set; } = null!; 23 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/Models/Feed.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Infrastructure.Data.Models; 2 | 3 | public class Feed 4 | { 5 | public Feed(Guid id, string url, bool isFeatured = false) 6 | { 7 | Id = id; 8 | Url = url; 9 | IsFeatured = isFeatured; 10 | } 11 | 12 | public Guid Id { get; private set; } 13 | public string Url { get; private set; } 14 | public bool IsFeatured { get; private set; } 15 | public Show? Show { get; set; } 16 | public ICollection Categories { get; private set; } = new List(); 17 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/Models/FeedCategory.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Infrastructure.Data.Models; 2 | 3 | public class FeedCategory 4 | { 5 | public FeedCategory(Guid categoryId, Guid feedId) 6 | { 7 | CategoryId = categoryId; 8 | FeedId = feedId; 9 | } 10 | 11 | public Guid FeedId { get; private set; } 12 | public Guid CategoryId { get; private set; } 13 | public Category? Category { get; private set; } 14 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/Models/Show.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Infrastructure.Data.Models; 2 | 3 | public class Show 4 | { 5 | public Show(string author, string description, string email, string language, string title, string link, string image, DateTime updated) 6 | { 7 | Author = author; 8 | Description = description; 9 | Email = email; 10 | Language = language; 11 | Title = title; 12 | Link = link; 13 | Image = image; 14 | Updated = updated; 15 | } 16 | 17 | public Guid Id { get; private set; } 18 | public string Title { get; private set; } 19 | public string Author { get; private set; } 20 | public string Description { get; private set; } 21 | public string Image { get; private set; } 22 | public DateTime Updated { get; private set; } 23 | public string Link { get; private set; } 24 | public string Email { get; private set;} 25 | public string Language { get; private set; } 26 | public Guid FeedId { get; private set; } 27 | public Feed? Feed { get; private set; } 28 | public ICollection Episodes { get; private set; } = new List(); 29 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/Models/UserSubmittedFeed.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Infrastructure.Data.Models; 2 | 3 | public class UserSubmittedFeed 4 | { 5 | public UserSubmittedFeed(string url, string title, string categories) 6 | { 7 | Url = url; 8 | Title = title; 9 | Categories = categories; 10 | } 11 | 12 | public Guid Id { get; private set; } 13 | public string Url { get; private set; } 14 | public string Title { get; set; } 15 | public DateTime Timestamp { get; set; } = DateTime.UtcNow; 16 | public string Categories { get; set; } 17 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Data/PodcastDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Podcast.Infrastructure.Data.Models; 3 | 4 | namespace Podcast.Infrastructure.Data; 5 | 6 | public class PodcastDbContext : DbContext 7 | { 8 | protected PodcastDbContext() 9 | { 10 | } 11 | 12 | public PodcastDbContext(DbContextOptions options) : base(options) 13 | { 14 | } 15 | 16 | public DbSet Shows => Set(); 17 | public DbSet Categories => Set(); 18 | public DbSet Episodes => Set(); 19 | public DbSet Feeds => Set(); 20 | public DbSet UserSubmittedFeeds => Set(); 21 | 22 | protected override void OnModelCreating(ModelBuilder modelBuilder) 23 | { 24 | modelBuilder.Entity().HasData(Seed.Feeds); 25 | modelBuilder.Entity().HasData(Seed.Categories); 26 | modelBuilder.Entity().HasData(Seed.FeedCategories); 27 | modelBuilder.Entity().HasKey(prop => new { prop.FeedId, prop.CategoryId }); 28 | base.OnModelCreating(modelBuilder); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Http/Feeds/IFeedClient.cs: -------------------------------------------------------------------------------- 1 | using Podcast.Infrastructure.Data; 2 | using Podcast.Infrastructure.Data.Models; 3 | 4 | namespace Podcast.Infrastructure.Http.Feeds; 5 | 6 | public interface IFeedClient 7 | { 8 | Task GetShowAsync(Feed feed, CancellationToken cancellationToken); 9 | Task AddFeedAsync(PodcastDbContext podcastDbContext, string url, IReadOnlyCollection feedCategories, CancellationToken cancellationToken); 10 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Http/ShowClient.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 Podcast.Infrastructure.Http; 8 | 9 | public class ShowClient 10 | { 11 | private readonly HttpClient _httpClient; 12 | 13 | public ShowClient(HttpClient httpClient) 14 | { 15 | _httpClient = httpClient; 16 | } 17 | 18 | public async Task CheckLink(string showLink) 19 | { 20 | var response = await _httpClient.GetAsync(showLink, HttpCompletionOption.ResponseHeadersRead); 21 | return response.IsSuccessStatusCode; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Podcast.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Infrastructure/Queues/Messages/NewFeedRequested.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Podcast.Infrastructure.Queues.Messages; 3 | public record NewFeedRequested(string Title, string Url, List Categories); -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Ingestion.Worker/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base 4 | WORKDIR /app 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 7 | WORKDIR /src 8 | COPY ["src/Services/Podcasts/Podcast.Ingestion.Worker/Podcast.Ingestion.Worker.csproj", "src/Services/Podcasts/Podcast.Ingestion.Worker/"] 9 | COPY ["src/Services/Podcasts/Podcast.Infrastructure/Podcast.Infrastructure.csproj", "src/Services/Podcasts/Podcast.Infrastructure/"] 10 | RUN dotnet restore "src/Services/Podcasts/Podcast.Ingestion.Worker/Podcast.Ingestion.Worker.csproj" 11 | COPY . . 12 | WORKDIR "/src/src/Services/Podcasts/Podcast.Ingestion.Worker" 13 | RUN dotnet build "Podcast.Ingestion.Worker.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "Podcast.Ingestion.Worker.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "Podcast.Ingestion.Worker.dll"] -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Ingestion.Worker/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Azure.Storage.Queues; 2 | global using Microsoft.EntityFrameworkCore; 3 | global using Podcast.Infrastructure.Data; 4 | global using Podcast.Infrastructure.Data.Models; 5 | global using Podcast.Infrastructure.Http.Feeds; 6 | global using Podcast.Infrastructure.Queues.Messages; 7 | global using Podcast.Ingestion.Worker; 8 | 9 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Ingestion.Worker/Program.cs: -------------------------------------------------------------------------------- 1 | var host = Host.CreateDefaultBuilder(args) 2 | .ConfigureServices((hostContext, services) => 3 | { 4 | services.AddDbContext(options => 5 | { 6 | options.UseSqlServer( 7 | hostContext.Configuration.GetConnectionString("PodcastDb"), 8 | builder => 9 | { 10 | builder.EnableRetryOnFailure(5, TimeSpan.FromSeconds(10), null); 11 | builder.CommandTimeout(10); 12 | } 13 | ); 14 | }); 15 | var feedQueueClient = new QueueClient(hostContext.Configuration.GetConnectionString("FeedQueue"), "feed-queue"); 16 | feedQueueClient.CreateIfNotExists(); 17 | services.AddSingleton(feedQueueClient); 18 | services.AddScoped(); 19 | services.AddHttpClient(); 20 | services.AddHostedService(); 21 | }) 22 | .Build(); 23 | 24 | await host.RunAsync(); -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Ingestion.Worker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Podcast.Ingestion.Worker": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true 9 | }, 10 | "Docker": { 11 | "commandName": "Docker" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Ingestion.Worker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "PodcastDb": "Server=localhost, 5433;Database=Podcast;User Id=sa;Password=Pass@word;Encrypt=False", 10 | "FeedQueue": "UseDevelopmentStorage=true" 11 | } 12 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Ingestion.Worker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/runtime:7.0 AS base 4 | WORKDIR /app 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 7 | WORKDIR /src 8 | COPY ["src/Services/Podcasts/Podcast.Updater.Worker/Podcast.Updater.Worker.csproj", "src/Services/Podcasts/Podcast.Updater.Worker/"] 9 | COPY ["src/Services/Podcasts/Podcast.Infrastructure/Podcast.Infrastructure.csproj", "src/Services/Podcasts/Podcast.Infrastructure/"] 10 | RUN dotnet restore "src/Services/Podcasts/Podcast.Updater.Worker/Podcast.Updater.Worker.csproj" 11 | COPY . . 12 | WORKDIR "/src/src/Services/Podcasts/Podcast.Updater.Worker" 13 | RUN dotnet build "Podcast.Updater.Worker.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "Podcast.Updater.Worker.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "Podcast.Updater.Worker.dll"] -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.EntityFrameworkCore; 2 | global using Podcast.Infrastructure.Data; 3 | global using Podcast.Infrastructure.Data.Models; 4 | global using Podcast.Infrastructure.Http.Feeds; 5 | 6 | -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/Program.cs: -------------------------------------------------------------------------------- 1 | using Podcast.Updater.Worker; 2 | 3 | var host = Host.CreateDefaultBuilder(args) 4 | .ConfigureServices((hostContext, services) => 5 | { 6 | services.AddHostedService(); 7 | services 8 | .AddDbContext(options => 9 | { 10 | options.UseSqlServer( 11 | hostContext.Configuration.GetConnectionString("PodcastDb"), 12 | builder => 13 | { 14 | builder.EnableRetryOnFailure(10, TimeSpan.FromSeconds(60), null); 15 | builder.CommandTimeout(30); 16 | } 17 | ); 18 | }) 19 | .AddTransient() 20 | .AddHttpClient() 21 | .Services 22 | .AddLogging(); 23 | }) 24 | .Build(); 25 | 26 | await host.RunAsync(); -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Podcast.Updater.Worker": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | }, 8 | "dotnetRunMessages": true 9 | }, 10 | "Docker": { 11 | "commandName": "Docker" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/Worker.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Updater.Worker; 2 | 3 | internal sealed class Worker : BackgroundService 4 | { 5 | private readonly TimeSpan _delay = TimeSpan.FromHours(1); 6 | private readonly ILogger _logger; 7 | private readonly IServiceScopeFactory _services; 8 | 9 | public Worker(ILogger logger, IServiceScopeFactory services) 10 | { 11 | _logger = logger; 12 | _services = services; 13 | } 14 | 15 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 16 | { 17 | _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 18 | 19 | while (!stoppingToken.IsCancellationRequested) 20 | { 21 | using (var scope = _services.CreateScope()) 22 | { 23 | var handler = scope.ServiceProvider.GetRequiredService(); 24 | await handler.HandleUpdateAsync(stoppingToken); 25 | } 26 | 27 | await Task.Delay(_delay, stoppingToken); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "PodcastDb": "Server=localhost, 5433;Database=Podcast;User Id=sa;Password=Pass@word;Encrypt=False" 10 | } 11 | } -------------------------------------------------------------------------------- /src/Services/Podcasts/Podcast.Updater.Worker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Web/Client/App.razor: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | .NET Podcasts - Not found 9 | 10 | 11 | Sorry, there's nothing at this address. 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Web/Client/Podcast.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | true 6 | enable 7 | enable 8 | service-worker-assets.js 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Web/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | using Podcast.Components; 3 | using Podcast.Pages.Data; 4 | using Podcast.Shared; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | 8 | builder.Services.AddHttpClient( 9 | client => { 10 | client.BaseAddress = new Uri(builder.Configuration["PodcastApi:BaseAddress"]!); 11 | client.DefaultRequestHeaders.Add("api-version", "1.0"); 12 | }); 13 | builder.Services.AddScoped(); 14 | builder.Services.AddScoped(); 15 | builder.Services.AddScoped(); 16 | builder.Services.AddScoped(); 17 | builder.Services.AddScoped(); 18 | builder.Services.AddScoped(); 19 | builder.Services.AddSingleton(); 20 | builder.Services.AddScoped(_ => 21 | new ListenTogetherHubClient(builder.Configuration["ListenTogetherHub"]!)); 22 | 23 | await builder.Build().RunAsync(); -------------------------------------------------------------------------------- /src/Web/Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:28270", 7 | "sslPort": 44341 8 | } 9 | }, 10 | "profiles": { 11 | "Podcast.Client": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:7126;http://localhost:5126", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Web/Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Podcast.Pages 10 | @using Podcast.Pages.Shared 11 | -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "PodcastApi": { 3 | "BaseAddress": "http://localhost:5003" 4 | }, 5 | "ListenTogetherHub": "http://localhost:5001/listentogether" 6 | } 7 | -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "PodcastApi": { 3 | "BaseAddress": "" 4 | }, 5 | "ListenTogetherHub": "" 6 | } 7 | -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/images/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Client/wwwroot/images/icon-192x192.png -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/images/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Client/wwwroot/images/icon-256x256.png -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/images/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Client/wwwroot/images/icon-384x384.png -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/images/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Client/wwwroot/images/icon-512x512.png -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": ".NET Podcasts", 3 | "description": "Discover the application that allows you to listen to the most interesting development podcasts: Azure, .NET, Microsoft 365, and much more.", 4 | "start_url": "/discover", 5 | "scope": "/", 6 | "display": "standalone", 7 | "orientation": "any", 8 | "theme_color": "#c00cc0", 9 | "background_color": "#faf9f8", 10 | "icons": [ 11 | { 12 | "src": "images/icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "images/icon-256x256.png", 18 | "sizes": "256x256", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "images/icon-384x384.png", 23 | "sizes": "384x384", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "images/icon-512x512.png", 28 | "sizes": "512x512", 29 | "type": "image/png" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /src/Web/Client/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /src/Web/Components/ClipboardInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace Podcast.Components; 4 | 5 | public class ClipboardInterop 6 | { 7 | private readonly IJSRuntime _jsRuntime; 8 | 9 | public ClipboardInterop(IJSRuntime jsRuntime) 10 | { 11 | _jsRuntime = jsRuntime; 12 | } 13 | 14 | public ValueTask ReadTextAsync() => 15 | _jsRuntime.InvokeAsync("navigator.clipboard.readText"); 16 | 17 | public ValueTask WriteTextAsync(string text) => 18 | _jsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); 19 | } -------------------------------------------------------------------------------- /src/Web/Components/Enums/Theme.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Components; 2 | 3 | public enum Theme 4 | { 5 | Dark, 6 | Light, 7 | System 8 | }; 9 | -------------------------------------------------------------------------------- /src/Web/Components/Events/EventHandlers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Podcast.Components.Events; 4 | 5 | [EventHandler("onanimationend", typeof(EventArgs), 6 | enableStopPropagation: true, enablePreventDefault: false)] 7 | public static class EventHandlers 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/Web/Components/Extensions/TimeSpanExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace System; 2 | 3 | public static class TimeSpanExtensions 4 | { 5 | public static string ToDurationString(this TimeSpan value) 6 | { 7 | var format = value.TotalHours >= 1 ? "h\\:mm\\:ss" : "mm\\:ss"; 8 | return value.ToString(format); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Web/Components/IAudioInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Podcast.Components; 4 | 5 | public interface IAudioInterop 6 | { 7 | void SetUri(string? audioURI); 8 | 9 | Task PlayAsync(ElementReference element); 10 | 11 | Task PauseAsync(ElementReference element); 12 | 13 | Task StopAsync(ElementReference element); 14 | 15 | Task SetMutedAsync(ElementReference element, bool value); 16 | 17 | Task SetVolumeAsync(ElementReference element, int value); 18 | 19 | Task SetCurrentTimeAsync(ElementReference element, double value); 20 | 21 | Task SetPlaybackRateAsync(ElementReference element, double value); 22 | 23 | ValueTask DisposeAsync(); 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/Components/ListenTogether/Emojis/EmojiPanelButton.razor: -------------------------------------------------------------------------------- 1 | 2 | @ChildContent 3 | 4 | 5 | @code { 6 | [EditorRequired] 7 | [Parameter] 8 | public RenderFragment ChildContent { get; set; } = default!; 9 | 10 | [Parameter] 11 | public EventCallback OnClick { get; set; } = default!; 12 | 13 | [Parameter(CaptureUnmatchedValues = true)] 14 | public IDictionary AdditionalAttributes { get; set; } = default!; 15 | 16 | private bool isPressed = false; 17 | 18 | private async Task HandleClick(MouseEventArgs e) 19 | { 20 | isPressed = false; 21 | await OnClick.InvokeAsync(e); 22 | isPressed = true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/Components/ListenTogether/ListenTogetherEmptyRoom.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @Message 4 | @Description 5 | 6 | 7 | 8 | Create new room 9 | 10 | 11 | 12 | 13 | 14 | 15 | @code { 16 | [EditorRequired] 17 | [Parameter] 18 | public string Message { get; set; } = default!; 19 | 20 | [EditorRequired] 21 | [Parameter] 22 | public string Description { get; set; } = default!; 23 | 24 | [EditorRequired] 25 | [Parameter] 26 | public bool IsDisabled { get; set; } = false; 27 | 28 | [Parameter] 29 | public EventCallback OnCreateRoom { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /src/Web/Components/ListenTogether/ListenTogetherJoinRoom.razor.css: -------------------------------------------------------------------------------- 1 | .listen-together-join-room-code { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 12px; 5 | } 6 | 7 | .listen-together-join-room-code .roomCode__input { 8 | margin-bottom: 16px; 9 | } 10 | 11 | @media screen and (max-width: 1279px) { 12 | .listen-together .listen-together-main.listen-together-main__rooms { 13 | display: flex; 14 | background: initial; 15 | padding: 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/Components/ListenTogether/UsernameForm.razor.css: -------------------------------------------------------------------------------- 1 | .usernameModal { 2 | } 3 | 4 | .usernameModal__info { 5 | font-family: var(--ff-regular); 6 | font-size: var(--text-m-fs); 7 | line-height: var(--text-m-lh); 8 | margin-bottom: 32px; 9 | } 10 | 11 | .usernameModal__title { 12 | font-family: var(--ff-semibold); 13 | font-size: var(--subheadings-s6-fs); 14 | line-height: var(--subheadings-s6-lh); 15 | margin-bottom: 12px; 16 | } 17 | 18 | .usernameModal__input { 19 | display: flex; 20 | flex-direction: column; 21 | gap: 10px; 22 | margin-bottom: 32px; 23 | } 24 | 25 | @media screen and (max-width: 767px) { 26 | .usernameModal { 27 | overflow-y: auto; 28 | padding: 0 12px 0 0; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Web/Components/Models/PlayerState.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Components; 2 | 3 | public enum PlayerState 4 | { 5 | Paused, 6 | Playing 7 | }; 8 | -------------------------------------------------------------------------------- /src/Web/Components/Models/Room.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Components; 2 | 3 | public record Room( 4 | TimeSpan Progress, 5 | PlayerState PlayerState, 6 | string Code, 7 | RoomEpisode Episode, 8 | List Users); 9 | -------------------------------------------------------------------------------- /src/Web/Components/Models/RoomEpisode.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Components; 2 | 3 | public record RoomEpisode( 4 | Guid Id, 5 | string Title, 6 | string Description, 7 | string Url, 8 | DateTime Published, 9 | TimeSpan? Duration, 10 | RoomShow Show); 11 | -------------------------------------------------------------------------------- /src/Web/Components/Models/RoomShow.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Components; 2 | 3 | public record RoomShow( 4 | Guid Id, 5 | string Title, 6 | string Author, 7 | string Image); 8 | -------------------------------------------------------------------------------- /src/Web/Components/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Components; 2 | 3 | public record User(string Name); 4 | -------------------------------------------------------------------------------- /src/Web/Components/Podcast.Components.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Grid/Card.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @PrimaryAction 7 | 8 | @ActionMenu 9 | 10 | 11 | @Title 12 | @Subtitle 13 | 14 | 15 | 16 | @code { 17 | [EditorRequired] 18 | [Parameter] 19 | public string Title { get; set; } = default!; 20 | 21 | [Parameter] 22 | public string? Subtitle { get; set; } 23 | 24 | [EditorRequired] 25 | [Parameter] 26 | public string Image { get; set; } = default!; 27 | 28 | [Parameter] 29 | public RenderFragment? PrimaryAction { get; set; } 30 | 31 | [EditorRequired] 32 | [Parameter] 33 | public RenderFragment ActionMenu { get; set; } = default!; 34 | } 35 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Grid/Grid.razor: -------------------------------------------------------------------------------- 1 | @typeparam TItem 2 | 3 | @if (Items == null || !Items.Any()) 4 | { 5 | @if (EmptyResults != null) 6 | { 7 | @EmptyResults 8 | } 9 | else 10 | { 11 | 12 | } 13 | } 14 | else 15 | { 16 | 17 | @foreach (TItem item in Items) 18 | { 19 | @ItemTemplate(item) 20 | } 21 | 22 | } 23 | 24 | @code { 25 | [EditorRequired] 26 | [Parameter] 27 | public IEnumerable? Items { get; set; } 28 | 29 | [EditorRequired] 30 | [Parameter] 31 | public RenderFragment ItemTemplate { get; set; } = default!; 32 | 33 | [Parameter] 34 | public RenderFragment? EmptyResults { get; set; } 35 | } -------------------------------------------------------------------------------- /src/Web/Components/Shared/Grid/Grid.razor.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | display: grid; 3 | grid-auto-rows: auto; 4 | grid-template-rows: 1fr; 5 | gap: 8px; 6 | grid-auto-flow: column; 7 | justify-content: flex-start; 8 | overflow-x: auto; 9 | overflow-y: hidden; 10 | margin: 0 16px 36px; 11 | padding: 0 0 16px; 12 | } 13 | 14 | .grid ::deep .card { 15 | width: 184px; 16 | } 17 | 18 | @media screen and (min-width: 992px) { 19 | .grid { 20 | margin: -20px -30px 0; 21 | padding: 0 50px 30px 60px; 22 | grid-template-columns: repeat(auto-fill,minmax(260px,1fr)); 23 | grid-auto-flow: inherit; 24 | overflow-x: inherit; 25 | } 26 | 27 | .grid ::deep .card { 28 | width: auto; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Modal.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @ChildContent 4 | 5 | 6 | 7 | @code { 8 | [EditorRequired] 9 | [Parameter] 10 | public RenderFragment ChildContent { get; set; } = default!; 11 | 12 | [Parameter] 13 | public EventCallback OnClose { get; set; } 14 | 15 | private Task BackdropClick() => OnClose.InvokeAsync(); 16 | } 17 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Modal.razor.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: absolute; 3 | inset: 0; 4 | display: flex; 5 | } 6 | 7 | .modal__container { 8 | flex: 1; 9 | background: var(--c-neutral-grey1); 10 | display: flex; 11 | padding: 0 16px; 12 | } 13 | 14 | @media screen and (min-width: 768px) { 15 | .modal { 16 | position: fixed; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | .modal::before { 22 | position: absolute; 23 | inset: 0; 24 | content: ""; 25 | background-color: var(--c-neutral-grey4); 26 | opacity: 0.4; 27 | } 28 | 29 | .modal__container { 30 | box-shadow: var(--box-shadow-l); 31 | border-radius: 16px; 32 | display: flex; 33 | z-index: 9999; 34 | padding: 24px; 35 | max-width: 440px; 36 | padding: 32px; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/NoResults.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @Message 4 | @if (Description != null) 5 | { 6 | @Description 7 | } 8 | @if (ShowDiscoverButton == null || ShowDiscoverButton.Value) 9 | { 10 | 11 | Discover podcasts 12 | 13 | } 14 | 15 | 16 | @code { 17 | [EditorRequired] 18 | [Parameter] 19 | public string Message { get; set; } = default!; 20 | 21 | [Parameter] 22 | public string? Description { get; set; } 23 | 24 | [EditorRequired] 25 | [Parameter] 26 | public string Image { get; set; } = default!; 27 | 28 | [Parameter] 29 | public bool? ShowDiscoverButton { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Picture.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @code { 6 | [EditorRequired] 7 | [Parameter] 8 | public string Title { get; set; } = default!; 9 | 10 | [EditorRequired] 11 | [Parameter] 12 | public string Image { get; set; } = default!; 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Picture.razor.css: -------------------------------------------------------------------------------- 1 | .picture { 2 | filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.1)) 3 | drop-shadow(0 1px 4px rgba(0, 0, 0, 0.1)) 4 | drop-shadow(0 1px 1px rgba(0, 0, 0, 0.05)); 5 | width: 100%; 6 | display: flex; 7 | } 8 | 9 | .picture img { 10 | color: var(--c-neutral-dark); 11 | display: block; 12 | height: auto; 13 | width: 100%; 14 | object-fit: cover; 15 | object-position: center center; 16 | aspect-ratio: 1/1; 17 | background-color: var(--c-neutral-grey3); 18 | } 19 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/PlayerBars.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @code { 17 | [EditorRequired] 18 | [Parameter] 19 | public string Color { get; set; } = default!; 20 | 21 | [EditorRequired] 22 | [Parameter] 23 | public bool IsPaused { get; set; } = false; 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Spinner.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Spinner.razor.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | z-index: 1; 6 | height: 100%; 7 | } 8 | 9 | .spinner .spinner-image { 10 | width: 80px; 11 | } 12 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/SubtitlePage.razor: -------------------------------------------------------------------------------- 1 | @Label 2 | 3 | @code { 4 | [EditorRequired] 5 | [Parameter] 6 | public string Label { get; set; } = default!; 7 | } 8 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/SubtitlePage.razor.css: -------------------------------------------------------------------------------- 1 | .subtitlePage { 2 | color: var(--c-neutral-grey8); 3 | font-family: var(--ff-semibold); 4 | font-size: var(--subheadings-s6-fs); 5 | line-height: var(--subheadings-s6-lh); 6 | grid-area: subtitle; 7 | margin-bottom: 16px; 8 | } 9 | 10 | @media screen and (min-width: 992px) { 11 | .subtitlePage { 12 | font-size: var(--headlines-h6-fs); 13 | line-height: var(--headlines-h6-lh); 14 | margin-bottom: 32px; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Web/Components/Shared/Tags.razor: -------------------------------------------------------------------------------- 1 | @typeparam TItem 2 | 3 | 4 | @foreach (var item in Items ?? new TItem[] { }) 5 | { 6 | 7 | @ItemTemplate(item) 8 | 9 | } 10 | 11 | 12 | @code { 13 | [EditorRequired] 14 | [Parameter] 15 | public IEnumerable Items { get; set; } = default!; 16 | 17 | [EditorRequired] 18 | [Parameter] 19 | public RenderFragment ItemTemplate { get; set; } = default!; 20 | } 21 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Tags.razor.css: -------------------------------------------------------------------------------- 1 | .tags { 2 | display: inline-flex; 3 | flex-wrap: wrap; 4 | padding: 0; 5 | gap: 8px; 6 | } 7 | 8 | .tags-item ::deep > * { 9 | align-items: center; 10 | background-color: var(--c-neutral-grey3); 11 | border-radius: 20px; 12 | display: inline-flex; 13 | font-size: var(--text-xs-fs); 14 | line-height: var(--text-xs-lh); 15 | height: 24px; 16 | justify-content: center; 17 | padding: 0 10px; 18 | white-space: nowrap; 19 | } 20 | 21 | .tags-item ::deep > a:hover, 22 | .tags-item ::deep > a:focus { 23 | background-color: var(--c-category); 24 | } 25 | 26 | @media screen and (min-width: 992px) { 27 | .tags-item ::deep > * { 28 | height: 30px; 29 | padding: 0 12px; 30 | font-size: var(--text-s-fs); 31 | line-height: var(--text-s-lh); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/TitlePage.razor: -------------------------------------------------------------------------------- 1 | @Label 2 | 3 | @code { 4 | [EditorRequired] 5 | [Parameter] 6 | public string Label { get; set; } = default!; 7 | } 8 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/TitlePage.razor.css: -------------------------------------------------------------------------------- 1 | .titlePage { 2 | font-family: var(--ff-semibold); 3 | font-size: var(--headlines-h5-fs); 4 | line-height: var(--headlines-h5-lh); 5 | margin-bottom: 2px; 6 | } 7 | -------------------------------------------------------------------------------- /src/Web/Components/Shared/Tooltip.razor: -------------------------------------------------------------------------------- 1 | showTooltip = true)"> 2 | @Container 3 | @Content 5 | 6 | 7 | @code { 8 | [EditorRequired] 9 | [Parameter] 10 | public RenderFragment Content { get; set; } = default!; 11 | 12 | [EditorRequired] 13 | [Parameter] 14 | public RenderFragment Container { get; set; } = default!; 15 | 16 | private bool showTooltip = false; 17 | } 18 | -------------------------------------------------------------------------------- /src/Web/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Forms 2 | @using Microsoft.AspNetCore.Components.Web 3 | @using Microsoft.AspNetCore.Components.Routing 4 | @using Microsoft.AspNetCore.Components.Web.Virtualization 5 | @using System.ComponentModel.DataAnnotations 6 | @using Podcast.Components 7 | @using Podcast.Components.ListenTogether 8 | @using Podcast.Components.ListenTogether.Emojis 9 | @using Podcast.Components.Shared 10 | @using Podcast.Components.Shared.Emojis 11 | @using Podcast.Components.Shared.Grid -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/audioJsInterop.js: -------------------------------------------------------------------------------- 1 | export const play = (element) => element.play(); 2 | export const pause = (element) => element.pause(); 3 | export const stop = (element) => element.stop(); 4 | export const setMuted = (element, value) => element.muted = value; 5 | export const setVolume = (element, value) => element.volume = value; 6 | export const setCurrentTime = (element, value) => element.currentTime = value; 7 | export const setPlaybackRate = (element, value) => element.playbackRate = value; -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/fonts/SegoeUI.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/fonts/SegoeUI.woff -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/fonts/SegoeUIBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/fonts/SegoeUIBold.woff -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/fonts/SegoeUILight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/fonts/SegoeUILight.woff -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/fonts/SegoeUISemibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/fonts/SegoeUISemibold.woff -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/fonts/SegoeUISemilight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/fonts/SegoeUISemilight.woff -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/icons/fonts/podcast-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/icons/fonts/podcast-icons.ttf -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/icons/fonts/podcast-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/css/icons/fonts/podcast-icons.woff -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/css/input.css: -------------------------------------------------------------------------------- 1 | .inputApp { 2 | appearance: none; 3 | outline: 0 none; 4 | font-size: var(--text-m-fs); 5 | line-height: var(--text-m-lh); 6 | background-color: var(--c-searchbar); 7 | border: 1px solid var(--c-neutral-grey4); 8 | border-radius: 4px; 9 | box-sizing: border-box; 10 | height: 36px; 11 | padding: 6px; 12 | display: block; 13 | width: 100%; 14 | color: var(--c-neutral-grey8); 15 | } 16 | 17 | .inputApp::placeholder { 18 | color: var(--c-neutral-grey7); 19 | } 20 | 21 | .inputApp:hover { 22 | border-color: var(--c-neutral-grey6); 23 | } 24 | 25 | .inputApp:active, 26 | .inputApp:focus { 27 | border-color: var(--c-primary); 28 | } 29 | -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/images/bg-splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/images/bg-splash.jpg -------------------------------------------------------------------------------- /src/Web/Components/wwwroot/images/empty-results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Components/wwwroot/images/empty-results.png -------------------------------------------------------------------------------- /src/Web/E2E/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotnet-podcasts", 3 | "version": "1.0.0", 4 | "description": "---\r page_type: sample\r description: \".NET reference application shown at .NET Conf featuring ASP.NET Core, Blazor, .NET MAUI, Microservices, and more!\"\r languages:\r - csharp\r products:\r - dotnet-core\r - ef-core\r - blazor\r - dotnet-maui\r - azure-sql-database\r - azure-storage\r - azure-container-apps\r - azure-container-registry\r - azure-app-service-web\r ---", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": {}, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Microsoft/dotnet-podcasts.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/Microsoft/dotnet-podcasts/issues" 19 | }, 20 | "homepage": "https://github.com/Microsoft/dotnet-podcasts#readme", 21 | "devDependencies": { 22 | "@playwright/test": "^1.28.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/E2E/tests/listen-together.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Listen Together', () => { 4 | test('should allow me to listen together', async ({ page }) => { 5 | await page.goto('/discover'); 6 | await page.locator('.item-primary-action').first().click(); 7 | await page.locator('.icon-play').first().click(); 8 | await page.getByRole('link', { name: 'Listen Together' }).click(); 9 | await expect(page).toHaveURL('/listen-together'); 10 | await page.getByRole('button', { name: 'Create new room' }).click(); 11 | await page.getByPlaceholder('Your name').fill('test'); 12 | await page.getByRole('button', { name: 'Open room' }).click(); 13 | await page.getByRole('button', { name: 'Leave the room' }).click(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/Web/E2E/tests/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Login', () => { 4 | test('should allow me to login', async ({ page }) => { 5 | await page.goto(''); 6 | await page.getByRole('link', { name: 'Sign In' }).click(); 7 | await expect(page).toHaveURL('/discover'); 8 | await expect(page).toHaveTitle('.NET Podcasts - Discover') 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/Web/E2E/tests/subscriptions.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Subscriptions', () => { 4 | 5 | test('should start with no subscriptions', async ({ page }) => { 6 | await page.goto('/subscriptions'); 7 | await expect(page.getByRole('heading', { name: 'You haven’t subscribed to any channel yet.' })).toBeVisible(); 8 | await page.getByRole('link', { name: 'Discover podcasts' }).click(); 9 | await expect(page).toHaveURL('/discover'); 10 | }); 11 | 12 | test.fixme('should allow me to subscribe and unsubscribe', async ({ page }) => { 13 | await page.goto('/discover'); 14 | await page.getByTitle('Subscribe').first().click(); 15 | await page.getByRole('link', { name: 'Subscriptions' }).click(); 16 | await expect.poll(() => 17 | page.locator('.card').count()).toBeGreaterThan(0); 18 | await page.getByTitle('Unsubscribe').click(); 19 | await expect(page.getByRole('heading', { name: 'You haven\'t subscribed to any podcasts yet.' })).toBeVisible(); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /src/Web/Pages/Events/DurationChangeEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Pages.Events; 2 | 3 | public class DurationChangeEventArgs : EventArgs 4 | { 5 | public double Duration { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Web/Pages/Events/EventHandlers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Podcast.Pages.Events; 4 | 5 | [EventHandler("oncustomdurationchange", typeof(DurationChangeEventArgs), 6 | enableStopPropagation: true, enablePreventDefault: true)] 7 | [EventHandler("oncustomtimeupdate", typeof(TimeUpdateEventArgs), 8 | enableStopPropagation: true, enablePreventDefault: true)] 9 | [EventHandler("playbackratechange", typeof(PlaybackRateChangeEventArgs), 10 | enableStopPropagation: true, enablePreventDefault: true)] 11 | public static class EventHandlers 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Pages/Events/PlaybackRateChangeEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Pages.Events; 2 | 3 | public class PlaybackRateChangeEventArgs : EventArgs 4 | { 5 | public double PlaybackRate { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/Web/Pages/Events/TimeUpdateEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Pages.Events; 2 | 3 | public class TimeUpdateEventArgs : EventArgs 4 | { 5 | public double CurrentTime { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Web/Pages/Models/EpisodeInfo.cs: -------------------------------------------------------------------------------- 1 | using Podcast.Shared; 2 | 3 | namespace Podcast.Pages.Models; 4 | 5 | public record EpisodeInfo 6 | { 7 | public Guid Id { get; init; } = default!; 8 | public string Title { get; init; } = default!; 9 | public string Description { get; init; } = default!; 10 | public string Url { get; init; } = default!; 11 | public DateTime Published { get; init; } = default!; 12 | public TimeSpan? Duration { get; init; } 13 | public ShowInfo Show { get; init; } = default!; 14 | 15 | public EpisodeInfo() { } 16 | 17 | public EpisodeInfo(Guid id, string title, string description, string url, DateTime published, TimeSpan? duration, ShowInfo show) => 18 | (Id, Title, Description, Url, Published, Duration, Show) = (id, title, description, url, published, duration, show); 19 | 20 | public EpisodeInfo(Show show, Episode episode) 21 | : this(episode.Id, episode.Title, episode.Description, episode.Url, episode.Published, episode.Duration, new ShowInfo(show)) { } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/Web/Pages/Models/ShowInfo.cs: -------------------------------------------------------------------------------- 1 | using Podcast.Shared; 2 | 3 | namespace Podcast.Pages.Models; 4 | 5 | public record ShowInfo 6 | { 7 | public Guid Id { get; init; } = default!; 8 | public string Title { get; init; } = default!; 9 | public string Author { get; init; } = default!; 10 | public string Image { get; init; } = default!; 11 | 12 | public ShowInfo() { } 13 | 14 | public ShowInfo(Guid id, string title, string author, string image) => 15 | (Id, Title, Author, Image) = (id, title, author, image); 16 | 17 | public ShowInfo(Show show) => 18 | (Id, Title, Author, Image) = (show.Id, show.Title, show.Author, show.Image); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/Web/Pages/Pages/DiscoverPage.razor.css: -------------------------------------------------------------------------------- 1 | .categories { 2 | align-items: flex-end; 3 | flex-direction: column; 4 | margin-bottom: 8px; 5 | padding: 0 16px; 6 | } 7 | 8 | ::deep .categories .appLink { 9 | margin-bottom: 16px; 10 | } 11 | 12 | @media screen and (min-width: 1280px) { 13 | .categories { 14 | display: flex; 15 | margin-bottom: 0; 16 | padding: 0 48px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Web/Pages/Pages/SettingsPage.razor.css: -------------------------------------------------------------------------------- 1 | .settings-content { 2 | display: grid; 3 | gap: 24px; 4 | } 5 | 6 | .settings-developed { 7 | margin-top: 48px; 8 | } -------------------------------------------------------------------------------- /src/Web/Pages/Pages/ShowPage.razor.css: -------------------------------------------------------------------------------- 1 | .showEpisodes { 2 | padding: 0 16px; 3 | } 4 | 5 | .showEpisodes ::deep .subtitlePage { 6 | margin-bottom: 0; 7 | } 8 | 9 | @media screen and (min-width: 992px) { 10 | .showEpisodes { 11 | padding: 0 48px; 12 | } 13 | } 14 | 15 | @media screen and (min-width: 1366px) { 16 | .showEpisodes { 17 | margin-left: 332px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Web/Pages/Podcast.Pages.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Web/Pages/Shared/Header.razor.css: -------------------------------------------------------------------------------- 1 | .header ::deep .header__logo { 2 | display: flex; 3 | width: 120px; 4 | padding: 12px 14px; 5 | align-items: center; 6 | } 7 | 8 | .header ::deep .header__logo img, 9 | .header ::deep .header__logo svg { 10 | width: 100%; 11 | height: auto; 12 | } 13 | 14 | .header ::deep .header__logo .logo-label { 15 | fill: var(--c-neutral-grey8); 16 | } 17 | 18 | @media screen and (min-width: 992px) { 19 | .header ::deep .header__logo { 20 | width: 230px; 21 | padding: 43px 41px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Web/Pages/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @Body 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Web/Pages/Shared/SearchBar.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @code { 9 | [Parameter] 10 | public EventCallback OnSearch { get; set; } 11 | 12 | private string? searchValue; 13 | 14 | private Task HandleSubmit() => OnSearch.InvokeAsync(searchValue); 15 | } -------------------------------------------------------------------------------- /src/Web/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.JSInterop 8 | @using Podcast.Pages 9 | @using Podcast.Pages.Events 10 | @using Podcast.Pages.Models 11 | @using Podcast.Pages.Shared 12 | @using Podcast.Pages.Shared.Cards 13 | @using Podcast.Pages.Shared.Lists 14 | @using Podcast.Pages.Shared.Player 15 | @using Podcast.Pages.Data 16 | @using Podcast.Components 17 | @using Podcast.Components.ListenTogether 18 | @using Podcast.Components.Player 19 | @using Podcast.Components.Shared 20 | @using Podcast.Components.Shared.Grid 21 | @using Podcast.Shared 22 | -------------------------------------------------------------------------------- /src/Web/Pages/wwwroot/images/no-listen-later.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Pages/wwwroot/images/no-listen-later.png -------------------------------------------------------------------------------- /src/Web/Pages/wwwroot/images/no-subscriptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Pages/wwwroot/images/no-subscriptions.png -------------------------------------------------------------------------------- /src/Web/Pages/wwwroot/js/customEvents.js: -------------------------------------------------------------------------------- 1 | Blazor.registerCustomEventType('customdurationchange', { 2 | browserEventName: 'durationchange', 3 | createEventArgs: event => ({ 4 | duration: event.srcElement.duration 5 | }) 6 | }); 7 | 8 | Blazor.registerCustomEventType('customtimeupdate', { 9 | browserEventName: 'timeupdate', 10 | createEventArgs: event => ({ 11 | currentTime: event.srcElement.currentTime 12 | }) 13 | }); 14 | 15 | Blazor.registerCustomEventType('playbackratechange', { 16 | browserEventName: 'playbackratechange', 17 | createEventArgs: event => ({ 18 | playbackRate: event.srcElement.playbackRate 19 | }) 20 | }); -------------------------------------------------------------------------------- /src/Web/Pages/wwwroot/js/theme.js: -------------------------------------------------------------------------------- 1 | const choice = localStorage.getItem('theme'); 2 | if (choice != null) { 3 | document.body.setAttribute('data-theme', choice) 4 | } 5 | else { 6 | const theme = localStorage.getItem('theme') 7 | ?? matchMedia('(prefers-color-scheme: dark)').matches 8 | ? 'Dark' : 'Light'; 9 | if (theme === 'Dark') { 10 | document.body.setAttribute('data-theme', theme); 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Web/Server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 7 | RUN apt-get update -y 8 | RUN apt-get install -y python3 9 | RUN dotnet workload install wasm-tools 10 | WORKDIR /src 11 | COPY ["src/Web/Server/Podcast.Server.csproj", "src/Web/Server/"] 12 | COPY ["src/Web/Shared/Podcast.Shared.csproj", "src/Web/Shared/"] 13 | COPY ["src/Web/Client/Podcast.Client.csproj", "src/Web/Client/"] 14 | COPY ["src/Web/Pages/Podcast.Pages.csproj", "src/Web/Pages/"] 15 | COPY ["src/Web/Components/Podcast.Components.csproj", "src/Web/Components/"] 16 | RUN dotnet restore "src/Web/Server/Podcast.Server.csproj" 17 | COPY . . 18 | WORKDIR "/src/src/Web/Server" 19 | RUN dotnet build "Podcast.Server.csproj" -c Release -o /app/build 20 | 21 | FROM build AS publish 22 | RUN dotnet publish "Podcast.Server.csproj" -c Release -o /app/publish 23 | 24 | FROM base AS final 25 | WORKDIR /app 26 | COPY --from=publish /app/publish . 27 | ENTRYPOINT ["dotnet", "Podcast.Server.dll"] -------------------------------------------------------------------------------- /src/Web/Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace BlazorApp1.Server.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Web/Server/Pages/Landing.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Podcast.Shared; 4 | 5 | namespace Podcast.Server.Pages 6 | { 7 | public class Landing : PageModel 8 | { 9 | private readonly PodcastService _podcastService; 10 | 11 | public Show[]? FeaturedShows { get; set; } 12 | 13 | public Landing(PodcastService podcastService) 14 | { 15 | _podcastService = podcastService; 16 | } 17 | 18 | public async Task OnGet() 19 | { 20 | var shows = await _podcastService.GetShows(50, null); 21 | FeaturedShows = shows?.Where(s => s.IsFeatured).ToArray(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/Server/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @namespace Podcast.Client 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = "_Layout"; 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Web/Server/Podcast.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 46367140-1a28-47c4-95a1-927376c260ac 8 | Linux 9 | ..\..\.. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Web/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "PodcastApi": { 10 | "BaseAddress": "http://localhost:5003" 11 | }, 12 | "ListenTogetherHub": "http://localhost:5001/listentogether" 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "PodcastApi": { 10 | "BaseAddress": "" 11 | }, 12 | "ListenTogetherHub": "" 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/favicon.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/fonts/SegoeUI.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/fonts/SegoeUI.woff -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/fonts/SegoeUIBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/fonts/SegoeUIBold.woff -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/fonts/SegoeUILight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/fonts/SegoeUILight.woff -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/fonts/SegoeUISemibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/fonts/SegoeUISemibold.woff -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/fonts/SegoeUISemilight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/fonts/SegoeUISemilight.woff -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/available-apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/available-web.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/available-windows.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/device_Android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/device_Android.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/device_Screen_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/device_Screen_home.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/device_iPhone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/device_iPhone.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/download-app_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/download-app_store.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/download-google_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/download-google_play.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/download-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/download-microsoft.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/features-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/features-01.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/features-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/features-02.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/features-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/features-03.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/features-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/features-04.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/features-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/features-05.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/hero_landing_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/hero_landing_image.jpg -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/waves1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/waves1.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/waves2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/waves2.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/waves3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/waves3.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/images/waves4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/Server/wwwroot/images/waves4.png -------------------------------------------------------------------------------- /src/Web/Server/wwwroot/js/scripts.js: -------------------------------------------------------------------------------- 1 | const onScroll = () => { 2 | const scroll = window.scrollY; 3 | const header = document.querySelector('#header'); 4 | if (!header) return; 5 | 6 | if (scroll > 50) { 7 | header.classList.add("sticky"); 8 | } else { 9 | header.classList.remove("sticky"); 10 | } 11 | } 12 | 13 | window.addEventListener('scroll', onScroll, { passive: true }); 14 | 15 | const addClass = () => { 16 | const element = document.getElementById("menu"); 17 | if (element) element.classList.add("visible"); 18 | } 19 | 20 | const removeClass = () => { 21 | const element = document.getElementById("menu"); 22 | if (element) element.classList.remove("visible"); 23 | } 24 | 25 | onScroll(); -------------------------------------------------------------------------------- /src/Web/Shared/Category.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Shared; 2 | 3 | public record Category(Guid Id, string Genre); 4 | -------------------------------------------------------------------------------- /src/Web/Shared/Episode.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Shared; 2 | 3 | public record Episode( 4 | Guid Id, 5 | string Title, 6 | string Description, 7 | string Explicit, 8 | DateTime Published, 9 | TimeSpan? Duration, 10 | string Url); 11 | -------------------------------------------------------------------------------- /src/Web/Shared/Podcast.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Web/Shared/PodcastService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | 3 | namespace Podcast.Shared; 4 | 5 | public class PodcastService 6 | { 7 | private readonly HttpClient _httpClient; 8 | 9 | public PodcastService(HttpClient httpClient) 10 | { 11 | _httpClient = httpClient; 12 | } 13 | 14 | public Task GetCategories() => 15 | _httpClient.GetFromJsonAsync("categories"); 16 | 17 | public Task GetShows(int limit, string? term = null) => 18 | _httpClient.GetFromJsonAsync($"shows?limit={limit}&term={term}"); 19 | 20 | public Task GetShows(int limit, string? term = null, Guid? categoryId = null) => 21 | _httpClient.GetFromJsonAsync($"shows?limit={limit}&term={term}&categoryId={categoryId}"); 22 | 23 | public Task GetShow(Guid id) => 24 | _httpClient.GetFromJsonAsync($"shows/{id}"); 25 | } 26 | -------------------------------------------------------------------------------- /src/Web/Shared/Show.cs: -------------------------------------------------------------------------------- 1 | namespace Podcast.Shared; 2 | 3 | public record Show( 4 | Guid Id, 5 | string Title, 6 | string Author, 7 | string Description, 8 | string Image, 9 | DateTime Updated, 10 | string Link, 11 | string Email, 12 | string Language, 13 | IEnumerable Categories, 14 | IEnumerable Episodes, 15 | bool IsFeatured); 16 | -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/discover-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/discover-dark.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/discover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/discover.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/full-landing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/full-landing.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/listen-later.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/listen-later.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/listen-together-room.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/listen-together-room.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/listen-together.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/listen-together.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/podcast-detail.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/podcast-detail.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/settings.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/settings.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/desktop/subscriptions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/desktop/subscriptions.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/mobile/discover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/mobile/discover.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/mobile/listen-together-room.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/mobile/listen-together-room.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/mobile/listen-together.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/mobile/listen-together.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/mobile/podcast-detail.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/mobile/podcast-detail.jpeg -------------------------------------------------------------------------------- /src/Web/docs/screens/mobile/settings.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/dotnet-podcasts/5ee8be2990b81eb681bbd100875c263aaa5ab68a/src/Web/docs/screens/mobile/settings.jpeg --------------------------------------------------------------------------------
Sorry, there's nothing at this address.
@Description