adjust UI

This commit is contained in:
Tsanie 2021-08-10 17:17:32 +08:00
parent f8850073cd
commit 6507f7cadf
100 changed files with 3138 additions and 963 deletions

View File

@ -6,6 +6,7 @@ using Gallery.Util;
using Gallery.Resources.Theme; using Gallery.Resources.Theme;
using System.Collections.Generic; using System.Collections.Generic;
using Gallery.Util.Interface; using Gallery.Util.Interface;
using Gallery.Resources.UI;
namespace Gallery namespace Gallery
{ {
@ -26,12 +27,15 @@ namespace Gallery
Preferences.Set(Config.IsProxiedKey, true); Preferences.Set(Config.IsProxiedKey, true);
Preferences.Set(Config.ProxyHostKey, "192.168.25.9"); Preferences.Set(Config.ProxyHostKey, "192.168.25.9");
Preferences.Set(Config.ProxyPortKey, 1081); Preferences.Set(Config.ProxyPortKey, 1081);
DependencyService.Register<MockDataStore>();
} }
private void InitResource() private void InitResource()
{ {
foreach (var source in GallerySources)
{
source.InitDynamicResources(Definition.IconSolidFamily, LightTheme.Instance, DarkTheme.Instance);
}
var theme = AppInfo.RequestedTheme; var theme = AppInfo.RequestedTheme;
SetTheme(theme, true); SetTheme(theme, true);
} }

View File

@ -1,62 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms" <Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Gallery.Views" xmlns:local="clr-namespace:Gallery"
Title="Gallery" xmlns:r="clr-namespace:Gallery.Resources"
x:Class="Gallery.AppShell"> xmlns:ui="clr-namespace:Gallery.Resources.UI"
xmlns:util="clr-namespace:Gallery.Util;assembly=Gallery.Util"
<!-- x:Class="Gallery.AppShell"
The overall app visual hierarchy is defined here, along with navigation. x:Name="appShell"
BackgroundColor="{DynamicResource NavigationColor}"
https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/ FlyoutBackgroundColor="{DynamicResource WindowColor}"
--> x:DataType="{x:Type local:AppShell}"
BindingContext="{x:Reference appShell}">
<Shell.Resources> <Shell.Resources>
<ResourceDictionary> <ResourceDictionary>
<Style x:Key="BaseStyle" TargetType="Element"> <Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{DynamicResource Primary}" /> <Setter Property="Shell.BackgroundColor" Value="{DynamicResource NavigationColor}" />
<Setter Property="Shell.ForegroundColor" Value="White" /> <Setter Property="Shell.ForegroundColor" Value="{DynamicResource TintColor}" />
<Setter Property="Shell.TitleColor" Value="White" /> <Setter Property="Shell.TitleColor" Value="{DynamicResource TextColor}" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" /> <Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" /> <Setter Property="Shell.UnselectedColor" Value="{DynamicResource TintColor}" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource Primary}" /> <Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource NavigationColor}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/> <Setter Property="Shell.TabBarForegroundColor" Value="{DynamicResource TintColor}"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/> <Setter Property="Shell.TabBarUnselectedColor" Value="{DynamicResource TintColor}"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/> <Setter Property="Shell.TabBarTitleColor" Value="{DynamicResource TextColor}"/>
</Style> </Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" /> <Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" /> <Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<!--
Default Styles for all Flyout Items
https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/flyout#flyoutitem-and-menuitem-style-classes
-->
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{DynamicResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{DynamicResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!-- <!--
Custom Style you can apply to any Flyout Item Custom Style you can apply to any Flyout Item
-->
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True"> <Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups"> <Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList> <VisualStateGroupList>
@ -70,24 +42,77 @@
</VisualStateGroupList> </VisualStateGroupList>
</Setter> </Setter>
</Style> </Style>
-->
</ResourceDictionary> </ResourceDictionary>
</Shell.Resources> </Shell.Resources>
<Shell.FlyoutHeaderTemplate>
<DataTemplate>
<Grid RowSpacing="0" BackgroundColor="{DynamicResource WindowColor}" Padding="20, 0, 0, 20">
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ui:CircleImage Aspect="AspectFill" Source="xamarin_logo.png"
HeightRequest="60" WidthRequest="60"
VerticalOptions="Center"/>
<Label Grid.Column="1" VerticalOptions="Center" FontAttributes="Bold"
Margin="10, 0, 0, 0"
Text="{r:Text Title}" TextColor="{DynamicResource TextColor}"/>
</Grid>
</DataTemplate>
</Shell.FlyoutHeaderTemplate>
<Shell.ItemTemplate>
<DataTemplate x:DataType="BaseShellItem">
<Grid HeightRequest="40">
<VisualStateManager.VisualStateGroups>
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{DynamicResource WindowColor}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor"
Value="{DynamicResource NavigationSelectedColor}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{x:OnPlatform Android=54, iOS=50}"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Source="{Binding FlyoutIcon}"
HorizontalOptions="Center" VerticalOptions="Center"
HeightRequest="22"/>
<Label Grid.Column="1" TextColor="{DynamicResource TextColor}"
Text="{Binding Title}"
FontSize="{x:OnPlatform Android=14, iOS=Small}"
VerticalTextAlignment="Center"/>
</Grid>
</DataTemplate>
</Shell.ItemTemplate>
<!-- <!--
When the Flyout is visible this defines the content to display in the flyout. When the Flyout is visible this defines the content to display in the flyout.
FlyoutDisplayOptions="AsMultipleItems" will create a separate flyout item for each child element FlyoutDisplayOptions="AsMultipleItems" will create a separate flyout item for each child element
https://docs.microsoft.com/dotnet/api/xamarin.forms.shellgroupitem.flyoutdisplayoptions?view=xamarin-forms https://docs.microsoft.com/dotnet/api/xamarin.forms.shellgroupitem.flyoutdisplayoptions?view=xamarin-forms
--> -->
<FlyoutItem Title="About" Icon="icon_about.png"> <FlyoutItem x:Name="flyoutItems"
<ShellContent Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" /> FlyoutDisplayOptions="AsMultipleItems"
</FlyoutItem> Route="{x:Static util:Routes.Gallery}" />
<FlyoutItem Title="Browse" Icon="icon_feed.png">
<ShellContent Route="ItemsPage" ContentTemplate="{DataTemplate local:ItemsPage}" />
</FlyoutItem>
<!-- When the Flyout is visible this will be a menu item you can tie a click behavior to --> <!-- When the Flyout is visible this will be a menu item you can tie a click behavior to -->
<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked"> <!--<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked" />-->
</MenuItem>
<!-- <!--
TabBar lets you define content that won't show up in a flyout menu. When this content is active TabBar lets you define content that won't show up in a flyout menu. When this content is active
@ -95,32 +120,12 @@
you don't want users to be able to navigate away from. If you would like to navigate to this you don't want users to be able to navigate away from. If you would like to navigate to this
content you can do so by calling content you can do so by calling
await Shell.Current.GoToAsync("//LoginPage"); await Shell.Current.GoToAsync("//LoginPage");
-->
<TabBar> <TabBar>
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}" /> <ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}" />
</TabBar> </TabBar>
-->
<!-- Optional Templates <!-- Optional Templates
// These may be provided inline as below or as separate classes.
// This header appears at the top of the Flyout.
// https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/flyout#flyout-header
<Shell.FlyoutHeaderTemplate>
<DataTemplate>
<Grid>ContentHere</Grid>
</DataTemplate>
</Shell.FlyoutHeaderTemplate>
// ItemTemplate is for ShellItems as displayed in a Flyout
// https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/flyout#define-flyoutitem-appearance
<Shell.ItemTemplate>
<DataTemplate>
<ContentView>
Bindable Properties: Title, Icon
</ContentView>
</DataTemplate>
</Shell.ItemTemplate>
// MenuItemTemplate is for MenuItems as displayed in a Flyout // MenuItemTemplate is for MenuItems as displayed in a Flyout
// https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/flyout#define-menuitem-appearance // https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/flyout#define-menuitem-appearance
<Shell.MenuItemTemplate> <Shell.MenuItemTemplate>
@ -130,7 +135,6 @@
</ContentView> </ContentView>
</DataTemplate> </DataTemplate>
</Shell.MenuItemTemplate> </Shell.MenuItemTemplate>
--> -->
</Shell> </Shell>

View File

@ -1,4 +1,7 @@
using System; using System;
using Gallery.Resources;
using Gallery.Resources.UI;
using Gallery.Util;
using Gallery.Views; using Gallery.Views;
using Xamarin.Forms; using Xamarin.Forms;
@ -6,16 +9,52 @@ namespace Gallery
{ {
public partial class AppShell : Shell public partial class AppShell : Shell
{ {
public static new AppShell Current => Shell.Current as AppShell;
public static Thickness NavigationBarOffset { get; private set; }
public static Thickness TotalBarOffset { get; private set; }
public AppShell() public AppShell()
{ {
InitializeComponent(); InitializeComponent();
Routing.RegisterRoute(nameof(ItemDetailPage), typeof(ItemDetailPage));
Routing.RegisterRoute(nameof(NewItemPage), typeof(NewItemPage)); #if DEBUG
Log.Print($"folder: {Store.PersonalFolder}");
Log.Print($"cache: {Store.CacheFolder}");
#endif
InitFlyouts();
} }
private async void OnMenuItemClicked(object sender, EventArgs e) private void InitFlyouts()
{ {
await Current.GoToAsync("//LoginPage"); foreach (var source in App.GallerySources)
{
var s = source;
var tab = new Tab
{
Title = source.Name,
Route = source.Route,
Items =
{
new ShellContent
{
ContentTemplate = new DataTemplate(() => new GalleryPage(s))
}
}
}
.DynamicResource(BaseShellItem.FlyoutIconProperty, source.FlyoutIconKey);
flyoutItems.Items.Add(tab);
}
}
public void SetNavigationBarHeight(double height)
{
NavigationBarOffset = new Thickness(0, height, 0, 0);
}
public void SetStatusBarHeight(double navigation, double height)
{
TotalBarOffset = new Thickness(0, navigation + height, 0, 0);
} }
} }
} }

View File

@ -10,30 +10,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)App.cs" /> <Compile Include="$(MSBuildThisFileDirectory)App.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Item.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\IDataStore.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\MockDataStore.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ViewModels\AboutViewModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ViewModels\BaseViewModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ViewModels\ItemDetailViewModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ViewModels\ItemsViewModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ViewModels\LoginViewModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ViewModels\NewItemViewModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\AboutPage.xaml.cs">
<DependentUpon>Views\AboutPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\ItemDetailPage.xaml.cs">
<DependentUpon>Views\ItemDetailPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\ItemsPage.xaml.cs">
<DependentUpon>Views\ItemsPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\LoginPage.xaml.cs">
<DependentUpon>Views\LoginPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Views\NewItemPage.xaml.cs">
<DependentUpon>Views\NewItemPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)AppShell.xaml.cs"> <Compile Include="$(MSBuildThisFileDirectory)AppShell.xaml.cs">
<DependentUpon>AppShell.xaml</DependentUpon> <DependentUpon>AppShell.xaml</DependentUpon>
</Compile> </Compile>
@ -44,11 +20,17 @@
<Compile Include="$(MSBuildThisFileDirectory)Resources\Theme\LightTheme.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Resources\Theme\LightTheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\Theme\DarkTheme.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Resources\Theme\DarkTheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\UI\Definition.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Resources\UI\Definition.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\UI\AdaptedPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\UI\CardView.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\UI\GalleryCollectionPage.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Services\GalleryCollection.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\Converters.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Views\GalleryPage.xaml.cs">
<DependentUpon>GalleryPage.xaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
<Folder Include="$(MSBuildThisFileDirectory)Services\" /> <Folder Include="$(MSBuildThisFileDirectory)Services\" />
<Folder Include="$(MSBuildThisFileDirectory)ViewModels\" />
<Folder Include="$(MSBuildThisFileDirectory)Views\" /> <Folder Include="$(MSBuildThisFileDirectory)Views\" />
<Folder Include="$(MSBuildThisFileDirectory)Resources\" /> <Folder Include="$(MSBuildThisFileDirectory)Resources\" />
<Folder Include="$(MSBuildThisFileDirectory)Resources\Theme\" /> <Folder Include="$(MSBuildThisFileDirectory)Resources\Theme\" />
@ -56,32 +38,14 @@
<Folder Include="$(MSBuildThisFileDirectory)Resources\UI\" /> <Folder Include="$(MSBuildThisFileDirectory)Resources\UI\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\AboutPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\ItemDetailPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\ItemsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\LoginPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\NewItemPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)AppShell.xaml"> <EmbeddedResource Include="$(MSBuildThisFileDirectory)AppShell.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> <EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\GalleryPage.xaml">
<ItemGroup> <SubType>Designer</SubType>
<None Include="$(MSBuildThisFileDirectory)Resources\Languages\zh-CN.xml" /> <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Resources\Languages\zh-CN.xml" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,11 +0,0 @@
using System;
namespace Gallery.Models
{
public class Item
{
public string Id { get; set; }
public string Text { get; set; }
public string Description { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Globalization;
using Gallery.Resources.UI;
using Xamarin.Forms;
namespace Gallery.Resources
{
public class FavoriteIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value == null ?
Definition.IconLove :
Definition.IconCircleLove;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Xml; using System.Xml;
using Gallery.Util; using Gallery.Util;
using Xamarin.Forms;
using Xamarin.Forms.Xaml; using Xamarin.Forms.Xaml;
namespace Gallery.Resources namespace Gallery.Resources
@ -84,6 +85,7 @@ namespace Gallery.Resources
} }
} }
[ContentProperty(nameof(Text))]
public class TextExtension : IMarkupExtension public class TextExtension : IMarkupExtension
{ {
public string Text { get; set; } public string Text { get; set; }

View File

@ -27,9 +27,16 @@ namespace Gallery.Resources.Theme
private void InitColors() private void InitColors()
{ {
Add(StatusBarStyle, StatusBarStyles.WhiteText); Add(StatusBarStyle, StatusBarStyles.WhiteText);
Add(WindowColor, Color.Black);
Add(TintColor, Color.FromRgb(0x94, 0x95, 0x9a));
Add(TextColor, Color.White);
Add(SubTextColor, Color.LightGray);
Add(CardBackgroundColor, Color.FromRgb(0x33, 0x33, 0x33));
Add(NavigationColor, Color.FromRgb(0x11, 0x11, 0x11)); Add(NavigationColor, Color.FromRgb(0x11, 0x11, 0x11));
Add(NavigationSelectedColor, Color.FromRgb(0x22, 0x22, 0x22));
Add(OptionBackColor, Color.Black);
Add(OptionTintColor, Color.FromRgb(0x11, 0x11, 0x11));
Add(Primary, Color.FromRgb(33, 150, 243));
} }
} }
} }

