mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
widgetizing note detail WIP
This commit is contained in:
parent
8b9c235465
commit
c9bc4ad108
@ -10,6 +10,7 @@ import keyboardActionService from "./keyboard_actions.js";
|
|||||||
import TabRowWidget from "./tab_row.js";
|
import TabRowWidget from "./tab_row.js";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
|
|
||||||
class AppContext {
|
class AppContext {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -29,29 +30,40 @@ class AppContext {
|
|||||||
|
|
||||||
$("#global-menu-wrapper").after(contents);
|
$("#global-menu-wrapper").after(contents);
|
||||||
|
|
||||||
this.promotedAttributes = new PromotedAttributesWidget(this);
|
|
||||||
$("#center-pane").prepend(this.promotedAttributes.render());
|
|
||||||
|
|
||||||
this.noteTitleWidget = new NoteTitleWidget(this);
|
|
||||||
$("#center-pane").prepend(this.noteTitleWidget.render());
|
|
||||||
|
|
||||||
this.noteTreeWidget = new NoteTreeWidget(this);
|
this.noteTreeWidget = new NoteTreeWidget(this);
|
||||||
|
|
||||||
this.widgets = [
|
const leftPaneWidgets = [
|
||||||
new GlobalButtonsWidget(this),
|
new GlobalButtonsWidget(this),
|
||||||
new SearchBoxWidget(this),
|
new SearchBoxWidget(this),
|
||||||
new SearchResultsWidget(this),
|
new SearchResultsWidget(this),
|
||||||
this.noteTreeWidget
|
this.noteTreeWidget
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const widget of this.widgets) {
|
for (const widget of leftPaneWidgets) {
|
||||||
const $widget = widget.render();
|
const $widget = widget.render();
|
||||||
|
|
||||||
$leftPane.append($widget);
|
$leftPane.append($widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.widgets.push(this.noteTitleWidget);
|
const $centerPane = $("#center-pane");
|
||||||
this.widgets.push(this.promotedAttributes);
|
|
||||||
|
const centerPaneWidgets = [
|
||||||
|
new NoteTitleWidget(this),
|
||||||
|
new PromotedAttributesWidget(this),
|
||||||
|
new NoteDetailWidget(this)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const widget of centerPaneWidgets) {
|
||||||
|
const $widget = widget.render();
|
||||||
|
|
||||||
|
$centerPane.append($widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.widgets = [
|
||||||
|
this.tabRow,
|
||||||
|
...leftPaneWidgets,
|
||||||
|
...centerPaneWidgets
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger(name, data) {
|
trigger(name, data) {
|
||||||
|
@ -5,6 +5,11 @@ import server from "./server.js";
|
|||||||
import noteDetailService from "./note_detail.js";
|
import noteDetailService from "./note_detail.js";
|
||||||
import keyboardActionService from "./keyboard_actions.js";
|
import keyboardActionService from "./keyboard_actions.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div class="note-detail-code note-detail-component">
|
||||||
|
<div class="note-detail-code-editor"></div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
class NoteDetailCode {
|
class NoteDetailCode {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,13 +39,46 @@ const mentionSetup = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div class="note-detail-text note-detail-component">
|
||||||
|
<style>
|
||||||
|
.note-detail-text h1 { font-size: 2.0em; }
|
||||||
|
.note-detail-text h2 { font-size: 1.8em; }
|
||||||
|
.note-detail-text h3 { font-size: 1.6em; }
|
||||||
|
.note-detail-text h4 { font-size: 1.4em; }
|
||||||
|
.note-detail-text h5 { font-size: 1.2em; }
|
||||||
|
.note-detail-text h6 { font-size: 1.1em; }
|
||||||
|
|
||||||
|
.note-detail-text {
|
||||||
|
overflow: auto;
|
||||||
|
font-family: var(--detail-text-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-text-editor {
|
||||||
|
padding-top: 10px;
|
||||||
|
border: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-text p:first-child, .note-detail-text::before {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="note-detail-text-editor" tabindex="10000"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
class NoteDetailText {
|
class NoteDetailText {
|
||||||
/**
|
/**
|
||||||
* @param {TabContext} ctx
|
* @param {TabContext} ctx
|
||||||
*/
|
*/
|
||||||
constructor(ctx) {
|
constructor(ctx, $parent) {
|
||||||
|
this.$component = $(TPL);
|
||||||
|
$parent.append(this.$component);
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.$component = ctx.$tabContent.find('.note-detail-text');
|
|
||||||
this.$editorEl = this.$component.find('.note-detail-text-editor');
|
this.$editorEl = this.$component.find('.note-detail-text-editor');
|
||||||
this.textEditorPromise = null;
|
this.textEditorPromise = null;
|
||||||
this.textEditor = null;
|
this.textEditor = null;
|
||||||
|
@ -8,21 +8,6 @@ import optionsService from "./options.js";
|
|||||||
import Sidebar from "./sidebar.js";
|
import Sidebar from "./sidebar.js";
|
||||||
import appContext from "./app_context.js";
|
import appContext from "./app_context.js";
|
||||||
|
|
||||||
const $tabContentsContainer = $("#note-tab-container");
|
|
||||||
|
|
||||||
const componentClasses = {
|
|
||||||
'empty': "./note_detail_empty.js",
|
|
||||||
'text': "./note_detail_text.js",
|
|
||||||
'code': "./note_detail_code.js",
|
|
||||||
'file': "./note_detail_file.js",
|
|
||||||
'image': "./note_detail_image.js",
|
|
||||||
'search': "./note_detail_search.js",
|
|
||||||
'render': "./note_detail_render.js",
|
|
||||||
'relation-map': "./note_detail_relation_map.js",
|
|
||||||
'protected-session': "./note_detail_protected_session.js",
|
|
||||||
'book': "./note_detail_book.js"
|
|
||||||
};
|
|
||||||
|
|
||||||
let showSidebarInNewTab = true;
|
let showSidebarInNewTab = true;
|
||||||
|
|
||||||
optionsService.addLoadListener(options => {
|
optionsService.addLoadListener(options => {
|
||||||
@ -49,15 +34,8 @@ class TabContext {
|
|||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
this.$tabContent = $(".note-tab-content-template").clone();
|
this.$tabContent = $("<div>"); // FIXME
|
||||||
this.$tabContent.removeClass('note-tab-content-template');
|
|
||||||
this.$tabContent.attr('data-tab-id', this.tabId);
|
|
||||||
this.$tabContent.hide();
|
|
||||||
|
|
||||||
$tabContentsContainer.append(this.$tabContent);
|
|
||||||
|
|
||||||
this.$noteDetailComponents = this.$tabContent.find(".note-detail-component");
|
|
||||||
this.$scriptArea = this.$tabContent.find(".note-detail-script-area");
|
|
||||||
this.noteChangeDisabled = false;
|
this.noteChangeDisabled = false;
|
||||||
this.isNoteChanged = false;
|
this.isNoteChanged = false;
|
||||||
this.attributes = new Attributes(this);
|
this.attributes = new Attributes(this);
|
||||||
@ -69,20 +47,6 @@ class TabContext {
|
|||||||
|
|
||||||
this.sidebar = new Sidebar(this, sidebarState);
|
this.sidebar = new Sidebar(this, sidebarState);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.components = {};
|
|
||||||
|
|
||||||
await this.initComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initComponent(disableAutoBook = false) {
|
|
||||||
this.type = this.getComponentType(disableAutoBook);
|
|
||||||
|
|
||||||
if (!(this.type in this.components)) {
|
|
||||||
const clazz = await import(componentClasses[this.type]);
|
|
||||||
|
|
||||||
this.components[this.type] = new clazz.default(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setNote(note, notePath) {
|
async setNote(note, notePath) {
|
||||||
@ -95,8 +59,6 @@ class TabContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$scriptArea.empty();
|
|
||||||
|
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
this.attributes.refreshAttributes();
|
this.attributes.refreshAttributes();
|
||||||
} else {
|
} else {
|
||||||
@ -115,7 +77,7 @@ class TabContext {
|
|||||||
this.noteChangeDisabled = true;
|
this.noteChangeDisabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.renderComponent();
|
|
||||||
} finally {
|
} finally {
|
||||||
this.noteChangeDisabled = false;
|
this.noteChangeDisabled = false;
|
||||||
}
|
}
|
||||||
@ -137,7 +99,8 @@ class TabContext {
|
|||||||
bundleService.executeRelationBundles(this.note, 'runOnNoteView', this);
|
bundleService.executeRelationBundles(this.note, 'runOnNoteView', this);
|
||||||
|
|
||||||
// after loading new note make sure editor is scrolled to the top
|
// after loading new note make sure editor is scrolled to the top
|
||||||
this.getComponent().scrollToTop();
|
// FIXME
|
||||||
|
//this.getComponent().scrollToTop();
|
||||||
|
|
||||||
appContext.trigger('activeNoteChanged');
|
appContext.trigger('activeNoteChanged');
|
||||||
}
|
}
|
||||||
@ -146,18 +109,15 @@ class TabContext {
|
|||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
await this.initTabContent();
|
await this.initTabContent();
|
||||||
|
|
||||||
this.$tabContent.show(); // show immediately so that user can see something
|
|
||||||
|
|
||||||
if (this.note) {
|
if (this.note) {
|
||||||
await this.setNote(this.note, this.notePath);
|
await this.setNote(this.note, this.notePath);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// FIXME
|
||||||
await this.renderComponent(); // render empty page
|
await this.renderComponent(); // render empty page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$tabContent.show();
|
|
||||||
|
|
||||||
if (this.sidebar) {
|
if (this.sidebar) {
|
||||||
this.sidebar.show();
|
this.sidebar.show();
|
||||||
}
|
}
|
||||||
@ -167,18 +127,7 @@ class TabContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renderComponent(disableAutoBook = false) {
|
async renderComponent(disableAutoBook = false) {
|
||||||
await this.initComponent(disableAutoBook);
|
// FIXME
|
||||||
|
|
||||||
for (const componentType in this.components) {
|
|
||||||
if (componentType !== this.type) {
|
|
||||||
this.components[componentType].cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$noteDetailComponents.hide();
|
|
||||||
|
|
||||||
this.getComponent().show();
|
|
||||||
await this.getComponent().render();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTitleBar() {
|
setTitleBar() {
|
||||||
@ -235,36 +184,11 @@ class TabContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getComponent() {
|
getComponent() {
|
||||||
if (!this.components[this.type]) {
|
// FIXME
|
||||||
throw new Error("Could not find component for type: " + this.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.components[this.type];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getComponentType(disableAutoBook) {
|
getComponentType(disableAutoBook) {
|
||||||
if (!this.note) {
|
// FIXME
|
||||||
return "empty";
|
|
||||||
}
|
|
||||||
|
|
||||||
let type = this.note.type;
|
|
||||||
|
|
||||||
if (type === 'text' && !disableAutoBook && utils.isHtmlEmpty(this.note.content) && this.note.hasChildren()) {
|
|
||||||
type = 'book';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.note.isProtected) {
|
|
||||||
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
|
||||||
protectedSessionHolder.touchProtectedSession();
|
|
||||||
} else {
|
|
||||||
type = 'protected-session';
|
|
||||||
|
|
||||||
// user shouldn't be able to edit note title
|
|
||||||
this.$noteTitle.prop("readonly", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
async activate() {
|
||||||
@ -272,6 +196,8 @@ class TabContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveNote() {
|
async saveNote() {
|
||||||
|
return; // FIXME
|
||||||
|
|
||||||
if (this.note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
|
if (this.note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -316,8 +242,8 @@ class TabContext {
|
|||||||
|
|
||||||
this.isNoteChanged = true;
|
this.isNoteChanged = true;
|
||||||
|
|
||||||
// FIMXE: trigger noteChanged event
|
// FIXME: trigger noteChanged event
|
||||||
this.$savedIndicator.fadeOut();
|
//this.$savedIndicator.fadeOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove() {
|
async remove() {
|
||||||
|
108
src/public/javascripts/widgets/note_detail.js
Normal file
108
src/public/javascripts/widgets/note_detail.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import TabAwareWidget from "./tab_aware_widget.js";
|
||||||
|
import utils from "../services/utils.js";
|
||||||
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div class="note-detail">
|
||||||
|
<style>
|
||||||
|
.note-detail-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 100;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const componentClasses = {
|
||||||
|
'empty': "../services/note_detail_empty.js",
|
||||||
|
'text': "../services/note_detail_text.js",
|
||||||
|
'code': "../services/note_detail_code.js",
|
||||||
|
'file': "../services/note_detail_file.js",
|
||||||
|
'image': "../services/note_detail_image.js",
|
||||||
|
'search': "../services/note_detail_search.js",
|
||||||
|
'render': "../services/note_detail_render.js",
|
||||||
|
'relation-map': "../services/note_detail_relation_map.js",
|
||||||
|
'protected-session': "../services/note_detail_protected_session.js",
|
||||||
|
'book': "../services/note_detail_book.js"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class NoteDetailWidget extends TabAwareWidget {
|
||||||
|
constructor(appContext) {
|
||||||
|
super(appContext);
|
||||||
|
|
||||||
|
this.components = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
|
||||||
|
return this.$widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
async activeTabChanged() {
|
||||||
|
await this.initComponent(/**disableAutoBook*/);
|
||||||
|
|
||||||
|
for (const componentType in this.components) {
|
||||||
|
// FIXME
|
||||||
|
this.components[componentType].ctx = this.tabContext;
|
||||||
|
|
||||||
|
if (componentType !== this.type) {
|
||||||
|
this.components[componentType].cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$widget.find('.note-detail-component').hide();
|
||||||
|
|
||||||
|
this.getComponent().show();
|
||||||
|
await this.getComponent().render();
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent() {
|
||||||
|
if (!this.components[this.type]) {
|
||||||
|
throw new Error("Could not find component for type: " + this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.components[this.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
async initComponent(disableAutoBook = false) {
|
||||||
|
this.type = this.getComponentType(disableAutoBook);
|
||||||
|
|
||||||
|
if (!(this.type in this.components)) {
|
||||||
|
const clazz = await import(componentClasses[this.type]);
|
||||||
|
|
||||||
|
this.components[this.type] = new clazz.default(this, this.$widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponentType(disableAutoBook) {
|
||||||
|
const note = this.tabContext.note;
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
return "empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = note.type;
|
||||||
|
|
||||||
|
if (type === 'text' && !disableAutoBook && utils.isHtmlEmpty(note.content) && note.hasChildren()) {
|
||||||
|
type = 'book';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.isProtected) {
|
||||||
|
if (protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||||
|
protectedSessionHolder.touchProtectedSession();
|
||||||
|
} else {
|
||||||
|
type = 'protected-session';
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
// user shouldn't be able to edit note title
|
||||||
|
//this.$noteTitle.prop("readonly", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
@ -100,14 +100,6 @@ span.fancytree-node.muted { opacity: 0.6; }
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 100;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-detail-component-wrapper {
|
.note-detail-component-wrapper {
|
||||||
flex-grow: 100;
|
flex-grow: 100;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -127,30 +119,6 @@ span.fancytree-node.muted { opacity: 0.6; }
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-detail-text h1 { font-size: 2.0em; }
|
|
||||||
.note-detail-text h2 { font-size: 1.8em; }
|
|
||||||
.note-detail-text h3 { font-size: 1.6em; }
|
|
||||||
.note-detail-text h4 { font-size: 1.4em; }
|
|
||||||
.note-detail-text h5 { font-size: 1.2em; }
|
|
||||||
.note-detail-text h6 { font-size: 1.1em; }
|
|
||||||
|
|
||||||
.note-detail-text {
|
|
||||||
overflow: auto;
|
|
||||||
font-family: var(--detail-text-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-detail-text-editor {
|
|
||||||
padding-top: 10px;
|
|
||||||
border: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
/* This is because with empty content height of editor is 0 and it's impossible to click into it */
|
|
||||||
min-height: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-detail-text p:first-child, .note-detail-text::before {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** we disable shield background when in zen mode because I couldn't get it to stay static
|
/** we disable shield background when in zen mode because I couldn't get it to stay static
|
||||||
(it kept growing with content) */
|
(it kept growing with content) */
|
||||||
#container:not(.zen-mode) .note-tab-content.protected {
|
#container:not(.zen-mode) .note-tab-content.protected {
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
<div id="center-pane">
|
|
||||||
<div id="note-tab-container">
|
|
||||||
<div class="note-tab-content note-tab-content-template">
|
|
||||||
<div class="note-detail-content">
|
|
||||||
<div class="note-detail-component-wrapper">
|
|
||||||
<div class="note-detail-text note-detail-component">
|
|
||||||
<div class="note-detail-text-editor" tabindex="10000"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="note-detail-code note-detail-component">
|
|
||||||
<div class="note-detail-code-editor"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% include details/empty.ejs %>
|
|
||||||
|
|
||||||
<% include details/search.ejs %>
|
|
||||||
|
|
||||||
<% include details/render.ejs %>
|
|
||||||
|
|
||||||
<% include details/file.ejs %>
|
|
||||||
|
|
||||||
<% include details/image.ejs %>
|
|
||||||
|
|
||||||
<% include details/relation_map.ejs %>
|
|
||||||
|
|
||||||
<% include details/protected_session_password.ejs %>
|
|
||||||
|
|
||||||
<% include details/book.ejs %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -136,7 +136,7 @@
|
|||||||
<div style="display: flex; flex-grow: 1; flex-shrink: 1; min-height: 0;">
|
<div style="display: flex; flex-grow: 1; flex-shrink: 1; min-height: 0;">
|
||||||
<div id="left-pane" class="hide-in-zen-mode"></div>
|
<div id="left-pane" class="hide-in-zen-mode"></div>
|
||||||
|
|
||||||
<% include center.ejs %>
|
<div id="center-pane"></div>
|
||||||
|
|
||||||
<% include sidebar.ejs %>
|
<% include sidebar.ejs %>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user