Skip to content

Commit

Permalink
Merge pull request #9 from Toine-db/full_example
Browse files Browse the repository at this point in the history
placeholder support and full example pages
  • Loading branch information
Toine-db authored Feb 6, 2025
2 parents a059168 + 88d7140 commit 84f06ef
Show file tree
Hide file tree
Showing 31 changed files with 1,480 additions and 325 deletions.
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

- Easy to use
- Highly customizable
- :fire: Hot Reload support :fire:
- For local and remote use cases
- Hot Reload support :fire:
- IgnoreSafeArea support
- Massively scalable
- Fully works with the default MAUI UI rendering
- No hacking or other fragile mechanisms
Expand Down Expand Up @@ -115,7 +116,28 @@ This plugin has been created with only structured object-oriented code, without
For customizability, against the principles of some developers, almost all methods are virtual and ready to be overridden. Use this power wisely, you may break this system but the freedom it gives to make it your own is endless :smirk:.

### Use UI Hot-Reload on multiple platforms simultaneously
- TODO:
For UI Development purposes a dedicated dummy sample iOS project has been created to be used in a ‘multi startup projects’ scenario. This can be used to simultaneously use UI Hot-Reload on multiple platforms at the same time, that is iOS + a second desired platform.

Because this scenario requires everything to be build for all desired platforms its recommended to manually build everything before starting a debug session with UI Hot-Reload.
1. Set ‘Plugin.Maui.MarkdownView.Sample’ as startup project
2. Set target to some iOS simulator
3. Build
4. Set target to some Android emulator or Windows device
5. Build
6. Set ‘Plugin.Maui.MarkdownView.Sample.iOS’ as startup project
7. Set target to some iOS simulator
8. Build
9. Start ‘Configure startup projects’
1. pick ‘Multiple startup projects’
2. set MarkdownView to action ‘None’
3. set MarkdownView.Sample to action ‘Start’ and target to an Android emulator or Windows device
4. set MarkdownView.Sample.iOS to action ‘Start’ and target to an iOS simulator
10. Start debug session

When you loose Hot-Reload for one of the platforms, do the following:
1. Temporarily set ‘Plugin.Maui.MarkdownView.Sample’ as startup project and shortly start a debug session.
2. Set the ‘Plugin.Maui.MarkdownView.Sample’ to previous target
3. Start ‘Configure startup projects’ and set the Multiple startup projects settings as before

## Side Notes
- Italic and Bold/Strong do not work on iOS with most custom fonts like OpenSans. Reset the Font with `<Setter Property="FontFamily" Value="" />` will force to use the platform designated Font. So check if your font is supported on iOS for using italic and bold if you wan to use it.
Expand Down
1 change: 1 addition & 0 deletions samples/Plugin.Maui.MarkdownView.Sample/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles/MarkdownStyles.xaml" />
<ResourceDictionary Source="Resources/Styles/FullMarkdownExampleStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Expand Down
10 changes: 10 additions & 0 deletions samples/Plugin.Maui.MarkdownView.Sample/AppShell.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,15 @@
<ShellContent ContentTemplate="{DataTemplate pages:MarkdownFromRemotePage}" />
</Tab>
</FlyoutItem>
<FlyoutItem Title="Full Example">
<FlyoutItem.FlyoutIcon>
<FontImageSource
FontFamily="FontAwesomeSolid"
Glyph="&#xe682;" />
</FlyoutItem.FlyoutIcon>
<Tab>
<ShellContent ContentTemplate="{DataTemplate pages:MarkdownFullExamplePage}" />
</Tab>
</FlyoutItem>

</Shell>
6 changes: 5 additions & 1 deletion samples/Plugin.Maui.MarkdownView.Sample/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Plugin.Maui.MarkdownView.Sample;
using Plugin.Maui.MarkdownView.Sample.Pages;

namespace Plugin.Maui.MarkdownView.Sample;

