fix issue
This commit is contained in:
@ -5,6 +5,8 @@ namespace Billing.Themes
|
|||||||
{
|
{
|
||||||
public abstract class BaseTheme : ResourceDictionary
|
public abstract class BaseTheme : ResourceDictionary
|
||||||
{
|
{
|
||||||
|
public static Color CurrentPrimaryColor => (Color)Application.Current.Resources[PrimaryColor];
|
||||||
|
|
||||||
public const string CascadiaFontRegular = nameof(CascadiaFontRegular);
|
public const string CascadiaFontRegular = nameof(CascadiaFontRegular);
|
||||||
public const string CascadiaFontBold = nameof(CascadiaFontBold);
|
public const string CascadiaFontBold = nameof(CascadiaFontBold);
|
||||||
public const string RobotoCondensedFontRegular = nameof(RobotoCondensedFontRegular);
|
public const string RobotoCondensedFontRegular = nameof(RobotoCondensedFontRegular);
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
using Billing.Themes;
|
using System;
|
||||||
|
using Billing.Themes;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Billing.UI
|
namespace Billing.UI
|
||||||
{
|
{
|
||||||
public abstract class BillingPage : ContentPage
|
public abstract class BillingPage : ContentPage
|
||||||
{
|
{
|
||||||
|
public event EventHandler Loaded;
|
||||||
|
|
||||||
public BillingPage()
|
public BillingPage()
|
||||||
{
|
{
|
||||||
SetDynamicResource(BackgroundColorProperty, BaseTheme.WindowBackgroundColor);
|
SetDynamicResource(BackgroundColorProperty, BaseTheme.WindowBackgroundColor);
|
||||||
Shell.SetTabBarIsVisible(this, false);
|
Shell.SetTabBarIsVisible(this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void OnLoaded()
|
||||||
|
{
|
||||||
|
Loaded?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using Billing.Themes;
|
using Billing.Themes;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Billing.UI
|
namespace Billing.UI
|
||||||
@ -377,7 +378,17 @@ namespace Billing.UI
|
|||||||
set => SetValue(PlaceholderProperty, value);
|
set => SetValue(PlaceholderProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override View Content => new OptionEditor
|
OptionEditor editor;
|
||||||
|
|
||||||
|
public void SetFocus()
|
||||||
|
{
|
||||||
|
if (editor != null)
|
||||||
|
{
|
||||||
|
editor.Focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override View Content => editor = new OptionEditor
|
||||||
{
|
{
|
||||||
HorizontalOptions = LayoutOptions.Fill,
|
HorizontalOptions = LayoutOptions.Fill,
|
||||||
VerticalOptions = LayoutOptions.Fill
|
VerticalOptions = LayoutOptions.Fill
|
||||||
|
@ -21,13 +21,15 @@
|
|||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<TableView Intent="Settings" HasUnevenRows="True">
|
<TableView Intent="Settings" HasUnevenRows="True">
|
||||||
<TableSection Title=" ">
|
<TableSection Title=" ">
|
||||||
<ui:OptionEditorCell Height="120" Icon="pencil.png" FontSize="20" Keyboard="Text"
|
<ui:OptionEditorCell x:Name="editorName" Height="120" Icon="pencil.png"
|
||||||
|
FontSize="20" Keyboard="Text"
|
||||||
Title="{r:Text AccountName}"
|
Title="{r:Text AccountName}"
|
||||||
Text="{Binding AccountName, Mode=TwoWay}"
|
Text="{Binding AccountName, Mode=TwoWay}"
|
||||||
Placeholder="{r:Text AccountNamePlaceholder}"/>
|
Placeholder="{r:Text AccountNamePlaceholder}"/>
|
||||||
<ui:OptionImageCell Height="44" Icon="face.png"
|
<ui:OptionImageCell Height="44" Icon="face.png"
|
||||||
Title="{r:Text Icon}"
|
Title="{r:Text Icon}"
|
||||||
ImageSource="{Binding AccountIcon, Converter={StaticResource iconConverter}}"
|
ImageSource="{Binding AccountIcon, Converter={StaticResource iconConverter}}"
|
||||||
|
TintColor="{DynamicResource PrimaryColor}"
|
||||||
Command="{Binding SelectIcon}"/>
|
Command="{Binding SelectIcon}"/>
|
||||||
<ui:OptionSelectCell Height="44" Icon="project.png"
|
<ui:OptionSelectCell Height="44" Icon="project.png"
|
||||||
Title="{r:Text Category}"
|
Title="{r:Text Category}"
|
||||||
|
@ -73,6 +73,17 @@ namespace Billing.Views
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool focused;
|
||||||
|
|
||||||
|
public override void OnLoaded()
|
||||||
|
{
|
||||||
|
if (!focused)
|
||||||
|
{
|
||||||
|
focused = true;
|
||||||
|
editorName.SetFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnCheckAccount()
|
private async void OnCheckAccount()
|
||||||
{
|
{
|
||||||
if (Tap.IsBusy)
|
if (Tap.IsBusy)
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<TableView Intent="Settings" HasUnevenRows="True">
|
<TableView Intent="Settings" HasUnevenRows="True">
|
||||||
<TableSection Title=" ">
|
<TableSection Title=" ">
|
||||||
<ui:OptionEditorCell Height="120" Icon="yuan.png" FontSize="20" Keyboard="Numeric"
|
<ui:OptionEditorCell x:Name="editorAmount" Height="120" Icon="yuan.png"
|
||||||
|
FontSize="20" Keyboard="Numeric"
|
||||||
Text="{Binding Amount, Mode=TwoWay}"
|
Text="{Binding Amount, Mode=TwoWay}"
|
||||||
Placeholder="0.00"/>
|
Placeholder="0.00"/>
|
||||||
</TableSection>
|
</TableSection>
|
||||||
|
@ -116,6 +116,17 @@ namespace Billing.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool focused;
|
||||||
|
|
||||||
|
public override void OnLoaded()
|
||||||
|
{
|
||||||
|
if (!focused)
|
||||||
|
{
|
||||||
|
focused = true;
|
||||||
|
editorAmount.SetFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnCheckBill()
|
private async void OnCheckBill()
|
||||||
{
|
{
|
||||||
if (Tap.IsBusy)
|
if (Tap.IsBusy)
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
<ContentPage.Content>
|
<ContentPage.Content>
|
||||||
<TableView Intent="Settings" HasUnevenRows="True">
|
<TableView Intent="Settings" HasUnevenRows="True">
|
||||||
<TableSection Title=" ">
|
<TableSection Title=" ">
|
||||||
<ui:OptionEditorCell Height="120" Icon="pencil.png" FontSize="20" Keyboard="Text"
|
<ui:OptionEditorCell x:Name="editorName" Height="120" Icon="pencil.png"
|
||||||
|
FontSize="20" Keyboard="Text"
|
||||||
Title="{r:Text Name}"
|
Title="{r:Text Name}"
|
||||||
Text="{Binding CategoryName, Mode=TwoWay}"
|
Text="{Binding CategoryName, Mode=TwoWay}"
|
||||||
Placeholder="{r:Text NamePlaceholder}"/>
|
Placeholder="{r:Text NamePlaceholder}"/>
|
||||||
|
@ -58,7 +58,7 @@ namespace Billing.Views
|
|||||||
if (category.TintColor == Color.Transparent ||
|
if (category.TintColor == Color.Transparent ||
|
||||||
category.TintColor == default)
|
category.TintColor == default)
|
||||||
{
|
{
|
||||||
TintColor = (Color)Application.Current.Resources[BaseTheme.PrimaryColor];
|
TintColor = BaseTheme.CurrentPrimaryColor;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -67,7 +67,7 @@ namespace Billing.Views
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TintColor = (Color)Application.Current.Resources[BaseTheme.PrimaryColor];
|
TintColor = BaseTheme.CurrentPrimaryColor;
|
||||||
}
|
}
|
||||||
TintColorString = Helper.WrapColorString(TintColor.ToHex());
|
TintColorString = Helper.WrapColorString(TintColor.ToHex());
|
||||||
|
|
||||||
@ -77,6 +77,17 @@ namespace Billing.Views
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool focused;
|
||||||
|
|
||||||
|
public override void OnLoaded()
|
||||||
|
{
|
||||||
|
if (!focused)
|
||||||
|
{
|
||||||
|
focused = true;
|
||||||
|
editorName.SetFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ColorPicker_ColorChanged(object sender, Color e)
|
private void ColorPicker_ColorChanged(object sender, Color e)
|
||||||
{
|
{
|
||||||
TintColor = e;
|
TintColor = e;
|
||||||
@ -91,6 +102,8 @@ namespace Billing.Views
|
|||||||
}
|
}
|
||||||
using (Tap.Start())
|
using (Tap.Start())
|
||||||
{
|
{
|
||||||
|
var currentColor = BaseTheme.CurrentPrimaryColor;
|
||||||
|
var tintColor = TintColor;
|
||||||
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
|
var category = App.Categories.FirstOrDefault(c => c.Id == categoryId);
|
||||||
if (category == null)
|
if (category == null)
|
||||||
{
|
{
|
||||||
@ -99,8 +112,8 @@ namespace Billing.Views
|
|||||||
Id = -1,
|
Id = -1,
|
||||||
Name = CategoryName,
|
Name = CategoryName,
|
||||||
Icon = CategoryIcon,
|
Icon = CategoryIcon,
|
||||||
TintColor = TintColor,
|
TintColor = tintColor == currentColor ? Color.Transparent : tintColor,
|
||||||
ParentId = parent?.Id ?? -1,
|
ParentId = parent?.Id,
|
||||||
Type = parent?.Type ?? CategoryType.Spending
|
Type = parent?.Type ?? CategoryType.Spending
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -108,7 +121,7 @@ namespace Billing.Views
|
|||||||
{
|
{
|
||||||
category.Name = CategoryName;
|
category.Name = CategoryName;
|
||||||
category.Icon = CategoryIcon;
|
category.Icon = CategoryIcon;
|
||||||
category.TintColor = TintColor;
|
category.TintColor = tintColor == currentColor ? Color.Transparent : tintColor;
|
||||||
CategoryChecked?.Invoke(this, category);
|
CategoryChecked?.Invoke(this, category);
|
||||||
}
|
}
|
||||||
await Navigation.PopAsync();
|
await Navigation.PopAsync();
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</ContentPage.Resources>
|
</ContentPage.Resources>
|
||||||
|
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Categories}" Margin="0, 10, 0, 0"
|
<ui:GroupStackLayout x:Name="groupLayout" ItemsSource="{Binding Categories}" Padding="0, 10, 0, 0"
|
||||||
GroupHeight="36" RowHeight="44">
|
GroupHeight="36" RowHeight="44">
|
||||||
<ui:GroupStackLayout.GroupHeaderTemplate>
|
<ui:GroupStackLayout.GroupHeaderTemplate>
|
||||||
<DataTemplate x:DataType="v:CategoryGrouping">
|
<DataTemplate x:DataType="v:CategoryGrouping">
|
||||||
|
@ -26,14 +26,15 @@ namespace Billing.Views
|
|||||||
public Command Tapped { get; }
|
public Command Tapped { get; }
|
||||||
|
|
||||||
private readonly int parentId;
|
private readonly int parentId;
|
||||||
|
private readonly Category parent;
|
||||||
|
|
||||||
public CategoryPage(int pid = -1)
|
public CategoryPage(int pid = -1)
|
||||||
{
|
{
|
||||||
parentId = pid;
|
parentId = pid;
|
||||||
var category = App.Categories.FirstOrDefault(c => c.Id == pid);
|
parent = App.Categories.FirstOrDefault(c => c.Id == pid);
|
||||||
Title = category?.Name ?? Resource.CategoryManage;
|
Title = parent?.Name ?? Resource.CategoryManage;
|
||||||
|
|
||||||
if (category != null)
|
if (parent != null)
|
||||||
{
|
{
|
||||||
SetValue(IsTopCategoryProperty, false);
|
SetValue(IsTopCategoryProperty, false);
|
||||||
Categories = App.Categories.Where(c => c.ParentId == pid).Select(c => WrapCategory(c)).ToList();
|
Categories = App.Categories.Where(c => c.ParentId == pid).Select(c => WrapCategory(c)).ToList();
|
||||||
@ -68,7 +69,7 @@ namespace Billing.Views
|
|||||||
Name = category.Name,
|
Name = category.Name,
|
||||||
IsTopCategory = IsTopCategory,
|
IsTopCategory = IsTopCategory,
|
||||||
TintColor = category.TintColor == Color.Transparent || category.TintColor == default ?
|
TintColor = category.TintColor == Color.Transparent || category.TintColor == default ?
|
||||||
(Color)Application.Current.Resources[BaseTheme.PrimaryColor] :
|
BaseTheme.CurrentPrimaryColor :
|
||||||
category.TintColor
|
category.TintColor
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -81,7 +82,7 @@ namespace Billing.Views
|
|||||||
}
|
}
|
||||||
using (Tap.Start())
|
using (Tap.Start())
|
||||||
{
|
{
|
||||||
var page = new AddCategoryPage();
|
var page = new AddCategoryPage(parent: parent);
|
||||||
page.CategoryChecked += OnCategoryChecked;
|
page.CategoryChecked += OnCategoryChecked;
|
||||||
await Navigation.PushAsync(page);
|
await Navigation.PushAsync(page);
|
||||||
}
|
}
|
||||||
@ -189,7 +190,9 @@ namespace Billing.Views
|
|||||||
{
|
{
|
||||||
c.Name = c.Category.Name;
|
c.Name = c.Category.Name;
|
||||||
c.Icon = c.Category.Icon;
|
c.Icon = c.Category.Icon;
|
||||||
c.TintColor = c.Category.TintColor;
|
c.TintColor = c.Category.TintColor == Color.Transparent || c.Category.TintColor == default ?
|
||||||
|
BaseTheme.CurrentPrimaryColor :
|
||||||
|
c.Category.TintColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<Grid ColumnDefinitions=".5*, .5*">
|
<Grid ColumnDefinitions=".5*, .5*">
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<ui:GroupStackLayout ItemsSource="{Binding TopCategories}" Margin="0, 10, 0, 0"
|
<ui:GroupStackLayout ItemsSource="{Binding TopCategories}" Padding="0, 10, 0, 0"
|
||||||
GroupHeight="36" RowHeight="44">
|
GroupHeight="36" RowHeight="44">
|
||||||
<ui:GroupStackLayout.GroupHeaderTemplate>
|
<ui:GroupStackLayout.GroupHeaderTemplate>
|
||||||
<DataTemplate x:DataType="v:CategoryGrouping">
|
<DataTemplate x:DataType="v:CategoryGrouping">
|
||||||
@ -48,7 +48,7 @@
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<ScrollView Grid.Column="1">
|
<ScrollView Grid.Column="1">
|
||||||
<ui:GroupStackLayout ItemsSource="{Binding SubCategories}" Margin="0, 10, 0, 0" RowHeight="44" Padding="0, 40, 0, 0">
|
<ui:GroupStackLayout ItemsSource="{Binding SubCategories}" RowHeight="44" Padding="0, 50, 0, 0">
|
||||||
<ui:GroupStackLayout.ItemTemplate>
|
<ui:GroupStackLayout.ItemTemplate>
|
||||||
<DataTemplate x:DataType="v:UICategory">
|
<DataTemplate x:DataType="v:UICategory">
|
||||||
<Grid Padding="20, 0, 10, 0" ColumnSpacing="10" ColumnDefinitions="Auto, *"
|
<Grid Padding="20, 0, 10, 0" ColumnSpacing="10" ColumnDefinitions="Auto, *"
|
||||||
|
@ -36,7 +36,7 @@ namespace Billing.Views
|
|||||||
public CategorySelectPage(int id)
|
public CategorySelectPage(int id)
|
||||||
{
|
{
|
||||||
categoryId = id;
|
categoryId = id;
|
||||||
defaultColor = (Color)Application.Current.Resources[BaseTheme.PrimaryColor];
|
defaultColor = BaseTheme.CurrentPrimaryColor;
|
||||||
TapTopCategory = new Command(OnTopCategoryTapped);
|
TapTopCategory = new Command(OnTopCategoryTapped);
|
||||||
TapSubCategory = new Command(OnSubCategoryTapped);
|
TapSubCategory = new Command(OnSubCategoryTapped);
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<Grid RowDefinitions="*, Auto">
|
<Grid RowDefinitions="*, Auto">
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<ui:WrapLayout ItemsSource="{Binding IconsSource}" Margin="10">
|
<ui:WrapLayout ItemsSource="{Binding IconsSource}" Padding="10">
|
||||||
<BindableLayout.ItemTemplate>
|
<BindableLayout.ItemTemplate>
|
||||||
<DataTemplate x:DataType="v:BillingIcon">
|
<DataTemplate x:DataType="v:BillingIcon">
|
||||||
<Grid WidthRequest="60" HeightRequest="60">
|
<Grid WidthRequest="60" HeightRequest="60">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="4" android:versionName="0.4.306" package="org.tsanie.billing" android:installLocation="auto">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="6" android:versionName="0.6.307" package="org.tsanie.billing" android:installLocation="auto">
|
||||||
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="31" />
|
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="31" />
|
||||||
<application android:label="@string/applabel" android:theme="@style/MainTheme"></application>
|
<application android:label="@string/applabel" android:theme="@style/MainTheme"></application>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
@ -88,6 +88,7 @@
|
|||||||
<BundleResource Include="Base.lproj\InfoPlist.strings" />
|
<BundleResource Include="Base.lproj\InfoPlist.strings" />
|
||||||
<BundleResource Include="en.lproj\InfoPlist.strings" />
|
<BundleResource Include="en.lproj\InfoPlist.strings" />
|
||||||
<BundleResource Include="zh-Hans.lproj\InfoPlist.strings" />
|
<BundleResource Include="zh-Hans.lproj\InfoPlist.strings" />
|
||||||
|
<Compile Include="Renderers\BillingPageRenderer.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json">
|
<ImageAsset Include="Assets.xcassets\AppIcon.appiconset\Contents.json">
|
||||||
|
@ -45,8 +45,8 @@
|
|||||||
<key>UIFileSharingEnabled</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>4</string>
|
<string>6</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.4.306</string>
|
<string>0.6.307</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
20
Billing/Billing.iOS/Renderers/BillingPageRenderer.cs
Normal file
20
Billing/Billing.iOS/Renderers/BillingPageRenderer.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using Billing.iOS.Renderers;
|
||||||
|
using Billing.UI;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.iOS;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(BillingPage), typeof(BillingPageRenderer))]
|
||||||
|
namespace Billing.iOS.Renderers
|
||||||
|
{
|
||||||
|
public class BillingPageRenderer : PageRenderer
|
||||||
|
{
|
||||||
|
public override void ViewDidAppear(bool animated)
|
||||||
|
{
|
||||||
|
base.ViewDidAppear(animated);
|
||||||
|
if (Element is BillingPage page)
|
||||||
|
{
|
||||||
|
page.OnLoaded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,13 +19,14 @@ namespace Billing.iOS.Renderers
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.PropertyName == nameof(Image.Source))
|
if (e.PropertyName == nameof(Image.Source) ||
|
||||||
|
e.PropertyName == nameof(TintImage.PrimaryColor))
|
||||||
{
|
{
|
||||||
Control.Image = Control.Image.ImageWithRenderingMode(UIKit.UIImageRenderingMode.AlwaysTemplate);
|
Control.Image = Control.Image.ImageWithRenderingMode(UIKit.UIImageRenderingMode.AlwaysTemplate);
|
||||||
}
|
if (Element is TintImage image)
|
||||||
else if (e.PropertyName == nameof(TintImage.PrimaryColor) && Element is TintImage image)
|
{
|
||||||
{
|
Control.TintColor = image.PrimaryColor?.ToUIColor();
|
||||||
Control.TintColor = image.PrimaryColor?.ToUIColor();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
|
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
|
||||||
|
Reference in New Issue
Block a user