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));