Compare commits

...

9 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 32f93a670e Fix link text extraction in MarkdownToInlinesConverter to handle formatted inline children
- Add GetPlainText helper that recursively collects text from all inline children
  (LiteralInline for leaf text, ContainerInline recurses into children)
- Use GetPlainText(link) instead of .OfType<LiteralInline>() so bold/italic
  labels like [**text**](url) are rendered correctly
- Fix BulletType: ListBlock.BulletType is a char in Markdig, not an enum;
  remove the incorrect switch expression and use the char directly

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 18:20:58 +00:00
Oleksii Holub 752263a4da Update DiscordChatExporter.Gui/Converters/MarkdownToInlinesConverter.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-24 20:07:27 +02:00
copilot-swe-agent[bot] 910b1d9334 Rename ErrorPullingServersTitle to ErrorPullingGuildsTitle for naming consistency
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 17:54:59 +00:00
copilot-swe-agent[bot] 81fd5001b7 Use dash lists in OutputPathTooltip; add curly braces to HyperLink else-if
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 17:45:13 +00:00
copilot-swe-agent[bot] 2efddb3b2c Inline local functions; simplify URL handling; localize OutputPathTooltip
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 17:13:16 +00:00
Oleksii Holub 2b20bf97e3 Apply suggestion from @Tyrrrz 2026-02-24 18:55:23 +02:00
copilot-swe-agent[bot] dbc16f9f63 Render HyperLink for all links; allow any valid URI scheme
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 16:38:24 +00:00
copilot-swe-agent[bot] 3daf49cb52 Add Url property to HyperLink; simplify converter link handling; add http/https scheme guard
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 15:58:26 +00:00
copilot-swe-agent[bot] f6ded3fc8c Add markdown link support; fold bot instructions into markdown; set TextBlock properties directly
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
2026-02-24 10:20:25 +00:00
11 changed files with 240 additions and 177 deletions
@@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Data.Converters;
using Avalonia.Media;
using DiscordChatExporter.Gui.Views.Controls;
using Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -19,6 +20,14 @@ public class MarkdownToInlinesConverter : IValueConverter
.UseEmphasisExtras()
.Build();
private static string GetPlainText(MarkdownInline inline) =>
inline switch
{
LiteralInline literal => literal.Content.ToString(),
ContainerInline container => string.Concat(container.Select(GetPlainText)),
_ => string.Empty,
};
private static void ProcessInline(
InlineCollection inlines,
MarkdownInline markdownInline,
@@ -78,6 +87,17 @@ public class MarkdownToInlinesConverter : IValueConverter
break;
}
case LinkInline link:
{
inlines.Add(
new InlineUIContainer(
new HyperLink { Text = GetPlainText(link), Url = link.Url }
)
);
break;
}
case ContainerInline container:
{
foreach (var child in container)
@@ -96,55 +116,52 @@ public class MarkdownToInlinesConverter : IValueConverter
var isFirst = true;
void RenderParagraph(ParagraphBlock paragraph)
{
if (!isFirst)
{
// Insert a blank line between paragraphs
inlines.Add(new LineBreak());
inlines.Add(new LineBreak());
}
isFirst = false;
foreach (var markdownInline in paragraph.Inline!)
ProcessInline(inlines, markdownInline);
}
void RenderListBlock(ListBlock list)
{
var itemOrder = 1;
if (list.IsOrdered && int.TryParse(list.OrderedStart, out var startNum))
itemOrder = startNum;
foreach (var listItem in list.OfType<ListItemBlock>())
{
if (!isFirst)
inlines.Add(new LineBreak());
isFirst = false;
var prefix = list.IsOrdered ? $"{itemOrder++}. " : $"{list.BulletType} ";
inlines.Add(new Run(prefix));
foreach (var subBlock in listItem.OfType<ParagraphBlock>())
{
if (subBlock is { Inline: not null } p)
foreach (var markdownInline in p.Inline)
ProcessInline(inlines, markdownInline);
}
}
}
foreach (var block in Markdown.Parse(text, MarkdownPipeline))
{
switch (block)
{
case ParagraphBlock { Inline: not null } paragraph:
RenderParagraph(paragraph);
{
if (!isFirst)
{
// Insert a blank line between paragraphs
inlines.Add(new LineBreak());
inlines.Add(new LineBreak());
}
isFirst = false;
foreach (var markdownInline in paragraph.Inline!)
ProcessInline(inlines, markdownInline);
break;
}
case ListBlock list:
RenderListBlock(list);
{
var itemOrder = 1;
if (list.IsOrdered && int.TryParse(list.OrderedStart, out var startNum))
itemOrder = startNum;
foreach (var listItem in list.OfType<ListItemBlock>())
{
if (!isFirst)
inlines.Add(new LineBreak());
isFirst = false;
var prefix = list.IsOrdered ? $"{itemOrder++}. " : $"{list.BulletType} ";
inlines.Add(new Run(prefix));
foreach (var subBlock in listItem.OfType<ParagraphBlock>())
{
if (subBlock is { Inline: not null } p)
foreach (var markdownInline in p.Inline)
ProcessInline(inlines, markdownInline);
}
}
break;
}
}
}
@@ -30,10 +30,10 @@ public partial class LocalizationManager
""",
// Token instructions (bot)
[nameof(TokenBotHeader)] = "To get the token for your bot:",
[nameof(TokenBotIntro)] =
"The token is generated during bot creation. If you lost it, generate a new one:",
[nameof(TokenBotStep1)] = "1. Open Discord",
[nameof(TokenBotInstructions)] = """
The token is generated during bot creation. If you lost it, generate a new one:
1. Open Discord [developer portal](https://discord.com/developers/applications)
2. Open your application's settings
3. Navigate to the **Bot** section on the left
4. Under **Token** click **Reset Token**
@@ -41,9 +41,8 @@ public partial class LocalizationManager
* Integrations using the previous token will stop working until updated
* Your bot needs to have the **Message Content Intent** enabled to read messages
""",
[nameof(TokenDeveloperPortalLinkText)] = "developer portal",
[nameof(TokenDocumentationLinkText)] = "documentation",
[nameof(TokenHelpText)] = "If you have questions or issues, please refer to the",
[nameof(TokenHelpText)] =
"If you have questions or issues, please refer to the [documentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)",
// Settings
[nameof(SettingsTitle)] = "Settings",
[nameof(ThemeLabel)] = "Theme",
@@ -69,6 +68,26 @@ public partial class LocalizationManager
// Export Setup
[nameof(ChannelsSelectedText)] = "channels selected",
[nameof(OutputPathLabel)] = "Output path",
[nameof(OutputPathTooltip)] = """
Output file or directory path.
If a directory is specified, file names will be generated automatically based on the channel names and export parameters.
Directory paths must end with a slash to avoid ambiguity.
Available template tokens:
- **%g** server ID
- **%G** server name
- **%t** category ID
- **%T** category name
- **%c** channel ID
- **%C** channel name
- **%p** channel position
- **%P** category position
- **%a** after date
- **%b** before date
- **%d** current date
""",
[nameof(FormatLabel)] = "Format",
[nameof(FormatTooltip)] = "Export format",
[nameof(AfterDateLabel)] = "After (date)",
@@ -124,7 +143,7 @@ public partial class LocalizationManager
"Update has been downloaded and will be installed when you exit",
[nameof(UpdateInstallNowButton)] = "INSTALL NOW",
[nameof(UpdateFailedMessage)] = "Failed to perform application update",
[nameof(ErrorPullingServersTitle)] = "Error pulling servers",
[nameof(ErrorPullingGuildsTitle)] = "Error pulling servers",
[nameof(ErrorPullingChannelsTitle)] = "Error pulling channels",
[nameof(ErrorExportingTitle)] = "Error exporting channel(s)",
[nameof(SuccessfulExportMessage)] = "Successfully exported {0} channel(s)",
@@ -32,10 +32,10 @@ public partial class LocalizationManager
""",
// Token instructions (bot)
[nameof(TokenBotHeader)] = "Obtenir le token pour votre bot :",
[nameof(TokenBotIntro)] =
"Le token est généré lors de la création du bot. Si vous l'avez perdu, générez-en un nouveau :",
[nameof(TokenBotStep1)] = "1. Ouvrez Discord",
[nameof(TokenBotInstructions)] = """
Le token est généré lors de la création du bot. Si vous l'avez perdu, générez-en un nouveau :
1. Ouvrez Discord [portail développeur](https://discord.com/developers/applications)
2. Ouvrez les paramètres de votre application
3. Naviguez vers la section **Bot** à gauche
4. Sous **Token**, cliquez sur **Reset Token**
@@ -43,9 +43,8 @@ public partial class LocalizationManager
* Les intégrations utilisant l'ancien token cesseront de fonctionner jusqu'à leur mise à jour
* Votre bot doit avoir l'option **Message Content Intent** activée pour lire les messages
""",
[nameof(TokenDeveloperPortalLinkText)] = "portail développeur",
[nameof(TokenDocumentationLinkText)] = "documentation",
[nameof(TokenHelpText)] = "Pour les questions ou problèmes, veuillez consulter la",
[nameof(TokenHelpText)] =
"Pour les questions ou problèmes, veuillez consulter la [documentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)",
// Settings
[nameof(SettingsTitle)] = "Paramètres",
[nameof(ThemeLabel)] = "Thème",
@@ -71,6 +70,26 @@ public partial class LocalizationManager
// Export Setup
[nameof(ChannelsSelectedText)] = "canaux sélectionnés",
[nameof(OutputPathLabel)] = "Chemin de sortie",
[nameof(OutputPathTooltip)] = """
Chemin du fichier ou répertoire de sortie.
Si un répertoire est spécifié, les noms de fichiers seront générés automatiquement en fonction des noms de canaux et des paramètres d'exportation.
Les chemins de répertoire doivent se terminer par un slash pour éviter toute ambiguïté.
Jetons de modèle disponibles :
- **%g** ID du serveur
- **%G** nom du serveur
- **%t** ID de la catégorie
- **%T** nom de la catégorie
- **%c** ID du canal
- **%C** nom du canal
- **%p** position du canal
- **%P** position de la catégorie
- **%a** date après
- **%b** date avant
- **%d** date actuelle
""",
[nameof(FormatLabel)] = "Format",
[nameof(FormatTooltip)] = "Format d'exportation",
[nameof(AfterDateLabel)] = "Après (date)",
@@ -126,7 +145,7 @@ public partial class LocalizationManager
"La mise à jour a été téléchargée et sera installée à la fermeture",
[nameof(UpdateInstallNowButton)] = "INSTALLER MAINTENANT",
[nameof(UpdateFailedMessage)] = "Échec de la mise à jour de l'application",
[nameof(ErrorPullingServersTitle)] = "Erreur lors du chargement des serveurs",
[nameof(ErrorPullingGuildsTitle)] = "Erreur lors du chargement des serveurs",
[nameof(ErrorPullingChannelsTitle)] = "Erreur lors du chargement des canaux",
[nameof(ErrorExportingTitle)] = "Erreur lors de l'exportation des canaux",
[nameof(SuccessfulExportMessage)] = "{0} canal(-aux) exporté(s) avec succès",
@@ -32,10 +32,10 @@ public partial class LocalizationManager
""",
// Token instructions (bot)
[nameof(TokenBotHeader)] = "Token für Ihren Bot abrufen:",
[nameof(TokenBotIntro)] =
"Der Token wird bei der Bot-Erstellung generiert. Falls er verloren gegangen ist, generieren Sie einen neuen:",
[nameof(TokenBotStep1)] = "1. Öffnen Sie Discord",
[nameof(TokenBotInstructions)] = """
Der Token wird bei der Bot-Erstellung generiert. Falls er verloren gegangen ist, generieren Sie einen neuen:
1. Öffnen Sie Discord [Entwicklerportal](https://discord.com/developers/applications)
2. Öffnen Sie die Einstellungen Ihrer Anwendung
3. Navigieren Sie zum Abschnitt **Bot** auf der linken Seite
4. Klicken Sie unter **Token** auf **Reset Token**
@@ -43,9 +43,8 @@ public partial class LocalizationManager
* Integrationen, die den alten Token verwenden, hören auf zu funktionieren, bis sie aktualisiert werden
* Ihr Bot benötigt die aktivierte **Message Content Intent**, um Nachrichten zu lesen
""",
[nameof(TokenDeveloperPortalLinkText)] = "Entwicklerportal",
[nameof(TokenDocumentationLinkText)] = "Dokumentation",
[nameof(TokenHelpText)] = "Bei Fragen oder Problemen lesen Sie die",
[nameof(TokenHelpText)] =
"Bei Fragen oder Problemen lesen Sie die [Dokumentation](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)",
// Settings
[nameof(SettingsTitle)] = "Einstellungen",
[nameof(ThemeLabel)] = "Design",
@@ -71,6 +70,26 @@ public partial class LocalizationManager
// Export Setup
[nameof(ChannelsSelectedText)] = "Kanäle ausgewählt",
[nameof(OutputPathLabel)] = "Ausgabepfad",
[nameof(OutputPathTooltip)] = """
Ausgabedatei- oder Verzeichnispfad.
Wenn ein Verzeichnis angegeben wird, werden Dateinamen automatisch basierend auf den Kanalnamen und Exportparametern generiert.
Verzeichnispfade müssen mit einem Schrägstrich enden, um Mehrdeutigkeiten zu vermeiden.
Verfügbare Vorlagen-Token:
- **%g** Server-ID
- **%G** Servername
- **%t** Kategorie-ID
- **%T** Kategoriename
- **%c** Kanal-ID
- **%C** Kanalname
- **%p** Kanalposition
- **%P** Kategorieposition
- **%a** Datum ab
- **%b** Datum bis
- **%d** aktuelles Datum
""",
[nameof(FormatLabel)] = "Format",
[nameof(FormatTooltip)] = "Exportformat",
[nameof(AfterDateLabel)] = "Nach (Datum)",
@@ -130,7 +149,7 @@ public partial class LocalizationManager
"Update wurde heruntergeladen und wird beim Beenden installiert",
[nameof(UpdateInstallNowButton)] = "JETZT INSTALLIEREN",
[nameof(UpdateFailedMessage)] = "Anwendungsupdate konnte nicht durchgeführt werden",
[nameof(ErrorPullingServersTitle)] = "Fehler beim Laden der Server",
[nameof(ErrorPullingGuildsTitle)] = "Fehler beim Laden der Server",
[nameof(ErrorPullingChannelsTitle)] = "Fehler beim Laden der Kanäle",
[nameof(ErrorExportingTitle)] = "Fehler beim Exportieren der Kanäle",
[nameof(SuccessfulExportMessage)] = "{0} Kanal/-äle erfolgreich exportiert",
@@ -30,10 +30,10 @@ public partial class LocalizationManager
""",
// Token instructions (bot)
[nameof(TokenBotHeader)] = "Cómo obtener el token para tu bot:",
[nameof(TokenBotIntro)] =
"El token se genera al crear el bot. Si lo perdiste, genera uno nuevo:",
[nameof(TokenBotStep1)] = "1. Abre Discord",
[nameof(TokenBotInstructions)] = """
El token se genera al crear el bot. Si lo perdiste, genera uno nuevo:
1. Abre Discord [portal de desarrolladores](https://discord.com/developers/applications)
2. Abre la configuración de tu aplicación
3. Navega a la sección **Bot** en el lado izquierdo
4. En **Token**, haz clic en **Reset Token**
@@ -41,9 +41,8 @@ public partial class LocalizationManager
* Las integraciones que usen el token anterior dejarán de funcionar hasta que se actualicen
* Tu bot necesita tener habilitado **Message Content Intent** para leer mensajes
""",
[nameof(TokenDeveloperPortalLinkText)] = "portal de desarrolladores",
[nameof(TokenDocumentationLinkText)] = "documentación",
[nameof(TokenHelpText)] = "Si tienes preguntas o problemas, consulta la",
[nameof(TokenHelpText)] =
"Si tienes preguntas o problemas, consulta la [documentación](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)",
// Settings
[nameof(SettingsTitle)] = "Ajustes",
[nameof(ThemeLabel)] = "Tema",
@@ -69,6 +68,26 @@ public partial class LocalizationManager
// Export Setup
[nameof(ChannelsSelectedText)] = "canales seleccionados",
[nameof(OutputPathLabel)] = "Ruta de salida",
[nameof(OutputPathTooltip)] = """
Ruta del archivo o directorio de salida.
Si se especifica un directorio, los nombres de archivo se generarán automáticamente según los nombres de los canales y los parámetros de exportación.
Las rutas de directorio deben terminar con una barra diagonal para evitar ambigüedades.
Tokens de plantilla disponibles:
- **%g** ID del servidor
- **%G** nombre del servidor
- **%t** ID de categoría
- **%T** nombre de categoría
- **%c** ID del canal
- **%C** nombre del canal
- **%p** posición del canal
- **%P** posición de la categoría
- **%a** fecha desde
- **%b** fecha hasta
- **%d** fecha actual
""",
[nameof(FormatLabel)] = "Formato",
[nameof(FormatTooltip)] = "Formato de exportación",
[nameof(AfterDateLabel)] = "Después (fecha)",
@@ -124,7 +143,7 @@ public partial class LocalizationManager
"La actualización se ha descargado y se instalará al salir",
[nameof(UpdateInstallNowButton)] = "INSTALAR AHORA",
[nameof(UpdateFailedMessage)] = "Error al realizar la actualización de la aplicación",
[nameof(ErrorPullingServersTitle)] = "Error al cargar servidores",
[nameof(ErrorPullingGuildsTitle)] = "Error al cargar servidores",
[nameof(ErrorPullingChannelsTitle)] = "Error al cargar canales",
[nameof(ErrorExportingTitle)] = "Error al exportar canal(es)",
[nameof(SuccessfulExportMessage)] = "{0} canal(es) exportado(s) con éxito",
@@ -30,10 +30,10 @@ public partial class LocalizationManager
""",
// Token instructions (bot)
[nameof(TokenBotHeader)] = "Як отримати токен для бота:",
[nameof(TokenBotIntro)] =
"Токен генерується під час створення бота. Якщо ви його втратили, згенеруйте новий:",
[nameof(TokenBotStep1)] = "1. Відкрийте Discord",
[nameof(TokenBotInstructions)] = """
Токен генерується під час створення бота. Якщо ви його втратили, згенеруйте новий:
1. Відкрийте Discord [портал розробника](https://discord.com/developers/applications)
2. Відкрийте налаштування вашого застосунку
3. Перейдіть до розділу **Bot** ліворуч
4. В розділі **Token** натисніть **Reset Token**
@@ -41,9 +41,8 @@ public partial class LocalizationManager
* Інтеграції, що використовують попередній токен, перестануть працювати
* Ваш бот повинен мати включений **Message Content Intent** для читання повідомлень
""",
[nameof(TokenDeveloperPortalLinkText)] = "портал розробника",
[nameof(TokenDocumentationLinkText)] = "документацію",
[nameof(TokenHelpText)] = "Якщо у вас є запитання або проблеми, зверніться до",
[nameof(TokenHelpText)] =
"Якщо у вас є запитання або проблеми, зверніться до [документації](https://github.com/Tyrrrz/DiscordChatExporter/tree/master/.docs)",
// Settings
[nameof(SettingsTitle)] = "Налаштування",
[nameof(ThemeLabel)] = "Тема",
@@ -69,6 +68,26 @@ public partial class LocalizationManager
// Export Setup
[nameof(ChannelsSelectedText)] = "каналів вибрано",
[nameof(OutputPathLabel)] = "Шлях збереження",
[nameof(OutputPathTooltip)] = """
Шлях до файлу або директорії виводу.
Якщо вказано директорію, імена файлів генеруватимуться автоматично на основі назв каналів та параметрів експорту.
Шляхи до директорій повинні закінчуватись слешем для уникнення неоднозначності.
Доступні шаблонні токени:
- **%g** ID сервера
- **%G** назва сервера
- **%t** ID категорії
- **%T** назва категорії
- **%c** ID каналу
- **%C** назва каналу
- **%p** позиція каналу
- **%P** позиція категорії
- **%a** дата після
- **%b** дата до
- **%d** поточна дата
""",
[nameof(FormatLabel)] = "Формат",
[nameof(FormatTooltip)] = "Формат експорту",
[nameof(AfterDateLabel)] = "Після (дата)",
@@ -123,7 +142,7 @@ public partial class LocalizationManager
[nameof(UpdateReadyMessage)] = "Оновлення завантажено та буде встановлено після виходу",
[nameof(UpdateInstallNowButton)] = "ВСТАНОВИТИ ЗАРАЗ",
[nameof(UpdateFailedMessage)] = "Не вдалося виконати оновлення програми",
[nameof(ErrorPullingServersTitle)] = "Помилка завантаження серверів",
[nameof(ErrorPullingGuildsTitle)] = "Помилка завантаження серверів",
[nameof(ErrorPullingChannelsTitle)] = "Помилка завантаження каналів",
[nameof(ErrorExportingTitle)] = "Помилка експорту каналу(-ів)",
[nameof(SuccessfulExportMessage)] = "Успішно експортовано {0} канал(-ів)",
@@ -91,11 +91,7 @@ public partial class LocalizationManager
// Token instructions (bot)
public string TokenBotHeader => Get();
public string TokenBotIntro => Get();
public string TokenBotStep1 => Get();
public string TokenBotInstructions => Get();
public string TokenDeveloperPortalLinkText => Get();
public string TokenDocumentationLinkText => Get();
public string TokenHelpText => Get();
// ---- Settings ----
@@ -124,6 +120,7 @@ public partial class LocalizationManager
public string ChannelsSelectedText => Get();
public string OutputPathLabel => Get();
public string OutputPathTooltip => Get();
public string FormatLabel => Get();
public string FormatTooltip => Get();
public string AfterDateLabel => Get();
@@ -166,7 +163,7 @@ public partial class LocalizationManager
public string UpdateReadyMessage => Get();
public string UpdateInstallNowButton => Get();
public string UpdateFailedMessage => Get();
public string ErrorPullingServersTitle => Get();
public string ErrorPullingGuildsTitle => Get();
public string ErrorPullingChannelsTitle => Get();
public string ErrorExportingTitle => Get();
public string SuccessfulExportMessage => Get();
@@ -107,9 +107,6 @@ public partial class DashboardViewModel : ViewModelBase
private async Task ShowSettingsAsync() =>
await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel());
[RelayCommand]
private void ShowHelp() => Process.StartShellExecute(Program.ProjectDocumentationUrl);
private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token);
[RelayCommand(CanExecute = nameof(CanPullGuilds))]
@@ -146,7 +143,7 @@ public partial class DashboardViewModel : ViewModelBase
catch (Exception ex)
{
var dialog = _viewModelManager.CreateMessageBoxViewModel(
LocalizationManager.ErrorPullingServersTitle,
LocalizationManager.ErrorPullingGuildsTitle,
ex.ToString()
);
@@ -330,13 +327,6 @@ public partial class DashboardViewModel : ViewModelBase
}
}
[RelayCommand]
private void OpenDiscord() => Process.StartShellExecute("https://discord.com/app");
[RelayCommand]
private void OpenDiscordDeveloperPortal() =>
Process.StartShellExecute("https://discord.com/developers/applications");
protected override void Dispose(bool disposing)
{
if (disposing)
@@ -4,7 +4,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:components="clr-namespace:DiscordChatExporter.Gui.ViewModels.Components"
xmlns:controls="clr-namespace:DiscordChatExporter.Gui.Views.Controls"
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:materialStyles="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
@@ -220,14 +219,6 @@
<Panel IsVisible="{Binding !AvailableGuilds.Count}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="32,16" Spacing="0">
<StackPanel.Styles>
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="Light" />
<Setter Property="LineHeight" Value="23" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
</StackPanel.Styles>
<!-- User token -->
<TextBlock>
@@ -246,9 +237,17 @@
Text="{Binding LocalizationManager.TokenPersonalHeader}" />
</TextBlock>
<TextBlock Inlines="{Binding LocalizationManager.TokenPersonalTosWarning, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}" />
<TextBlock Inlines="{Binding LocalizationManager.TokenPersonalTosWarning, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}"
FontSize="14"
FontWeight="Light"
LineHeight="23"
TextWrapping="Wrap" />
<TextBlock Inlines="{Binding LocalizationManager.TokenPersonalInstructions, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}" />
<TextBlock Inlines="{Binding LocalizationManager.TokenPersonalInstructions, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}"
FontSize="14"
FontWeight="Light"
LineHeight="23"
TextWrapping="Wrap" />
<!-- Bot token -->
<TextBlock Margin="0,12,0,0">
@@ -267,19 +266,18 @@
Text="{Binding LocalizationManager.TokenBotHeader}" />
</TextBlock>
<TextBlock Text="{Binding LocalizationManager.TokenBotIntro}" />
<TextBlock Inlines="{Binding LocalizationManager.TokenBotInstructions, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}"
FontSize="14"
FontWeight="Light"
LineHeight="23"
TextWrapping="Wrap" />
<TextBlock>
<Run Text="{Binding LocalizationManager.TokenBotStep1}" />
<controls:HyperLink Command="{Binding OpenDiscordDeveloperPortalCommand}" Text="{Binding LocalizationManager.TokenDeveloperPortalLinkText}" />
</TextBlock>
<TextBlock Inlines="{Binding LocalizationManager.TokenBotInstructions, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}" />
<TextBlock Margin="0,12,0,0">
<Run Text="{Binding LocalizationManager.TokenHelpText}" />
<controls:HyperLink Command="{Binding ShowHelpCommand}" Text="{Binding LocalizationManager.TokenDocumentationLinkText}" />
</TextBlock>
<TextBlock Margin="0,12,0,0"
FontSize="14"
FontWeight="Light"
LineHeight="23"
TextWrapping="Wrap"
Inlines="{Binding LocalizationManager.TokenHelpText, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}" />
</StackPanel>
</ScrollViewer>
</Panel>
@@ -1,7 +1,9 @@
using System.Windows.Input;
using System.Diagnostics;
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using DiscordChatExporter.Gui.Utils.Extensions;
namespace DiscordChatExporter.Gui.Views.Controls;
@@ -16,6 +18,12 @@ public partial class HyperLink : UserControl
public static readonly StyledProperty<object?> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<HyperLink>();
// If Url is set and Command is not set, clicking will open this URL in the default browser.
public static readonly StyledProperty<string?> UrlProperty = AvaloniaProperty.Register<
HyperLink,
string?
>(nameof(Url));
public HyperLink() => InitializeComponent();
public string? Text
@@ -36,14 +44,22 @@ public partial class HyperLink : UserControl
set => SetValue(CommandParameterProperty, value);
}
public string? Url
{
get => GetValue(UrlProperty);
set => SetValue(UrlProperty, value);
}
private void TextBlock_OnPointerReleased(object? sender, PointerReleasedEventArgs args)
{
if (Command is null)
return;
if (!Command.CanExecute(CommandParameter))
return;
Command.Execute(CommandParameter);
if (Command is not null)
{
if (Command.CanExecute(CommandParameter))
Command.Execute(CommandParameter);
}
else if (!string.IsNullOrWhiteSpace(Url))
{
Process.StartShellExecute(Url);
}
}
}
@@ -73,60 +73,10 @@
Text="{Binding OutputPath}"
Theme="{DynamicResource FilledTextBox}">
<ToolTip.Tip>
<TextBlock>
<Run Text="Output file or directory path." />
<LineBreak />
<Run Text="If a directory is specified, file names will be generated automatically based on the channel names and export parameters." />
<LineBreak />
<Run Text="Directory paths must end with a slash to avoid ambiguity." />
<LineBreak />
<LineBreak />
<Run Text="Available template tokens:" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%g" />
<Run Text="— server ID" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%G" />
<Run Text="— server name" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%t" />
<Run Text="— category ID" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%T" />
<Run Text="— category name" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%c" />
<Run Text="— channel ID" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%C" />
<Run Text="— channel name" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%p" />
<Run Text="— channel position" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%P" />
<Run Text="— category position" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%a" />
<Run Text="— after date" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%b" />
<Run Text="— before date" />
<LineBreak />
<Run Text=" " />
<Run FontWeight="SemiBold" Text="%d" />
<Run Text="— current date" />
</TextBlock>
<TextBlock
MaxWidth="400"
TextWrapping="Wrap"
Inlines="{Binding LocalizationManager.OutputPathTooltip, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}" />
</ToolTip.Tip>
<TextBox.InnerRightContent>
<Button