feat(SettingsPage): add keyboard shortcuts section for customizing workflow, including shortcut recording and management functionalities
This commit is contained in:
@@ -565,6 +565,46 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Keyboard Shortcuts -->
|
||||||
|
<section class="glass-card lg:col-span-2">
|
||||||
|
<div class="glass-card__header">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="p-2 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-xl"
|
||||||
|
>
|
||||||
|
<MaterialDesignIcon icon-name="keyboard-outline" class="size-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Keyboard Shortcuts</h2>
|
||||||
|
<p>Customize your workflow with quick keyboard actions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="glass-card__body">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="shortcut in KeyboardShortcuts.getDefaultShortcuts()"
|
||||||
|
:key="shortcut.action"
|
||||||
|
class="bg-gray-50/50 dark:bg-zinc-800/30 rounded-2xl p-5 border border-gray-100 dark:border-zinc-800"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<span
|
||||||
|
class="text-sm font-bold text-gray-900 dark:text-zinc-100 uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
{{ shortcut.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ShortcutRecorder
|
||||||
|
:model-value="getShortcutKeys(shortcut.action)"
|
||||||
|
:action="shortcut.action"
|
||||||
|
@save="(keys) => saveShortcut(shortcut.action, keys)"
|
||||||
|
@delete="() => deleteShortcut(shortcut.action)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -578,15 +618,19 @@ import DialogUtils from "../../js/DialogUtils";
|
|||||||
import ToastUtils from "../../js/ToastUtils";
|
import ToastUtils from "../../js/ToastUtils";
|
||||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||||
import Toggle from "../forms/Toggle.vue";
|
import Toggle from "../forms/Toggle.vue";
|
||||||
|
import ShortcutRecorder from "./ShortcutRecorder.vue";
|
||||||
|
import KeyboardShortcuts from "../../js/KeyboardShortcuts";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SettingsPage",
|
name: "SettingsPage",
|
||||||
components: {
|
components: {
|
||||||
MaterialDesignIcon,
|
MaterialDesignIcon,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
ShortcutRecorder,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
KeyboardShortcuts,
|
||||||
config: {
|
config: {
|
||||||
auto_resend_failed_messages_when_announce_received: null,
|
auto_resend_failed_messages_when_announce_received: null,
|
||||||
allow_auto_resending_failed_messages_with_attachments: null,
|
allow_auto_resending_failed_messages_with_attachments: null,
|
||||||
@@ -596,6 +640,8 @@ export default {
|
|||||||
lxmf_preferred_propagation_node_destination_hash: null,
|
lxmf_preferred_propagation_node_destination_hash: null,
|
||||||
archives_max_storage_gb: 1,
|
archives_max_storage_gb: 1,
|
||||||
},
|
},
|
||||||
|
saveTimeouts: {},
|
||||||
|
shortcuts: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@@ -616,21 +662,52 @@ export default {
|
|||||||
this.config = json.config;
|
this.config = json.config;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "keyboard_shortcuts": {
|
||||||
|
this.shortcuts = json.shortcuts;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getConfig() {
|
async getConfig() {
|
||||||
try {
|
try {
|
||||||
const response = await window.axios.get("/api/v1/config");
|
const response = await window.axios.get("/api/v1/config");
|
||||||
this.config = response.data.config;
|
this.config = response.data.config;
|
||||||
|
this.getKeyboardShortcuts();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// do nothing if failed to load config
|
// do nothing if failed to load config
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async updateConfig(config) {
|
getKeyboardShortcuts() {
|
||||||
|
WebSocketConnection.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "keyboard_shortcuts.get",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getShortcutKeys(action) {
|
||||||
|
const shortcut = this.shortcuts.find((s) => s.action === action);
|
||||||
|
if (shortcut) return shortcut.keys;
|
||||||
|
|
||||||
|
// Fallback to default
|
||||||
|
const def = KeyboardShortcuts.getDefaultShortcuts().find((s) => s.action === action);
|
||||||
|
return def ? def.keys : [];
|
||||||
|
},
|
||||||
|
async saveShortcut(action, keys) {
|
||||||
|
await KeyboardShortcuts.saveShortcut(action, keys);
|
||||||
|
ToastUtils.success("Shortcut saved");
|
||||||
|
},
|
||||||
|
async deleteShortcut(action) {
|
||||||
|
await KeyboardShortcuts.deleteShortcut(action);
|
||||||
|
ToastUtils.success("Shortcut deleted");
|
||||||
|
},
|
||||||
|
async updateConfig(config, label = null) {
|
||||||
try {
|
try {
|
||||||
const response = await window.axios.patch("/api/v1/config", config);
|
const response = await window.axios.patch("/api/v1/config", config);
|
||||||
this.config = response.data.config;
|
this.config = response.data.config;
|
||||||
|
if (label) {
|
||||||
|
ToastUtils.success(this.$t("app.setting_auto_saved", { label: this.$t(`app.${label}`) }));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToastUtils.error("Failed to save config!");
|
ToastUtils.error("Failed to save config!");
|
||||||
console.log(e);
|
console.log(e);
|
||||||
@@ -649,113 +726,176 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onThemeChange() {
|
async onThemeChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
theme: this.config.theme,
|
{
|
||||||
});
|
theme: this.config.theme,
|
||||||
|
},
|
||||||
|
"theme"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onLanguageChange() {
|
async onLanguageChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
language: this.config.language,
|
{
|
||||||
});
|
language: this.config.language,
|
||||||
|
},
|
||||||
|
"language"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onAutoResendFailedMessagesWhenAnnounceReceivedChangeWrapper(value) {
|
async onAutoResendFailedMessagesWhenAnnounceReceivedChangeWrapper(value) {
|
||||||
this.config.auto_resend_failed_messages_when_announce_received = value;
|
this.config.auto_resend_failed_messages_when_announce_received = value;
|
||||||
await this.onAutoResendFailedMessagesWhenAnnounceReceivedChange();
|
await this.onAutoResendFailedMessagesWhenAnnounceReceivedChange();
|
||||||
},
|
},
|
||||||
async onAutoResendFailedMessagesWhenAnnounceReceivedChange() {
|
async onAutoResendFailedMessagesWhenAnnounceReceivedChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
auto_resend_failed_messages_when_announce_received:
|
{
|
||||||
this.config.auto_resend_failed_messages_when_announce_received,
|
auto_resend_failed_messages_when_announce_received:
|
||||||
});
|
this.config.auto_resend_failed_messages_when_announce_received,
|
||||||
|
},
|
||||||
|
"auto_resend"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onAllowAutoResendingFailedMessagesWithAttachmentsChangeWrapper(value) {
|
async onAllowAutoResendingFailedMessagesWithAttachmentsChangeWrapper(value) {
|
||||||
this.config.allow_auto_resending_failed_messages_with_attachments = value;
|
this.config.allow_auto_resending_failed_messages_with_attachments = value;
|
||||||
await this.onAllowAutoResendingFailedMessagesWithAttachmentsChange();
|
await this.onAllowAutoResendingFailedMessagesWithAttachmentsChange();
|
||||||
},
|
},
|
||||||
async onAllowAutoResendingFailedMessagesWithAttachmentsChange() {
|
async onAllowAutoResendingFailedMessagesWithAttachmentsChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
allow_auto_resending_failed_messages_with_attachments:
|
{
|
||||||
this.config.allow_auto_resending_failed_messages_with_attachments,
|
allow_auto_resending_failed_messages_with_attachments:
|
||||||
});
|
this.config.allow_auto_resending_failed_messages_with_attachments,
|
||||||
|
},
|
||||||
|
"retry_attachments"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onAutoSendFailedMessagesToPropagationNodeChangeWrapper(value) {
|
async onAutoSendFailedMessagesToPropagationNodeChangeWrapper(value) {
|
||||||
this.config.auto_send_failed_messages_to_propagation_node = value;
|
this.config.auto_send_failed_messages_to_propagation_node = value;
|
||||||
await this.onAutoSendFailedMessagesToPropagationNodeChange();
|
await this.onAutoSendFailedMessagesToPropagationNodeChange();
|
||||||
},
|
},
|
||||||
async onAutoSendFailedMessagesToPropagationNodeChange() {
|
async onAutoSendFailedMessagesToPropagationNodeChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
auto_send_failed_messages_to_propagation_node:
|
{
|
||||||
this.config.auto_send_failed_messages_to_propagation_node,
|
auto_send_failed_messages_to_propagation_node:
|
||||||
});
|
this.config.auto_send_failed_messages_to_propagation_node,
|
||||||
|
},
|
||||||
|
"auto_fallback"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onShowSuggestedCommunityInterfacesChangeWrapper(value) {
|
async onShowSuggestedCommunityInterfacesChangeWrapper(value) {
|
||||||
this.config.show_suggested_community_interfaces = value;
|
this.config.show_suggested_community_interfaces = value;
|
||||||
await this.onShowSuggestedCommunityInterfacesChange();
|
await this.onShowSuggestedCommunityInterfacesChange();
|
||||||
},
|
},
|
||||||
async onShowSuggestedCommunityInterfacesChange() {
|
async onShowSuggestedCommunityInterfacesChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
show_suggested_community_interfaces: this.config.show_suggested_community_interfaces,
|
{
|
||||||
});
|
show_suggested_community_interfaces: this.config.show_suggested_community_interfaces,
|
||||||
|
},
|
||||||
|
"community_interfaces"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onLxmfPreferredPropagationNodeDestinationHashChange() {
|
async onLxmfPreferredPropagationNodeDestinationHashChange() {
|
||||||
await this.updateConfig({
|
if (this.saveTimeouts.preferred_node) clearTimeout(this.saveTimeouts.preferred_node);
|
||||||
lxmf_preferred_propagation_node_destination_hash:
|
this.saveTimeouts.preferred_node = setTimeout(async () => {
|
||||||
this.config.lxmf_preferred_propagation_node_destination_hash,
|
await this.updateConfig(
|
||||||
});
|
{
|
||||||
|
lxmf_preferred_propagation_node_destination_hash:
|
||||||
|
this.config.lxmf_preferred_propagation_node_destination_hash,
|
||||||
|
},
|
||||||
|
"preferred_node"
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
async onLxmfLocalPropagationNodeEnabledChangeWrapper(value) {
|
async onLxmfLocalPropagationNodeEnabledChangeWrapper(value) {
|
||||||
this.config.lxmf_local_propagation_node_enabled = value;
|
this.config.lxmf_local_propagation_node_enabled = value;
|
||||||
await this.onLxmfLocalPropagationNodeEnabledChange();
|
await this.onLxmfLocalPropagationNodeEnabledChange();
|
||||||
},
|
},
|
||||||
async onLxmfLocalPropagationNodeEnabledChange() {
|
async onLxmfLocalPropagationNodeEnabledChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
lxmf_local_propagation_node_enabled: this.config.lxmf_local_propagation_node_enabled,
|
{
|
||||||
});
|
lxmf_local_propagation_node_enabled: this.config.lxmf_local_propagation_node_enabled,
|
||||||
|
},
|
||||||
|
"local_node"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onLxmfPreferredPropagationNodeAutoSyncIntervalSecondsChange() {
|
async onLxmfPreferredPropagationNodeAutoSyncIntervalSecondsChange() {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
lxmf_preferred_propagation_node_auto_sync_interval_seconds:
|
{
|
||||||
this.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds,
|
lxmf_preferred_propagation_node_auto_sync_interval_seconds:
|
||||||
});
|
this.config.lxmf_preferred_propagation_node_auto_sync_interval_seconds,
|
||||||
|
},
|
||||||
|
"auto_sync"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onLxmfInboundStampCostChange() {
|
async onLxmfInboundStampCostChange() {
|
||||||
await this.updateConfig({
|
if (this.saveTimeouts.inbound_stamp) clearTimeout(this.saveTimeouts.inbound_stamp);
|
||||||
lxmf_inbound_stamp_cost: this.config.lxmf_inbound_stamp_cost,
|
this.saveTimeouts.inbound_stamp = setTimeout(async () => {
|
||||||
});
|
await this.updateConfig(
|
||||||
|
{
|
||||||
|
lxmf_inbound_stamp_cost: this.config.lxmf_inbound_stamp_cost,
|
||||||
|
},
|
||||||
|
"inbound_stamp_cost_label"
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
async onLxmfPropagationNodeStampCostChange() {
|
async onLxmfPropagationNodeStampCostChange() {
|
||||||
await this.updateConfig({
|
if (this.saveTimeouts.propagation_stamp) clearTimeout(this.saveTimeouts.propagation_stamp);
|
||||||
lxmf_propagation_node_stamp_cost: this.config.lxmf_propagation_node_stamp_cost,
|
this.saveTimeouts.propagation_stamp = setTimeout(async () => {
|
||||||
});
|
await this.updateConfig(
|
||||||
|
{
|
||||||
|
lxmf_propagation_node_stamp_cost: this.config.lxmf_propagation_node_stamp_cost,
|
||||||
|
},
|
||||||
|
"propagation_stamp_cost_label"
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
async onPageArchiverEnabledChangeWrapper(value) {
|
async onPageArchiverEnabledChangeWrapper(value) {
|
||||||
this.config.page_archiver_enabled = value;
|
this.config.page_archiver_enabled = value;
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
page_archiver_enabled: this.config.page_archiver_enabled,
|
{
|
||||||
});
|
page_archiver_enabled: this.config.page_archiver_enabled,
|
||||||
|
},
|
||||||
|
"page_archiver"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onPageArchiverConfigChange() {
|
async onPageArchiverConfigChange() {
|
||||||
await this.updateConfig({
|
if (this.saveTimeouts.page_archiver) clearTimeout(this.saveTimeouts.page_archiver);
|
||||||
page_archiver_max_versions: this.config.page_archiver_max_versions,
|
this.saveTimeouts.page_archiver = setTimeout(async () => {
|
||||||
archives_max_storage_gb: this.config.archives_max_storage_gb,
|
await this.updateConfig(
|
||||||
});
|
{
|
||||||
|
page_archiver_max_versions: this.config.page_archiver_max_versions,
|
||||||
|
archives_max_storage_gb: this.config.archives_max_storage_gb,
|
||||||
|
},
|
||||||
|
"page_archiver"
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
async onCrawlerEnabledChange(value) {
|
async onCrawlerEnabledChange(value) {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
crawler_enabled: value,
|
{
|
||||||
});
|
crawler_enabled: value,
|
||||||
|
},
|
||||||
|
"smart_crawler"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
async onCrawlerConfigChange() {
|
async onCrawlerConfigChange() {
|
||||||
await this.updateConfig({
|
if (this.saveTimeouts.crawler) clearTimeout(this.saveTimeouts.crawler);
|
||||||
crawler_max_retries: this.config.crawler_max_retries,
|
this.saveTimeouts.crawler = setTimeout(async () => {
|
||||||
crawler_retry_delay_seconds: this.config.crawler_retry_delay_seconds,
|
await this.updateConfig(
|
||||||
crawler_max_concurrent: this.config.crawler_max_concurrent,
|
{
|
||||||
});
|
crawler_max_retries: this.config.crawler_max_retries,
|
||||||
|
crawler_retry_delay_seconds: this.config.crawler_retry_delay_seconds,
|
||||||
|
crawler_max_concurrent: this.config.crawler_max_concurrent,
|
||||||
|
},
|
||||||
|
"smart_crawler"
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
async onAuthEnabledChange(value) {
|
async onAuthEnabledChange(value) {
|
||||||
await this.updateConfig({
|
await this.updateConfig(
|
||||||
auth_enabled: value,
|
{
|
||||||
});
|
auth_enabled: value,
|
||||||
|
},
|
||||||
|
"authentication"
|
||||||
|
);
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
// if enabled, redirect to setup page if password not set
|
// if enabled, redirect to setup page if password not set
|
||||||
|
|||||||
Reference in New Issue
Block a user