diff --git a/scenes/map_browser.tscn b/scenes/map_browser.tscn index 609bb9fa..d1e68121 100644 --- a/scenes/map_browser.tscn +++ b/scenes/map_browser.tscn @@ -8,13 +8,14 @@ [ext_resource type="Script" uid="uid://cexyomdgasvme" path="res://scripts/scenes/MapCard.cs" id="5_vr0hc"] [ext_resource type="FontFile" uid="uid://dwojpwm7o4l1a" path="res://fonts/Nunito/Nunito-Black.ttf" id="6_ueqk1"] [ext_resource type="Texture2D" uid="uid://bvjkhxgqjmg7y" path="res://user/skins/default/ui/buttons/import.png" id="7_ueqk1"] +[ext_resource type="Shader" uid="uid://rngh60dyh10b" path="res://scripts/shaders/outline.gdshader" id="9_vjpuo"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a8y0u"] bg_color = Color(0, 0, 0, 0.8784314) -corner_radius_top_left = 10 -corner_radius_top_right = 10 -corner_radius_bottom_right = 10 -corner_radius_bottom_left = 10 +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i5ncw"] bg_color = Color(0, 0, 0, 0.98039216) @@ -162,6 +163,30 @@ bg_color = Color(0, 0, 0, 0.39215687) [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_27haj"] +[sub_resource type="ShaderMaterial" id="ShaderMaterial_jydku"] +shader = ExtResource("9_vjpuo") +shader_parameter/outline_color = Color(0.05, 0.05, 0.05, 1) +shader_parameter/outline_opacity = 1.0 +shader_parameter/light_color = Color(0.79, 0, 0.27, 1) +shader_parameter/cursor_light_color = Color(0.15, 0.15, 0.15, 1) +shader_parameter/fill = 0.0 +shader_parameter/glow_radius = 350.0 +shader_parameter/glow_exponent = 2.0 +shader_parameter/light_position = Vector2(-10000, 0) +shader_parameter/cursor_position = Vector2(0, 0) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u7sc7"] +draw_center = false +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 1, 1, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + [node name="Map Browser" type="ColorRect" unique_id=1634674963] anchors_preset = 15 anchor_right = 1.0 @@ -302,10 +327,11 @@ popup/item_5/checked = true popup/item_5/id = 5 [node name="Status" type="OptionButton" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/Filters" unique_id=330241703] +visible = false layout_mode = 2 size_flags_horizontal = 3 alignment = 1 -selected = 0 +selected = 2 item_count = 3 popup/item_0/text = "Unranked" popup/item_0/id = 0 @@ -314,50 +340,16 @@ popup/item_1/id = 1 popup/item_2/text = "Both" popup/item_2/id = 2 -[node name="Installed" type="OptionButton" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/Filters" unique_id=1262770447] -layout_mode = 2 -size_flags_horizontal = 3 -alignment = 1 -selected = 2 -item_count = 3 -popup/item_0/text = "Online" -popup/item_0/id = 1 -popup/item_1/text = "Downloaded" -popup/item_1/id = 0 -popup/item_2/text = "Both" -popup/item_2/id = 2 - -[node name="ActiveFilters" type="HBoxContainer" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar" unique_id=1401022026] -visible = false -layout_mode = 2 - -[node name="Label" type="Label" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/ActiveFilters" unique_id=1844162903] -visible = false -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="Scroll" type="ScrollContainer" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/ActiveFilters" unique_id=882056051] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="Tags" type="HBoxContainer" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/ActiveFilters/Scroll" unique_id=403074119] -layout_mode = 2 - -[node name="Clear" type="Button" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/ActiveFilters" unique_id=1284947479] +[node name="Verified" type="CheckBox" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/Filters" unique_id=3268424] layout_mode = 2 -text = "Clear filters" +tooltip_text = "Maps that have been manually picked for beginners" +button_pressed = true +text = "Verified only" -[node name="DownloadStatus" type="HBoxContainer" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar" unique_id=1484611059] -visible = false +[node name="Notable" type="CheckBox" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/Filters" unique_id=400853412] layout_mode = 2 - -[node name="Label" type="Label" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/DownloadStatus" unique_id=1525450065] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="Retry" type="Button" parent="Holder/Background/Layout/TopBarBackground/Margin/TopBar/DownloadStatus" unique_id=1000402623] -layout_mode = 2 -text = "Retry failed" +tooltip_text = "Maps made by at least one notable mapper" +text = "Notable only" [node name="Results" type="ScrollContainer" parent="Holder/Background/Layout" unique_id=716497821] layout_mode = 2 @@ -503,16 +495,18 @@ layout_mode = 2 size_flags_horizontal = 3 theme_override_constants/separation = 0 -[node name="Mapper" type="RichTextLabel" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row/Map/Content/Info/Top/Text/Details/VBoxContainer" unique_id=2078438243] +[node name="Mapper" type="HBoxContainer" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row/Map/Content/Info/Top/Text/Details/VBoxContainer" unique_id=2078438243] layout_mode = 2 mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="By" type="Label" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row/Map/Content/Info/Top/Text/Details/VBoxContainer/Mapper" unique_id=1938475021] +layout_mode = 2 +theme_override_colors/font_color = Color(0.78431374, 0.78431374, 0.78431374, 1) theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.39215687) -theme_override_constants/shadow_outline_size = 1 theme_override_constants/shadow_offset_y = 2 -bbcode_enabled = true -text = "[color=#c8c8c8]by[/color] Mapper " -fit_content = true -autowrap_mode = 0 +theme_override_constants/shadow_outline_size = 1 +text = "by" [node name="Difficulty" type="Label" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row/Map/Content/Info/Top/Text/Details/VBoxContainer" unique_id=2103154130] layout_mode = 2 @@ -532,6 +526,7 @@ layout_mode = 2 mouse_filter = 2 [node name="StatusPill" type="PanelContainer" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row/Map/Content/Info/Bottom" unique_id=359816098] +visible = false layout_mode = 2 mouse_filter = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_ikspe") @@ -557,9 +552,11 @@ vertical_alignment = 1 [node name="Duration" type="Label" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row/Map/Content/Info/Bottom" unique_id=1613545318] layout_mode = 2 -size_flags_horizontal = 2 +size_flags_horizontal = 3 +size_flags_vertical = 1 text = "00:00" label_settings = SubResource("LabelSettings_vofg3") +vertical_alignment = 1 text_overrun_behavior = 3 [node name="RightPanel" type="Control" parent="Holder/Background/Layout/Results/RowsMargin/Rows/MapCardTemplate/Row" unique_id=1150644064] @@ -696,6 +693,17 @@ layout_mode = 2 disabled = true text = "Next →" +[node name="Outline" type="Panel" parent="Holder" unique_id=688247658] +material = SubResource("ShaderMaterial_jydku") +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_u7sc7") + [node name="SearchTimer" type="Timer" parent="." unique_id=1241663088] wait_time = 0.3 one_shot = true diff --git a/scripts/api/QueryParameters.cs b/scripts/api/QueryParameters.cs index 947fac27..9650e95e 100644 --- a/scripts/api/QueryParameters.cs +++ b/scripts/api/QueryParameters.cs @@ -13,4 +13,6 @@ public class MapQueryParameters : QueryParameters public string Artist { get; set; } public int[] Difficulties { get; set; } = []; public string[] Status { get; set; } = []; + public bool Notable { get; set; } + public bool Verified { get; set; } } diff --git a/scripts/api/RateLimitedException.cs b/scripts/api/RateLimitedException.cs new file mode 100644 index 00000000..975b7e5d --- /dev/null +++ b/scripts/api/RateLimitedException.cs @@ -0,0 +1,12 @@ +using System; + +public class RateLimitedException : Exception +{ + public TimeSpan? RetryAfter { get; } + + public RateLimitedException(TimeSpan? retryAfter) + : base("Rate limited") + { + RetryAfter = retryAfter; + } +} diff --git a/scripts/api/RateLimitedException.cs.uid b/scripts/api/RateLimitedException.cs.uid new file mode 100644 index 00000000..a3f7c0b1 --- /dev/null +++ b/scripts/api/RateLimitedException.cs.uid @@ -0,0 +1 @@ +uid://dodfyv3h61s1s diff --git a/scripts/api/endpoints/Maps.cs b/scripts/api/endpoints/Maps.cs index 46a3010a..6f13038f 100644 --- a/scripts/api/endpoints/Maps.cs +++ b/scripts/api/endpoints/Maps.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Net; using System.Text.Json; using System.Threading.Tasks; using System.Web; @@ -23,10 +24,18 @@ public static async Task Search(MapQueryParameters queryParameters query["difficulty"] = string.Join(",", queryParameters.Difficulties); if (queryParameters.Status.Length > 0) query["status"] = string.Join(",", queryParameters.Status); + if (queryParameters.Notable) + query["notable"] = "true"; + if (queryParameters.Verified) + query["verified"] = "true"; string url = $"/maps?{query}"; var response = await ApiClient.CLIENT.GetAsync(url); + + if (response.StatusCode == HttpStatusCode.TooManyRequests) + throw new RateLimitedException(response.Headers.RetryAfter?.Delta); + response.EnsureSuccessStatusCode(); string json = await response.Content.ReadAsStringAsync(); return JsonDocument.Parse(json); diff --git a/scripts/map/MapBrowserService.cs b/scripts/map/MapBrowserService.cs index b56674ad..1e0bb2ad 100644 --- a/scripts/map/MapBrowserService.cs +++ b/scripts/map/MapBrowserService.cs @@ -5,12 +5,16 @@ using System.Threading.Tasks; using Godot; +public record MapSearchResult(JsonElement[] Maps, int Total); + public static class MapBrowserService { - public static async Task Search(MapQueryParameters queryParameters) + public static async Task Search(MapQueryParameters queryParameters) { using var result = await Maps.Search(queryParameters); - return [.. result.RootElement.GetProperty("maps").EnumerateArray().Select(e => e.Clone())]; + JsonElement[] maps = [.. result.RootElement.GetProperty("maps").EnumerateArray().Select(e => e.Clone())]; + + return new MapSearchResult(maps, result.RootElement.GetProperty("total").GetInt32()); } public static async Task GetMapFile(string fileUrl, CancellationToken token) diff --git a/scripts/scenes/MapBrowser.cs b/scripts/scenes/MapBrowser.cs index a08c7b38..d3fc305e 100644 --- a/scripts/scenes/MapBrowser.cs +++ b/scripts/scenes/MapBrowser.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Godot; @@ -12,6 +11,8 @@ public partial class MapBrowser : Control public bool Shown; + public bool IsDragging { get; private set; } + private Button hideButton; private Panel holder; private VBoxContainer topBar; @@ -22,12 +23,28 @@ public partial class MapBrowser : Control private Button refresh; private MenuButton difficulty; private OptionButton status; - private OptionButton installed; + private CheckBox verified; + private CheckBox notable; private Timer searchTimer; private HBoxContainer filters; private ScrollContainer results; private MapCard mapCardTemplate; + private OptionButton browseMode; + private HBoxContainer pagination; + private Button previous; + private Button next; + private Label page; private SortDirection sortDirection; + private int offset; + private int total; + private bool stalled; + private double scrollMomentum; + private double scrollTarget; + private double scrollPosition; + private bool dragCandidate; + private float dragStartY; + private float lastDragY; + private bool isScrolling => browseMode.Selected == 1; private enum SortDirection { @@ -35,7 +52,19 @@ private enum SortDirection Ascending, } + // multiple of 3 because the results grid has 3 columns + private const int page_size = 18; + + private const int load_more_margin = 240; + + private const float scroll_step = 1500; + private const float scroll_friction = 20; + private const float scroll_smoothing = 12; + private const float drag_threshold = 6; + private const float drag_scroll_factor = 30; + private CancellationTokenSource source = new(); + private CancellationTokenSource loadingSource; public override void _Ready() { @@ -51,12 +80,19 @@ public override void _Ready() refresh = searchBar.GetNode