View File

@ -27,9 +27,16 @@ namespace Gallery.Resources.Theme
private void InitColors() private void InitColors()
{ {
Add(StatusBarStyle, StatusBarStyles.DarkText); Add(StatusBarStyle, StatusBarStyles.DarkText);
Add(WindowColor, Color.White);
Add(TintColor, Color.FromRgb(0x87, 0x87, 0x8b)); // 0x7f, 0x99, 0xc6
Add(TextColor, Color.Black);
Add(SubTextColor, Color.DimGray);
Add(CardBackgroundColor, Color.FromRgb(0xf3, 0xf3, 0xf3));
Add(NavigationColor, Color.FromRgb(0xf0, 0xf0, 0xf0)); Add(NavigationColor, Color.FromRgb(0xf0, 0xf0, 0xf0));
Add(NavigationSelectedColor, Color.LightGray);
Add(OptionBackColor, Color.FromRgb(0xf0, 0xf0, 0xf0));
Add(OptionTintColor, Color.White);
Add(Primary, Color.FromRgb(33, 150, 243));
} }
} }
} }

View File

@ -6,7 +6,15 @@ namespace Gallery.Resources.Theme
public abstract class Theme : ResourceDictionary public abstract class Theme : ResourceDictionary
{ {
public const string StatusBarStyle = nameof(StatusBarStyle); public const string StatusBarStyle = nameof(StatusBarStyle);
public const string WindowColor = nameof(WindowColor);
public const string TintColor = nameof(TintColor);
public const string TextColor = nameof(TextColor);
public const string SubTextColor = nameof(SubTextColor);
public const string CardBackgroundColor = nameof(CardBackgroundColor);
public const string NavigationColor = nameof(NavigationColor); public const string NavigationColor = nameof(NavigationColor);
public const string NavigationSelectedColor = nameof(NavigationSelectedColor);
public const string OptionBackColor = nameof(OptionBackColor);
public const string OptionTintColor = nameof(OptionTintColor);
public const string IconLightFamily = nameof(IconLightFamily); public const string IconLightFamily = nameof(IconLightFamily);
public const string IconRegularFamily = nameof(IconRegularFamily); public const string IconRegularFamily = nameof(IconRegularFamily);
@ -16,8 +24,6 @@ namespace Gallery.Resources.Theme
public const string IconClose = nameof(IconClose); public const string IconClose = nameof(IconClose);
public const string FontIconRefresh = nameof(FontIconRefresh); public const string FontIconRefresh = nameof(FontIconRefresh);
public const string Primary = nameof(Primary);
protected void InitResources() protected void InitResources()
{ {
Add(IconLightFamily, Definition.IconLightFamily); Add(IconLightFamily, Definition.IconLightFamily);

View File

@ -0,0 +1,148 @@
using System;
using Gallery.Services;
using Gallery.Util;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public class AdaptedPage : ContentPage
{
public static readonly BindableProperty TopMarginProperty = BindableProperty.Create(nameof(TopMargin), typeof(Thickness), typeof(AdaptedPage));
public Thickness TopMargin
{
get => (Thickness)GetValue(TopMarginProperty);
set => SetValue(TopMarginProperty, value);
}
public event EventHandler Load;
public event EventHandler Unload;
protected static readonly bool isPhone = DeviceInfo.Idiom == DeviceIdiom.Phone;
public AdaptedPage()
{
SetDynamicResource(Screen.StatusBarStyleProperty, Theme.Theme.StatusBarStyle);
Shell.SetNavBarHasShadow(this, true);
}
public virtual void OnLoad() => Load?.Invoke(this, EventArgs.Empty);
public virtual void OnUnload() => Unload?.Invoke(this, EventArgs.Empty);
public virtual void OnOrientationChanged(bool landscape)
{
var old = TopMargin;
Thickness @new;
if (Definition.IsFullscreenDevice)
{
@new = landscape ?
AppShell.NavigationBarOffset :
AppShell.TotalBarOffset;
}
else if (isPhone)
{
@new = landscape ?
Definition.TopOffset32 :
AppShell.TotalBarOffset;
}
else
{
// iPad
@new = AppShell.TotalBarOffset;
}
if (old != @new)
{
TopMargin = @new;
OnTopMarginChanged(old, @new);
}
}
protected virtual void OnTopMarginChanged(Thickness old, Thickness @new) { }
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
OnOrientationChanged(width > height);
}
protected void AnimateToMargin(View element, Thickness margin, bool animate = true)
{
var m = margin;
var start = element.Margin.Top - m.Top;
element.Margin = m;
element.CancelAnimations();
if (start > 0 && animate)
{
element.Animate("margin", top =>
{
element.TranslationY = top;
},
start, 0,
#if DEBUG
length: 500,
#else
length: 300,
#endif
easing: Easing.SinInOut,
finished: (v, r) =>
{
element.TranslationY = 0;
});
}
else if (element.TranslationY != 0)
{
element.TranslationY = 0;
}
}
protected void Start(Action action)
{
if (Tap.IsBusy)
{
Log.Error($"{GetType()}.tap", "gesture recognizer is now busy...");
return;
}
using (Tap.Start())
{
action();
}
}
private class Tap : IDisposable
{
public static bool IsBusy
{
get
{
lock (sync)
{
return instance?.isBusy == true;
}
}
}
private static readonly object sync = new();
private static readonly Tap instance = new();
private Tap() { }
public static Tap Start()
{
lock (sync)
{
instance.isBusy = true;
}
return instance;
}
private bool isBusy = false;
public void Dispose()
{
isBusy = false;
}
}
}
}

View File

@ -0,0 +1,44 @@
using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public class CardView : ContentView
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(CardView));
public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create(nameof(ShadowColor), typeof(Color), typeof(CardView));
public static readonly BindableProperty ShadowRadiusProperty = BindableProperty.Create(nameof(ShadowRadius), typeof(float), typeof(CardView), 3f);
public static readonly BindableProperty ShadowOffsetProperty = BindableProperty.Create(nameof(ShadowOffset), typeof(Size), typeof(CardView));
public float CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public Color ShadowColor
{
get => (Color)GetValue(ShadowColorProperty);
set => SetValue(ShadowColorProperty, value);
}
public float ShadowRadius
{
get => (float)GetValue(ShadowRadiusProperty);
set => SetValue(ShadowRadiusProperty, value);
}
public Size ShadowOffset
{
get => (Size)GetValue(ShadowOffsetProperty);
set => SetValue(ShadowOffsetProperty, value);
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
if (BindingContext is GalleryItem item &&
item.Width > 0 && item.ImageHeight.IsAuto)
{
item.ImageHeight = widthConstraint * item.Height / item.Width;
}
return base.OnMeasure(widthConstraint, heightConstraint);
}
}
}

View File

@ -10,6 +10,12 @@ namespace Gallery.Resources.UI
public const double FontSizeTitle = 18.0; public const double FontSizeTitle = 18.0;
public static readonly Thickness ScreenBottomPadding; public static readonly Thickness ScreenBottomPadding;
public static readonly Thickness TopOffset32 = new(0, 32, 0, 0);
public static readonly Color ColorLightShadow = Color.FromRgba(0, 0, 0, 0x20);
public static readonly Color ColorRedBackground = Color.FromRgb(0xfd, 0x43, 0x63);
public static readonly Color ColorDownloadBackground = Color.FromRgb(0xd7, 0xd9, 0xe0);
public static readonly ImageSource DownloadBackground = ImageSource.FromFile("download.png");
public static readonly double FontSizeSmall = Device.GetNamedSize(NamedSize.Small, typeof(Label));
#if __IOS__ #if __IOS__
public const string IconLightFamily = "FontAwesome5Pro-Light"; public const string IconLightFamily = "FontAwesome5Pro-Light";
@ -26,6 +32,8 @@ namespace Gallery.Resources.UI
#endif #endif
public const string IconRefresh = "\uf2f9"; public const string IconRefresh = "\uf2f9";
public const string IconLove = "\uf004";
public const string IconCircleLove = "\uf4c7";
public const string IconClose = "\uf057"; public const string IconClose = "\uf057";
static Definition() static Definition()

View File

