adjust UI

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

View File

@@ -1,62 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Gallery.Views"
Title="Gallery"
x:Class="Gallery.AppShell">
<!--
The overall app visual hierarchy is defined here, along with navigation.
https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/
-->
xmlns:local="clr-namespace:Gallery"
xmlns:r="clr-namespace:Gallery.Resources"
xmlns:ui="clr-namespace:Gallery.Resources.UI"
xmlns:util="clr-namespace:Gallery.Util;assembly=Gallery.Util"
x:Class="Gallery.AppShell"
x:Name="appShell"
BackgroundColor="{DynamicResource NavigationColor}"
FlyoutBackgroundColor="{DynamicResource WindowColor}"
x:DataType="{x:Type local:AppShell}"
BindingContext="{x:Reference appShell}">
<Shell.Resources>
<ResourceDictionary>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{DynamicResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.BackgroundColor" Value="{DynamicResource NavigationColor}" />
<Setter Property="Shell.ForegroundColor" Value="{DynamicResource TintColor}" />
<Setter Property="Shell.TitleColor" Value="{DynamicResource TextColor}" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
<Setter Property="Shell.UnselectedColor" Value="{DynamicResource TintColor}" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource NavigationColor}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{DynamicResource TintColor}"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="{DynamicResource TintColor}"/>
<Setter Property="Shell.TabBarTitleColor" Value="{DynamicResource TextColor}"/>
</Style>
<Style TargetType="TabBar" 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
-->
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
@@ -70,24 +42,77 @@
</VisualStateGroupList>
</Setter>
</Style>
-->
</ResourceDictionary>
</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.
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
-->
<FlyoutItem Title="About" Icon="icon_about.png">
<ShellContent Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" />
</FlyoutItem>
<FlyoutItem Title="Browse" Icon="icon_feed.png">
<ShellContent Route="ItemsPage" ContentTemplate="{DataTemplate local:ItemsPage}" />
</FlyoutItem>
<FlyoutItem x:Name="flyoutItems"
FlyoutDisplayOptions="AsMultipleItems"
Route="{x:Static util:Routes.Gallery}" />
<!-- 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>
<!--<MenuItem Text="Logout" StyleClass="MenuItemLayoutStyle" Clicked="OnMenuItemClicked" />-->
<!--
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
content you can do so by calling
await Shell.Current.GoToAsync("//LoginPage");
-->
<TabBar>
<ShellContent Route="LoginPage" ContentTemplate="{DataTemplate local:LoginPage}" />
</TabBar>
-->
<!-- 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
// https://docs.microsoft.com/xamarin/xamarin-forms/app-fundamentals/shell/flyout#define-menuitem-appearance
<Shell.MenuItemTemplate>
@@ -130,7 +135,6 @@
</ContentView>
</DataTemplate>
</Shell.MenuItemTemplate>
-->
</Shell>

View File

@@ -1,4 +1,7 @@
using System;
using Gallery.Resources;
using Gallery.Resources.UI;
using Gallery.Util;
using Gallery.Views;
using Xamarin.Forms;
@@ -6,16 +9,52 @@ namespace Gallery
{
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()
{
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>
<ItemGroup>
<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">
<DependentUpon>AppShell.xaml</DependentUpon>
</Compile>
@@ -44,11 +20,17 @@
<Compile Include="$(MSBuildThisFileDirectory)Resources\Theme\LightTheme.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Resources\Theme\DarkTheme.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>
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
<Folder Include="$(MSBuildThisFileDirectory)Services\" />
<Folder Include="$(MSBuildThisFileDirectory)ViewModels\" />
<Folder Include="$(MSBuildThisFileDirectory)Views\" />
<Folder Include="$(MSBuildThisFileDirectory)Resources\" />
<Folder Include="$(MSBuildThisFileDirectory)Resources\Theme\" />
@@ -56,32 +38,14 @@
<Folder Include="$(MSBuildThisFileDirectory)Resources\UI\" />
</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">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)Resources\Languages\zh-CN.xml" />
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Views\GalleryPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Resources\Languages\zh-CN.xml" />
</ItemGroup>
</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.Xml;
using Gallery.Util;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Gallery.Resources
@@ -84,6 +85,7 @@ namespace Gallery.Resources
}
}
[ContentProperty(nameof(Text))]
public class TextExtension : IMarkupExtension
{
public string Text { get; set; }

View File

@@ -27,9 +27,16 @@ namespace Gallery.Resources.Theme
private void InitColors()
{
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(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()
{
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(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 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 NavigationSelectedColor = nameof(NavigationSelectedColor);
public const string OptionBackColor = nameof(OptionBackColor);
public const string OptionTintColor = nameof(OptionTintColor);
public const string IconLightFamily = nameof(IconLightFamily);
public const string IconRegularFamily = nameof(IconRegularFamily);
@@ -16,8 +24,6 @@ namespace Gallery.Resources.Theme
public const string IconClose = nameof(IconClose);
public const string FontIconRefresh = nameof(FontIconRefresh);
public const string Primary = nameof(Primary);
protected void InitResources()
{
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 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__
public const string IconLightFamily = "FontAwesome5Pro-Light";
@@ -26,6 +32,8 @@ namespace Gallery.Resources.UI
#endif
public const string IconRefresh = "\uf2f9";
public const string IconLove = "\uf004";
public const string IconCircleLove = "\uf4c7";
public const string IconClose = "\uf057";
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();
}
}
}