refactor: Update window creation and file dialog handling for improved consistency
- Replaced `SDL_Deleter` with `util::SDL_Deleter` in window creation for better utility usage. - Updated file dialog methods to consistently reference `core::FeatureFlags` for feature flag checks. - Refactored file extension retrieval in `EditorManager` to use `util::GetFileExtension` for consistency. - Adjusted `MusicEditor` constructor to accept a ROM pointer, enhancing initialization clarity. - Commented out unused code in `MusicEditor` to improve readability and maintainability. - Updated include paths in `overworld_editor.cc` for better organization. - Cleaned up commented-out code in `editor_selection_dialog.cc` and `welcome_screen.cc` for clarity.
This commit is contained in:
@@ -112,7 +112,7 @@ std::string yaze::util::FileDialogWrapper::ShowSaveFileDialogBespoke(const std::
|
||||
|
||||
// Global feature flag-based dispatch methods
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
|
||||
if (FeatureFlags::get().kUseNativeFileDialog) {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowOpenFileDialogNFD();
|
||||
} else {
|
||||
return ShowOpenFileDialogBespoke();
|
||||
@@ -120,7 +120,7 @@ std::string yaze::util::FileDialogWrapper::ShowOpenFileDialog() {
|
||||
}
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() {
|
||||
if (FeatureFlags::get().kUseNativeFileDialog) {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowOpenFolderDialogNFD();
|
||||
} else {
|
||||
return ShowOpenFolderDialogBespoke();
|
||||
@@ -129,7 +129,7 @@ std::string yaze::util::FileDialogWrapper::ShowOpenFolderDialog() {
|
||||
|
||||
std::string yaze::util::FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name,
|
||||
const std::string& default_extension) {
|
||||
if (FeatureFlags::get().kUseNativeFileDialog) {
|
||||
if (core::FeatureFlags::get().kUseNativeFileDialog) {
|
||||
return ShowSaveFileDialogNFD(default_name, default_extension);
|
||||
} else {
|
||||
return ShowSaveFileDialogBespoke(default_name, default_extension);
|
||||
|
||||
@@ -27,11 +27,11 @@ absl::Status CreateWindow(Window& window, int flags) {
|
||||
int screen_width = display_mode.w * 0.8;
|
||||
int screen_height = display_mode.h * 0.8;
|
||||
|
||||
window.window_ = std::unique_ptr<SDL_Window, SDL_Deleter>(
|
||||
window.window_ = std::unique_ptr<SDL_Window, util::SDL_Deleter>(
|
||||
SDL_CreateWindow("Yet Another Zelda3 Editor", SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL_WINDOWPOS_UNDEFINED, screen_width, screen_height,
|
||||
flags),
|
||||
SDL_Deleter());
|
||||
util::SDL_Deleter());
|
||||
if (window.window_ == nullptr) {
|
||||
return absl::InternalError(
|
||||
absl::StrFormat("SDL_CreateWindow: %s\n", SDL_GetError()));
|
||||
|
||||
@@ -1967,7 +1967,7 @@ void AgentChatWidget::OpenFileInEditor(const std::string& filepath) {
|
||||
filepath.substr(last_slash + 1) : filepath;
|
||||
|
||||
// Set language based on extension
|
||||
std::string ext = core::GetFileExtension(filepath);
|
||||
std::string ext = util::GetFileExtension(filepath);
|
||||
if (ext == "cpp" || ext == "cc" || ext == "h" || ext == "hpp") {
|
||||
tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
|
||||
} else if (ext == "c") {
|
||||
@@ -1994,7 +1994,7 @@ void AgentChatWidget::CreateNewFileInEditor(const std::string& filename) {
|
||||
tab.modified = true;
|
||||
|
||||
// Set language based on extension
|
||||
std::string ext = core::GetFileExtension(filename);
|
||||
std::string ext = util::GetFileExtension(filename);
|
||||
if (ext == "cpp" || ext == "cc" || ext == "h" || ext == "hpp") {
|
||||
tab.editor.SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
|
||||
} else if (ext == "c") {
|
||||
|
||||
@@ -259,7 +259,7 @@ std::string AgentCollaborationCoordinator::GenerateSessionCode() const {
|
||||
}
|
||||
|
||||
std::filesystem::path AgentCollaborationCoordinator::SessionsDirectory() const {
|
||||
std::filesystem::path base = ExpandUserPath(core::GetConfigDirectory());
|
||||
std::filesystem::path base = ExpandUserPath(util::GetConfigDirectory());
|
||||
if (base.empty()) {
|
||||
base = ExpandUserPath(".yaze");
|
||||
}
|
||||
|
||||
@@ -1457,7 +1457,7 @@ void EditorManager::DrawMenuBar() {
|
||||
ImGui::Text("%s", util::GetFileName(file).c_str());
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
std::string ext = core::GetFileExtension(file);
|
||||
std::string ext = util::GetFileExtension(file);
|
||||
if (ext == "sfc" || ext == "smc") {
|
||||
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s ROM",
|
||||
ICON_MD_VIDEOGAME_ASSET);
|
||||
|
||||
@@ -48,7 +48,7 @@ class EditorSet {
|
||||
: assembly_editor_(rom),
|
||||
dungeon_editor_(rom),
|
||||
graphics_editor_(rom),
|
||||
music_editor_(),
|
||||
music_editor_(rom),
|
||||
overworld_editor_(rom),
|
||||
palette_editor_(rom),
|
||||
screen_editor_(rom),
|
||||
|
||||
@@ -97,7 +97,7 @@ void MusicEditor::Initialize() {
|
||||
// 2. Initialize an audio device/stream using a library like SDL. This will
|
||||
// provide a callback for feeding the APU's output samples.
|
||||
// 3. Set up the AssemblyEditor for displaying SPC command text.
|
||||
apu_.Init();
|
||||
// apu_.Init();
|
||||
// TODO: Initialize SDL audio here.
|
||||
}
|
||||
|
||||
@@ -110,16 +110,16 @@ absl::Status MusicEditor::Load() {
|
||||
// 3. Populate `kGameSongs` (or a dynamic equivalent) with the song list
|
||||
// to be displayed in the UI.
|
||||
// 4. Set the default selected song to the first one.
|
||||
music_tracker_.LoadSongs(*rom());
|
||||
// TODO: Populate song list dynamically.
|
||||
song_names_.clear();
|
||||
for (size_t i = 0; i < music_tracker_.songs.size(); ++i) {
|
||||
const auto& song = music_tracker_.songs[i];
|
||||
song_names_.push_back(absl::StrFormat("Song %zu (Addr: 0x%04X)", i + 1, song.addr));
|
||||
}
|
||||
if (!song_names_.empty()) {
|
||||
current_song_index_ = 0;
|
||||
}
|
||||
// music_tracker_.LoadSongs(*rom());
|
||||
// // TODO: Populate song list dynamically.
|
||||
// song_names_.clear();
|
||||
// for (size_t i = 0; i < music_tracker_.songs.size(); ++i) {
|
||||
// const auto& song = music_tracker_.songs[i];
|
||||
// song_names_.push_back(absl::StrFormat("Song %zu (Addr: 0x%04X)", i + 1, song.addr));
|
||||
// }
|
||||
// if (!song_names_.empty()) {
|
||||
// current_song_index_ = 0;
|
||||
// }
|
||||
}
|
||||
|
||||
absl::Status MusicEditor::Update() {
|
||||
@@ -316,82 +316,82 @@ void MusicEditor::DrawTrackerView() {
|
||||
ImGui::Text("%02X", row);
|
||||
ImGui::SameLine();
|
||||
|
||||
for (int channel = 0; channel < num_channels; ++channel) {
|
||||
ImGui::BeginGroup();
|
||||
if (music_tracker_.songs.empty() || current_song_index_ >= music_tracker_.songs.size()) {
|
||||
ImGui::Text("%-4s", "---"); // Fixed width 4 for note
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-2s", "--"); // Fixed width 2 for instrument
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-4s", "----"); // Fixed width 4 for volume/effect
|
||||
} else {
|
||||
const auto& current_song = music_tracker_.songs[current_song_index_];
|
||||
// TODO: Need to get the SongPart for the current channel.
|
||||
// The Song struct has a `tbl` (table of SongPart pointers).
|
||||
// We need to map `channel` (0-7) to the correct `SongPart` in `current_song.tbl`.
|
||||
// For now, assume `current_song.tbl` is directly indexed by `channel`.
|
||||
if (channel < current_song.numparts) {
|
||||
const auto* song_part = current_song.tbl[channel];
|
||||
if (song_part) {
|
||||
short spc_command_index = song_part->tbl[0]; // The SongPart struct has `tbl[8]` which are indices into the SpcCommand array. For now, we'll just use the first track (index 0) of the SongPart.
|
||||
short current_row_time = 0; // Accumulate time to map to rows
|
||||
// for (int channel = 0; channel < num_channels; ++channel) {
|
||||
// ImGui::BeginGroup();
|
||||
// if (music_tracker_.songs.empty() || current_song_index_ >= music_tracker_.songs.size()) {
|
||||
// ImGui::Text("%-4s", "---"); // Fixed width 4 for note
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-2s", "--"); // Fixed width 2 for instrument
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-4s", "----"); // Fixed width 4 for volume/effect
|
||||
// } else {
|
||||
// const auto& current_song = music_tracker_.songs[current_song_index_];
|
||||
// // TODO: Need to get the SongPart for the current channel.
|
||||
// // The Song struct has a `tbl` (table of SongPart pointers).
|
||||
// // We need to map `channel` (0-7) to the correct `SongPart` in `current_song.tbl`.
|
||||
// // For now, assume `current_song.tbl` is directly indexed by `channel`.
|
||||
// if (channel < current_song.numparts) {
|
||||
// const auto* song_part = current_song.tbl[channel];
|
||||
// if (song_part) {
|
||||
// short spc_command_index = song_part->tbl[0]; // The SongPart struct has `tbl[8]` which are indices into the SpcCommand array. For now, we'll just use the first track (index 0) of the SongPart.
|
||||
// short current_row_time = 0; // Accumulate time to map to rows
|
||||
|
||||
// Iterate through SpcCommands
|
||||
while (spc_command_index != -1 && spc_command_index < music_tracker_.m_size) {
|
||||
const auto& spc_command = music_tracker_.current_spc_command_[spc_command_index];
|
||||
// // Iterate through SpcCommands
|
||||
// while (spc_command_index != -1 && spc_command_index < music_tracker_.m_size) {
|
||||
// const auto& spc_command = music_tracker_.current_spc_command_[spc_command_index];
|
||||
|
||||
// TODO: Map spc_command.tim and spc_command.tim2 to rows.
|
||||
// For now, just display the command itself if it falls on the current row.
|
||||
// This logic needs to be refined to correctly interpret command durations.
|
||||
if (current_row_time == row) { // Simplified mapping for now
|
||||
// TODO: Decode SpcCommand to Note, Instrument, Effect strings
|
||||
ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.cmd).data()); // Display command byte
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-2s", absl::StrFormat("%02X", spc_command.p1).data()); // Display p1
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.p2).data()); // Display p2
|
||||
} else if (current_row_time > row) {
|
||||
// If we've passed the current row, no more commands for this row
|
||||
break;
|
||||
}
|
||||
// // TODO: Map spc_command.tim and spc_command.tim2 to rows.
|
||||
// // For now, just display the command itself if it falls on the current row.
|
||||
// // This logic needs to be refined to correctly interpret command durations.
|
||||
// if (current_row_time == row) { // Simplified mapping for now
|
||||
// // TODO: Decode SpcCommand to Note, Instrument, Effect strings
|
||||
// ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.cmd).data()); // Display command byte
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-2s", absl::StrFormat("%02X", spc_command.p1).data()); // Display p1
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-4s", absl::StrFormat("%02X", spc_command.p2).data()); // Display p2
|
||||
// } else if (current_row_time > row) {
|
||||
// // If we've passed the current row, no more commands for this row
|
||||
// break;
|
||||
// }
|
||||
|
||||
// Advance time (simplified for now, needs proper GetBlockTime integration)
|
||||
current_row_time++; // Assume each command takes 1 row for now
|
||||
// // Advance time (simplified for now, needs proper GetBlockTime integration)
|
||||
// current_row_time++; // Assume each command takes 1 row for now
|
||||
|
||||
spc_command_index = spc_command.next;
|
||||
}
|
||||
// spc_command_index = spc_command.next;
|
||||
// }
|
||||
|
||||
// If no command was displayed for this row, display empty
|
||||
if (current_row_time <= row) {
|
||||
ImGui::Text("%-4s", "---");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-2s", "--");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-4s", "----");
|
||||
}
|
||||
// // If no command was displayed for this row, display empty
|
||||
// if (current_row_time <= row) {
|
||||
// ImGui::Text("%-4s", "---");
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-2s", "--");
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-4s", "----");
|
||||
// }
|
||||
|
||||
} else {
|
||||
ImGui::Text("%-4s", "---");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-2s", "--");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-4s", "----");
|
||||
} } else {
|
||||
ImGui::Text("%-4s", "---");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-2s", "--");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%-4s", "----");
|
||||
}
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::NewLine(); // Move to the next line after all channels for the row
|
||||
// } else {
|
||||
// ImGui::Text("%-4s", "---");
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-2s", "--");
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-4s", "----");
|
||||
// } } else {
|
||||
// ImGui::Text("%-4s", "---");
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-2s", "--");
|
||||
// ImGui::SameLine();
|
||||
// ImGui::Text("%-4s", "----");
|
||||
// }
|
||||
// }
|
||||
// ImGui::EndGroup();
|
||||
// ImGui::SameLine();
|
||||
// }
|
||||
// ImGui::NewLine(); // Move to the next line after all channels for the row
|
||||
|
||||
if (row == current_pattern_index_) {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
// if (row == current_pattern_index_) {
|
||||
// ImGui::PopStyleColor();
|
||||
// }
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "app/editor/code/assembly_editor.h"
|
||||
#include "app/editor/editor.h"
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/music/tracker.h"
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "imgui/imgui.h"
|
||||
|
||||
namespace yaze {
|
||||
@@ -57,8 +57,10 @@ const ImGuiTableFlags music_editor_flags_ = ImGuiTableFlags_SizingFixedFit |
|
||||
*/
|
||||
class MusicEditor : public Editor {
|
||||
public:
|
||||
MusicEditor() : Editor(EditorType::kMusic) {}
|
||||
|
||||
explicit MusicEditor(Rom* rom = nullptr) : rom_(rom) {
|
||||
type_ = EditorType::kMusic;
|
||||
}
|
||||
|
||||
void Initialize() override;
|
||||
absl::Status Load() override;
|
||||
absl::Status Save() override { return absl::UnimplementedError("Save"); }
|
||||
@@ -70,6 +72,12 @@ class MusicEditor : public Editor {
|
||||
absl::Status Redo() override { return absl::UnimplementedError("Redo"); }
|
||||
absl::Status Find() override { return absl::UnimplementedError("Find"); }
|
||||
|
||||
// Set the ROM pointer
|
||||
void set_rom(Rom* rom) { rom_ = rom; }
|
||||
|
||||
// Get the ROM pointer
|
||||
Rom* rom() const { return rom_; }
|
||||
|
||||
private:
|
||||
// UI Drawing Methods
|
||||
void DrawTrackerView();
|
||||
@@ -84,7 +92,7 @@ class MusicEditor : public Editor {
|
||||
|
||||
AssemblyEditor assembly_editor_;
|
||||
zelda3::music::Tracker music_tracker_;
|
||||
emu::Apu apu_;
|
||||
// Note: APU requires ROM memory, will be initialized when needed
|
||||
|
||||
// UI State
|
||||
int current_song_index_ = 0;
|
||||
@@ -102,6 +110,8 @@ class MusicEditor : public Editor {
|
||||
ImGuiTableFlags toolset_table_flags_ =
|
||||
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersOuter |
|
||||
ImGuiTableFlags_BordersV | ImGuiTableFlags_PadOuterX;
|
||||
|
||||
Rom* rom_;
|
||||
};
|
||||
|
||||
} // namespace editor
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "app/gui/icons.h"
|
||||
#include "app/gui/input.h"
|
||||
#include "app/gui/style.h"
|
||||
#include "app/gui/widget_id_registry.h"
|
||||
#include "app/gui/widgets/widget_id_registry.h"
|
||||
#include "app/rom.h"
|
||||
#include "app/zelda3/common.h"
|
||||
#include "app/zelda3/overworld/overworld.h"
|
||||
|
||||
@@ -42,9 +42,10 @@ EditorSelectionDialog::EditorSelectionDialog() {
|
||||
|
||||
{EditorType::kAssembly, "Assembly", ICON_MD_CODE,
|
||||
"Write and edit assembly code", "Ctrl+9", false, false},
|
||||
|
||||
{EditorType::kMemory, "Hex Editor", ICON_MD_DATA_ARRAY,
|
||||
"Direct ROM memory editing", "Ctrl+0", false, true},
|
||||
|
||||
// TODO: Fix this
|
||||
// {EditorType::kHex, "Hex Editor", ICON_MD_DATA_ARRAY,
|
||||
// "Direct ROM memory editing", "Ctrl+0", false, true},
|
||||
|
||||
{EditorType::kSettings, "Settings", ICON_MD_SETTINGS,
|
||||
"Configure ROM and project settings", "", false, true},
|
||||
@@ -92,14 +93,15 @@ bool EditorSelectionDialog::Show(bool* p_open) {
|
||||
for (size_t i = 0; i < editors_.size(); ++i) {
|
||||
ImGui::TableNextColumn();
|
||||
DrawEditorCard(editors_[i], static_cast<int>(i));
|
||||
|
||||
if (selected_editor_ != EditorType::kNone) {
|
||||
editor_selected = true;
|
||||
MarkRecentlyUsed(selected_editor_);
|
||||
if (selection_callback_) {
|
||||
selection_callback_(selected_editor_);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix this
|
||||
// if (selected_editor_ != EditorType::kNone) {
|
||||
// editor_selected = true;
|
||||
// MarkRecentlyUsed(selected_editor_);
|
||||
// if (selection_callback_) {
|
||||
// selection_callback_(selected_editor_);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
@@ -230,7 +232,7 @@ void EditorSelectionDialog::LoadRecentEditors() {
|
||||
while (std::getline(ss, line) &&
|
||||
recent_editors_.size() < kMaxRecentEditors) {
|
||||
int type_int = std::stoi(line);
|
||||
if (type_int >= 0 && type_int < static_cast<int>(EditorType::kLast)) {
|
||||
if (type_int >= 0 && type_int < static_cast<int>(EditorType::kSettings)) {
|
||||
recent_editors_.push_back(static_cast<EditorType>(type_int));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,20 +30,21 @@ ImVec4 GetThemedColor(const char* color_name, const ImVec4& fallback) {
|
||||
auto& theme_mgr = gui::ThemeManager::Get();
|
||||
const auto& theme = theme_mgr.GetCurrentTheme();
|
||||
|
||||
// TODO: Fix this
|
||||
// Map color names to theme colors
|
||||
if (strcmp(color_name, "triforce_gold") == 0) {
|
||||
return theme.accent.to_im_vec4();
|
||||
} else if (strcmp(color_name, "hyrule_green") == 0) {
|
||||
return theme.success.to_im_vec4();
|
||||
} else if (strcmp(color_name, "master_sword_blue") == 0) {
|
||||
return theme.info.to_im_vec4();
|
||||
} else if (strcmp(color_name, "ganon_purple") == 0) {
|
||||
return theme.secondary.to_im_vec4();
|
||||
} else if (strcmp(color_name, "heart_red") == 0) {
|
||||
return theme.error.to_im_vec4();
|
||||
} else if (strcmp(color_name, "spirit_orange") == 0) {
|
||||
return theme.warning.to_im_vec4();
|
||||
}
|
||||
// if (strcmp(color_name, "triforce_gold") == 0) {
|
||||
// return theme.accent.to_im_vec4();
|
||||
// } else if (strcmp(color_name, "hyrule_green") == 0) {
|
||||
// return theme.success.to_im_vec4();
|
||||
// } else if (strcmp(color_name, "master_sword_blue") == 0) {
|
||||
// return theme.info.to_im_vec4();
|
||||
// } else if (strcmp(color_name, "ganon_purple") == 0) {
|
||||
// return theme.secondary.to_im_vec4();
|
||||
// } else if (strcmp(color_name, "heart_red") == 0) {
|
||||
// return theme.error.to_im_vec4();
|
||||
// } else if (strcmp(color_name, "spirit_orange") == 0) {
|
||||
// return theme.warning.to_im_vec4();
|
||||
// }
|
||||
|
||||
return fallback;
|
||||
}
|
||||
@@ -262,7 +263,7 @@ void WelcomeScreen::RefreshRecentProjects() {
|
||||
recent_projects_.clear();
|
||||
|
||||
// Use the ProjectManager singleton to get recent files
|
||||
auto& recent_files = core::ProjectManager::GetInstance().GetRecentFiles();
|
||||
auto& recent_files = core::RecentFilesManager::GetInstance().GetRecentFiles();
|
||||
|
||||
for (const auto& filepath : recent_files) {
|
||||
if (recent_projects_.size() >= kMaxRecentProjects) break;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "app/core/project.h"
|
||||
#include "cli/z3ed.h"
|
||||
#include "core/platform/file_dialog.h"
|
||||
#include "util/file_util.h"
|
||||
#include "util/bps.h"
|
||||
#ifndef _WIN32
|
||||
#include <glob.h>
|
||||
|
||||
Reference in New Issue
Block a user