@ -0,0 +1,557 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Services;
using Gallery.Util;
using Gallery.Util.Interface;
using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public abstract class GalleryCollectionPage : GalleryScrollableCollectionPage<GalleryItem[]>
{
protected readonly IGallerySource source;
public GalleryCollectionPage(IGallerySource source)
{
this.source = source;
}
}
public interface IGalleryCollectionPage
{
GalleryCollection GalleryCollection { get; set; }
}
public abstract class GalleryCollectionPage<T> : AdaptedPage, IGalleryCollectionPage
{
const int EXPIRED_MINUTES = 5;
protected const double loadingOffset = -40;
public static readonly BindableProperty GalleryProperty = BindableProperty.Create(nameof(Gallery), typeof(GalleryCollection), typeof(GalleryCollectionPage<T>));
public static readonly BindableProperty ColumnsProperty = BindableProperty.Create(nameof(Columns), typeof(int), typeof(GalleryCollectionPage<T>),
defaultValue: 2);
public static readonly BindableProperty IsLoadingProperty = BindableProperty.Create(nameof(IsLoading), typeof(bool), typeof(GalleryCollectionPage<T>),
defaultValue: true);
public static readonly BindableProperty IsBottomLoadingProperty = BindableProperty.Create(nameof(IsBottomLoading), typeof(bool), typeof(GalleryCollectionPage<T>));
public GalleryCollection Gallery
{
get => (GalleryCollection)GetValue(GalleryProperty);
set => SetValue(GalleryProperty, value);
}
public int Columns
{
get => (int)GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, value);
}
public bool IsLoading
{
get => (bool)GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public bool IsBottomLoading
{
get => (bool)GetValue(IsBottomLoadingProperty);
set => SetValue(IsBottomLoadingProperty, value);
}
public GalleryCollection GalleryCollection { get; set; }
protected virtual ActivityIndicator LoadingIndicator => null;
protected virtual double IndicatorMarginTop => 16;
protected bool Expired => lastUpdated == default || (DateTime.Now - lastUpdated).TotalMinutes > EXPIRED_MINUTES;
protected readonly Command<GalleryItem> commandGalleryItemTapped;
protected DateTime lastUpdated;
protected double topOffset;
protected string lastError;
private readonly object sync = new();
private readonly Stack<ParallelTask> tasks = new();
private T galleryData;
public GalleryCollectionPage()
{
commandGalleryItemTapped = new Command<GalleryItem>(OnGalleryItemTapped);
}
private void OnGalleryItemTapped(GalleryItem item)
{
if (item == null)
{
return;
}
//Start(async () =>
//{
// var page = new GalleryItemPage(item);
// await Navigation.PushAsync(page);
//});
}
public override void OnUnload()
{
lock (sync)
{
while (tasks.TryPop(out var task))
{
if (task != null)
{
task.Dispose();
}
}
}
InvalidateCollection();
Gallery = null;
lastUpdated = default;
}
protected override void OnAppearing()
{
base.OnAppearing();
if (lastUpdated == default)
{
StartLoading();
}
}
#if __IOS__
public override void OnOrientationChanged(bool landscape)
{
base.OnOrientationChanged(landscape);
if (Definition.IsFullscreenDevice)
{
topOffset = landscape ?
AppShell.NavigationBarOffset.Top :
AppShell.TotalBarOffset.Top;
}
else if (isPhone)
{
topOffset = landscape ?
Definition.TopOffset32.Top :
AppShell.TotalBarOffset.Top;
}
else
{
// iPad
topOffset = AppShell.TotalBarOffset.Top;
}
}
#endif
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
int columns;
if (width > height)
{
columns = isPhone ? 4 : 6;
}
else
{
columns = isPhone ? 2 : 4;
}
if (Columns != columns)
{
Columns = columns;
#if DEBUG
Log.Print($"changing columns to {columns}");
#endif
}
}
protected abstract Task<T> DoloadGalleryData(bool force);
protected abstract IEnumerable<GalleryItem> DoGetGalleryList(T data, out int tag);
protected virtual GalleryCollection FilterGalleryCollection(GalleryCollection collection, bool bottom)
{
GalleryCollection = collection;
return collection;
}
protected void InvalidateCollection()
{
var collection = GalleryCollection;
if (collection != null)
{
collection.Running = false;
GalleryCollection = null;
}
}
protected virtual void StartLoading(bool force = false, bool isBottom = false)
{
if (force || Expired)
{
var indicator = LoadingIndicator;
if (indicator == null || isBottom)
{
if (isBottom)
{
IsBottomLoading = true;
}
else
{
InvalidateCollection();
IsLoading = true;
}
#if __IOS__
Device.StartTimer(TimeSpan.FromMilliseconds(48), () =>
#else
Device.StartTimer(TimeSpan.FromMilliseconds(150), () =>
#endif
{
_ = DoloadGallerySource(force, isBottom);
return false;
});
}
else
{
InvalidateCollection();
IsLoading = true;
var offset = 16 - IndicatorMarginTop;
indicator.CancelAnimations();
indicator.Animate("margin", top =>
{
indicator.Margin = new Thickness(0, top, 0, offset);
},
loadingOffset - offset, 16 - offset,
easing: Easing.CubicOut,
finished: (v, r) =>
{
_ = DoloadGallerySource(force, isBottom);
});
}
}
}
protected virtual void DoGalleryLoaded(GalleryCollection collection, bool bottom)
{
collection = FilterGalleryCollection(collection, bottom);
var indicator = LoadingIndicator;
if (indicator == null || bottom)
{
IsLoading = false;
IsBottomLoading = false;
#if __IOS__
Device.StartTimer(TimeSpan.FromMilliseconds(48), () =>
#else
Device.StartTimer(TimeSpan.FromMilliseconds(150), () =>
#endif
{
Gallery = collection;
return false;
});
}
else
{
var offset = 16 - IndicatorMarginTop;
indicator.CancelAnimations();
indicator.Animate("margin", top =>
{
indicator.Margin = new Thickness(0, top, 0, offset);
},
16 - offset, loadingOffset - offset,
easing: Easing.CubicIn,
finished: (v, r) =>
{
indicator.Margin = new Thickness(0, v, 0, offset);
IsLoading = false;
IsBottomLoading = false;
#if __IOS__
Device.StartTimer(TimeSpan.FromMilliseconds(48), () =>
#else
Device.StartTimer(TimeSpan.FromMilliseconds(150), () =>
#endif
{
Gallery = collection;
return false;
});
});
}
}
protected async Task ScrollToTopAsync(ScrollView scrollView)
{
if (scrollView.ScrollY > -topOffset)
{
#if __IOS__
await scrollView.ScrollToAsync(scrollView.ScrollX, -topOffset, true);
#else
await scrollView.ScrollToAsync(0, -topOffset, false);
#endif
}
}
protected DataTemplate GetCardViewTemplate(string titleBinding = null)
{
return new DataTemplate(() =>
{
var image = new RoundImage
{
BackgroundColor = Definition.ColorDownloadBackground,
CornerRadius = 10,
CornerMasks = CornerMask.Top,
HorizontalOptions = LayoutOptions.Fill,
Aspect = Aspect.AspectFill,
GestureRecognizers =
{
new TapGestureRecognizer
{
Command = commandGalleryItemTapped
}
.Binding(TapGestureRecognizer.CommandParameterProperty, ".")
}
}
.Binding(Image.SourceProperty, nameof(GalleryItem.PreviewImage));
var title = new Label
{
Padding = new Thickness(8, 2),
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.Center,
LineBreakMode = LineBreakMode.TailTruncation,
FontSize = Definition.FontSizeSmall
}
.DynamicResource(Label.TextColorProperty, Theme.Theme.TextColor);
var favorite = new Label
{
WidthRequest = 26,
HorizontalOptions = LayoutOptions.End,
HorizontalTextAlignment = TextAlignment.End,
VerticalOptions = LayoutOptions.Center,
FontSize = Definition.FontSizeSmall,
TextColor = Definition.ColorRedBackground,
IsVisible = false
}
.Binding(Label.TextProperty, nameof(GalleryItem.BookmarkId), converter: new FavoriteIconConverter())
.Binding(IsVisibleProperty, nameof(GalleryItem.IsFavorite))
.DynamicResource(Label.FontFamilyProperty, Theme.Theme.IconSolidFamily);
return new CardView
{
Padding = 0,
Margin = 0,
CornerRadius = 10,
ShadowColor = Definition.ColorLightShadow,
ShadowOffset = new Size(1, 1),
Content = new Grid
{
HorizontalOptions = LayoutOptions.Fill,
RowSpacing = 0,
RowDefinitions =
{
new RowDefinition().Binding(RowDefinition.HeightProperty, nameof(GalleryItem.ImageHeight)),
new RowDefinition { Height = 30 }
},
Children =
{
image,
new Grid
{
ColumnDefinitions =
{
new ColumnDefinition(),
new ColumnDefinition { Width = 20 }
},
VerticalOptions = LayoutOptions.Center,
Padding = new Thickness(0, 0, 8, 0),
Children =
{
title.Binding(Label.TextProperty, titleBinding ?? nameof(GalleryItem.TagDescription)),
favorite.GridColumn(1)
}
}
.GridRow(1)
}
}
}
.DynamicResource(BackgroundColorProperty, Theme.Theme.CardBackgroundColor);
});
}
protected async Task DoloadGallerySource(bool force = false, bool bottom = false)
{
#if DEBUG
Log.Print($"start loading data, force: {force}");
#endif
galleryData = await DoloadGalleryData(force);
if (galleryData == null)
{
Log.Error("gallery.load", "failed to load gallery data.");
return;
}
if (force)
{
lastUpdated = DateTime.Now;
}
var data = DoGetGalleryList(galleryData, out int tag).Where(i => i != null);
var collection = new GalleryCollection(data);
foreach (var item in collection)
{
if (item.PreviewImage == null)
{
var image = await Store.LoadPreviewImage(item.PreviewUrl, false);
if (image != null)
{
item.PreviewImage = image;
}
}
}
DoGalleryLoaded(collection, bottom);
DoloadImages(collection, tag);
}
private void DoloadImages(GalleryCollection collection, int tag)
{
lock (sync)
{
if (tasks.TryPeek(out var peek))
{
if (peek != null && peek.TagIndex >= tag)
{
Log.Print($"tasks expired ({tasks.Count}, peek: {peek.TagIndex}, now: {tag}, will be disposed.");
while (tasks.TryPop(out var t))
{
t?.Dispose();
}
}
}
}
var list = collection.Where(i => i.PreviewImage == null).ToArray();
var task = ParallelTask.Start("collection.load", 0, list.Length, 2, i =>
{
if (!collection.Running)
{
return false;
}
var item = list[i];
if (item.PreviewImage == null && item.PreviewUrl != null)
{
item.PreviewImage = Definition.DownloadBackground;
var image = Store.LoadPreviewImage(item.PreviewUrl, true, force: true).Result;
if (image != null)
{
item.PreviewImage = image;
}
}
return true;
}, tagIndex: tag);
if (task != null)
{
lock (sync)
{
tasks.Push(task);
}
}
}
}
public abstract class GalleryScrollableCollectionPage<T> : GalleryCollectionPage<T>
{
protected const int SCROLL_OFFSET = 33;
protected ScrollDirection scrollDirection = ScrollDirection.Stop;
protected double lastScrollY = double.MinValue;
private double lastRefreshY = double.MinValue;
private double offset;
protected bool IsScrollingDown(double y)
{
if (y > lastScrollY)
{
if (scrollDirection != ScrollDirection.Down)
{
scrollDirection = ScrollDirection.Down;
}
return true;
}
else
{
if (scrollDirection != ScrollDirection.Up)
{
scrollDirection = ScrollDirection.Up;
}
return false;
}
}
protected void SetOffset(double off)
{
offset = off;
}
protected abstract bool CheckRefresh();
protected override void StartLoading(bool force = false, bool isBottom = false)
{
if (!isBottom)
{
lastRefreshY = double.MinValue;
}
base.StartLoading(force, isBottom);
}
protected override GalleryCollection FilterGalleryCollection(GalleryCollection collection, bool bottom)
{
var now = GalleryCollection;
if (now == null)
{
now = collection;
GalleryCollection = now;
}
else
{
now.AddRange(collection);
}
return now;
}
protected void OnScrolled(double y)
{
lastScrollY = y;
if (scrollDirection == ScrollDirection.Up)
{
return;
}
if (y > 0 && offset > 0 && y - topOffset > offset)
{
if (IsLoading || IsBottomLoading)
{
return;
}
if (y - lastRefreshY > 200)
{
if (CheckRefresh())
{
lastRefreshY = y;
#if DEBUG
Log.Print("start to load next page");
#endif
StartLoading(true, true);
}
}
}
}
}
public enum ScrollDirection
{
Stop,
Up,
Down
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using Gallery.Resources.UI;
using Gallery.Util.Model;
using Xamarin.Essentials;
namespace Gallery.Services
{
public class GalleryCollection : List<GalleryItem>, ICollectionChanged
{
private static GalleryCollection empty;
public static GalleryCollection Empty
{
get
{
if (empty == null)
{
empty = new GalleryCollection();
}
return empty;
}
}
public event EventHandler<CollectionChangedEventArgs> CollectionChanged;
public bool Running { get; set; }
public GalleryCollection() : base()
{
Running = true;
}
public GalleryCollection(IEnumerable<GalleryItem> gallery) : base(gallery)
{
Running = true;
}
public void AddRange(List<GalleryItem> items)
{
var e = new CollectionChangedEventArgs
{
NewStartingIndex = Count,
NewItems = items
};
base.AddRange(items);
if (MainThread.IsMainThread)
{
CollectionChanged?.Invoke(this, e);
}
else
{
MainThread.BeginInvokeOnMainThread(() => CollectionChanged?.Invoke(this, e));
}
}
}
}

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Gallery.Services
{
public interface IDataStore<T>
{
Task<bool> AddItemAsync(T item);
Task<bool> UpdateItemAsync(T item);
Task<bool> DeleteItemAsync(string id);
Task<T> GetItemAsync(string id);
Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false);
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Gallery.Models;
namespace Gallery.Services
{
public class MockDataStore : IDataStore<Item>
{
readonly List<Item> items;
public MockDataStore()
{
items = new List<Item>()
{
new Item { Id = Guid.NewGuid().ToString(), Text = "First item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Second item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Third item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Fourth item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Fifth item", Description="This is an item description." },
new Item { Id = Guid.NewGuid().ToString(), Text = "Sixth item", Description="This is an item description." }
};
}
public async Task<bool> AddItemAsync(Item item)
{
items.Add(item);
return await Task.FromResult(true);
}
public async Task<bool> UpdateItemAsync(Item item)
{
var oldItem = items.Where((Item arg) => arg.Id == item.Id).FirstOrDefault();
items.Remove(oldItem);
items.Add(item);
return await Task.FromResult(true);
}
public async Task<bool> DeleteItemAsync(string id)
{
var oldItem = items.Where((Item arg) => arg.Id == id).FirstOrDefault();
items.Remove(oldItem);
return await Task.FromResult(true);
}
public async Task<Item> GetItemAsync(string id)
{
return await Task.FromResult(items.FirstOrDefault(s => s.Id == id));
}
public async Task<IEnumerable<Item>> GetItemsAsync(bool forceRefresh = false)
{
return await Task.FromResult(items);
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Windows.Input;
using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.ViewModels
{
public class AboutViewModel : BaseViewModel
{
public AboutViewModel()
{
Title = "About";
OpenWebCommand = new Command(async () => await Browser.OpenAsync("https://aka.ms/xamarin-quickstart"));
}
public ICommand OpenWebCommand { get; }
}
}

View File

@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using Gallery.Models;
using Gallery.Services;
namespace Gallery.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
public IDataStore<Item> DataStore => DependencyService.Get<IDataStore<Item>>();
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

View File

@ -1,57 +0,0 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Gallery.Models;
using Xamarin.Forms;
namespace Gallery.ViewModels
{
[QueryProperty(nameof(ItemId), nameof(ItemId))]
public class ItemDetailViewModel : BaseViewModel
{
private string itemId;
private string text;
private string description;
public string Id { get; set; }
public string Text
{
get => text;
set => SetProperty(ref text, value);
}
public string Description
{
get => description;
set => SetProperty(ref description, value);
}
public string ItemId
{
get
{
return itemId;
}
set
{
itemId = value;
LoadItemId(value);
}
}
public async void LoadItemId(string itemId)
{
try
{
var item = await DataStore.GetItemAsync(itemId);
Id = item.Id;
Text = item.Text;
Description = item.Description;
}
catch (Exception)
{
Debug.WriteLine("Failed to Load Item");
}
}
}
}

View File

@ -1,86 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;
using Gallery.Models;
using Gallery.Views;
namespace Gallery.ViewModels
{
public class ItemsViewModel : BaseViewModel
{
private Item _selectedItem;
public ObservableCollection<Item> Items { get; }
public Command LoadItemsCommand { get; }
public Command AddItemCommand { get; }
public Command<Item> ItemTapped { get; }
public ItemsViewModel()
{
Title = "Browse";
Items = new ObservableCollection<Item>();
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
ItemTapped = new Command<Item>(OnItemSelected);
AddItemCommand = new Command(OnAddItem);
}
async Task ExecuteLoadItemsCommand()
{
IsBusy = true;
try
{
Items.Clear();
var items = await DataStore.GetItemsAsync(true);
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
public void OnAppearing()
{
IsBusy = true;
SelectedItem = null;
}
public Item SelectedItem
{
get => _selectedItem;
set
{
SetProperty(ref _selectedItem, value);
OnItemSelected(value);
}
}
private async void OnAddItem(object obj)
{
await Shell.Current.GoToAsync(nameof(NewItemPage));
}
async void OnItemSelected(Item item)
{
if (item == null)
return;
// This will push the ItemDetailPage onto the navigation stack
await Shell.Current.GoToAsync($"{nameof(ItemDetailPage)}?{nameof(ItemDetailViewModel.ItemId)}={item.Id}");
}
}
}

View File

@ -1,24 +0,0 @@
using Gallery.Views;
using System;
using System.Collections.Generic;
using System.Text;
using Xamarin.Forms;
namespace Gallery.ViewModels
{
public class LoginViewModel : BaseViewModel
{
public Command LoginCommand { get; }
public LoginViewModel()
{
LoginCommand = new Command(OnLoginClicked);
}
private async void OnLoginClicked(object obj)
{
// Prefixing with `//` switches to a different navigation stack instead of pushing to the active one
await Shell.Current.GoToAsync($"//{nameof(AboutPage)}");
}
}
}

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Input;
using Gallery.Models;
using Xamarin.Forms;
namespace Gallery.ViewModels
{
public class NewItemViewModel : BaseViewModel
{
private string text;
private string description;
public NewItemViewModel()
{
SaveCommand = new Command(OnSave, ValidateSave);
CancelCommand = new Command(OnCancel);
this.PropertyChanged +=
(_, __) => SaveCommand.ChangeCanExecute();
}
private bool ValidateSave()
{
return !String.IsNullOrWhiteSpace(text)
&& !String.IsNullOrWhiteSpace(description);
}
public string Text
{
get => text;
set => SetProperty(ref text, value);
}
public string Description
{
get => description;
set => SetProperty(ref description, value);
}
public Command SaveCommand { get; }
public Command CancelCommand { get; }
private async void OnCancel()
{
// This will pop the current page off the navigation stack
await Shell.Current.GoToAsync("..");
}
private async void OnSave()
{
Item newItem = new Item()
{
Id = Guid.NewGuid().ToString(),
Text = Text,
Description = Description
};
await DataStore.AddItemAsync(newItem);
// This will pop the current page off the navigation stack
await Shell.Current.GoToAsync("..");
}
}
}

View File

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Gallery.Views.AboutPage"
xmlns:vm="clr-namespace:Gallery.ViewModels"
Title="{Binding Title}">
<ContentPage.BindingContext>
<vm:AboutViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<Color x:Key="Accent">#96d1ff</Color>
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackLayout BackgroundColor="{StaticResource Accent}" VerticalOptions="FillAndExpand" HorizontalOptions="Fill">
<StackLayout Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center">
<ContentView Padding="0,40,0,40" VerticalOptions="FillAndExpand">
<Image Source="xamarin_logo.png" VerticalOptions="Center" HeightRequest="64" />
</ContentView>
</StackLayout>
</StackLayout>
<ScrollView Grid.Row="1">
<StackLayout Orientation="Vertical" Padding="30,24,30,24" Spacing="10">
<Label Text="Start developing now" FontSize="Title"/>
<Label Text="Make changes to your XAML file and save to see your UI update in the running app with XAML Hot Reload. Give it a try!" FontSize="16" Padding="0,0,0,0"/>
<Label FontSize="16" Padding="0,24,0,0">
<Label.FormattedText>
<FormattedString>
<FormattedString.Spans>
<Span Text="Learn more at "/>
<Span Text="https://aka.ms/xamarin-quickstart" FontAttributes="Bold"/>
</FormattedString.Spans>
</FormattedString>
</Label.FormattedText>
</Label>
<Button Margin="0,10,0,0" Text="Learn more"
Command="{Binding OpenWebCommand}"
BackgroundColor="{DynamicResource Primary}"
TextColor="White" />
</StackLayout>
</ScrollView>
</Grid>
</ContentPage>

View File

@ -1,31 +0,0 @@
using System;
using System.ComponentModel;
using Gallery.Util;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Gallery.Views
{
public partial class AboutPage : ContentPage
{
public AboutPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
var result = await App.GallerySources[0].GetRecentItemsAsync(1);
if (result != null)
{
for (var i = 0; i < result.Length; i++)
{
var item = result[i];
Log.Print($"id: {item.Id}, url: {item.RawUrl}");
}
}
}
}
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ui:GalleryCollectionPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ui="clr-namespace:Gallery.Resources.UI"
x:Class="Gallery.Views.GalleryPage"
x:Name="yanderePage"
BackgroundColor="{DynamicResource WindowColor}"
BindingContext="{x:Reference yanderePage}">
<ContentPage.Content>
<Grid>
<ScrollView x:Name="scrollView" Scrolled="ScrollView_Scrolled"
HorizontalOptions="Fill" HorizontalScrollBarVisibility="Never">
<StackLayout>
<ActivityIndicator x:Name="activityLoading" Margin="0, -40, 0, 0"
HeightRequest="40"
IsRunning="{Binding IsLoading}"
IsVisible="{Binding IsLoading}"/>
<ui:FlowLayout ItemsSource="{Binding Gallery}"
MaxHeightChanged="FlowLayout_MaxHeightChanged"
HorizontalOptions="Fill" Column="{Binding Columns}"
Margin="16" RowSpacing="16" ColumnSpacing="16"
ItemTemplate="{StaticResource cardView}"/>
<ActivityIndicator x:Name="activityBottomLoading" Margin="0, -10, 0, 16"
IsRunning="{Binding IsBottomLoading}"
IsVisible="{Binding IsBottomLoading}"/>
</StackLayout>
</ScrollView>
</Grid>
</ContentPage.Content>
</ui:GalleryCollectionPage>

View File

@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Gallery.Resources.UI;
using Gallery.Util;
using Gallery.Util.Interface;
using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Views
{
public partial class GalleryPage : GalleryCollectionPage
{
private int currentPage;
public GalleryPage(IGallerySource source) : base(source)
{
Resources.Add("cardView", GetCardViewTemplate());
InitializeComponent();
currentPage = 1;
}
protected override ActivityIndicator LoadingIndicator => activityLoading;
protected override void OnAppearing()
{
if (currentPage != 1 && Gallery == null)
{
currentPage = 1;
}
base.OnAppearing();
}
protected override async Task<GalleryItem[]> DoloadGalleryData(bool force)
{
var result = await source.GetRecentItemsAsync(currentPage);
return result;
}
protected override IEnumerable<GalleryItem> DoGetGalleryList(GalleryItem[] data, out int tag)
{
tag = currentPage;
return data;
}
private void FlowLayout_MaxHeightChanged(object sender, HeightEventArgs e)
{
SetOffset(e.ContentHeight - scrollView.Bounds.Height - SCROLL_OFFSET);
}
protected override bool CheckRefresh()
{
currentPage++;
#if DEBUG
Log.Print($"loading page: {currentPage}");
#endif
return true;
}
private void ScrollView_Scrolled(object sender, ScrolledEventArgs e)
{
var y = e.ScrollY;
OnScrolled(y);
}
}
}

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Gallery.Views.ItemDetailPage"
Title="{Binding Title}">
<StackLayout Spacing="20" Padding="15">
<Label Text="Text:" FontSize="Medium" />
<Label Text="{Binding Text}" FontSize="Small"/>
<Label Text="Description:" FontSize="Medium" />
<Label Text="{Binding Description}" FontSize="Small"/>
</StackLayout>
</ContentPage>

View File

@ -1,15 +0,0 @@
using System.ComponentModel;
using Xamarin.Forms;
using Gallery.ViewModels;
namespace Gallery.Views
{
public partial class ItemDetailPage : ContentPage
{
public ItemDetailPage()
{
InitializeComponent();
BindingContext = new ItemDetailViewModel();
}
}
}

View File

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Gallery.Views.ItemsPage"
Title="{Binding Title}"
xmlns:local="clr-namespace:Gallery.ViewModels"
xmlns:model="clr-namespace:Gallery.Models"
x:Name="BrowseItemsPage">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add" Command="{Binding AddItemCommand}" />
</ContentPage.ToolbarItems>
<!--
x:DataType enables compiled bindings for better performance and compile time validation of binding expressions.
https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/data-binding/compiled-bindings
-->
<RefreshView x:DataType="local:ItemsViewModel" Command="{Binding LoadItemsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
<CollectionView x:Name="ItemsListView"
ItemsSource="{Binding Items}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:Item">
<Label Text="{Binding Text}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" />
<Label Text="{Binding Description}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
FontSize="13" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1"
Command="{Binding Source={RelativeSource AncestorType={x:Type local:ItemsViewModel}}, Path=ItemTapped}"
CommandParameter="{Binding .}">
</TapGestureRecognizer>
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
</ContentPage>

View File

@ -1,33 +0,0 @@
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Gallery.Models;
using Gallery.Views;
using Gallery.ViewModels;
namespace Gallery.Views
{
public partial class ItemsPage : ContentPage
{
ItemsViewModel _viewModel;
public ItemsPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
}
}

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Gallery.Views.LoginPage"
Shell.NavBarIsVisible="False">
<ContentPage.Content>
<StackLayout Padding="10,0,10,0" VerticalOptions="Center">
<Button VerticalOptions="Center" Text="Login" Command="{Binding LoginCommand}"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Gallery.ViewModels;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Gallery.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class LoginPage : ContentPage
{
public LoginPage()
{
InitializeComponent();
this.BindingContext = new LoginViewModel();
}
}
}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Gallery.Views.NewItemPage"
Shell.PresentationMode="ModalAnimated"
Title="New Item"
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
ios:Page.UseSafeArea="true">
<ContentPage.Content>
<StackLayout Spacing="3" Padding="15">
<Label Text="Text" FontSize="Medium" />
<Entry Text="{Binding Text, Mode=TwoWay}" FontSize="Medium" />
<Label Text="Description" FontSize="Medium" />
<Editor Text="{Binding Description, Mode=TwoWay}" AutoSize="TextChanges" FontSize="Medium" Margin="0" />
<StackLayout Orientation="Horizontal">
<Button Text="Cancel" Command="{Binding CancelCommand}" HorizontalOptions="FillAndExpand"></Button>
<Button Text="Save" Command="{Binding SaveCommand}" HorizontalOptions="FillAndExpand"></Button>
</StackLayout>
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Gallery.Models;
using Gallery.ViewModels;
namespace Gallery.Views
{
public partial class NewItemPage : ContentPage
{
public Item Item { get; set; }
public NewItemPage()
{
InitializeComponent();
BindingContext = new NewItemViewModel();
}
}
}

View File

@ -0,0 +1,8 @@
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public class BlurryPanel : ContentView { }
public class CircleImage : Image { }
}

54
Gallery.UI/Extensions.cs Normal file
View File

@ -0,0 +1,54 @@
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public static class Extensions
{
public const string TextColor = nameof(TextColor);
public const string SubTextColor = nameof(SubTextColor);
public const string OptionTintColor = nameof(OptionTintColor);
public static T Binding<T>(this T view, BindableProperty property, string name, BindingMode mode = BindingMode.Default, IValueConverter converter = null) where T : BindableObject
{
if (name == null)
{
view.SetValue(property, property.DefaultValue);
}
else
{
view.SetBinding(property, name, mode, converter);
}
return view;
}
public static T DynamicResource<T>(this T view, BindableProperty property, string key) where T : Element
{
view.SetDynamicResource(property, key);
return view;
}
public static T GridRow<T>(this T view, int row) where T : BindableObject
{
Grid.SetRow(view, row);
return view;
}
public static T GridRowSpan<T>(this T view, int rowSpan) where T : BindableObject
{
Grid.SetRowSpan(view, rowSpan);
return view;
}
public static T GridColumn<T>(this T view, int column) where T : BindableObject
{
Grid.SetColumn(view, column);
return view;
}
public static T GridColumnSpan<T>(this T view, int columnSpan) where T : BindableObject
{
Grid.SetColumnSpan(view, columnSpan);
return view;
}
}
}

275
Gallery.UI/FlowLayout.cs Normal file
View File

@ -0,0 +1,275 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public class FlowLayout : AbsoluteLayout
{
public static readonly BindableProperty ColumnProperty = BindableProperty.Create(nameof(Column), typeof(int), typeof(FlowLayout),
defaultValue: 2, propertyChanged: OnColumnPropertyChanged);
public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create(nameof(RowSpacing), typeof(double), typeof(FlowLayout), defaultValue: 10.0);
public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create(nameof(ColumnSpacing), typeof(double), typeof(FlowLayout), defaultValue: 10.0);
private static void OnColumnPropertyChanged(BindableObject obj, object old, object @new)
{
var flowLayout = (FlowLayout)obj;
if (old is int column && column != flowLayout.Column)
{
flowLayout.UpdateChildrenLayout();
//flowLayout.InvalidateLayout();
}
}
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(FlowLayout));
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(FlowLayout),
propertyChanged: OnItemsSourcePropertyChanged);
private static void OnItemsSourcePropertyChanged(BindableObject obj, object old, object @new)
{
var flowLayout = (FlowLayout)obj;
if (old is ICollectionChanged oldNotify)
{
oldNotify.CollectionChanged -= flowLayout.OnCollectionChanged;
}
flowLayout.lastWidth = -1;
if (@new == null)
{
flowLayout.freezed = true;
flowLayout.cachedLayout.Clear();
flowLayout.Children.Clear();
flowLayout.freezed = false;
flowLayout.InvalidateLayout();
}
else if (@new is IList list)
{
flowLayout.freezed = true;
flowLayout.cachedLayout.Clear();
flowLayout.Children.Clear();
for (var i = 0; i < list.Count; i++)
{
var child = flowLayout.ItemTemplate.CreateContent();
if (child is View view)
{
view.BindingContext = list[i];
flowLayout.Children.Add(view);
}
}
if (@new is ICollectionChanged newNotify)
{
newNotify.CollectionChanged += flowLayout.OnCollectionChanged;
}
flowLayout.freezed = false;
flowLayout.UpdateChildrenLayout();
//flowLayout.InvalidateLayout();
}
}
public int Column
{
get => (int)GetValue(ColumnProperty);
set => SetValue(ColumnProperty, value);
}
public double RowSpacing
{
get => (double)GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
public double ColumnSpacing
{
get => (double)GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
public DataTemplate ItemTemplate
{
get => (DataTemplate)GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
public IList ItemsSource
{
get => (IList)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public event EventHandler<HeightEventArgs> MaxHeightChanged;
public double ColumnWidth { get; private set; }
private bool freezed;
private double maximumHeight;
private readonly Dictionary<View, Rectangle> cachedLayout = new();
private double lastWidth = -1;
private SizeRequest lastSizeRequest;
protected override void LayoutChildren(double x, double y, double width, double height)
{
if (freezed)
{
return;
}
var column = Column;
if (column < 1)
{
return;
}
var source = ItemsSource;
if (source == null || source.Count <= 0)
{
return;
}
var columnSpacing = ColumnSpacing;
var rowSpacing = RowSpacing;
var columnHeights = new double[column];
var columnSpacingTotal = columnSpacing * (column - 1);
var columnWidth = (width - columnSpacingTotal) / column;
ColumnWidth = columnWidth;
foreach (var item in Children)
{
var measured = item.Measure(columnWidth, height, flags: MeasureFlags.IncludeMargins);
var col = 0;
for (var i = 1; i < column; i++)
{
if (columnHeights[i] < columnHeights[col])
{
col = i;
}
}
var rect = new Rectangle(
col * (columnWidth + columnSpacing),
columnHeights[col],
columnWidth,
measured.Request.Height);
if (cachedLayout.TryGetValue(item, out var r))
{
if (r != rect)
{
cachedLayout[item] = rect;
item.Layout(rect);
//SetLayoutBounds(item, rect);
}
}
else
{
cachedLayout.Add(item, rect);
item.Layout(rect);
//SetLayoutBounds(item, rect);
}
columnHeights[col] += measured.Request.Height + rowSpacing;
}
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
var column = Column;
if (column < 1)
{
return base.OnMeasure(widthConstraint, heightConstraint);
}
if (lastWidth == widthConstraint)
{
return lastSizeRequest;
}
lastWidth = widthConstraint;
var columnSpacing = ColumnSpacing;
var rowSpacing = RowSpacing;
var columnHeights = new double[column];
var columnSpacingTotal = columnSpacing * (column - 1);
var columnWidth = (widthConstraint - columnSpacingTotal) / column;
ColumnWidth = columnWidth;
foreach (var item in Children)
{
var measured = item.Measure(columnWidth, heightConstraint, flags: MeasureFlags.IncludeMargins);
var col = 0;
for (var i = 1; i < column; i++)
{
if (columnHeights[i] < columnHeights[col])
{
col = i;
}
}
columnHeights[col] += measured.Request.Height + rowSpacing;
}
maximumHeight = columnHeights.Max();
if (maximumHeight > 0)
{
MaxHeightChanged?.Invoke(this, new HeightEventArgs { ContentHeight = maximumHeight });
}
lastSizeRequest = new SizeRequest(new Size(widthConstraint, maximumHeight));
return lastSizeRequest;
}
private void OnCollectionChanged(object sender, CollectionChangedEventArgs e)
{
lastWidth = -1;
if (e.OldItems != null)
{
freezed = true;
cachedLayout.Clear();
var index = e.OldStartingIndex;
for (var i = index + e.OldItems.Count - 1; i >= index; i--)
{
Children.RemoveAt(i);
}
freezed = false;
}
if (e.NewItems == null)
{
UpdateChildrenLayout();
//InvalidateLayout();
return;
}
freezed = true;
var start = e.NewStartingIndex;
for (var i = 0; i < e.NewItems.Count; i++)
{
var child = ItemTemplate.CreateContent();
if (child is View view)
{
view.BindingContext = e.NewItems[i];
Children.Insert(start + i, view);
}
}
freezed = false;
UpdateChildrenLayout();
//InvalidateLayout();
}
}
public interface ICollectionChanged
{
event EventHandler<CollectionChangedEventArgs> CollectionChanged;
}
public class CollectionChangedEventArgs : EventArgs
{
public int OldStartingIndex { get; set; }
public IList OldItems { get; set; }
public int NewStartingIndex { get; set; }
public IList NewItems { get; set; }
}
public class HeightEventArgs : EventArgs
{
public double ContentHeight { get; set; }
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>{73AB85FB-D11A-43FB-BBC5-54BED5A056D1}</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Gallery.Resources.UI</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)CustomViews.cs" />
<Compile Include="$(MSBuildThisFileDirectory)RoundViews.cs" />
<Compile Include="$(MSBuildThisFileDirectory)OptionCells.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)FlowLayout.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{73AB85FB-D11A-43FB-BBC5-54BED5A056D1}</ProjectGuid>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<Import Project="Gallery.UI.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

157
Gallery.UI/OptionCells.cs Normal file
View File

@ -0,0 +1,157 @@
using System.Collections;
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public class OptionEntry : Entry { }
public class OptionPicker : Picker { }
public abstract class OptionCell : ViewCell
{
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(OptionCell));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
protected abstract View Content { get; }
public OptionCell()
{
View = new Grid
{
BindingContext = this,
Padding = new Thickness(20, 0),
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(.3, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(.7, GridUnitType.Star) }
},
Children =
{
new Label
{
LineBreakMode = LineBreakMode.TailTruncation,
VerticalOptions = LayoutOptions.Center
}
.Binding(Label.TextProperty, nameof(Title))
.DynamicResource(Label.TextColorProperty, Extensions.TextColor),
Content.GridColumn(1)
}
}
.DynamicResource(VisualElement.BackgroundColorProperty, Extensions.OptionTintColor);
}
}
public class OptionTextCell : OptionCell
{
public static readonly BindableProperty DetailProperty = BindableProperty.Create(nameof(Detail), typeof(string), typeof(OptionCell));
public string Detail
{
get => (string)GetValue(DetailProperty);
set => SetValue(DetailProperty, value);
}
protected override View Content =>
new Label
{
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
}
.Binding(Label.TextProperty, nameof(Detail))
.DynamicResource(Label.TextColorProperty, Extensions.SubTextColor);
}
public class OptionSwitchCell : OptionCell
{
public static readonly BindableProperty IsToggledProperty = BindableProperty.Create(nameof(IsToggled), typeof(bool), typeof(OptionSwitchCell));
public bool IsToggled
{
get => (bool)GetValue(IsToggledProperty);
set => SetValue(IsToggledProperty, value);
}
protected override View Content =>
new Switch
{
HorizontalOptions = LayoutOptions.End,
VerticalOptions = LayoutOptions.Center
}
.Binding(Switch.IsToggledProperty, nameof(IsToggled), mode: BindingMode.TwoWay);
}
public class OptionDropCell : OptionCell
{
public static readonly BindableProperty ItemsProperty = BindableProperty.Create(nameof(Items), typeof(IList), typeof(OptionDropCell));
public static readonly BindableProperty SelectedIndexProperty = BindableProperty.Create(nameof(SelectedIndex), typeof(int), typeof(OptionDropCell));
public IList Items
{
get => (IList)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public int SelectedIndex
{
get => (int)GetValue(SelectedIndexProperty);
set => SetValue(SelectedIndexProperty, value);
}
protected override View Content =>
new OptionPicker
{
HorizontalOptions = LayoutOptions.Fill,
VerticalOptions = LayoutOptions.Center
}
.Binding(Picker.ItemsSourceProperty, nameof(Items))
.Binding(Picker.SelectedIndexProperty, nameof(SelectedIndex), mode: BindingMode.TwoWay)
.DynamicResource(Picker.TextColorProperty, Extensions.TextColor)
.DynamicResource(VisualElement.BackgroundColorProperty, Extensions.OptionTintColor);
}
public class OptionEntryCell : OptionCell
{
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(OptionEntryCell));
public static readonly BindableProperty KeyboardProperty = BindableProperty.Create(nameof(Keyboard), typeof(Keyboard), typeof(OptionEntryCell));
public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(OptionEntryCell));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public Keyboard Keyboard
{
get => (Keyboard)GetValue(KeyboardProperty);
set => SetValue(KeyboardProperty, value);
}
public string Placeholder
{
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
protected override View Content =>
new OptionEntry
{
HorizontalOptions = LayoutOptions.Fill,
HorizontalTextAlignment = TextAlignment.End,
VerticalOptions = LayoutOptions.Center,
ReturnType = ReturnType.Next
}
.Binding(Entry.TextProperty, nameof(Text), mode: BindingMode.TwoWay)
.Binding(Entry.PlaceholderProperty, nameof(Placeholder))
.Binding(InputView.KeyboardProperty, nameof(Keyboard))
.DynamicResource(Entry.TextProperty, Extensions.TextColor)
.DynamicResource(Entry.PlaceholderColorProperty, Extensions.SubTextColor)
.DynamicResource(VisualElement.BackgroundColorProperty, Extensions.OptionTintColor);
}
}

63
Gallery.UI/RoundViews.cs Normal file
View File

@ -0,0 +1,63 @@
using Xamarin.Forms;
namespace Gallery.Resources.UI
{
public class RoundImage : Image
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(RoundImage));
public static readonly BindableProperty CornerMasksProperty = BindableProperty.Create(nameof(CornerMasks), typeof(CornerMask), typeof(RoundImage));
public float CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public CornerMask CornerMasks
{
get => (CornerMask)GetValue(CornerMasksProperty);
set => SetValue(CornerMasksProperty, value);
}
}
public class RoundLabel : Label
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(RoundLabel));
public static new readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(RoundLabel), Color.Transparent);
public float CornerRadius
{
get => (float)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public new Color BackgroundColor
{
get => (Color)GetValue(BackgroundColorProperty);
set => SetValue(BackgroundColorProperty, value);
}
}
public enum CornerMask
{
None = 0,
LeftTop = 1,
RightTop = 2,
LeftBottom = 4,
RightBottom = 8,
Top = LeftTop | RightTop, // 3
Left = LeftTop | LeftBottom, // 5
Slash = RightTop | LeftBottom, // 6
BackSlash = LeftTop | RightBottom, // 9
Right = RightTop | RightBottom, // 10
Bottom = LeftBottom | RightBottom, // 12
ExceptRightBottom = LeftTop | RightTop | LeftBottom, // 7
ExceptLeftBottom = LeftTop | RightTop | RightBottom, // 11
ExceptRightTop = LeftTop | LeftBottom | RightBottom, // 13
ExceptLeftTop = RightTop | LeftBottom | RightBottom, // 14
All = LeftTop | RightTop | LeftBottom | RightBottom // 15
}
}

View File

@ -1,53 +1,9 @@
using System; using System;
using Xamarin.Forms;
namespace Gallery.Util namespace Gallery.Util
{ {
public static class Extensions public static class Extensions
{ {
public static T Binding<T>(this T view, BindableProperty property, string name, BindingMode mode = BindingMode.Default, IValueConverter converter = null) where T : BindableObject
{
if (name == null)
{
view.SetValue(property, property.DefaultValue);
}
else
{
view.SetBinding(property, name, mode, converter);
}
return view;
}
public static T DynamicResource<T>(this T view, BindableProperty property, string key) where T : Element
{
view.SetDynamicResource(property, key);
return view;
}
public static T GridRow<T>(this T view, int row) where T : BindableObject
{
Grid.SetRow(view, row);
return view;
}
public static T GridRowSpan<T>(this T view, int rowSpan) where T : BindableObject
{
Grid.SetRowSpan(view, rowSpan);
return view;
}
public static T GridColumn<T>(this T view, int column) where T : BindableObject
{
Grid.SetColumn(view, column);
return view;
}
public static T GridColumnSpan<T>(this T view, int columnSpan) where T : BindableObject
{
Grid.SetColumnSpan(view, columnSpan);
return view;
}
public static int IndexOf<T>(this T[] array, Predicate<T> predicate) public static int IndexOf<T>(this T[] array, Predicate<T> predicate)
{ {
for (var i = 0; i < array.Length; i++) for (var i = 0; i < array.Length; i++)

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Gallery.Util.Model; using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Util.Interface namespace Gallery.Util.Interface
{ {
@ -7,10 +8,16 @@ namespace Gallery.Util.Interface
{ {
string Name { get; } string Name { get; }
string Route { get; }
string FlyoutIconKey { get; }
string HomePage { get; } string HomePage { get; }
void SetCookie(); void SetCookie();
void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark);
Task<GalleryItem[]> GetRecentItemsAsync(int page); Task<GalleryItem[]> GetRecentItemsAsync(int page);
} }
} }

View File

@ -14,14 +14,41 @@ namespace Gallery.Util.Model
public static readonly BindableProperty UserNameProperty = BindableProperty.Create(nameof(UserName), typeof(string), typeof(GalleryItem)); public static readonly BindableProperty UserNameProperty = BindableProperty.Create(nameof(UserName), typeof(string), typeof(GalleryItem));
public static readonly BindableProperty CreatedTimeProperty = BindableProperty.Create(nameof(CreatedTime), typeof(DateTime), typeof(GalleryItem)); public static readonly BindableProperty CreatedTimeProperty = BindableProperty.Create(nameof(CreatedTime), typeof(DateTime), typeof(GalleryItem));
public static readonly BindableProperty UpdatedTimeProperty = BindableProperty.Create(nameof(UpdatedTime), typeof(DateTime), typeof(GalleryItem)); public static readonly BindableProperty UpdatedTimeProperty = BindableProperty.Create(nameof(UpdatedTime), typeof(DateTime), typeof(GalleryItem));
public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create(nameof(ImageHeight), typeof(GridLength), typeof(GalleryItem), GridLength.Auto); public static readonly BindableProperty ImageHeightProperty = BindableProperty.Create(nameof(ImageHeight), typeof(GridLength), typeof(GalleryItem),
defaultValue: GridLength.Auto);
public static readonly BindableProperty IsFavoriteProperty = BindableProperty.Create(nameof(IsFavorite), typeof(bool), typeof(GalleryItem));
public static readonly BindableProperty BookmarkIdProperty = BindableProperty.Create(nameof(BookmarkId), typeof(string), typeof(GalleryItem));
[JsonIgnore] [JsonIgnore]
public string TagDescription { get; set; } public string TagDescription
{
get => (string)GetValue(TagDescriptionProperty);
set => SetValue(TagDescriptionProperty, value);
}
[JsonIgnore] [JsonIgnore]
public ImageSource PreviewImage { get; set; } public ImageSource PreviewImage
{
get => (ImageSource)GetValue(PreviewImageProperty);
set => SetValue(PreviewImageProperty, value);
}
[JsonIgnore] [JsonIgnore]
public GridLength ImageHeight { get; set; } public GridLength ImageHeight
{
get => (GridLength)GetValue(ImageHeightProperty);
set => SetValue(ImageHeightProperty, value);
}
[JsonIgnore]
public bool IsFavorite
{
get => (bool)GetValue(IsFavoriteProperty);
set => SetValue(IsFavoriteProperty, value);
}
[JsonIgnore]
public string BookmarkId
{
get => (string)GetValue(BookmarkIdProperty);
set => SetValue(BookmarkIdProperty, value);
}
public long Id { get; internal set; } public long Id { get; internal set; }
private string[] tags; private string[] tags;

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
@ -90,6 +92,138 @@ namespace Gallery.Util
} }
} }
public static async Task<string> DownloadImage(string url, string working, string folder)
{
try
{
var directory = Path.Combine(working, folder);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, Path.GetFileName(url));
var response = await Request(url, headers =>
{
headers.Add("User-Agent", Config.UserAgent);
headers.Add("Accept", Config.AcceptImage);
});
if (response == null)
{
return null;
}
using (response)
using (var fs = File.OpenWrite(file))
{
await response.Content.CopyToAsync(fs);
}
return file;
}
catch (Exception ex)
{
Log.Error("image.download", ex.Message);
return null;
}
}
public static async Task<string> DownloadImageAsync(string url, string id, string working, string folder)
{
try
{
var directory = Path.Combine(working, folder);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var file = Path.Combine(directory, Path.GetFileName(url));
var proxy = Config.Proxy;
var handler = new HttpClientHandler
{
UseCookies = false
};
if (proxy != null)
{
handler.Proxy = proxy;
handler.UseProxy = true;
}
var client = new HttpClient(handler, true)
{
Timeout = Config.Timeout
};
long size;
DateTimeOffset lastModified;
using (var request = new HttpRequestMessage(HttpMethod.Head, url))
{
var headers = request.Headers;
headers.Add("Accept", Config.AcceptImage);
headers.Add("Accept-Language", Config.AcceptLanguage);
headers.Add("User-Agent", Config.UserAgent);
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
size = response.Content.Headers.ContentLength.Value;
lastModified = response.Content.Headers.LastModified.Value;
#if DEBUG
Log.Print($"content length: {size:n0} bytes, last modified: {lastModified}");
#endif
}
// segments
const int SIZE = 150000;
var list = new List<(long from, long to)>();
for (long i = 0; i < size; i += SIZE)
{
long to;
if (i + SIZE >= size)
{
to = size - 1;
}
else
{
to = i + SIZE - 1;
}
list.Add((i, to));
}
var data = new byte[size];
var task = new TaskCompletionSource<string>();
ParallelTask.Start($"download.async.{id}", 0, list.Count, 2, i =>
{
var (from, to) = list[i];
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
var headers = request.Headers;
headers.Add("Accept", Config.AcceptImage);
headers.Add("Accept-Language", Config.AcceptLanguage);
headers.Add("Accept-Encoding", "identity");
headers.IfRange = new RangeConditionHeaderValue(lastModified);
headers.Range = new RangeHeaderValue(from, to);
headers.Add("User-Agent", Config.UserAgent);
using var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result;
using var ms = new MemoryStream(data, (int)from, (int)(to - from + 1));
response.Content.CopyToAsync(ms).Wait();
#if DEBUG
Log.Print($"downloaded range: from({from:n0}) to ({to:n0})");
#endif
}
return true;
},
complete: o =>
{
using (var fs = File.OpenWrite(file))
{
fs.Write(data, 0, data.Length);
}
task.SetResult(file);
});
return await task.Task;
}
catch (Exception ex)
{
Log.Error("image.download.async", $"failed to download image, error: {ex.Message}");
return null;
}
}
private static async Task<HttpResponseMessage> Request(string url, Action<HttpRequestHeaders> headerHandler, HttpContent post = null) private static async Task<HttpResponseMessage> Request(string url, Action<HttpRequestHeaders> headerHandler, HttpContent post = null)
{ {
#if DEBUG #if DEBUG

View File

@ -1,6 +1,9 @@
using System; using System;
using System.IO;
using System.Net; using System.Net;
using System.Threading.Tasks;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms;
namespace Gallery.Util namespace Gallery.Util
{ {
@ -8,6 +11,65 @@ namespace Gallery.Util
{ {
public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); public static readonly string PersonalFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
public static readonly string CacheFolder = FileSystem.CacheDirectory; public static readonly string CacheFolder = FileSystem.CacheDirectory;
private const string imageFolder = "img-original";
private const string previewFolder = "img-preview";
public static async Task<ImageSource> LoadRawImage(string url)
{
return await LoadImageAsync(url, null, PersonalFolder, imageFolder, force: true);
}
public static async Task<ImageSource> LoadPreviewImage(string url, bool downloading, bool force = false)
{
return await LoadImage(url, CacheFolder, previewFolder, downloading, force: force);
}
private static async Task<ImageSource> LoadImage(string url, string working, string folder, bool downloading, bool force = false)
{
var file = Path.Combine(working, folder, Path.GetFileName(url));
ImageSource image;
if (!force && File.Exists(file))
{
image = ImageSource.FromFile(file);
}
else
{
image = null;
}
if (downloading && image == null)
{
file = await NetHelper.DownloadImage(url, working, folder);
if (file != null)
{
return ImageSource.FromFile(file);
}
}
return image;
}
private static async Task<ImageSource> LoadImageAsync(string url, string id, string working, string folder, bool force = false)
{
var file = Path.Combine(working, folder, Path.GetFileName(url));
ImageSource image;
if (!force && File.Exists(file))
{
image = ImageSource.FromFile(file);
}
else
{
image = null;
}
if (image == null)
{
file = await NetHelper.DownloadImageAsync(url, id, working, folder);
if (file != null)
{
image = ImageSource.FromFile(file);
}
}
return image;
}
} }
public static class Config public static class Config
@ -20,7 +82,14 @@ namespace Gallery.Util
public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"; public const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36";
public const string AcceptLanguage = "zh-cn"; public const string AcceptLanguage = "zh-cn";
public const string AcceptImage = "image/png,image/*,*/*;q=0.8";
public static WebProxy Proxy; public static WebProxy Proxy;
} }
public static class Routes
{
public const string Gallery = "gallery";
public const string Option = "option";
}
} }

