Добавлено новое поле ai_enabled в запросы к базе данных для получения настроек AI пользователя. Реализована проверка включения функций ИИ в API для улучшения обработки запросов. Обновлены компоненты MergeNotesModal и NotesPage для поддержки удаления оригинальных заметок. Изменены уведомления и стили для улучшения пользовательского интерфейса.
This commit is contained in:
parent
300e881245
commit
772f5b1955
@ -2423,7 +2423,7 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
// Получаем AI настройки пользователя
|
// Получаем AI настройки пользователя
|
||||||
const getSettingsSql =
|
const getSettingsSql =
|
||||||
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?";
|
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||||||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка получения AI настроек:", err.message);
|
console.error("Ошибка получения AI настроек:", err.message);
|
||||||
@ -2441,6 +2441,13 @@ app.post("/api/ai/improve", requireApiAuth, async (req, res) => {
|
|||||||
.json({ error: "Настройте AI настройки в параметрах" });
|
.json({ error: "Настройте AI настройки в параметрах" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, включены ли функции ИИ
|
||||||
|
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Функции ИИ отключены в настройках" });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Парсим URL
|
// Парсим URL
|
||||||
const url = new URL(settings.openai_base_url);
|
const url = new URL(settings.openai_base_url);
|
||||||
@ -2559,7 +2566,7 @@ app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
// Получаем AI настройки пользователя
|
// Получаем AI настройки пользователя
|
||||||
const getSettingsSql =
|
const getSettingsSql =
|
||||||
"SELECT openai_api_key, openai_base_url, openai_model FROM users WHERE id = ?";
|
"SELECT openai_api_key, openai_base_url, openai_model, ai_enabled FROM users WHERE id = ?";
|
||||||
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
db.get(getSettingsSql, [req.session.userId], async (err, settings) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка получения AI настроек:", err.message);
|
console.error("Ошибка получения AI настроек:", err.message);
|
||||||
@ -2577,6 +2584,13 @@ app.post("/api/ai/merge", requireApiAuth, async (req, res) => {
|
|||||||
.json({ error: "Настройте AI настройки в параметрах" });
|
.json({ error: "Настройте AI настройки в параметрах" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, включены ли функции ИИ
|
||||||
|
if (!settings.ai_enabled || settings.ai_enabled === 0) {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.json({ error: "Функции ИИ отключены в настройках" });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Парсим URL
|
// Парсим URL
|
||||||
const url = new URL(settings.openai_base_url);
|
const url = new URL(settings.openai_base_url);
|
||||||
|
|||||||
@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "/index.html",
|
"url": "/index.html",
|
||||||
"revision": "0.u6qfhq29adg"
|
"revision": "0.pkl0gk07ge8"
|
||||||
}], {
|
}], {
|
||||||
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
|||||||
const [mergedContent, setMergedContent] = useState<string>("");
|
const [mergedContent, setMergedContent] = useState<string>("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [deleteOriginalNotes, setDeleteOriginalNotes] = useState(false);
|
||||||
const isClosedRef = useRef(false);
|
const isClosedRef = useRef(false);
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
|||||||
setMergedContent("");
|
setMergedContent("");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
|
setDeleteOriginalNotes(false);
|
||||||
isClosedRef.current = false;
|
isClosedRef.current = false;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -44,6 +46,7 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setMergedContent("");
|
setMergedContent("");
|
||||||
|
setDeleteOriginalNotes(false);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,7 +98,27 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
|||||||
time,
|
time,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Удаляем исходные заметки, если тумблер включен
|
||||||
|
if (deleteOriginalNotes) {
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
selectedNotes.map((note) => offlineNotesApi.delete(note.id))
|
||||||
|
);
|
||||||
|
showNotification(
|
||||||
|
`Объединенная заметка сохранена! Удалено ${selectedNotes.length} исходных заметок.`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
} catch (deleteError) {
|
||||||
|
console.error("Ошибка удаления исходных заметок:", deleteError);
|
||||||
|
showNotification(
|
||||||
|
"Объединенная заметка сохранена, но произошла ошибка при удалении исходных заметок",
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
showNotification("Объединенная заметка сохранена!", "success");
|
showNotification("Объединенная заметка сохранена!", "success");
|
||||||
|
}
|
||||||
|
|
||||||
onSuccess();
|
onSuccess();
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -169,6 +192,34 @@ export const MergeNotesModal: React.FC<MergeNotesModalProps> = ({
|
|||||||
>
|
>
|
||||||
<NotePreview content={mergedContent} />
|
<NotePreview content={mergedContent} />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="form-group ai-toggle-group"
|
||||||
|
style={{ marginTop: "20px", marginBottom: "10px" }}
|
||||||
|
>
|
||||||
|
<label className="ai-toggle-label">
|
||||||
|
<div className="toggle-label-content">
|
||||||
|
<span className="toggle-text-main">
|
||||||
|
Удалить исходные заметки
|
||||||
|
</span>
|
||||||
|
<span className="toggle-text-desc">
|
||||||
|
{deleteOriginalNotes
|
||||||
|
? "Исходные заметки будут удалены после сохранения объединенной заметки"
|
||||||
|
: "Исходные заметки останутся в списке после сохранения объединенной заметки"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="toggle-switch-wrapper">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="delete-original-notes-toggle"
|
||||||
|
className="toggle-checkbox"
|
||||||
|
checked={deleteOriginalNotes}
|
||||||
|
onChange={(e) => setDeleteOriginalNotes(e.target.checked)}
|
||||||
|
disabled={isSaving}
|
||||||
|
/>
|
||||||
|
<span className="toggle-slider"></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1136,19 +1136,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="date">
|
<div className="date">
|
||||||
<span className="date-text">
|
<span className="date-text">
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => onSelect && onSelect(note.id)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
style={{
|
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
cursor: "pointer",
|
|
||||||
marginRight: "10px",
|
|
||||||
verticalAlign: "middle",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{formatDate()}
|
{formatDate()}
|
||||||
{note.is_pinned ? (
|
{note.is_pinned ? (
|
||||||
<span className="pin-indicator">
|
<span className="pin-indicator">
|
||||||
@ -1182,13 +1169,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
>
|
>
|
||||||
<Icon icon="mdi:pencil" />
|
<Icon icon="mdi:pencil" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => onSelect && onSelect(note.id)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
{/* <div
|
||||||
className="notesHeaderBtn"
|
className="notesHeaderBtn"
|
||||||
onClick={handleArchiveClick}
|
onClick={handleArchiveClick}
|
||||||
title="В архив"
|
title="В архив"
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:delete" />
|
<Icon icon="mdi:delete" />
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ const NotesPage: React.FC = () => {
|
|||||||
const selectedDate = useAppSelector((state) => state.notes.selectedDate);
|
const selectedDate = useAppSelector((state) => state.notes.selectedDate);
|
||||||
const selectedTag = useAppSelector((state) => state.notes.selectedTag);
|
const selectedTag = useAppSelector((state) => state.notes.selectedTag);
|
||||||
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
|
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
|
||||||
|
const aiEnabled = useAppSelector((state) => state.profile.aiEnabled);
|
||||||
|
|
||||||
const hasFilters = !!(selectedDate || selectedTag || searchQuery);
|
const hasFilters = !!(selectedDate || selectedTag || searchQuery);
|
||||||
|
|
||||||
@ -115,13 +116,13 @@ const NotesPage: React.FC = () => {
|
|||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
try {
|
try {
|
||||||
// Удаляем все выбранные заметки
|
// Архивируем все выбранные заметки
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
selectedNoteIds.map((id) => offlineNotesApi.delete(id))
|
selectedNoteIds.map((id) => offlineNotesApi.archive(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
showNotification(
|
showNotification(
|
||||||
`Удалено заметок: ${selectedNoteIds.length}`,
|
`Архивировано заметок: ${selectedNoteIds.length}`,
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
setSelectedNoteIds([]);
|
setSelectedNoteIds([]);
|
||||||
@ -131,8 +132,8 @@ const NotesPage: React.FC = () => {
|
|||||||
notesListRef.current.reloadNotes();
|
notesListRef.current.reloadNotes();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка удаления заметок:", error);
|
console.error("Ошибка архивирования заметок:", error);
|
||||||
showNotification("Ошибка удаления заметок", "error");
|
showNotification("Ошибка архивирования заметок", "error");
|
||||||
} finally {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
}
|
}
|
||||||
@ -193,7 +194,7 @@ const NotesPage: React.FC = () => {
|
|||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedNoteIds.length >= 2 && (
|
{selectedNoteIds.length >= 2 && aiEnabled && (
|
||||||
<button
|
<button
|
||||||
onClick={handleMergeNotes}
|
onClick={handleMergeNotes}
|
||||||
style={{
|
style={{
|
||||||
@ -241,15 +242,15 @@ const NotesPage: React.FC = () => {
|
|||||||
width: "56px",
|
width: "56px",
|
||||||
height: "56px",
|
height: "56px",
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
backgroundColor: theme === "dark" ? "#F44336" : "#E53935",
|
backgroundColor: theme === "dark" ? "#FF9800" : "#FF9800",
|
||||||
color: "white",
|
color: "white",
|
||||||
border: "none",
|
border: "none",
|
||||||
cursor: isDeleting ? "not-allowed" : "pointer",
|
cursor: isDeleting ? "not-allowed" : "pointer",
|
||||||
opacity: isDeleting ? 0.6 : 1,
|
opacity: isDeleting ? 0.6 : 1,
|
||||||
boxShadow:
|
boxShadow:
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "0 4px 12px rgba(244, 67, 54, 0.4)"
|
? "0 4px 12px rgba(255, 152, 0, 0.4)"
|
||||||
: "0 4px 12px rgba(229, 57, 53, 0.4)",
|
: "0 4px 12px rgba(255, 152, 0, 0.4)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@ -261,20 +262,20 @@ const NotesPage: React.FC = () => {
|
|||||||
e.currentTarget.style.transform = "scale(1.1)";
|
e.currentTarget.style.transform = "scale(1.1)";
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "0 6px 16px rgba(244, 67, 54, 0.6)"
|
? "0 6px 16px rgba(255, 152, 0, 0.6)"
|
||||||
: "0 6px 16px rgba(229, 57, 53, 0.6)";
|
: "0 6px 16px rgba(255, 152, 0, 0.6)";
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.transform = "scale(1)";
|
e.currentTarget.style.transform = "scale(1)";
|
||||||
e.currentTarget.style.boxShadow =
|
e.currentTarget.style.boxShadow =
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "0 4px 12px rgba(244, 67, 54, 0.4)"
|
? "0 4px 12px rgba(255, 152, 0, 0.4)"
|
||||||
: "0 4px 12px rgba(229, 57, 53, 0.4)";
|
: "0 4px 12px rgba(255, 152, 0, 0.4)";
|
||||||
}}
|
}}
|
||||||
title={`Удалить ${selectedNoteIds.length} ${selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"}`}
|
title={`Архивировать ${selectedNoteIds.length} ${selectedNoteIds.length === 1 ? "заметку" : selectedNoteIds.length > 4 ? "заметок" : "заметки"}`}
|
||||||
>
|
>
|
||||||
<Icon icon="mdi:delete" />
|
<Icon icon="mdi:archive" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -289,22 +290,22 @@ const NotesPage: React.FC = () => {
|
|||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
onClose={() => setIsDeleteModalOpen(false)}
|
onClose={() => setIsDeleteModalOpen(false)}
|
||||||
onConfirm={handleDeleteConfirm}
|
onConfirm={handleDeleteConfirm}
|
||||||
title="Удаление заметок"
|
title="Архивирование заметок"
|
||||||
message={
|
message={
|
||||||
<p>
|
<p>
|
||||||
Вы уверены, что хотите удалить{" "}
|
Вы уверены, что хотите архивировать{" "}
|
||||||
<strong>{selectedNoteIds.length}</strong>{" "}
|
<strong>{selectedNoteIds.length}</strong>{" "}
|
||||||
{selectedNoteIds.length === 1
|
{selectedNoteIds.length === 1
|
||||||
? "заметку"
|
? "заметку"
|
||||||
: selectedNoteIds.length > 4
|
: selectedNoteIds.length > 4
|
||||||
? "заметок"
|
? "заметок"
|
||||||
: "заметки"}
|
: "заметки"}
|
||||||
? Это действие нельзя отменить.
|
? Заметки можно будет восстановить из архива в настройках.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
confirmText={isDeleting ? "Удаление..." : "Удалить"}
|
confirmText={isDeleting ? "Архивирование..." : "Архивировать"}
|
||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
confirmType="danger"
|
confirmType="primary"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -245,7 +245,7 @@ const SettingsPage: React.FC = () => {
|
|||||||
setAiEnabled(checked);
|
setAiEnabled(checked);
|
||||||
localStorage.setItem("ai_enabled", checked ? "1" : "0");
|
localStorage.setItem("ai_enabled", checked ? "1" : "0");
|
||||||
showNotification(
|
showNotification(
|
||||||
checked ? "Помощь ИИ включена" : "Помощь ИИ отключена",
|
checked ? "Функции ИИ включены" : "Функции ИИ отключены",
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -726,11 +726,16 @@ const SettingsPage: React.FC = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="toggle-label-content">
|
<div className="toggle-label-content">
|
||||||
<span className="toggle-text-main">Включить помощь ИИ</span>
|
<span className="toggle-text-main">Включить функции ИИ</span>
|
||||||
<span className="toggle-text-desc">
|
<span className="toggle-text-desc">
|
||||||
{checkAiSettingsFilled()
|
{checkAiSettingsFilled() ? (
|
||||||
? 'Показывать кнопку "Помощь ИИ" в редакторах заметок'
|
<ul style={{ margin: "8px 0 0 20px", padding: 0 }}>
|
||||||
: "Сначала заполните API Key, Base URL и Модель ниже"}
|
<li>Улучшение текста заметок</li>
|
||||||
|
<li>Объединение заметок</li>
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
"Сначала заполните API Key, Base URL и Модель ниже"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="toggle-switch-wrapper">
|
<div className="toggle-switch-wrapper">
|
||||||
|
|||||||
@ -1154,6 +1154,58 @@ textarea:focus {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-actions input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px solid var(--border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
accent-color: var(--accent-color);
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-actions input[type="checkbox"]:hover {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background-color: var(--bg-quaternary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-actions input[type="checkbox"]:checked {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-actions input[type="checkbox"]:checked::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -60%) rotate(45deg);
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-actions input[type="checkbox"]:checked:hover {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-actions input[type="checkbox"]:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(var(--accent-color-rgb), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.notesHeaderBtn {
|
.notesHeaderBtn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user