diff --git a/MainWindow.xaml b/MainWindow.xaml index 6fee4ce..485b725 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -795,12 +795,50 @@ - - + + + + + + + + + + + + + + + - - - - + + diff --git a/Models/Socials.cs b/Models/Socials.cs index 7977f01..2899723 100644 --- a/Models/Socials.cs +++ b/Models/Socials.cs @@ -136,4 +136,64 @@ public static class SocialServiceIcons _ => $"https://{h}", }; } + + /// + /// Auto-detects the social service from a URL or fediverse handle. Supports + /// full URLs (https://x.com/user), domain-only (x.com/user), and fediverse + /// handles (@user@instance.tube). Returns Link/Website for unknown domains + /// or bare handles — the caller can then ask which service it is. + /// + public static (SocialService service, string handle, string url) DetectService(string input) + { + var trimmed = input.Trim(); + + // Fediverse handle: @user@domain + if (trimmed.StartsWith("@") && trimmed.IndexOf('@', 1) > 0) + { + var parts = trimmed.TrimStart('@').Split('@', 2); + if (parts.Length == 2) + { + var (user, domain) = (parts[0], parts[1]); + return (SocialService.Link, user, $"https://{domain}/@{user}"); + } + } + + // Strip protocol for domain parsing + var url = trimmed; + if (!trimmed.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + url = $"https://{trimmed}"; + + try + { + var uri = new Uri(url); + var domain = uri.Host.ToLowerInvariant(); + var path = uri.AbsolutePath.Trim('/'); + + return domain switch + { + "youtube.com" or "www.youtube.com" or "youtu.be" => (SocialService.YouTube, path, url), + "twitch.tv" or "www.twitch.tv" => (SocialService.Twitch, path, url), + "x.com" or "twitter.com" => (SocialService.X, path, url), + "instagram.com" or "www.instagram.com" => (SocialService.Instagram, path, url), + "tiktok.com" or "www.tiktok.com" => (SocialService.TikTok, path, url), + "facebook.com" or "www.facebook.com" or "fb.com" => (SocialService.Facebook, path, url), + "discord.gg" or "discord.com" => (SocialService.Discord, path, url), + "kick.com" => (SocialService.Kick, path, url), + "threads.net" or "www.threads.net" => (SocialService.Threads, path, url), + "bsky.app" => (SocialService.Bluesky, path, url), + "github.com" => (SocialService.GitHub, path, url), + "linkedin.com" or "www.linkedin.com" => (SocialService.LinkedIn, path, url), + "pinterest.com" or "www.pinterest.com" => (SocialService.Pinterest, path, url), + "snapchat.com" => (SocialService.Snapchat, path, url), + "reddit.com" or "www.reddit.com" => (SocialService.Reddit, path, url), + "wa.me" or "whatsapp.com" => (SocialService.WhatsApp, path, url), + "t.me" => (SocialService.Telegram, path, url), + _ => (SocialService.Website, path, url), + }; + } + catch + { + return (SocialService.Link, trimmed.TrimStart('@'), SocialServiceIcons.CanonicalUrlFor(SocialService.Link, trimmed)); + } + } } diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 5884836..38c2caf 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -889,6 +889,7 @@ public class MainViewModel : ViewModelBase IsConnected = true; SyncConnectedAccount(); + AutoAddYouTubeSocial(); } catch (Exception ex) { @@ -903,6 +904,34 @@ public class MainViewModel : ViewModelBase AccountDisplayName = channel?.DisplayName ?? string.Empty; } + /// + /// When the creator is connected to YouTube and hasn't defined any socials yet, + /// auto-add YouTube as the first entry — the connected channel is the default + /// social, no extra input needed. + /// + private void AutoAddYouTubeSocial() + { + if (_socials != null && _socials.Entries.Count > 0) return; + var channel = _youtubeAuth.CurrentChannel; + if (channel == null || string.IsNullOrWhiteSpace(channel.DisplayName)) return; + + _socials ??= new SocialsConfig(); + var handle = channel.DisplayName.Trim().TrimStart('@'); + _socials.Entries.Add(new SocialEntry + { + Service = SocialService.YouTube, + Handle = handle, + ProfileUrl = !string.IsNullOrWhiteSpace(channel.ChannelId) + ? $"https://www.youtube.com/channel/{channel.ChannelId}" + : SocialServiceIcons.CanonicalUrlFor(SocialService.YouTube, handle), + }); + + OnPropertyChanged(nameof(HasSocials)); + OnPropertyChanged(nameof(CanToggleSceneSocialBar)); + OnPropertyChanged(nameof(CanAddMoreSocials)); + ScheduleSave(); + } + private static string DefaultLayoutPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ytLlive", @@ -1826,10 +1855,6 @@ public class MainViewModel : ViewModelBase private void OpenSocialDialog() { - // The social dialog is a simple input flow: pick a service, enter a - // handle/URL, the app validates by looking up the page, and on success - // adds the entry. For now this is a MessageBox-based flow — a proper - // dialog window follows once the bar rendering is validated. if (!CanAddMoreSocials) { MessageBox.Show( @@ -1840,34 +1865,69 @@ public class MainViewModel : ViewModelBase return; } - var services = string.Join(", ", Enum.GetNames()); - var input = ShowInputDialog($"Service ({services}):", "Add Social — Step 1 of 2", "YouTube"); - if (string.IsNullOrWhiteSpace(input)) return; - if (!Enum.TryParse(input, true, out var service)) + // Step 1: YouTube handle — pre-filled from the connected account if available. + var ytHandle = IsConnected && _youtubeAuth.CurrentChannel is { } ch + ? ch.DisplayName + : ShowInputDialog("Your YouTube handle (e.g. @yourchannel):", "Add Social — YouTube", "@yourchannel"); + if (string.IsNullOrWhiteSpace(ytHandle)) return; + ytHandle = ytHandle.Trim().TrimStart('@'); + + // Validate the YT handle (or skip validation if it came from the connected account). + if (!IsConnected || _socials?.Entries.Any(e => e.Service == SocialService.YouTube) != true) { - MessageBox.Show($"'{input}' isn't a recognized service. Try one of: {services}.", - "ytLlive", MessageBoxButton.OK, MessageBoxImage.Warning); + var ytResult = _socialValidator.LookupAsync(SocialService.YouTube, ytHandle).GetAwaiter().GetResult(); + if (!ytResult.Success) + { + var proceed = MessageBox.Show( + $"{ytResult.Error}\n\nAdd anyway?", "ytLlive", + MessageBoxButton.YesNo, MessageBoxImage.Warning); + if (proceed != MessageBoxResult.Yes) return; + } + _socials ??= new SocialsConfig(); + _socials.Entries.Add(new SocialEntry + { + Service = SocialService.YouTube, + Handle = ytHandle, + ProfileUrl = SocialServiceIcons.CanonicalUrlFor(SocialService.YouTube, ytHandle), + }); + } + + // Step 2: Another social — URL or handle string, auto-detect the service. + if (!CanAddMoreSocials) + { + NotifySocialsChanged(); return; } - var handle = ShowInputDialog($"Your {service} handle or profile URL:", "Add Social — Step 2 of 2", "@yourhandle"); - if (string.IsNullOrWhiteSpace(handle)) return; + var input = ShowInputDialog( + "Enter a URL or handle for another social:\n" + + "e.g. https://x.com/yourname or @you@instance.tube", + "Add Social — Step 2 of 2", ""); + if (string.IsNullOrWhiteSpace(input)) { NotifySocialsChanged(); return; } + var (service, handle, url) = SocialServiceIcons.DetectService(input); var result = _socialValidator.LookupAsync(service, handle).GetAwaiter().GetResult(); if (!result.Success) { - MessageBox.Show(result.Error, "ytLlive", MessageBoxButton.OK, MessageBoxImage.Warning); - return; + var proceed = MessageBox.Show( + $"{result.Error}\n\nAdd anyway?", "ytLlive", + MessageBoxButton.YesNo, MessageBoxImage.Warning); + if (proceed != MessageBoxResult.Yes) { NotifySocialsChanged(); return; } } _socials ??= new SocialsConfig(); _socials.Entries.Add(new SocialEntry { Service = service, - Handle = result.Handle, - ProfileUrl = result.ProfileUrl, + Handle = handle, + ProfileUrl = result.Success ? result.ProfileUrl : url, }); + NotifySocialsChanged(); + } + + private void NotifySocialsChanged() + { OnPropertyChanged(nameof(HasSocials)); OnPropertyChanged(nameof(CanToggleSceneSocialBar)); OnPropertyChanged(nameof(CanAddMoreSocials));