View File

@ -1,115 +1,114 @@
{ {
"images": [ "images": [
{ {
"scale": "2x", "filename": "Icon40.png",
"size": "20x20", "size": "20x20",
"idiom": "iphone", "scale": "2x",
"filename": "Icon40.png" "idiom": "iphone"
}, },
{ {
"scale": "3x", "filename": "Icon60.png",
"size": "20x20", "size": "20x20",
"idiom": "iphone",
"filename": "Icon60.png"
},
{
"scale": "2x",
"size": "29x29",
"idiom": "iphone",
"filename": "Icon58.png"
},
{
"scale": "3x", "scale": "3x",
"idiom": "iphone"
},
{
"filename": "Icon58.png",
"size": "29x29", "size": "29x29",
"idiom": "iphone",
"filename": "Icon87.png"
},
{
"scale": "2x", "scale": "2x",
"size": "40x40", "idiom": "iphone"
"idiom": "iphone",
"filename": "Icon80.png"
}, },
{ {
"filename": "Icon87.png",
"size": "29x29",
"scale": "3x", "scale": "3x",
"size": "40x40", "idiom": "iphone"
"idiom": "iphone",
"filename": "Icon120.png"
}, },
{ {
"filename": "Icon80.png",
"size": "40x40",
"scale": "2x", "scale": "2x",
"idiom": "iphone"
},
{
"filename": "Icon120.png",
"size": "40x40",
"scale": "3x",
"idiom": "iphone"
},
{
"filename": "Icon120.png",
"size": "60x60", "size": "60x60",
"idiom": "iphone", "scale": "2x",
"filename": "Icon120.png" "idiom": "iphone"
}, },
{ {
"scale": "3x", "filename": "Icon180.png",
"size": "60x60", "size": "60x60",
"idiom": "iphone", "scale": "3x",
"filename": "Icon180.png" "idiom": "iphone"
}, },
{ {
"scale": "1x", "filename": "Icon20.png",
"size": "20x20", "size": "20x20",
"idiom": "ipad", "scale": "1x",
"filename": "Icon20.png" "idiom": "ipad"
}, },
{ {
"scale": "2x", "filename": "Icon40.png",
"size": "20x20", "size": "20x20",
"idiom": "ipad", "scale": "2x",
"filename": "Icon40.png" "idiom": "ipad"
}, },
{ {
"scale": "1x", "filename": "Icon29.png",
"size": "29x29", "size": "29x29",
"idiom": "ipad", "scale": "1x",
"filename": "Icon29.png" "idiom": "ipad"
}, },
{ {
"scale": "2x", "filename": "Icon58.png",
"size": "29x29", "size": "29x29",
"idiom": "ipad", "scale": "2x",
"filename": "Icon58.png" "idiom": "ipad"
}, },
{ {
"scale": "1x", "filename": "Icon40.png",
"size": "40x40", "size": "40x40",
"idiom": "ipad",
"filename": "Icon40.png"
},
{
"scale": "2x",
"size": "40x40",
"idiom": "ipad",
"filename": "Icon80.png"
},
{
"scale": "1x", "scale": "1x",
"size": "76x76", "idiom": "ipad"
"idiom": "ipad",
"filename": "Icon76.png"
}, },
{ {
"filename": "Icon80.png",
"size": "40x40",
"scale": "2x", "scale": "2x",
"size": "76x76", "idiom": "ipad"
"idiom": "ipad",
"filename": "Icon152.png"
}, },
{ {
"scale": "2x", "filename": "Icon167.png",
"size": "83.5x83.5", "size": "83.5x83.5",
"idiom": "ipad", "scale": "2x",
"filename": "Icon167.png" "idiom": "ipad"
}, },
{ {
"filename": "Icon76.png",
"size": "76x76",
"scale": "1x", "scale": "1x",
"idiom": "ipad"
},
{
"filename": "Icon152.png",
"size": "76x76",
"scale": "2x",
"idiom": "ipad"
},
{
"filename": "Icon1024.png",
"size": "1024x1024", "size": "1024x1024",
"idiom": "ios-marketing", "scale": "1x",
"filename": "Icon1024.png" "idiom": "ios-marketing"
} }
], ],
"properties": {},
"info": { "info": {
"version": 1, "version": 1,
"author": "xcode" "author": "xcode"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 B

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,26 @@
{
"images": [
{
"idiom": "universal"
},
{
"filename": "source-solid.png",
"scale": "1x",
"idiom": "universal"
},
{
"filename": "source-solid@2x.png",
"scale": "2x",
"idiom": "universal"
},
{
"filename": "source-solid@3x.png",
"scale": "3x",
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1,26 @@
{
"images": [
{
"idiom": "universal"
},
{
"filename": "source-regular.png",
"scale": "1x",
"idiom": "universal"
},
{
"filename": "source-regular@2x.png",
"scale": "2x",
"idiom": "universal"
},
{
"filename": "source-regular@3x.png",
"scale": "3x",
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

View File

@ -0,0 +1,26 @@
{
"images": [
{
"idiom": "universal"
},
{
"filename": "yandere-solid.png",
"scale": "1x",
"idiom": "universal"
},
{
"filename": "yandere-solid@2x.png",
"scale": "2x",
"idiom": "universal"
},
{
"filename": "yandere-solid@3x.png",
"scale": "3x",
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

View File

@ -0,0 +1,26 @@
{
"images": [
{
"idiom": "universal"
},
{
"filename": "yandere-regular.png",
"scale": "1x",
"idiom": "universal"
},
{
"filename": "yandere-regular@2x.png",
"scale": "2x",
"idiom": "universal"
},
{
"filename": "yandere-regular@3x.png",
"scale": "3x",
"idiom": "universal"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

View File

@ -80,63 +80,58 @@
<None Include="Entitlements.plist" /> <None Include="Entitlements.plist" />
<None Include="Info.plist" /> <None Include="Info.plist" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Renderers\AppShellSection\AppAppearanceTracker.cs" />
<Compile Include="Renderers\AppShellSection\AppShellSectionRootHeader.cs" />
<Compile Include="Renderers\AppShellRenderer.cs" />
<Compile Include="Renderers\BlurryPanelRenderer.cs" />
<Compile Include="Renderers\CardViewRenderer.cs" />
<Compile Include="Renderers\CircleImageRenderer.cs" />
<Compile Include="Renderers\OptionEntryRenderer.cs" />
<Compile Include="Renderers\OptionPickerRenderer.cs" />
<Compile Include="Renderers\RoundImageRenderer.cs" />
<Compile Include="Renderers\RoundLabelRenderer.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json">
<Visible>false</Visible> <Visible>false</Visible>
</ImageAsset> </ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon1024.png"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon20.png" />
<Visible>false</Visible> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon29.png" />
</ImageAsset> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon40.png" />
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon180.png"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon58.png" />
<Visible>false</Visible> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon60.png" />
</ImageAsset> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon76.png" />
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon167.png"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon80.png" />
<Visible>false</Visible> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon87.png" />
</ImageAsset> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon120.png" />
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon152.png"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon152.png" />
<Visible>false</Visible> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon167.png" />
</ImageAsset> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon180.png" />
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon120.png"> <ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon1024.png" />
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon87.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon80.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon76.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon60.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon58.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon40.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon29.png">
<Visible>false</Visible>
</ImageAsset>
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Icon20.png">
<Visible>false</Visible>
</ImageAsset>
<BundleResource Include="Resources\icon_about.png" />
<BundleResource Include="Resources\icon_about%402x.png" />
<BundleResource Include="Resources\icon_about%403x.png" />
<BundleResource Include="Resources\icon_feed.png" />
<BundleResource Include="Resources\icon_feed%402x.png" />
<BundleResource Include="Resources\icon_feed%403x.png" />
<BundleResource Include="Resources\xamarin_logo.png" /> <BundleResource Include="Resources\xamarin_logo.png" />
<BundleResource Include="Resources\xamarin_logo%402x.png" /> <BundleResource Include="Resources\xamarin_logo%402x.png" />
<BundleResource Include="Resources\xamarin_logo%403x.png" /> <BundleResource Include="Resources\xamarin_logo%403x.png" />
<InterfaceDefinition Include="Resources\LaunchScreen.storyboard" />
<BundleResource Include="Resources\fa-light-300.ttf" /> <BundleResource Include="Resources\fa-light-300.ttf" />
<BundleResource Include="Resources\fa-regular-400.ttf" /> <BundleResource Include="Resources\fa-regular-400.ttf" />
<BundleResource Include="Resources\fa-solid-900.ttf" /> <BundleResource Include="Resources\fa-solid-900.ttf" />
<BundleResource Include="Resources\download.png" />
<ImageAsset Include="Assets.xcassets\IconSource.imageset\Contents.json" />
<ImageAsset Include="Assets.xcassets\IconSource.imageset\source-solid.png" />
<ImageAsset Include="Assets.xcassets\IconSource.imageset\source-solid%402x.png" />
<ImageAsset Include="Assets.xcassets\IconSource.imageset\source-solid%403x.png" />
<ImageAsset Include="Assets.xcassets\IconSourceRegular.imageset\Contents.json" />
<ImageAsset Include="Assets.xcassets\IconSourceRegular.imageset\source-regular.png" />
<ImageAsset Include="Assets.xcassets\IconSourceRegular.imageset\source-regular%402x.png" />
<ImageAsset Include="Assets.xcassets\IconSourceRegular.imageset\source-regular%403x.png" />
<ImageAsset Include="Assets.xcassets\IconYandere.imageset\Contents.json" />
<ImageAsset Include="Assets.xcassets\IconYandere.imageset\yandere-solid.png" />
<ImageAsset Include="Assets.xcassets\IconYandere.imageset\yandere-solid%402x.png" />
<ImageAsset Include="Assets.xcassets\IconYandere.imageset\yandere-solid%403x.png" />
<ImageAsset Include="Assets.xcassets\IconYandereRegular.imageset\Contents.json" />
<ImageAsset Include="Assets.xcassets\IconYandereRegular.imageset\yandere-regular.png" />
<ImageAsset Include="Assets.xcassets\IconYandereRegular.imageset\yandere-regular%402x.png" />
<ImageAsset Include="Assets.xcassets\IconYandereRegular.imageset\yandere-regular%403x.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
@ -168,6 +163,15 @@
<Name>Gallery.Danbooru</Name> <Name>Gallery.Danbooru</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Renderers\" />
<Folder Include="Renderers\AppShellSection\" />
<Folder Include="Assets.xcassets\IconYandere.imageset\" />
<Folder Include="Assets.xcassets\IconSource.imageset\" />
<Folder Include="Assets.xcassets\IconYandereRegular.imageset\" />
<Folder Include="Assets.xcassets\IconSourceRegular.imageset\" />
</ItemGroup>
<Import Project="..\Gallery.UI\Gallery.UI.projitems" Label="Shared" Condition="Exists('..\Gallery.UI\Gallery.UI.projitems')" />
<Import Project="..\Gallery.Share\Gallery.Share.projitems" Label="Shared" Condition="Exists('..\Gallery.Share\Gallery.Share.projitems')" /> <Import Project="..\Gallery.Share\Gallery.Share.projitems" Label="Shared" Condition="Exists('..\Gallery.Share\Gallery.Share.projitems')" />
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project> </Project>

View File

@ -0,0 +1,93 @@
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Gallery.iOS.Renderers;
using Gallery.iOS.Renderers.AppShellSection;
using Gallery.Services;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(Shell), typeof(AppShellRenderer))]
namespace Gallery.iOS.Renderers
{
public class AppShellRenderer : ShellRenderer
{
public override bool PrefersHomeIndicatorAutoHidden => Screen.GetHomeIndicatorAutoHidden(Element);
protected override IShellSectionRenderer CreateShellSectionRenderer(ShellSection shellSection)
{
var renderer = base.CreateShellSectionRenderer(shellSection); // new AppShellSectionRenderer(this);
if (renderer is ShellSectionRenderer sr && Element is AppShell shell)
{
shell.SetNavigationBarHeight(sr.NavigationBar.Frame.Height);
shell.SetStatusBarHeight(
sr.NavigationBar.Frame.Height,
UIApplication.SharedApplication.StatusBarFrame.Height);
}
return renderer;
}
protected override IShellItemTransition CreateShellItemTransition()
{
return new AppShellItemTransition();
}
protected override IShellTabBarAppearanceTracker CreateTabBarAppearanceTracker()
{
return new AppShellTabBarAppearanceTracker();
}
protected override IShellNavBarAppearanceTracker CreateNavBarAppearanceTracker()
{
return new AppShellNavBarAppearanceTracker();
}
protected override void UpdateBackgroundColor()
{
NativeView.BackgroundColor = Color.Transparent.ToUIColor();
}
}
public class AppShellItemTransition : IShellItemTransition
{
[SuppressMessage("Code Notifications", "XI0001:Notifies you with advices on how to use Apple APIs", Justification = "<Pending>")]
public Task Transition(IShellItemRenderer oldRenderer, IShellItemRenderer newRenderer)
{
var task = new TaskCompletionSource<bool>();
var oldView = oldRenderer.ViewController.View;
var newView = newRenderer.ViewController.View;
newView.Alpha = 0;
newView.Superview.InsertSubviewAbove(newView, oldView);
UIView.Animate(0.2, 0, UIViewAnimationOptions.BeginFromCurrentState, () => newView.Alpha = 1, () => task.TrySetResult(true));
return task.Task;
}
}
public class AppShellSectionRenderer : ShellSectionRenderer
{
public AppShellSectionRenderer(IShellContext context) : base(context)
{
}
protected override IShellSectionRootRenderer CreateShellSectionRootRenderer(ShellSection shellSection, IShellContext shellContext)
{
return new AppShellSectionRootRenderer(shellSection, shellContext);
}
}
public class AppShellSectionRootRenderer : ShellSectionRootRenderer
{
public AppShellSectionRootRenderer(ShellSection shellSection, IShellContext shellContext) : base(shellSection, shellContext)
{
}
protected override IShellSectionRootHeader CreateShellSectionRootHeader(IShellContext shellContext)
{
return new AppShellSectionRootHeader(shellContext);
}
}
}

View File

@ -0,0 +1,168 @@
using System.Diagnostics.CodeAnalysis;
using CoreGraphics;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
namespace Gallery.iOS.Renderers.AppShellSection
{
public class AppShellNavBarAppearanceTracker : IShellNavBarAppearanceTracker
{
UIColor _defaultBarTint;
UIColor _defaultTint;
UIStringAttributes _defaultTitleAttributes;
float _shadowOpacity = float.MinValue;
CGColor _shadowColor;
public void ResetAppearance(UINavigationController controller)
{
if (_defaultTint != null)
{
var navBar = controller.NavigationBar;
navBar.TintColor = _defaultBarTint;
navBar.TintColor = _defaultTint;
navBar.TitleTextAttributes = _defaultTitleAttributes;
}
}
public void SetAppearance(UINavigationController controller, ShellAppearance appearance)
{
var background = appearance.BackgroundColor;
var foreground = appearance.ForegroundColor;
var titleColor = appearance.TitleColor;
var navBar = controller.NavigationBar;
if (_defaultTint == null)
{
_defaultBarTint = navBar.BarTintColor;
_defaultTint = navBar.TintColor;
_defaultTitleAttributes = navBar.TitleTextAttributes;
}
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
navBar.TintColor = UIColor.SecondaryLabelColor;
}
else
{
if (!background.IsDefault)
navBar.BarTintColor = background.ToUIColor();
if (!foreground.IsDefault)
navBar.TintColor = foreground.ToUIColor();
if (!titleColor.IsDefault)
{
navBar.TitleTextAttributes = new UIStringAttributes
{
ForegroundColor = titleColor.ToUIColor()
};
}
}
}
public void SetHasShadow(UINavigationController controller, bool hasShadow)
{
var navigationBar = controller.NavigationBar;
if (_shadowOpacity == float.MinValue)
{
// Don't do anything if user hasn't changed the shadow to true
if (!hasShadow)
return;
_shadowOpacity = navigationBar.Layer.ShadowOpacity;
_shadowColor = navigationBar.Layer.ShadowColor;
}
if (hasShadow)
{
navigationBar.Layer.ShadowColor = UIColor.Black.CGColor;
navigationBar.Layer.ShadowOpacity = 1.0f;
}
else
{
navigationBar.Layer.ShadowColor = _shadowColor;
navigationBar.Layer.ShadowOpacity = _shadowOpacity;
}
}
public void Dispose()
{
}
public void UpdateLayout(UINavigationController controller)
{
}
}
public class AppShellTabBarAppearanceTracker : IShellTabBarAppearanceTracker
{
UIColor _defaultBarTint;
UIColor _defaultTint;
UIColor _defaultUnselectedTint;
public void ResetAppearance(UITabBarController controller)
{
if (_defaultTint == null)
return;
var tabBar = controller.TabBar;
tabBar.BarTintColor = _defaultBarTint;
tabBar.TintColor = _defaultTint;
tabBar.UnselectedItemTintColor = _defaultUnselectedTint;
}
public void SetAppearance(UITabBarController controller, ShellAppearance appearance)
{
IShellAppearanceElement appearanceElement = appearance;
var backgroundColor = appearanceElement.EffectiveTabBarBackgroundColor;
var unselectedColor = appearanceElement.EffectiveTabBarUnselectedColor;
var tintColor = appearanceElement.EffectiveTabBarForegroundColor; // appearanceElement.EffectiveTabBarTitleColor;
var tabBar = controller.TabBar;
if (_defaultTint == null)
{
_defaultBarTint = tabBar.BarTintColor;
_defaultTint = tabBar.TintColor;
_defaultUnselectedTint = tabBar.UnselectedItemTintColor;
}
if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
{
tabBar.TintColor = UIColor.LabelColor;
//tabBar.UnselectedItemTintColor = UIColor.TertiaryLabelColor;
}
else
{
if (!backgroundColor.IsDefault)
tabBar.BarTintColor = backgroundColor.ToUIColor();
if (!tintColor.IsDefault)
tabBar.TintColor = tintColor.ToUIColor();
if (!unselectedColor.IsDefault)
tabBar.UnselectedItemTintColor = unselectedColor.ToUIColor();
}
}
public void Dispose()
{
}
[SuppressMessage("Code Notifications", "XI0001:Notifies you with advices on how to use Apple APIs", Justification = "<Pending>")]
public void UpdateLayout(UITabBarController controller)
{
var tabBar = controller.TabBar;
if (tabBar != null && tabBar.Items != null && tabBar.Items.Length == 3)
{
var tabBarItem = tabBar.Items[0];
tabBarItem.Image = UIImage.FromBundle("IconYandereRegular");
tabBarItem.SelectedImage = UIImage.FromBundle("IconYandere");
tabBarItem = tabBar.Items[1];
tabBarItem.Image = UIImage.FromBundle("IconSourceRegular");
tabBarItem.SelectedImage = UIImage.FromBundle("IconSource");
tabBarItem = tabBar.Items[2];
tabBarItem.Image = UIImage.FromBundle("IconSourceRegular");
tabBarItem.SelectedImage = UIImage.FromBundle("IconSource");
}
}
}
}

View File

@ -0,0 +1,325 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
namespace Gallery.iOS.Renderers.AppShellSection
{
public class AppShellSectionRootHeader : UICollectionViewController, IAppearanceObserver, IShellSectionRootHeader
{
#region IAppearanceObserver
readonly Color _defaultBackgroundColor = new(0.964);
readonly Color _defaultForegroundColor = Color.Black;
readonly Color _defaultUnselectedColor = Color.Black.MultiplyAlpha(0.7);
void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance)
{
if (appearance == null)
ResetAppearance();
else
SetAppearance(appearance);
}
protected virtual void ResetAppearance()
{
SetValues(_defaultBackgroundColor, _defaultForegroundColor, _defaultUnselectedColor);
}
protected virtual void SetAppearance(ShellAppearance appearance)
{
SetValues(appearance.BackgroundColor.IsDefault ? _defaultBackgroundColor : appearance.BackgroundColor,
appearance.ForegroundColor.IsDefault ? _defaultForegroundColor : appearance.ForegroundColor,
appearance.UnselectedColor.IsDefault ? _defaultUnselectedColor : appearance.UnselectedColor);
}
void SetValues(Color backgroundColor, Color foregroundColor, Color unselectedColor)
{
CollectionView.BackgroundColor = new Color(backgroundColor.R, backgroundColor.G, backgroundColor.B, .663).ToUIColor();
_bar.BackgroundColor = foregroundColor.ToUIColor();
bool reloadData = _selectedColor != foregroundColor || _unselectedColor != unselectedColor;
_selectedColor = foregroundColor;
_unselectedColor = unselectedColor;
if (reloadData)
CollectionView.ReloadData();
}
#endregion IAppearanceObserver
static readonly NSString CellId = new("HeaderCell");
readonly IShellContext _shellContext;
UIView _bar;
UIView _bottomShadow;
Color _selectedColor;
Color _unselectedColor;
bool _isDisposed;
public AppShellSectionRootHeader(IShellContext shellContext) : base(new UICollectionViewFlowLayout())
{
_shellContext = shellContext;
}
public double SelectedIndex { get; set; }
public ShellSection ShellSection { get; set; }
IShellSectionController ShellSectionController => ShellSection;
public UIViewController ViewController => this;
public override bool CanMoveItem(UICollectionView collectionView, NSIndexPath indexPath)
{
return false;
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var reusedCell = (UICollectionViewCell)collectionView.DequeueReusableCell(CellId, indexPath);
if (reusedCell is not ShellSectionHeaderCell headerCell)
return reusedCell;
var selectedItems = collectionView.GetIndexPathsForSelectedItems();
var shellContent = ShellSectionController.GetItems()[indexPath.Row];
headerCell.Label.Text = shellContent.Title;
headerCell.Label.SetNeedsDisplay();
headerCell.SelectedColor = _selectedColor.ToUIColor();
headerCell.UnSelectedColor = _unselectedColor.ToUIColor();
if (selectedItems.Length > 0 && selectedItems[0].Row == indexPath.Row)
headerCell.Selected = true;
else
headerCell.Selected = false;
return headerCell;
}
public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
return ShellSectionController.GetItems().Count;
}
public override void ItemDeselected(UICollectionView collectionView, NSIndexPath indexPath)
{
if (CollectionView.CellForItem(indexPath) is ShellSectionHeaderCell cell)
cell.Label.TextColor = _unselectedColor.ToUIColor();
}
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
var row = indexPath.Row;
var item = ShellSectionController.GetItems()[row];
if (item != ShellSection.CurrentItem)
ShellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, item);
if (CollectionView.CellForItem(indexPath) is ShellSectionHeaderCell cell)
cell.Label.TextColor = _selectedColor.ToUIColor();
}
public override nint NumberOfSections(UICollectionView collectionView)
{
return 1;
}
public override bool ShouldSelectItem(UICollectionView collectionView, NSIndexPath indexPath)
{
var row = indexPath.Row;
var item = ShellSectionController.GetItems()[row];
IShellController shellController = _shellContext.Shell;
if (item == ShellSection.CurrentItem)
return true;
return shellController.ProposeNavigation(ShellNavigationSource.ShellContentChanged, (ShellItem)ShellSection.Parent, ShellSection, item, ShellSection.Stack, true);
}
public override void ViewDidLayoutSubviews()
{
if (_isDisposed)
return;
base.ViewDidLayoutSubviews();
LayoutBar();
_bottomShadow.Frame = new CGRect(0, CollectionView.Frame.Bottom, CollectionView.Frame.Width, 0.5);
}
public override void ViewDidLoad()
{
if (_isDisposed)
return;
base.ViewDidLoad();
CollectionView.ScrollsToTop = false;
CollectionView.Bounces = false;
CollectionView.AlwaysBounceHorizontal = false;
CollectionView.ShowsHorizontalScrollIndicator = false;
CollectionView.ClipsToBounds = false;
_bar = new UIView(new CGRect(0, 0, 20, 20));
_bar.Layer.ZPosition = 9001; //its over 9000!
CollectionView.AddSubview(_bar);
_bottomShadow = new UIView(new CGRect(0, 0, 10, 1))
{
BackgroundColor = Color.Black.MultiplyAlpha(0.3).ToUIColor()
};
_bottomShadow.Layer.ZPosition = 9002;
CollectionView.AddSubview(_bottomShadow);
var flowLayout = Layout as UICollectionViewFlowLayout;
flowLayout.ScrollDirection = UICollectionViewScrollDirection.Horizontal;
flowLayout.MinimumInteritemSpacing = 0;
flowLayout.MinimumLineSpacing = 0;
flowLayout.EstimatedItemSize = new CGSize(70, 35);
CollectionView.RegisterClassForCell(GetCellType(), CellId);
((IShellController)_shellContext.Shell).AddAppearanceObserver(this, ShellSection);
ShellSectionController.ItemsCollectionChanged += OnShellSectionItemsChanged;
UpdateSelectedIndex();
ShellSection.PropertyChanged += OnShellSectionPropertyChanged;
}
protected virtual Type GetCellType()
{
return typeof(ShellSectionHeaderCell);
}
protected override void Dispose(bool disposing)
{
if (_isDisposed)
return;
if (disposing)
{
((IShellController)_shellContext.Shell).RemoveAppearanceObserver(this);
ShellSectionController.ItemsCollectionChanged -= OnShellSectionItemsChanged;
ShellSection.PropertyChanged -= OnShellSectionPropertyChanged;
ShellSection = null;
_bar.RemoveFromSuperview();
RemoveFromParentViewController();
_bar.Dispose();
_bar = null;
}
_isDisposed = true;
base.Dispose(disposing);
}
protected void LayoutBar()
{
if (SelectedIndex < 0)
return;
if (ShellSectionController.GetItems().IndexOf(ShellSection.CurrentItem) != SelectedIndex)
return;
var layout = CollectionView.GetLayoutAttributesForItem(NSIndexPath.FromItemSection((int)SelectedIndex, 0));
if (layout == null)
return;
var frame = layout.Frame;
if (_bar.Frame.Height != 2)
{
_bar.Frame = new CGRect(frame.X, frame.Bottom - 2, frame.Width, 2);
}
else
{
UIView.Animate(.25, () => _bar.Frame = new CGRect(frame.X, frame.Bottom - 2, frame.Width, 2));
}
}
protected virtual void OnShellSectionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == ShellSection.CurrentItemProperty.PropertyName)
{
UpdateSelectedIndex();
}
}
protected virtual void UpdateSelectedIndex(bool animated = false)
{
if (ShellSection.CurrentItem == null)
return;
SelectedIndex = ShellSectionController.GetItems().IndexOf(ShellSection.CurrentItem);
if (SelectedIndex < 0)
return;
LayoutBar();
CollectionView.SelectItem(NSIndexPath.FromItemSection((int)SelectedIndex, 0), false, UICollectionViewScrollPosition.CenteredHorizontally);
}
void OnShellSectionItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_isDisposed)
return;
CollectionView.ReloadData();
}
public class ShellSectionHeaderCell : UICollectionViewCell
{
public UIColor SelectedColor { get; set; }
public UIColor UnSelectedColor { get; set; }
public ShellSectionHeaderCell()
{
}
[Export("initWithFrame:")]
public ShellSectionHeaderCell(CGRect frame) : base(frame)
{
Label = new UILabel
{
TextAlignment = UITextAlignment.Center,
Font = UIFont.BoldSystemFontOfSize(14)
};
ContentView.AddSubview(Label);
}
public override bool Selected
{
get => base.Selected;
set
{
base.Selected = value;
Label.TextColor = value ? SelectedColor : UnSelectedColor;
}
}
public UILabel Label { get; }
public override void LayoutSubviews()
{
base.LayoutSubviews();
Label.Frame = Bounds;
}
public override CGSize SizeThatFits(CGSize size)
{
return new CGSize(Label.SizeThatFits(size).Width + 30, 35);
}
}
}
}

View File

@ -0,0 +1,87 @@
using CoreAnimation;
using Gallery.iOS.Renderers;
using Gallery.Resources.UI;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(BlurryPanel), typeof(BlurryPanelRenderer))]
namespace Gallery.iOS.Renderers
{
public class BlurryPanelRenderer : ViewRenderer<BlurryPanel, UIVisualEffectView>
{
private UIVisualEffectView nativeControl;
private CALayer bottom;
protected override void OnElementChanged(ElementChangedEventArgs<BlurryPanel> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
if (bottom != null)
{
if (bottom.SuperLayer != null)
{
bottom.RemoveFromSuperLayer();
}
bottom.Dispose();
bottom = null;
}
}
if (e.NewElement != null)
{
e.NewElement.BackgroundColor = Color.Default;
if (Control == null)
{
var blur = UIBlurEffect.FromStyle(UIBlurEffectStyle.SystemMaterial);
nativeControl = new UIVisualEffectView(blur)
{
Frame = Frame
};
SetNativeControl(nativeControl);
}
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (nativeControl != null)
{
if (bottom == null)
{
bottom = new CALayer
{
BackgroundColor = UIColor.White.CGColor,
ShadowColor = UIColor.Black.CGColor,
ShadowOpacity = 1.0f
};
}
if (bottom.SuperLayer == null)
{
nativeControl.Layer.InsertSublayer(bottom, 0);
}
bottom.Frame = new CoreGraphics.CGRect(0, Frame.Height - 5, Frame.Width, 5);
nativeControl.Frame = Frame;
}
}
protected override void Dispose(bool disposing)
{
if (bottom != null)
{
if (bottom.SuperLayer != null)
{
bottom.RemoveFromSuperLayer();
}
bottom.Dispose();
bottom = null;
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,51 @@
using Gallery.iOS.Renderers;
using Gallery.Resources.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(CardView), typeof(CardViewRenderer))]
namespace Gallery.iOS.Renderers
{
public class CardViewRenderer : VisualElementRenderer<CardView>
{
protected override void OnElementChanged(ElementChangedEventArgs<CardView> e)
{
base.OnElementChanged(e);
var layer = Layer;
var element = e.NewElement;
if (layer != null && element != null)
{
var cornerRadius = element.CornerRadius;
if (cornerRadius > 0)
{
layer.CornerRadius = cornerRadius;
}
//if (element.BackgroundColor != default)
//{
// layer.BackgroundColor = element.BackgroundColor.ToCGColor();
//}
var shadowColor = element.ShadowColor;
if (shadowColor != default)
{
layer.ShadowColor = shadowColor.ToCGColor();
layer.ShadowOpacity = 1f;
var radius = element.ShadowRadius;
if (radius > 0)
{
layer.ShadowRadius = radius;
}
layer.ShadowOffset = element.ShadowOffset.ToSizeF();
}
else
{
layer.ShadowOpacity = 0f;
}
}
}
}
}

View File

@ -0,0 +1,33 @@
using Gallery.iOS.Renderers;
using Gallery.Resources.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
namespace Gallery.iOS.Renderers
{
public class CircleImageRenderer : ImageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
var layer = Layer;
if (layer != null)
{
layer.MasksToBounds = true;
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
var control = Control;
if (control != null)
{
control.Layer.CornerRadius = control.Frame.Size.Width / 2;
}
}
}
}

View File

@ -0,0 +1,22 @@
using Gallery.iOS.Renderers;
using Gallery.Resources.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(OptionEntry), typeof(OptionEntryRenderer))]
namespace Gallery.iOS.Renderers
{
public class OptionEntryRenderer : EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
var control = Control;
if (control != null)
{
control.BorderStyle = UIKit.UITextBorderStyle.None;
}
}
}
}

