mirror of
https://github.com/clangen/musikcube.git
synced 2025-01-05 21:55:24 +00:00
Added the ability to overwrite UP/DOWN/LEFT/PAGE_UP/PAGE_DOWN/HOME/END
keys via `hotkeys.json` to allow for vim-like bindings.
This commit is contained in:
parent
b71d5768de
commit
ef933c3649
@ -135,6 +135,7 @@ int main(int argc, char* argv[]) {
|
||||
PlaybackService playback(Window::MessageQueue(), library, transport);
|
||||
|
||||
GlobalHotkeys globalHotkeys(playback, library);
|
||||
Window::SetNavigationKeys(Hotkeys::NavigationKeys());
|
||||
|
||||
musik::core::plugin::InstallDependencies(library);
|
||||
|
||||
|
@ -327,16 +327,7 @@ void LibraryLayout::ProcessMessage(musik::core::runtime::IMessage &message) {
|
||||
}
|
||||
|
||||
bool LibraryLayout::KeyPress(const std::string& key) {
|
||||
if (key == "^[") { /* switches between browse/now playing */
|
||||
if (this->visibleLayout != this->browseLayout) {
|
||||
this->ShowBrowse();
|
||||
}
|
||||
else {
|
||||
this->ShowNowPlaying();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (Hotkeys::Is(Hotkeys::NavigateLibraryPlayQueue, key)) {
|
||||
if (Hotkeys::Is(Hotkeys::NavigateLibraryPlayQueue, key)) {
|
||||
this->ShowNowPlaying();
|
||||
return true;
|
||||
}
|
||||
@ -352,12 +343,12 @@ bool LibraryLayout::KeyPress(const std::string& key) {
|
||||
this->ShowTrackSearch();
|
||||
return true;
|
||||
}
|
||||
else if (this->GetFocus() == this->transportView && key == "KEY_UP") {
|
||||
else if (this->GetFocus() == this->transportView && Hotkeys::Is(Hotkeys::Up, key)) {
|
||||
this->transportView->Blur();
|
||||
this->visibleLayout->FocusLast();
|
||||
return true;
|
||||
}
|
||||
else if (this->GetFocus() == this->transportView && key == "KEY_DOWN") {
|
||||
else if (this->GetFocus() == this->transportView && Hotkeys::Is(Hotkeys::Down, key)) {
|
||||
this->transportView->Blur();
|
||||
this->visibleLayout->FocusFirst();
|
||||
return true;
|
||||
|
@ -300,7 +300,7 @@ bool MainLayout::KeyPress(const std::string& key) {
|
||||
shortcut bar focus... */
|
||||
if (key == "^[" ||
|
||||
(key == "KEY_ENTER" && this->shortcutsFocused) ||
|
||||
(key == "KEY_UP" && this->shortcutsFocused))
|
||||
(Hotkeys::Is(Hotkeys::Up, key) && this->shortcutsFocused))
|
||||
{
|
||||
this->shortcutsFocused = !this->shortcutsFocused;
|
||||
|
||||
@ -315,8 +315,8 @@ bool MainLayout::KeyPress(const std::string& key) {
|
||||
}
|
||||
|
||||
if (this->shortcutsFocused) {
|
||||
if (key == "KEY_DOWN" || key == "KEY_LEFT" ||
|
||||
key == "KEY_UP" || key == "KEY_RIGHT")
|
||||
if (Hotkeys::Is(Hotkeys::Down, key) || Hotkeys::Is(Hotkeys::Left, key) ||
|
||||
Hotkeys::Is(Hotkeys::Up, key) || Hotkeys::Is(Hotkeys::Right, key))
|
||||
{
|
||||
/* layouts allow focusing via TAB and sometimes arrow
|
||||
keys. suppress these from bubbling. */
|
||||
|
@ -38,6 +38,7 @@
|
||||
#include <cursespp/Screen.h>
|
||||
#include <cursespp/Text.h>
|
||||
#include <core/library/LocalLibraryConstants.h>
|
||||
#include <app/util/Hotkeys.h>
|
||||
#include "SearchLayout.h"
|
||||
|
||||
using namespace musik::core::library::constants;
|
||||
@ -158,13 +159,13 @@ bool SearchLayout::KeyPress(const std::string& key) {
|
||||
}
|
||||
}
|
||||
|
||||
if (key == "KEY_DOWN") {
|
||||
if (Hotkeys::Is(Hotkeys::Down, key)) {
|
||||
if (this->GetFocus() == this->input) {
|
||||
this->FocusNext();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (key == "KEY_UP") {
|
||||
else if (Hotkeys::Is(Hotkeys::Up, key)) {
|
||||
if (IS_CATEGORY(this->GetFocus())) {
|
||||
this->SetFocus(this->input);
|
||||
return true;
|
||||
|
@ -92,7 +92,7 @@ void TrackSearchLayout::InitializeWindows() {
|
||||
this->input->EnterPressed.connect(this, &TrackSearchLayout::OnEnterPressed);
|
||||
this->input->SetFocusOrder(0);
|
||||
this->AddWindow(this->input);
|
||||
|
||||
|
||||
this->trackList.reset(new TrackListView(this->playback, this->library));
|
||||
this->trackList->SetFocusOrder(1);
|
||||
this->trackList->SetAllowArrowKeyPropagation();
|
||||
@ -144,13 +144,13 @@ void TrackSearchLayout::OnEnterPressed(cursespp::TextInput* sender) {
|
||||
}
|
||||
|
||||
bool TrackSearchLayout::KeyPress(const std::string& key) {
|
||||
if (key == "KEY_DOWN") {
|
||||
if (Hotkeys::Is(Hotkeys::Down, key)) {
|
||||
if (this->GetFocus() == this->input) {
|
||||
this->FocusNext();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (key == "KEY_UP") {
|
||||
else if (Hotkeys::Is(Hotkeys::Up, key)) {
|
||||
if (this->GetFocus() == this->trackList) {
|
||||
this->SetFocus(this->input);
|
||||
return true;
|
||||
|
@ -54,6 +54,15 @@ struct EnumHasher {
|
||||
|
||||
/* map from internal ID to user-friendly JSON key name */
|
||||
static std::unordered_map<std::string, Id> NAME_TO_ID = {
|
||||
{ "key_up", Id::Up },
|
||||
{ "key_down", Id::Down },
|
||||
{ "key_left", Id::Left },
|
||||
{ "key_right", Id::Right },
|
||||
{ "key_page_up", Id::PageUp },
|
||||
{ "key_page_down", Id::PageDown },
|
||||
{ "key_home", Id::Home },
|
||||
{ "key_end", Id::End },
|
||||
|
||||
{ "navigate_library", Id::NavigateLibrary },
|
||||
{ "navigate_library_browse", Id::NavigateLibraryBrowse },
|
||||
{ "navigate_library_browse_artists", Id::NavigateLibraryBrowseArtists },
|
||||
@ -105,6 +114,15 @@ static std::unordered_map<std::string, Id> NAME_TO_ID = {
|
||||
|
||||
/* default hotkeys */
|
||||
static std::unordered_map<Id, std::string, EnumHasher> ID_TO_DEFAULT = {
|
||||
{ Id::Up, "KEY_UP" },
|
||||
{ Id::Down, "KEY_DOWN" },
|
||||
{ Id::Left, "KEY_LEFT" },
|
||||
{ Id::Right, "KEY_RIGHT" },
|
||||
{ Id::PageUp, "KEY_PPAGE" },
|
||||
{ Id::PageDown, "KEY_NPAGE" },
|
||||
{ Id::Home, "KEY_HOME" },
|
||||
{ Id::End, "KEY_END" },
|
||||
|
||||
{ Id::NavigateLibrary, "a" },
|
||||
{ Id::NavigateLibraryBrowse, "b" },
|
||||
{ Id::NavigateLibraryBrowseArtists, "1" },
|
||||
@ -254,3 +272,34 @@ std::string Hotkeys::Get(Id id) {
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
class NavigationKeysImpl : public cursespp::INavigationKeys {
|
||||
public:
|
||||
virtual bool Up(const std::string& key) override { return Up() == key; }
|
||||
virtual bool Down(const std::string& key) override { return Down() == key; }
|
||||
virtual bool Left(const std::string& key) override { return Left() == key; }
|
||||
virtual bool Right(const std::string& key) override { return Right() == key; }
|
||||
virtual bool PageUp(const std::string& key) override { return PageUp() == key; }
|
||||
virtual bool PageDown(const std::string& key) override { return PageDown() == key; }
|
||||
virtual bool Home(const std::string& key) override { return Home() == key; }
|
||||
virtual bool End(const std::string& key) override { return End() == key; }
|
||||
virtual bool Next(const std::string& key) override { return Next() == key; }
|
||||
virtual bool Prev(const std::string& key) override { return Prev() == key; }
|
||||
virtual bool Mode(const std::string& key) override { return Mode() == key; }
|
||||
|
||||
virtual std::string Up() override { return Hotkeys::Get(Hotkeys::Up); }
|
||||
virtual std::string Down() override { return Hotkeys::Get(Hotkeys::Down); }
|
||||
virtual std::string Left() override { return Hotkeys::Get(Hotkeys::Left); }
|
||||
virtual std::string Right() override { return Hotkeys::Get(Hotkeys::Right); }
|
||||
virtual std::string PageUp() override { return Hotkeys::Get(Hotkeys::PageUp); }
|
||||
virtual std::string PageDown() override { return Hotkeys::Get(Hotkeys::PageDown); }
|
||||
virtual std::string Home() override { return Hotkeys::Get(Hotkeys::Home); }
|
||||
virtual std::string End() override { return Hotkeys::Get(Hotkeys::End); }
|
||||
virtual std::string Next() override { return "KEY_TAB"; }
|
||||
virtual std::string Prev() override { return "KEY_BTAB"; }
|
||||
virtual std::string Mode() override { return "^["; }
|
||||
};
|
||||
|
||||
std::shared_ptr<cursespp::INavigationKeys> Hotkeys::NavigationKeys() {
|
||||
return std::shared_ptr<cursespp::INavigationKeys>(new NavigationKeysImpl());
|
||||
}
|
||||
|
@ -36,13 +36,23 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
|
||||
#include <core/library/ILibrary.h>
|
||||
#include <cursespp/INavigationKeys.h>
|
||||
|
||||
namespace musik {
|
||||
namespace cube {
|
||||
class Hotkeys {
|
||||
public:
|
||||
enum Id {
|
||||
/* selection */
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Home,
|
||||
End,
|
||||
|
||||
/* navigation */
|
||||
NavigateLibrary,
|
||||
NavigateLibraryBrowse,
|
||||
@ -100,6 +110,7 @@ namespace musik {
|
||||
|
||||
static bool Is(Id id, const std::string& kn);
|
||||
static std::string Get(Id id);
|
||||
static std::shared_ptr<cursespp::INavigationKeys> NavigationKeys();
|
||||
|
||||
private:
|
||||
Hotkeys();
|
||||
|
@ -46,6 +46,7 @@
|
||||
#include <core/library/LocalLibraryConstants.h>
|
||||
#include <core/runtime/Message.h>
|
||||
|
||||
#include <app/util/Hotkeys.h>
|
||||
#include <app/util/Messages.h>
|
||||
|
||||
#include <boost/format.hpp>
|
||||
@ -296,11 +297,11 @@ static size_t writePlayingFormat(
|
||||
}
|
||||
|
||||
static inline bool inc(const std::string& kn) {
|
||||
return (/*kn == "KEY_UP" ||*/ kn == "KEY_RIGHT");
|
||||
return (Hotkeys::Is(Hotkeys::Right, kn));
|
||||
}
|
||||
|
||||
static inline bool dec(const std::string& kn) {
|
||||
return (/*kn == "KEY_DOWN" ||*/ kn == "KEY_LEFT");
|
||||
return (Hotkeys::Is(Hotkeys::Left, kn));
|
||||
}
|
||||
|
||||
TransportWindow::TransportWindow(musik::core::audio::PlaybackService& playback)
|
||||
|
66
src/musikcube/cursespp/INavigationKeys.h
Normal file
66
src/musikcube/cursespp/INavigationKeys.h
Normal file
@ -0,0 +1,66 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (c) 2007-2017 musikcube team
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
//
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
//
|
||||
// * Neither the name of the author nor the names of other contributors may
|
||||
// be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace cursespp {
|
||||
class INavigationKeys {
|
||||
public:
|
||||
virtual ~INavigationKeys() { }
|
||||
|
||||
virtual bool Up(const std::string& key) = 0;
|
||||
virtual bool Down(const std::string& key) = 0;
|
||||
virtual bool Left(const std::string& key) = 0;
|
||||
virtual bool Right(const std::string& key) = 0;
|
||||
virtual bool Next(const std::string& key) = 0;
|
||||
virtual bool PageUp(const std::string& key) = 0;
|
||||
virtual bool PageDown(const std::string& key) = 0;
|
||||
virtual bool Home(const std::string& key) = 0;
|
||||
virtual bool End(const std::string& key) = 0;
|
||||
virtual bool Prev(const std::string& key) = 0;
|
||||
virtual bool Mode(const std::string& key) = 0;
|
||||
|
||||
virtual std::string Up() = 0;
|
||||
virtual std::string Down() = 0;
|
||||
virtual std::string Left() = 0;
|
||||
virtual std::string Right() = 0;
|
||||
virtual std::string Next() = 0;
|
||||
virtual std::string PageUp() = 0;
|
||||
virtual std::string PageDown() = 0;
|
||||
virtual std::string Home() = 0;
|
||||
virtual std::string End() = 0;
|
||||
virtual std::string Prev() = 0;
|
||||
virtual std::string Mode() = 0;
|
||||
};
|
||||
}
|
@ -386,11 +386,12 @@ void LayoutBase::SetFocusMode(FocusMode mode) {
|
||||
}
|
||||
|
||||
bool LayoutBase::KeyPress(const std::string& key) {
|
||||
if (key == "KEY_LEFT" || key == "KEY_UP") {
|
||||
auto& keys = NavigationKeys();
|
||||
if (keys.Left(key) || keys.Up(key)) {
|
||||
this->FocusPrev();
|
||||
return true;
|
||||
}
|
||||
else if (key == "KEY_RIGHT" || key == "KEY_DOWN") {
|
||||
else if (keys.Right(key) || keys.Down(key)) {
|
||||
this->FocusNext();
|
||||
return true;
|
||||
}
|
||||
|
@ -121,23 +121,24 @@ bool ScrollableWindow::KeyPress(const std::string& key) {
|
||||
the logical (selected) index doesn't actually change -- i.e. the
|
||||
user is at the beginning or end of the scrollable area. this is so
|
||||
controllers can change focus in response to UP/DOWN if necessary. */
|
||||
auto& keys = NavigationKeys();
|
||||
|
||||
if (key == "KEY_NPAGE") { this->PageDown(); return true; }
|
||||
else if (key == "KEY_PPAGE") { this->PageUp(); return true; }
|
||||
else if (key == "KEY_DOWN") {
|
||||
if (keys.PageDown(key)) { this->PageDown(); return true; }
|
||||
else if (keys.PageUp(key)) { this->PageUp(); return true; }
|
||||
else if (keys.Down(key)) {
|
||||
const size_t before = this->GetScrollPosition().logicalIndex;
|
||||
this->ScrollDown();
|
||||
const size_t after = this->GetScrollPosition().logicalIndex;
|
||||
return !this->allowArrowKeyPropagation || (before != after);
|
||||
}
|
||||
else if (key == "KEY_UP") {
|
||||
else if (keys.Up(key)) {
|
||||
const size_t before = this->GetScrollPosition().logicalIndex;
|
||||
this->ScrollUp();
|
||||
const size_t after = this->GetScrollPosition().logicalIndex;
|
||||
return !this->allowArrowKeyPropagation || (before != after);
|
||||
}
|
||||
else if (key == "KEY_HOME") { this->ScrollToTop(); return true; }
|
||||
else if (key == "KEY_END") { this->ScrollToBottom(); return true; }
|
||||
else if (keys.Home(key)) { this->ScrollToTop(); return true; }
|
||||
else if (keys.End(key)) { this->ScrollToBottom(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,9 @@ bool ShortcutsWindow::KeyPress(const std::string& key) {
|
||||
if (this->changedCallback && this->IsFocused()) {
|
||||
int count = (int) this->entries.size();
|
||||
if (count > 0) {
|
||||
if (key == "KEY_RIGHT") {
|
||||
auto& keys = NavigationKeys();
|
||||
|
||||
if (keys.Right(key)) {
|
||||
int active = getActiveIndex();
|
||||
if (active >= 0 && active + 1 < count) {
|
||||
this->activeKey = this->entries[active + 1]->key;
|
||||
@ -120,7 +122,7 @@ bool ShortcutsWindow::KeyPress(const std::string& key) {
|
||||
this->Redraw();
|
||||
return true;
|
||||
}
|
||||
else if (key == "KEY_LEFT") {
|
||||
else if (keys.Left(key)) {
|
||||
int active = getActiveIndex();
|
||||
if (active > 0) {
|
||||
this->activeKey = this->entries[active - 1]->key;
|
||||
|
@ -53,6 +53,7 @@ static bool freeze = false;
|
||||
static Window* top = nullptr;
|
||||
|
||||
static MessageQueue messageQueue;
|
||||
static std::shared_ptr<INavigationKeys> keys;
|
||||
|
||||
#define ENABLE_BOUNDS_CHECK 1
|
||||
|
||||
@ -782,3 +783,44 @@ void Window::Blur() {
|
||||
this->Redraw();
|
||||
}
|
||||
}
|
||||
|
||||
void Window::SetNavigationKeys(std::shared_ptr<INavigationKeys> keys) {
|
||||
::keys = keys;
|
||||
}
|
||||
|
||||
/* default keys for navigating around sub-views. apps can override this shim to
|
||||
provide VIM-like keybindings, if it wants... */
|
||||
static class DefaultNavigationKeys : public INavigationKeys {
|
||||
public:
|
||||
virtual bool Up(const std::string& key) override { return Up() == key; }
|
||||
virtual bool Down(const std::string& key) override { return Down() == key; }
|
||||
virtual bool Left(const std::string& key) override { return Left() == key; }
|
||||
virtual bool Right(const std::string& key) override { return Right() == key; }
|
||||
virtual bool Next(const std::string& key) override { return Next() == key; }
|
||||
virtual bool Prev(const std::string& key) override { return Prev() == key; }
|
||||
virtual bool Mode(const std::string& key) override { return Mode() == key; }
|
||||
virtual bool PageUp(const std::string& key) override { return PageUp() == key; }
|
||||
virtual bool PageDown(const std::string& key) override { return PageDown() == key; }
|
||||
virtual bool Home(const std::string& key) override { return Home() == key; }
|
||||
virtual bool End(const std::string& key) override { return End() == key; }
|
||||
|
||||
virtual std::string Up() override { return "KEY_UP"; }
|
||||
virtual std::string Down() override { return "KEY_DOWN"; }
|
||||
virtual std::string Left() override { return "KEY_LEFT"; }
|
||||
virtual std::string Right() override { return "KEY_RIGHT"; }
|
||||
virtual std::string Next() override { return "KEY_TAB"; }
|
||||
virtual std::string Prev() override { return "KEY_BTAB"; }
|
||||
virtual std::string Mode() override { return "^["; }
|
||||
virtual std::string PageUp() override { return "KEY_PPAGE"; }
|
||||
virtual std::string PageDown() override { return "KEY_NPAGE"; }
|
||||
virtual std::string Home() override { return "KEY_HOME"; }
|
||||
virtual std::string End() override { return "KEY_END"; }
|
||||
} defaultNavigationKeys;
|
||||
|
||||
INavigationKeys& Window::NavigationKeys() {
|
||||
if (::keys) {
|
||||
return *::keys.get();
|
||||
}
|
||||
|
||||
return defaultNavigationKeys;
|
||||
}
|
@ -36,7 +36,7 @@
|
||||
|
||||
#include "curses_config.h"
|
||||
#include "IWindow.h"
|
||||
|
||||
#include "INavigationKeys.h"
|
||||
#include <core/runtime/IMessageQueue.h>
|
||||
|
||||
#ifdef WIN32
|
||||
@ -123,6 +123,8 @@ namespace cursespp {
|
||||
static void Freeze();
|
||||
static void Unfreeze();
|
||||
|
||||
static void SetNavigationKeys(std::shared_ptr<INavigationKeys> keys);
|
||||
|
||||
static musik::core::runtime::IMessageQueue& MessageQueue();
|
||||
|
||||
protected:
|
||||
@ -131,6 +133,7 @@ namespace cursespp {
|
||||
void PostMessage(int messageType, int64_t user1 = 0, int64_t user2 = 0, int64_t delay = 0);
|
||||
void DebounceMessage(int messageType, int64_t user1 = 0, int64_t user2 = 0, int64_t delay = 0);
|
||||
void RemoveMessage(int messageType);
|
||||
static INavigationKeys& NavigationKeys();
|
||||
|
||||
virtual void Create();
|
||||
virtual void Destroy();
|
||||
|
@ -227,6 +227,7 @@ xcopy "$(SolutionDir)src\plugins\websocket_remote\3rdparty\win32_bin\$(Configura
|
||||
<ClInclude Include="cursespp\IInput.h" />
|
||||
<ClInclude Include="cursespp\IKeyHandler.h" />
|
||||
<ClInclude Include="cursespp\ILayout.h" />
|
||||
<ClInclude Include="cursespp\INavigationKeys.h" />
|
||||
<ClInclude Include="cursespp\InputOverlay.h" />
|
||||
<ClInclude Include="cursespp\IOrderable.h" />
|
||||
<ClInclude Include="cursespp\IOverlay.h" />
|
||||
|
@ -337,6 +337,9 @@
|
||||
<ClInclude Include="cursespp\Colors.h">
|
||||
<Filter>cursespp\util</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="cursespp\INavigationKeys.h">
|
||||
<Filter>cursespp</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="cursespp">
|
||||
|
Loading…
Reference in New Issue
Block a user