mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 00:42:55 +08:00
Move beatmap + mod info to header
This commit is contained in:
parent
72016a416b
commit
2e28f378de
58
osu.Game.Tests/Visual/TestCaseMatchHeader.cs
Normal file
58
osu.Game.Tests/Visual/TestCaseMatchHeader.cs
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Multi.Match.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public class TestCaseMatchHeader : OsuTestCase
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(Header)
|
||||
};
|
||||
|
||||
private readonly Bindable<BeatmapInfo> beatmap = new Bindable<BeatmapInfo>();
|
||||
private readonly Bindable<GameType> type = new Bindable<GameType>();
|
||||
private readonly Bindable<IEnumerable<Mod>> mods = new Bindable<IEnumerable<Mod>>();
|
||||
|
||||
public TestCaseMatchHeader()
|
||||
{
|
||||
var header = new Header(new Room());
|
||||
|
||||
header.Beatmap.BindTo(beatmap);
|
||||
header.Type.BindTo(type);
|
||||
header.Mods.BindTo(mods);
|
||||
|
||||
beatmap.Value = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Title = "Title",
|
||||
Artist = "Artist",
|
||||
AuthorString = "Author",
|
||||
},
|
||||
Version = "Version",
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
type.Value = new GameTypeTimeshift();
|
||||
mods.Value = new Mod[]
|
||||
{
|
||||
new OsuModDoubleTime(),
|
||||
new OsuModNoFail(),
|
||||
new OsuModRelax(),
|
||||
};
|
||||
|
||||
Child = header;
|
||||
}
|
||||
}
|
||||
}
|
@ -44,13 +44,10 @@ namespace osu.Game.Tests.Visual
|
||||
},
|
||||
});
|
||||
|
||||
AddStep(@"set type", () => info.Type.Value = new GameTypeTagTeam());
|
||||
|
||||
AddStep(@"change name", () => info.Name.Value = @"Room Name!");
|
||||
AddStep(@"change availability", () => info.Availability.Value = RoomAvailability.InviteOnly);
|
||||
AddStep(@"change status", () => info.Status.Value = new RoomStatusOpen());
|
||||
AddStep(@"null beatmap", () => info.Beatmap.Value = null);
|
||||
AddStep(@"change type", () => info.Type.Value = new GameTypeTeamVersus());
|
||||
AddStep(@"change beatmap", () => info.Beatmap.Value = new BeatmapInfo
|
||||
{
|
||||
StarDifficulty = 4.2,
|
||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo>
|
||||
{
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly IBindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Screens.Multi.Components
|
||||
set { beatmapTitle.TextSize = beatmapDash.TextSize = beatmapArtist.TextSize = value; }
|
||||
}
|
||||
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly IBindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
public BeatmapTitle()
|
||||
{
|
||||
|
@ -17,9 +17,9 @@ namespace osu.Game.Screens.Multi.Components
|
||||
{
|
||||
private readonly OsuSpriteText beatmapAuthor;
|
||||
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly IBindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
public readonly Bindable<GameType> Type = new Bindable<GameType>();
|
||||
public readonly IBindable<GameType> Type = new Bindable<GameType>();
|
||||
|
||||
public BeatmapTypeInfo()
|
||||
{
|
||||
@ -67,7 +67,7 @@ namespace osu.Game.Screens.Multi.Components
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
beatmapAuthor.Colour = colours.Gray9;
|
||||
beatmapAuthor.Colour = colours.GrayC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ namespace osu.Game.Screens.Multi.Components
|
||||
|
||||
private readonly Container rulesetContainer;
|
||||
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly Bindable<GameType> Type = new Bindable<GameType>();
|
||||
public readonly IBindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly IBindable<GameType> Type = new Bindable<GameType>();
|
||||
|
||||
public ModeTypeInfo()
|
||||
{
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
|
||||
private readonly IBindableCollection<PlaylistItem> playlistBind = new BindableCollection<PlaylistItem>();
|
||||
private readonly IBindable<DateTimeOffset> endDateBind = new Bindable<DateTimeOffset>();
|
||||
|
||||
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
||||
private readonly Bindable<BeatmapInfo> beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
private UpdateableBeatmapBackgroundSprite background;
|
||||
private BeatmapTitle beatmapTitle;
|
||||
@ -243,6 +243,10 @@ namespace osu.Game.Screens.Multi.Lounge.Components
|
||||
|
||||
endDateBind.BindValueChanged(d => endDate.Date = d, true);
|
||||
|
||||
background.Beatmap.BindTo(beatmap);
|
||||
modeTypeInfo.Beatmap.BindTo(beatmap);
|
||||
beatmapTitle.Beatmap.BindTo(beatmap);
|
||||
|
||||
modeTypeInfo.Type.BindTo(typeBind);
|
||||
|
||||
participantInfo.Host.BindTo(hostBind);
|
||||
@ -266,12 +270,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
|
||||
return;
|
||||
|
||||
// For now, only the first playlist item is supported
|
||||
var item = playlistBind.First();
|
||||
|
||||
beatmap.Value = beatmaps.GetWorkingBeatmap(item.Beatmap);
|
||||
background.Beatmap.Value = item.Beatmap;
|
||||
modeTypeInfo.Beatmap.Value = item.Beatmap;
|
||||
beatmapTitle.Beatmap.Value = item.Beatmap;
|
||||
beatmap.Value = playlistBind.First().Beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
|
||||
private readonly Bindable<IEnumerable<User>> participantsBind = new Bindable<IEnumerable<User>>();
|
||||
private readonly IBindableCollection<PlaylistItem> playlistBind = new BindableCollection<PlaylistItem>();
|
||||
|
||||
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
||||
private readonly Bindable<BeatmapInfo> beatmap = new Bindable<BeatmapInfo>();
|
||||
|
||||
private OsuColour colours;
|
||||
private Box statusStrip;
|
||||
@ -190,6 +190,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components
|
||||
|
||||
beatmapTypeInfo.Type.BindTo(typeBind);
|
||||
|
||||
background.Beatmap.BindTo(beatmap);
|
||||
beatmapTypeInfo.Beatmap.BindTo(beatmap);
|
||||
|
||||
Room.BindValueChanged(updateRoom, true);
|
||||
}
|
||||
|
||||
@ -244,11 +247,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components
|
||||
return;
|
||||
|
||||
// For now, only the first playlist item is supported
|
||||
var item = playlistBind.First();
|
||||
|
||||
beatmap.Value = beatmaps.GetWorkingBeatmap(item.Beatmap);
|
||||
background.Beatmap.Value = item.Beatmap;
|
||||
beatmapTypeInfo.Beatmap.Value = item.Beatmap;
|
||||
beatmap.Value = playlistBind.First().Beatmap;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -14,6 +15,10 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays.SearchableList;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Multi.Components;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Multi.Match.Components
|
||||
@ -23,6 +28,8 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
public const float HEIGHT = 200;
|
||||
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly IBindable<GameType> Type = new Bindable<GameType>();
|
||||
public readonly Bindable<IEnumerable<Mod>> Mods = new Bindable<IEnumerable<Mod>>();
|
||||
|
||||
private readonly Box tabStrip;
|
||||
|
||||
@ -30,25 +37,31 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
|
||||
public Action OnRequestSelectBeatmap;
|
||||
|
||||
public Header()
|
||||
public Header(Room room)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = HEIGHT;
|
||||
|
||||
BeatmapTypeInfo beatmapTypeInfo;
|
||||
BeatmapSelectButton beatmapButton;
|
||||
UpdateableBeatmapBackgroundSprite background;
|
||||
ModDisplay modDisplay;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = background = new HeaderBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black.Opacity(0.5f)),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new HeaderBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both },
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.4f), Color4.Black.Opacity(0.6f)),
|
||||
},
|
||||
}
|
||||
},
|
||||
tabStrip = new Box
|
||||
{
|
||||
@ -60,9 +73,27 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING },
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Left = SearchableListOverlay.WIDTH_PADDING,
|
||||
Top = 20
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
beatmapTypeInfo = new BeatmapTypeInfo(),
|
||||
modDisplay = new ModDisplay
|
||||
{
|
||||
Scale = new Vector2(0.75f),
|
||||
DisplayUnrankedText = false
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
@ -70,13 +101,13 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 200,
|
||||
Padding = new MarginPadding { Vertical = 5 },
|
||||
Child = beatmapButton = new BeatmapSelectButton
|
||||
Child = beatmapButton = new BeatmapSelectButton(room)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 1
|
||||
},
|
||||
},
|
||||
Tabs = new MatchTabControl
|
||||
Tabs = new MatchTabControl(room)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
@ -86,6 +117,10 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
},
|
||||
};
|
||||
|
||||
beatmapTypeInfo.Beatmap.BindTo(Beatmap);
|
||||
beatmapTypeInfo.Type.BindTo(Type);
|
||||
modDisplay.Current.BindTo(Mods);
|
||||
|
||||
beatmapButton.Action = () => OnRequestSelectBeatmap?.Invoke();
|
||||
|
||||
background.Beatmap.BindTo(Beatmap);
|
||||
@ -101,17 +136,10 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
{
|
||||
private readonly IBindable<int?> roomIDBind = new Bindable<int?>();
|
||||
|
||||
[Resolved]
|
||||
private Room room { get; set; }
|
||||
|
||||
public BeatmapSelectButton()
|
||||
public BeatmapSelectButton(Room room)
|
||||
{
|
||||
Text = "Select beatmap";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
roomIDBind.BindTo(room.RoomID);
|
||||
roomIDBind.BindValueChanged(v => this.FadeTo(v.HasValue ? 0 : 1), true);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions;
|
||||
@ -14,9 +13,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays.SearchableList;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Multi.Components;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Multi.Match.Components
|
||||
@ -33,8 +30,6 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
public readonly Bindable<RoomAvailability> Availability = new Bindable<RoomAvailability>();
|
||||
public readonly Bindable<RoomStatus> Status = new Bindable<RoomStatus>();
|
||||
public readonly Bindable<BeatmapInfo> Beatmap = new Bindable<BeatmapInfo>();
|
||||
public readonly Bindable<GameType> Type = new Bindable<GameType>();
|
||||
public readonly Bindable<IEnumerable<Mod>> Mods = new Bindable<IEnumerable<Mod>>();
|
||||
public readonly Bindable<DateTimeOffset> EndDate = new Bindable<DateTimeOffset>();
|
||||
|
||||
public Info(Room room)
|
||||
@ -44,9 +39,7 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
|
||||
ReadyButton readyButton;
|
||||
ViewBeatmapButton viewBeatmapButton;
|
||||
BeatmapTypeInfo beatmapTypeInfo;
|
||||
OsuSpriteText name;
|
||||
ModDisplay modDisplay;
|
||||
EndDateInfo endDate;
|
||||
|
||||
Children = new Drawable[]
|
||||
@ -82,20 +75,6 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
endDate = new EndDateInfo { TextSize = 14 }
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
beatmapTypeInfo = new BeatmapTypeInfo(),
|
||||
modDisplay = new ModDisplay
|
||||
{
|
||||
Scale = new Vector2(0.75f),
|
||||
DisplayUnrankedText = false
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
new FillFlowContainer
|
||||
@ -119,10 +98,6 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
},
|
||||
};
|
||||
|
||||
beatmapTypeInfo.Beatmap.BindTo(Beatmap);
|
||||
beatmapTypeInfo.Type.BindTo(Type);
|
||||
modDisplay.Current.BindTo(Mods);
|
||||
|
||||
viewBeatmapButton.Beatmap.BindTo(Beatmap);
|
||||
readyButton.Beatmap.BindTo(Beatmap);
|
||||
|
||||
|
@ -16,11 +16,10 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
{
|
||||
private readonly IBindable<int?> roomIdBind = new Bindable<int?>();
|
||||
|
||||
[Resolved]
|
||||
private Room room { get; set; }
|
||||
|
||||
public MatchTabControl()
|
||||
public MatchTabControl(Room room)
|
||||
{
|
||||
roomIdBind.BindTo(room.RoomID);
|
||||
|
||||
AddItem(new RoomMatchPage());
|
||||
AddItem(new SettingsMatchPage());
|
||||
}
|
||||
@ -28,7 +27,6 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
roomIdBind.BindTo(room.RoomID);
|
||||
roomIdBind.BindValueChanged(v =>
|
||||
{
|
||||
if (v.HasValue)
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -133,6 +134,7 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(30),
|
||||
TimeSpan.FromHours(1),
|
||||
TimeSpan.FromHours(2),
|
||||
@ -159,14 +161,29 @@ namespace osu.Game.Screens.Multi.Match.Components
|
||||
},
|
||||
},
|
||||
},
|
||||
ApplyButton = new CreateRoomButton
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = new Vector2(230, 35),
|
||||
Margin = new MarginPadding { Bottom = 20 },
|
||||
Action = apply,
|
||||
},
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Y = 2,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 60,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = OsuColour.FromHex(@"28242d").Darken(0.5f).Opacity(1f),
|
||||
},
|
||||
ApplyButton = new CreateRoomButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(230, 35),
|
||||
Action = apply,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -80,7 +80,7 @@ namespace osu.Game.Screens.Multi.Match
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[] { header = new Components.Header { Depth = -1 } },
|
||||
new Drawable[] { header = new Components.Header(room) { Depth = -1 } },
|
||||
new Drawable[] { info = new Info(room) { OnStart = onStart } },
|
||||
new Drawable[]
|
||||
{
|
||||
@ -135,9 +135,10 @@ namespace osu.Game.Screens.Multi.Match
|
||||
info.Name.BindTo(nameBind);
|
||||
info.Status.BindTo(statusBind);
|
||||
info.Availability.BindTo(availabilityBind);
|
||||
info.Type.BindTo(typeBind);
|
||||
info.EndDate.BindTo(endDateBind);
|
||||
|
||||
header.Type.BindTo(typeBind);
|
||||
|
||||
participants.Users.BindTo(participantsBind);
|
||||
participants.MaxParticipants.BindTo(maxParticipantsBind);
|
||||
|
||||
@ -167,8 +168,8 @@ namespace osu.Game.Screens.Multi.Match
|
||||
var item = playlistBind.First();
|
||||
|
||||
header.Beatmap.Value = item.Beatmap;
|
||||
header.Mods.Value = item.RequiredMods;
|
||||
info.Beatmap.Value = item.Beatmap;
|
||||
info.Mods.Value = item.RequiredMods;
|
||||
|
||||
// Todo: item.Beatmap can be null here...
|
||||
var localBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == item.BeatmapID) ?? item.Beatmap;
|
||||
|
Loading…
Reference in New Issue
Block a user