View File

@ -0,0 +1,22 @@
using Gallery.iOS.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(Picker), typeof(OptionPickerRenderer))]
namespace Gallery.iOS.Renderers
{
public class OptionPickerRenderer : PickerRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
var control = Control;
if (control != null)
{
control.TextAlignment = UIKit.UITextAlignment.Right;
control.BorderStyle = UIKit.UITextBorderStyle.None;
}
}
}
}

View File

@ -0,0 +1,56 @@
using CoreAnimation;
using Gallery.iOS.Renderers;
using Gallery.Resources.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(RoundImage), typeof(RoundImageRenderer))]
namespace Gallery.iOS.Renderers
{
public class RoundImageRenderer : ImageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
var layer = Layer;
if (layer != null && e.NewElement is RoundImage image)
{
bool flag = false;
if (image.CornerRadius > 0)
{
layer.CornerRadius = image.CornerRadius;
flag = true;
}
var mask = image.CornerMasks;
if (mask != CornerMask.None)
{
var m = default(CACornerMask);
if ((mask & CornerMask.LeftTop) == CornerMask.LeftTop)
{
m |= CACornerMask.MinXMinYCorner;
}
if ((mask & CornerMask.RightTop) == CornerMask.RightTop)
{
m |= CACornerMask.MaxXMinYCorner;
}
if ((mask & CornerMask.LeftBottom) == CornerMask.LeftBottom)
{
m |= CACornerMask.MinXMaxYCorner;
}
if ((mask & CornerMask.RightBottom) == CornerMask.RightBottom)
{
m |= CACornerMask.MaxXMaxYCorner;
}
layer.MaskedCorners = m;
flag = true;
}
if (flag)
{
layer.MasksToBounds = true;
}
}
}
}
}