public partial class AppShell : Shell
{
Expand All @@ -7,6 +9,8 @@ public partial class AppShell : Shell
public AppShell()
{
InitializeComponent();

Routing.RegisterRoute(nameof(MarkdownFullExampleDetailPage), typeof(MarkdownFullExampleDetailPage));
}

protected override void OnAppearing()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
using MarkdownParser.Models.Segments.Indicators;
using MarkdownParser.Models.Segments;
using MarkdownParser.Models;
using Plugin.Maui.MarkdownView.Sample.Views;
using Plugin.Maui.MarkdownView.ViewSuppliers;

namespace Plugin.Maui.MarkdownView.Sample.CustomViewSuppliers;

public class MyViewSupplier : MauiFormattedTextViewSupplier
{
public SupplierExtraStyles? ExtraStyles { get; set; }

public Func<string?, Task>? OnMenuItemTapped { get; set; }

public override View? CreateHeaderView(TextBlock textBlock, int headerLevel)
{
var header = base.CreateHeaderView(textBlock, headerLevel);
if (headerLevel is 1 or 2)
{
var bottomLine = new BoxView
{
Style = ExtraStyles?.MenuItemSeparatorStyle
};

var gridLayout = new Grid
{
RowDefinitions =
[
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = GridLength.Star }
],
IgnoreSafeArea = IgnoreSafeArea,
Style = headerLevel switch
{
1 => ExtraStyles?.Header1LayoutViewStyle,
2 => ExtraStyles?.Header2LayoutViewStyle
}
};

gridLayout.Add(header);
Grid.SetRow(header, 0);

gridLayout.Add(bottomLine);
Grid.SetRow(bottomLine, 1);

return gridLayout;
}

return header;
}

public override View? CreateTextView(TextBlock textBlock)
{
var textViewStyle = GetTextBlockStyleFor(textBlock);
var spanStyle = GetSpanStyleFor(textBlock);

var collectedRootViews = new List<View?>();
var textSpansCache = new List<Span>();

var activeIndicators = new List<SegmentIndicator>();
LinkSegment? activeLinkSegment = null;

var isCreatingPlaceholderMenu = false;
MenuItemView? placeholderMenuItemCache = null;

foreach (var segment in textBlock.TextSegments)
{
// Placeholders like [my placeholder] can be one of two types:
// 1. Inline placeholder like a (hyper)link => this is handled by the default span and label creation
// 2. Designated Placeholder to be a custom view => this is handled by the TryCreateDesignatedPlaceholderView method
if (segment is PlaceholderSegment placeholderSegment
&& !DoesReferenceDefinitionsContainPlaceholder(placeholderSegment.PlaceholderId))
{
if (placeholderSegment.PlaceholderId == "MENU|START")
{
isCreatingPlaceholderMenu = true;
FlushSpansToRootViews(ref textSpansCache, ref collectedRootViews, textViewStyle);

continue;
}

if (placeholderSegment.PlaceholderId == "MENU|END")
{
isCreatingPlaceholderMenu = false;

continue;
}

FlushSpansToRootViews(ref textSpansCache, ref collectedRootViews, textViewStyle);

// add designated Placeholder between to TextViews
var designatedPlaceholderView = CreateDesignatedPlaceholderView(placeholderSegment);
collectedRootViews.Add(designatedPlaceholderView);

continue;
}

if (isCreatingPlaceholderMenu)
{
if (segment is LinkSegment linkSegment)
{
if (linkSegment.IndicatorPosition == SegmentIndicatorPosition.Start)
{
placeholderMenuItemCache = new MenuItemView
{
Url = linkSegment.Url,
Icon = linkSegment.Title,
IgnoreSafeArea = IgnoreSafeArea,
Style = ExtraStyles?.MenuItemViewStyle,
LabelStyle = ExtraStyles?.MenuItemLabelStyle,
SeparatorStyle = ExtraStyles?.MenuItemSeparatorStyle,
OnItemTapped = async url =>
{
if (OnMenuItemTapped != null)
{
await OnMenuItemTapped.Invoke(url);
}
}
};
}

if (linkSegment.IndicatorPosition == SegmentIndicatorPosition.End)
{
collectedRootViews.Add(placeholderMenuItemCache);
placeholderMenuItemCache = null;
}
}

if (segment.HasLiteralContent
&& placeholderMenuItemCache != null)
{
placeholderMenuItemCache.Text = segment.ToString();
}

continue;
}

// Special segments like indicators
if (segment is IndicatorSegment indicatorSegment)
{
switch (indicatorSegment.Indicator)
{
case SegmentIndicator.Strong:
case SegmentIndicator.Italic:
case SegmentIndicator.Strikethrough:
case SegmentIndicator.Code:
case SegmentIndicator.Link:
if (indicatorSegment.IndicatorPosition == SegmentIndicatorPosition.Start)
{
activeIndicators.Add(indicatorSegment.Indicator);
}
else
{
activeIndicators.RemoveAll(x => x == indicatorSegment.Indicator);
}

if (indicatorSegment.Indicator == SegmentIndicator.Link)
{
activeLinkSegment = indicatorSegment as LinkSegment;
}

break;
case SegmentIndicator.LineBreak:
textSpansCache.Add(new Span { Text = Environment.NewLine });
break;
}
}

// Segments with text
if (segment.HasLiteralContent)
{
var span = CreateSpan(segment, activeIndicators, activeLinkSegment, spanStyle);
AddFormattingToSpan(span, activeIndicators);

textSpansCache.Add(span);
}
}

FlushSpansToRootViews(ref textSpansCache, ref collectedRootViews, textViewStyle);

var flattenedView = FlattenToSingleView(collectedRootViews);
return flattenedView;
}

protected override View? CreateDesignatedPlaceholderView(PlaceholderSegment placeholderSegment)
{
var placeholderParts = placeholderSegment.Title.Split('|');
if (placeholderParts.Any()
&& placeholderParts.First().Equals("audio", StringComparison.OrdinalIgnoreCase))
{
if (placeholderParts.Length > 1)
{
var audioSource = placeholderParts[1];
var button = new AudioButton
{
Text = $"Play {audioSource}",
AudioFileName = audioSource,
ButtonStyle = ExtraStyles?.ButtonViewStyle
};

return button;
}
}

return base.CreateDesignatedPlaceholderView(placeholderSegment);
}

private void FlushSpansToRootViews(ref List<Span> spans, ref List<View?> rootViewSink, Style? textViewStyle)
{
var flushedFormattedLabel = TryCreateFormattedLabel(spans, textViewStyle);
rootViewSink.Add(flushedFormattedLabel);
spans.Clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Plugin.Maui.MarkdownView.Sample.CustomViewSuppliers;

public class SupplierExtraStyles
{
/// <summary>
/// Button style for button with image
/// </summary>
public Style? ButtonViewStyle { get; set; }

/// <summary>
/// MenuItemView (Grid) style for menu items
/// </summary>
public Style? MenuItemViewStyle { get; set; }

/// <summary>
/// Label style for menu items
/// </summary>
public Style? MenuItemLabelStyle { get; set; }

/// <summary>
/// Boxview style for separator in menu items
/// </summary>
public Style? MenuItemSeparatorStyle { get; set; }

/// <summary>
/// Header1 (Grid) style
/// </summary>
public Style? Header1LayoutViewStyle { get; set; }

/// <summary>
/// Header2 (Grid) style
/// </summary>
public Style? Header2LayoutViewStyle { get; set; }
}
Loading

0 comments on commit 84f06ef

Please sign in to comment.