feat(SettingsPage): add keyboard shortcuts section for customizing workflow, including shortcut recording and management functionalities

This commit is contained in:
2026-01-02 17:27:35 -06:00
parent 6db10e3e8f
commit e974758d3e

View File

@@ -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