View File

@ -0,0 +1,56 @@
using System.ComponentModel;
using Gallery.iOS.Renderers;
using Gallery.Resources.UI;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(RoundLabel), typeof(RoundLabelRenderer))]
namespace Gallery.iOS.Renderers
{
public class RoundLabelRenderer : LabelRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
var layer = Layer;
if (layer != null && e.NewElement is RoundLabel label)
{
var radius = label.CornerRadius;
if (radius > 0)
{
layer.CornerRadius = radius;
//layer.MasksToBounds = true;
}
else
{
layer.CornerRadius = 0;
}
if (layer.BackgroundColor != default)
{
layer.BackgroundColor = label.BackgroundColor.ToCGColor();
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == RoundLabel.CornerRadiusProperty.PropertyName)
{
var layer = Layer;
if (layer != null && Element is RoundLabel label)
{
var radius = label.CornerRadius;
if (radius > 0)
{
layer.CornerRadius = radius;
layer.BackgroundColor = label.BackgroundColor.ToCGColor();
//layer.MasksToBounds = true;
}
}
}
}
}
}

View File

@ -13,15 +13,15 @@
<viewControllerLayoutGuide type="bottom" id="9ZL-r4-8FZ"/> <viewControllerLayoutGuide type="bottom" id="9ZL-r4-8FZ"/>
</layoutGuides> </layoutGuides>
<view key="view" contentMode="scaleToFill" id="yd7-JS-zBw"> <view key="view" contentMode="scaleToFill" id="yd7-JS-zBw">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/> <rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<subviews> <subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" misplaced="YES" image="Icon-60.png" translatesAutoresizingMaskIntoConstraints="NO" id="23"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" misplaced="YES" image="Icon-60.png" translatesAutoresizingMaskIntoConstraints="NO" id="23">
<rect key="frame" x="270" y="270" width="60" height="60"/> <rect key="frame" x="59" y="356" width="60" height="60"/>
<rect key="contentStretch" x="0.0" y="0.0" width="0.0" height="0.0"/> <rect key="contentStretch" x="0.0" y="0.0" width="0.0" height="0.0"/>
</imageView> </imageView>
</subviews> </subviews>
<color key="backgroundColor" red="0.20392156862745098" green="0.59607843137254901" blue="0.85882352941176465" alpha="1" colorSpace="calibratedRGB"/>
<constraints> <constraints>
<constraint firstItem="23" firstAttribute="centerY" secondItem="yd7-JS-zBw" secondAttribute="centerY" priority="1" id="39"/> <constraint firstItem="23" firstAttribute="centerY" secondItem="yd7-JS-zBw" secondAttribute="centerY" priority="1" id="39"/>
<constraint firstItem="23" firstAttribute="centerX" secondItem="yd7-JS-zBw" secondAttribute="centerX" priority="1" id="41"/> <constraint firstItem="23" firstAttribute="centerX" secondItem="yd7-JS-zBw" secondAttribute="centerX" priority="1" id="41"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Danbooru", "Gallery
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Gelbooru", "GallerySources\Gallery.Gelbooru\Gallery.Gelbooru.csproj", "{83760017-F2A6-4450-A4F8-8E143E800C2F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.Gelbooru", "GallerySources\Gallery.Gelbooru\Gallery.Gelbooru.csproj", "{83760017-F2A6-4450-A4F8-8E143E800C2F}"
EndProject EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Gallery.UI", "Gallery.UI\Gallery.UI.shproj", "{73AB85FB-D11A-43FB-BBC5-54BED5A056D1}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|iPhoneSimulator = Debug|iPhoneSimulator Debug|iPhoneSimulator = Debug|iPhoneSimulator

View File

@ -1,24 +1,63 @@
using System; using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Gallery.Util;
using Gallery.Util.Interface; using Gallery.Util.Interface;
using Gallery.Util.Model; using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Danbooru namespace Gallery.Danbooru
{ {
public class GallerySource : IGallerySource public class GallerySource : IGallerySource
{ {
public string Name => "Danbooru"; public string Name => "Danbooru";
public string Route => "danbooru";
public string FlyoutIconKey => "Danbooru";
public string HomePage => "https://danbooru.donmai.us"; public string HomePage => "https://danbooru.donmai.us";
public Task<GalleryItem[]> GetRecentItemsAsync(int page) public async Task<GalleryItem[]> GetRecentItemsAsync(int page)
{ {
throw new NotImplementedException(); var url = $"https://danbooru.donmai.us/posts?page={page}";
var (result, error) = await NetHelper.RequestObject<GalleryItem[]>(url, contentHandler: ContentHandler);
if (result == null || !string.IsNullOrEmpty(error))
{
Log.Error("danbooru.content.load", $"failed to load content array, error: {error}");
return null;
}
return result;
}
private string ContentHandler(string content)
{
var regex = new Regex(@"<article id=""post_\d+"".+?data-id=""(\d+)"".+?data-tags=""([^""]+?)"".+?data-width=""(\d+?)"" data-height=""(\d+?)"".+?data-source=""([^""]+?)"" data-uploader-id=""(\d+?)"" data-normalized-source=""([^""]+?)"".+?data-file-url=""([^""]+?)"".+?data-preview-file-url=""([^""]+?)""", RegexOptions.Multiline);
var matches = regex.Matches(content);
var array = new string[matches.Count];
for (var i = 0; i < array.Length; i++)
{
var g = matches[i].Groups;
var tags = g[2].Value.Replace(" ", "\",\"");
array[i] = $"{{\"Id\":{g[0].Value},\"Tags\":[\"{tags}\"]}}";
}
return $"[{string.Join(',', array)}]";
} }
public void SetCookie() public void SetCookie()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark)
{
var icon = new FontImageSource
{
FontFamily = family,
Glyph = "\uf5d2",
Size = 18.0
};
light.Add(FlyoutIconKey, icon);
dark.Add(FlyoutIconKey, icon);
}
} }
} }

