[Update] UI: ローマ字/かな 表示するように。
This commit is contained in:
@@ -250,6 +250,13 @@ config_page:
|
|||||||
received_message_format:
|
received_message_format:
|
||||||
label: "Message Format (Speaker2Chatbox)"
|
label: "Message Format (Speaker2Chatbox)"
|
||||||
desc: "Currently, it is used in Speaker2Chatbox."
|
desc: "Currently, it is used in Speaker2Chatbox."
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label: Show Romaji
|
||||||
|
desc: Supported only when Japanese is selected as the translation language. When enabled along with '{{convert_message_to_hiragana}}', romaji will be shown on mouse hover.
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label: Show Hiragana
|
||||||
|
desc: Supported only when Japanese is selected as the translation language.
|
||||||
|
|
||||||
|
|
||||||
hotkeys:
|
hotkeys:
|
||||||
toggle_vrct_visibility:
|
toggle_vrct_visibility:
|
||||||
|
|||||||
@@ -250,6 +250,12 @@ config_page:
|
|||||||
received_message_format:
|
received_message_format:
|
||||||
label: メッセージフォーマット(Speaker2Chatbox)
|
label: メッセージフォーマット(Speaker2Chatbox)
|
||||||
desc: 今のところ、Speaker2Chatboxで送信した時の表示に使われます。
|
desc: 今のところ、Speaker2Chatboxで送信した時の表示に使われます。
|
||||||
|
convert_message_to_romaji:
|
||||||
|
label: ローマ字を表示
|
||||||
|
desc: 翻訳言語として日本語を選択した時のみサポート。「{{convert_message_to_hiragana}}」と同時に有効にした場合は、マウスホバーで表示されます。
|
||||||
|
convert_message_to_hiragana:
|
||||||
|
label: ひらがなを表示
|
||||||
|
desc: 翻訳言語として日本語を選択した時のみサポート。
|
||||||
|
|
||||||
hotkeys:
|
hotkeys:
|
||||||
toggle_vrct_visibility:
|
toggle_vrct_visibility:
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export const Others = () => {
|
|||||||
<SendMessageFormatPartsContainer />
|
<SendMessageFormatPartsContainer />
|
||||||
<ReceivedMessageFormatPartsContainer />
|
<ReceivedMessageFormatPartsContainer />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ConvertMessageToRomajiContainer />
|
||||||
|
<ConvertMessageToHiraganaContainer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -201,4 +205,35 @@ const ReceivedMessageFormatPartsContainer = () => {
|
|||||||
format_id="received"
|
format_id="received"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConvertMessageToRomajiContainer = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { currentConvertMessageToRomaji, toggleConvertMessageToRomaji } = useOthers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxContainer
|
||||||
|
label={t("config_page.others.convert_message_to_romaji.label")}
|
||||||
|
desc={t(
|
||||||
|
"config_page.others.convert_message_to_romaji.desc",
|
||||||
|
{ convert_message_to_hiragana: t("config_page.others.convert_message_to_hiragana.label") }
|
||||||
|
)}
|
||||||
|
variable={currentConvertMessageToRomaji}
|
||||||
|
toggleFunction={toggleConvertMessageToRomaji}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConvertMessageToHiraganaContainer = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { currentConvertMessageToHiragana, toggleConvertMessageToHiragana } = useOthers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CheckboxContainer
|
||||||
|
label={t("config_page.others.convert_message_to_hiragana.label")}
|
||||||
|
desc={t("config_page.others.convert_message_to_hiragana.desc")}
|
||||||
|
variable={currentConvertMessageToHiragana}
|
||||||
|
toggleFunction={toggleConvertMessageToHiragana}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -39,7 +39,7 @@ export const MessageContainer = ({ messages, status, category, created_at }) =>
|
|||||||
setIsLocked(true);
|
setIsLocked(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const is_translated_exist = messages.translated?.length >= 1;
|
const is_translation_exist = messages.translations?.length > 0;
|
||||||
const is_pending = status === "pending";
|
const is_pending = status === "pending";
|
||||||
const is_sent_message = category === "sent";
|
const is_sent_message = category === "sent";
|
||||||
const is_system_message = category === "system";
|
const is_system_message = category === "system";
|
||||||
@@ -69,11 +69,11 @@ export const MessageContainer = ({ messages, status, category, created_at }) =>
|
|||||||
</div>
|
</div>
|
||||||
<div className={clsx(styles.message_box, message_type_class_name)}>
|
<div className={clsx(styles.message_box, message_type_class_name)}>
|
||||||
{is_system_message ? (
|
{is_system_message ? (
|
||||||
<p className={styles.message_main_system}>{messages.message}</p>
|
<p className={styles.message_main_system}>{messages.original.message}</p>
|
||||||
) : is_translated_exist ? (
|
) : is_translation_exist ? (
|
||||||
<WithTranslatedMessages messages={messages} />
|
<WithTranslatedMessages messages={messages} />
|
||||||
) : (
|
) : (
|
||||||
<p className={styles.message_main}>{messages.original}</p>
|
<OriginalMessage messages={messages} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,13 +88,74 @@ export const MessageContainer = ({ messages, status, category, created_at }) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WithTranslatedMessages = ({ messages }) => {
|
const MessageWithTransliteration = ({ item }) => {
|
||||||
const translated_data = Array.isArray(messages.translated) ? messages.translated : [messages.translated];
|
const renderTokenNode = (token, key) => {
|
||||||
|
const orig = token.orig ?? "";
|
||||||
|
const hira = token.hira ?? "";
|
||||||
|
const hepburn = token.hepburn ?? "";
|
||||||
|
|
||||||
|
if ((hira && orig === hira) || (hepburn && orig === hepburn) || (!hira && !hepburn)) {
|
||||||
|
return (
|
||||||
|
<span key={key} className={styles.token}>
|
||||||
|
{orig}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hira && hira !== orig) {
|
||||||
|
const needHepburn = hepburn && hepburn !== orig;
|
||||||
|
const titleAttr = needHepburn ? hepburn : undefined;
|
||||||
|
return (
|
||||||
|
<ruby key={key} title={titleAttr} className={styles.tokenRuby}>
|
||||||
|
{orig}
|
||||||
|
<rt>{hira}</rt>
|
||||||
|
</ruby>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hepburn && hepburn !== orig) {
|
||||||
|
return (
|
||||||
|
<ruby key={key} className={styles.tokenRuby}>
|
||||||
|
{orig}
|
||||||
|
<rt>{hepburn}</rt>
|
||||||
|
</ruby>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={key} className={styles.token}>
|
||||||
|
{orig}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!item.transliteration.length) {
|
||||||
|
return <p className={styles.message_main}>{item.message}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={styles.message_main}>
|
||||||
|
{item.transliteration.map((token, idx) => renderTokenNode(token, idx))}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OriginalMessage = ({ messages }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className={styles.message_second}>{messages.original}</p>
|
<MessageWithTransliteration item={messages.original} />
|
||||||
{translated_data.map((message, index) => (
|
</>
|
||||||
<p key={index} className={styles.message_main}>{message}</p>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const WithTranslatedMessages = ({ messages }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className={styles.message_second}>{messages.original.message}</p>
|
||||||
|
{messages.translations.map((item, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
<MessageWithTransliteration item={item} />
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export const useMessage = () => {
|
|||||||
status: "pending",
|
status: "pending",
|
||||||
created_at: generateTimeData(),
|
created_at: generateTimeData(),
|
||||||
messages: {
|
messages: {
|
||||||
original: message,
|
original: { message: message, transliteration: [] },
|
||||||
translated: [],
|
translations: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -39,20 +39,26 @@ export const useMessage = () => {
|
|||||||
category: "system",
|
category: "system",
|
||||||
status: "system",
|
status: "system",
|
||||||
created_at: date,
|
created_at: date,
|
||||||
messages: {message: message},
|
messages: {
|
||||||
|
original: { message: message, transliteration: [] },
|
||||||
|
translations: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSystemMessageLog_FromBackend = (payload) => {
|
const addSystemMessageLog_FromBackend = (payload) => {
|
||||||
addSystemMessageLog(payload.message);
|
addSystemMessageLog(payload.message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSentMessageLogById = (payload) => {
|
const updateSentMessageLogById = (payload) => {
|
||||||
updateMessageLogs(updateItemById(payload.id, payload.translation));
|
updateMessageLogs(updateItemById(payload.id, payload));
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSentMessageLog = (payload) => {
|
const addSentMessageLog = (payload) => {
|
||||||
const message_object = generateMessageObject(payload, "sent");
|
const message_object = generateMessageObject(payload, "sent");
|
||||||
addMessageLogs(message_object);
|
addMessageLogs(message_object);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addReceivedMessageLog = (payload) => {
|
const addReceivedMessageLog = (payload) => {
|
||||||
const message_object = generateMessageObject(payload, "received");
|
const message_object = generateMessageObject(payload, "received");
|
||||||
addMessageLogs(message_object);
|
addMessageLogs(message_object);
|
||||||
@@ -61,6 +67,7 @@ export const useMessage = () => {
|
|||||||
const startTyping = () => {
|
const startTyping = () => {
|
||||||
asyncStdoutToPython("/run/typing_message_box");
|
asyncStdoutToPython("/run/typing_message_box");
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopTyping = () => {
|
const stopTyping = () => {
|
||||||
asyncStdoutToPython("/run/stop_typing_message_box");
|
asyncStdoutToPython("/run/stop_typing_message_box");
|
||||||
};
|
};
|
||||||
@@ -83,11 +90,10 @@ export const useMessage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generateTimeData = () => {
|
const generateTimeData = () => {
|
||||||
const data = new Date().toLocaleTimeString(
|
return new Date().toLocaleTimeString(
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
{ hour12: false, hour: "2-digit", minute: "2-digit" },
|
{ hour12: false, hour: "2-digit", minute: "2-digit" }
|
||||||
);
|
);
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateMessageObject = (data, category) => {
|
const generateMessageObject = (data, category) => {
|
||||||
@@ -97,17 +103,17 @@ const generateMessageObject = (data, category) => {
|
|||||||
category: category,
|
category: category,
|
||||||
status: "ok",
|
status: "ok",
|
||||||
messages: {
|
messages: {
|
||||||
original: data.message,
|
original: data.original,
|
||||||
translated: data.translation,
|
translations: data.translations ?? [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateItemById = (id, translated_data) => (current_items) => {
|
const updateItemById = (id, updated_data) => (current_items) => {
|
||||||
return current_items.data.map(item => {
|
return current_items.data.map(item => {
|
||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
item.status = "ok";
|
item.status = "ok";
|
||||||
item.messages.translated = translated_data;
|
if (updated_data.translations) item.messages.translations = updated_data.translations;
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
useStore_MessageFormat_ExampleViewFilter,
|
useStore_MessageFormat_ExampleViewFilter,
|
||||||
useStore_SendMessageFormatParts,
|
useStore_SendMessageFormatParts,
|
||||||
useStore_ReceivedMessageFormatParts,
|
useStore_ReceivedMessageFormatParts,
|
||||||
|
useStore_ConvertMessageToRomaji,
|
||||||
|
useStore_ConvertMessageToHiragana,
|
||||||
} from "@store";
|
} from "@store";
|
||||||
import { useStdoutToPython } from "@useStdoutToPython";
|
import { useStdoutToPython } from "@useStdoutToPython";
|
||||||
import { useNotificationStatus } from "@logics_common";
|
import { useNotificationStatus } from "@logics_common";
|
||||||
@@ -39,6 +41,11 @@ export const useOthers = () => {
|
|||||||
// Received
|
// Received
|
||||||
const { currentReceivedMessageFormatParts, updateReceivedMessageFormatParts, pendingReceivedMessageFormatParts } = useStore_ReceivedMessageFormatParts();
|
const { currentReceivedMessageFormatParts, updateReceivedMessageFormatParts, pendingReceivedMessageFormatParts } = useStore_ReceivedMessageFormatParts();
|
||||||
|
|
||||||
|
// Convert Message To Romaji
|
||||||
|
const { currentConvertMessageToRomaji, updateConvertMessageToRomaji, pendingConvertMessageToRomaji } = useStore_ConvertMessageToRomaji();
|
||||||
|
// Convert Message To Hiragana
|
||||||
|
const { currentConvertMessageToHiragana, updateConvertMessageToHiragana, pendingConvertMessageToHiragana } = useStore_ConvertMessageToHiragana();
|
||||||
|
|
||||||
const { showNotification_SaveSuccess } = useNotificationStatus();
|
const { showNotification_SaveSuccess } = useNotificationStatus();
|
||||||
|
|
||||||
// Auto Clear Message Input Box
|
// Auto Clear Message Input Box
|
||||||
@@ -233,6 +240,45 @@ export const useOthers = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert Message To Romaji
|
||||||
|
const getConvertMessageToRomaji = () => {
|
||||||
|
pendingConvertMessageToRomaji();
|
||||||
|
asyncStdoutToPython("/get/data/convert_message_to_romaji");
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleConvertMessageToRomaji = () => {
|
||||||
|
pendingConvertMessageToRomaji();
|
||||||
|
if (currentConvertMessageToRomaji.data) {
|
||||||
|
asyncStdoutToPython("/set/disable/convert_message_to_romaji");
|
||||||
|
} else {
|
||||||
|
asyncStdoutToPython("/set/enable/convert_message_to_romaji");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSuccessConvertMessageToRomaji = (enabled) => {
|
||||||
|
updateConvertMessageToRomaji(enabled);
|
||||||
|
showNotification_SaveSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert Message To Hiragana
|
||||||
|
const getConvertMessageToHiragana = () => {
|
||||||
|
pendingConvertMessageToHiragana();
|
||||||
|
asyncStdoutToPython("/get/data/convert_message_to_hiragana");
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleConvertMessageToHiragana = () => {
|
||||||
|
pendingConvertMessageToHiragana();
|
||||||
|
if (currentConvertMessageToHiragana.data) {
|
||||||
|
asyncStdoutToPython("/set/disable/convert_message_to_hiragana");
|
||||||
|
} else {
|
||||||
|
asyncStdoutToPython("/set/enable/convert_message_to_hiragana");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSuccessConvertMessageToHiragana = (enabled) => {
|
||||||
|
updateConvertMessageToHiragana(enabled);
|
||||||
|
showNotification_SaveSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Auto Clear Message Input Box
|
// Auto Clear Message Input Box
|
||||||
@@ -303,5 +349,19 @@ export const useOthers = () => {
|
|||||||
getReceivedMessageFormatParts,
|
getReceivedMessageFormatParts,
|
||||||
setReceivedMessageFormatParts,
|
setReceivedMessageFormatParts,
|
||||||
setSuccessReceivedMessageFormatParts,
|
setSuccessReceivedMessageFormatParts,
|
||||||
|
|
||||||
|
// Convert Message To Romaji
|
||||||
|
currentConvertMessageToRomaji,
|
||||||
|
getConvertMessageToRomaji,
|
||||||
|
toggleConvertMessageToRomaji,
|
||||||
|
updateConvertMessageToRomaji,
|
||||||
|
setSuccessConvertMessageToRomaji,
|
||||||
|
|
||||||
|
// Convert Message To Hiragana
|
||||||
|
currentConvertMessageToHiragana,
|
||||||
|
getConvertMessageToHiragana,
|
||||||
|
toggleConvertMessageToHiragana,
|
||||||
|
updateConvertMessageToHiragana,
|
||||||
|
setSuccessConvertMessageToHiragana,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -292,6 +292,14 @@ export const ROUTE_META_LIST = [
|
|||||||
{ endpoint: "/get/data/received_message_format_parts", ns: configs, hook_name: "useOthers", method_name: "updateReceivedMessageFormatParts" },
|
{ endpoint: "/get/data/received_message_format_parts", ns: configs, hook_name: "useOthers", method_name: "updateReceivedMessageFormatParts" },
|
||||||
{ endpoint: "/set/data/received_message_format_parts", ns: configs, hook_name: "useOthers", method_name: "setSuccessReceivedMessageFormatParts" },
|
{ endpoint: "/set/data/received_message_format_parts", ns: configs, hook_name: "useOthers", method_name: "setSuccessReceivedMessageFormatParts" },
|
||||||
|
|
||||||
|
{ endpoint: "/get/data/convert_message_to_romaji", ns: configs, hook_name: "useOthers", method_name: "updateConvertMessageToRomaji" },
|
||||||
|
{ endpoint: "/set/enable/convert_message_to_romaji", ns: configs, hook_name: "useOthers", method_name: "setSuccessConvertMessageToRomaji" },
|
||||||
|
{ endpoint: "/set/disable/convert_message_to_romaji", ns: configs, hook_name: "useOthers", method_name: "setSuccessConvertMessageToRomaji" },
|
||||||
|
|
||||||
|
{ endpoint: "/get/data/convert_message_to_hiragana", ns: configs, hook_name: "useOthers", method_name: "updateConvertMessageToHiragana" },
|
||||||
|
{ endpoint: "/set/enable/convert_message_to_hiragana", ns: configs, hook_name: "useOthers", method_name: "setSuccessConvertMessageToHiragana" },
|
||||||
|
{ endpoint: "/set/disable/convert_message_to_hiragana", ns: configs, hook_name: "useOthers", method_name: "setSuccessConvertMessageToHiragana" },
|
||||||
|
|
||||||
// Hotkeys
|
// Hotkeys
|
||||||
{ endpoint: "/get/data/hotkeys", ns: configs, hook_name: "useHotkeys", method_name: "updateHotkeys" },
|
{ endpoint: "/get/data/hotkeys", ns: configs, hook_name: "useHotkeys", method_name: "updateHotkeys" },
|
||||||
{ endpoint: "/set/data/hotkeys", ns: configs, hook_name: "useHotkeys", method_name: "setSuccessHotkeys" },
|
{ endpoint: "/set/data/hotkeys", ns: configs, hook_name: "useHotkeys", method_name: "setSuccessHotkeys" },
|
||||||
@@ -323,8 +331,6 @@ export const ROUTE_META_LIST = [
|
|||||||
{ endpoint: "/get/data/mic_no_speech_prob", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
{ endpoint: "/get/data/mic_no_speech_prob", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
||||||
{ endpoint: "/get/data/speaker_avg_logprob", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
{ endpoint: "/get/data/speaker_avg_logprob", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
||||||
{ endpoint: "/get/data/speaker_no_speech_prob", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
{ endpoint: "/get/data/speaker_no_speech_prob", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
||||||
{ endpoint: "/get/data/convert_message_to_romaji", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
|
||||||
{ endpoint: "/get/data/convert_message_to_hiragana", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet
|
|
||||||
{ endpoint: "/get/data/transcription_engines", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet. (if ai_models has not been detected, this will be blank array[]. if the ai_models are ok but just network has not connected, it'l be only ["Whisper"])
|
{ endpoint: "/get/data/transcription_engines", ns: null, hook_name: null, method_name: null }, // Not implemented on UI yet. (if ai_models has not been detected, this will be blank array[]. if the ai_models are ok but just network has not connected, it'l be only ["Whisper"])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -309,6 +309,8 @@ export const { atomInstance: Atom_ReceivedMessageFormatParts, useHook: useStore_
|
|||||||
},
|
},
|
||||||
translation_first: false,
|
translation_first: false,
|
||||||
}, "ReceivedMessageFormatParts");
|
}, "ReceivedMessageFormatParts");
|
||||||
|
export const { atomInstance: Atom_ConvertMessageToRomaji, useHook: useStore_ConvertMessageToRomaji } = createAtomWithHook(false, "ConvertMessageToRomaji");
|
||||||
|
export const { atomInstance: Atom_ConvertMessageToHiragana, useHook: useStore_ConvertMessageToHiragana } = createAtomWithHook(false, "ConvertMessageToHiragana");
|
||||||
|
|
||||||
|
|
||||||
// Hotkeys
|
// Hotkeys
|
||||||
|
|||||||
Reference in New Issue
Block a user