View File

@ -2,13 +2,15 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Gallery.Util.Interface; using Gallery.Util.Interface;
using Gallery.Util.Model; using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Gelbooru namespace Gallery.Gelbooru
{ {
public class GallerySource : IGallerySource public class GallerySource : IGallerySource
{ {
public string Name => "Gelbooru"; public string Name => "Gelbooru";
public string Route => "gelbooru";
public string FlyoutIconKey => "Gelbooru";
public string HomePage => "https://gelbooru.com"; public string HomePage => "https://gelbooru.com";
public Task<GalleryItem[]> GetRecentItemsAsync(int page) public Task<GalleryItem[]> GetRecentItemsAsync(int page)
@ -20,5 +22,17 @@ namespace Gallery.Gelbooru
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark)
{
var icon = new FontImageSource
{
FontFamily = family,
Glyph = "\uf5d2",
Size = 18.0
};
light.Add(FlyoutIconKey, icon);
dark.Add(FlyoutIconKey, icon);
}
} }
} }

View File

@ -4,12 +4,15 @@ using System.Threading.Tasks;
using Gallery.Util; using Gallery.Util;
using Gallery.Util.Interface; using Gallery.Util.Interface;
using Gallery.Util.Model; using Gallery.Util.Model;
using Xamarin.Forms;
namespace Gallery.Yandere namespace Gallery.Yandere
{ {
public class GallerySource : IGallerySource public class GallerySource : IGallerySource
{ {
public string Name => "Yande.re"; public string Name => "Yande.re";
public string Route => "yandere";
public string FlyoutIconKey => "Yandere";
public string HomePage => "https://yande.re"; public string HomePage => "https://yande.re";
public async Task<GalleryItem[]> GetRecentItemsAsync(int page) public async Task<GalleryItem[]> GetRecentItemsAsync(int page)
@ -61,5 +64,17 @@ namespace Gallery.Yandere
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void InitDynamicResources(string family, ResourceDictionary light, ResourceDictionary dark)
{
var icon = new FontImageSource
{
FontFamily = family,
Glyph = "\uf302",
Size = 18.0
};
light.Add(FlyoutIconKey, icon);
dark.Add(FlyoutIconKey, icon);
}
} }
} }