Settings macros pages

This commit is contained in:
Marc Brooks 2025-10-14 02:30:32 -05:00
parent 214bd69d10
commit 1647b80b8c
No known key found for this signature in database
GPG Key ID: 583A6AF2D6AE1DC6
5 changed files with 145 additions and 112 deletions

View File

@ -185,7 +185,7 @@
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
"connection_stats_video": "Video",
"continue": "Continue",
"creating_peer_connection": "Creating peer connection...",
"creating_peer_connection": "Creating peer connection",
"dc_power_control_current_unit": "A",
"dc_power_control_current": "Current",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
@ -236,7 +236,7 @@
"extensions_dc_power_control_description": "Control your DC Power extension",
"extensions_dc_power_control": "DC Power Control",
"extensions_popover_extensions": "Extensions",
"gathering_ice_candidates": "Gathering ICE candidates...",
"gathering_ice_candidates": "Gathering ICE candidates",
"general_app_version": "App: {version}",
"general_auto_update_description": "Automatically update the device to the latest version",
"general_auto_update_error": "Failed to set auto-update: {error}",
@ -258,7 +258,7 @@
"general_update_background_button": "Update in Background",
"general_update_check_again_button": "Check Again",
"general_update_checking_description": "We're ensuring your device has the latest features and improvements.",
"general_update_checking_title": "Checking for updates...",
"general_update_checking_title": "Checking for updates",
"general_update_completed_description": "Your device has been successfully updated to the latest version. Enjoy the new features and improvements!",
"general_update_completed_title": "Update Completed Successfully",
"general_update_error_description": "An error occurred while updating your device. Please try again later.",
@ -266,12 +266,12 @@
"general_update_error_title": "Update Error",
"general_update_later_button": "Do it later",
"general_update_now_button": "Update Now",
"general_update_rebooting": "Rebooting to complete the update...",
"general_update_rebooting": "Rebooting to complete the update",
"general_update_status_awaiting_reboot": "Awaiting reboot",
"general_update_status_downloading": "Downloading {update_type} update...",
"general_update_status_fetching": "Fetching update information...",
"general_update_status_installing": "Installing {update_type} update...",
"general_update_status_verifying": "Verifying {update_type} update...",
"general_update_status_downloading": "Downloading {update_type} update",
"general_update_status_fetching": "Fetching update information",
"general_update_status_installing": "Installing {update_type} update",
"general_update_status_verifying": "Verifying {update_type} update",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux System Update",
"general_update_up_to_date_description": "Your system is running the latest version. No updates are currently available.",
@ -438,6 +438,54 @@
"macro_step_search_for_key": "Search for key…",
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
"macro_steps_label": "Steps",
"macros_add_description": "Create a new keyboard macro",
"macros_add_new": "Add New Macro",
"macros_create_first": "Create your first macro to get started",
"macros_created_success": "Macro \"{name}\" created successfully",
"macros_delete_confirm": "Are you sure you want to delete this macro? This action cannot be undone.",
"macros_delete_macro": "Delete Macro",
"macros_deleted_success": "Macro \"{name}\" deleted successfully",
"macros_deleting": "Deleting",
"macros_duplicate": "Duplicate",
"macros_duplicated_success": "Macro \"{name}\" duplicated successfully",
"macros_edit_description": "Modify your keyboard macro",
"macros_edit_title": "Edit Macro",
"macros_edit": "Edit",
"macros_failed_create": "Failed to create macro",
"macros_failed_create_error": "Failed to create macro: {error}",
"macros_failed_delete": "Failed to delete macro",
"macros_failed_delete_error": "Failed to delete macro: {error}",
"macros_failed_duplicate": "Failed to duplicate macro",
"macros_failed_duplicate_error": "Failed to duplicate macro: {error}",
"macros_failed_reorder": "Failed to reorder macros",
"macros_failed_reorder_error": "Failed to reorder macros: {error}",
"macros_failed_update": "Failed to update macro",
"macros_failed_update_error": "Failed to update macro: {error}",
"macros_invalid_data": "Invalid macro data",
"macros_maximum_macros_reached": "You have reached the maximum number of {maximum} macros allowed.",
"macros_move_down": "Move Down",
"macros_move_up": "Move Up",
"macros_no_macros_available": "No macros available",
"macros_no_macros_found": "No macros found",
"macros_order_updated": "Macro order updated successfully",
"macros_title": "Keyboard Macros",
"macros_updated_success": "Macro \"{name}\" updated successfully",
"macros_aria_delete": "Delete macro {name}",
"macros_aria_duplicate": "Duplicate macro {name}",
"macros_aria_edit": "Edit macro {name}",
"macros_aria_move_down": "Move {name} down",
"macros_aria_move_up": "Move {name} up",
"macros_confirm_delete_description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
"macros_confirm_delete_title": "Delete Macro",
"macros_confirm_deleting": "Deleting…",
"macros_add_new_macro": "Add New Macro",
"macros_aria_add_new": "Add new macro",
"macros_create_first_headline": "Create Your First Macro",
"macros_create_first_description": "Combine keystrokes into a single action",
"macros_delay_only": "Delay only",
"macros_edit_button": "Edit",
"macros_loading": "Loading macros…",
"macros_max_reached": "Max Reached",
"metric_not_supported": "Metric not supported",
"metric_waiting_for_data": "Waiting for data…",
"mount_add_file_to_get_started": "Add a file to get started",

View File

@ -1,24 +1,19 @@
import { useNavigate } from "react-router";
import { useState } from "react";
import { useNavigate } from "react-router";
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { MacroForm } from "@/components/MacroForm";
import { KeySequence, useMacrosStore, generateMacroId } from "@hooks/stores";
import { MacroForm } from "@components/MacroForm";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { DEFAULT_DELAY } from "@/constants/macros";
import notifications from "@/notifications";
import { normalizeSortOrders } from "@/utils";
import { m } from "@localizations/messages.js";
export default function SettingsMacrosAddRoute() {
const { macros, saveMacros } = useMacrosStore();
const [isSaving, setIsSaving] = useState(false);
const navigate = useNavigate();
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({
...macro,
sortOrder: index + 1,
}));
};
const handleAddMacro = async (macro: Partial<KeySequence>) => {
setIsSaving(true);
try {
@ -30,13 +25,13 @@ export default function SettingsMacrosAddRoute() {
};
await saveMacros(normalizeSortOrders([...macros, newMacro]));
notifications.success(`Macro "${newMacro.name}" created successfully`);
notifications.success(m.macros_created_success({name: newMacro.name}));
navigate("../");
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to create macro: ${error.message}`);
notifications.error(m.macros_failed_create_error({error: error.message || m.unknown_error() }));
} else {
notifications.error("Failed to create macro");
notifications.error(m.macros_failed_create());
}
} finally {
setIsSaving(false);
@ -46,8 +41,8 @@ export default function SettingsMacrosAddRoute() {
return (
<div className="space-y-4">
<SettingsPageHeader
title="Add New Macro"
description="Create a new keyboard macro"
title={m.macros_add_new()}
description={m.macros_add_description()}
/>
<MacroForm
initialData={{

View File

@ -1,20 +1,15 @@
import { useNavigate, useParams } from "react-router";
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import { LuTrash2 } from "react-icons/lu";
import { KeySequence, useMacrosStore } from "@/hooks/stores";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { MacroForm } from "@/components/MacroForm";
import { KeySequence, useMacrosStore } from "@hooks/stores";
import { Button } from "@components/Button";
import { ConfirmDialog } from "@components/ConfirmDialog";
import { MacroForm } from "@components/MacroForm";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";
import { Button } from "@/components/Button";
import { ConfirmDialog } from "@/components/ConfirmDialog";
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({
...macro,
sortOrder: index + 1,
}));
};
import { normalizeSortOrders } from "@/utils";
import { m } from "@localizations/messages.js";
export default function SettingsMacrosEditRoute() {
const { macros, saveMacros } = useMacrosStore();
@ -56,13 +51,13 @@ export default function SettingsMacrosEditRoute() {
);
await saveMacros(normalizeSortOrders(newMacros));
notifications.success(`Macro "${updatedMacro.name}" updated successfully`);
notifications.success(m.macros_updated_success({ name: updatedMacro.name }));
navigate("../");
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to update macro: ${error.message}`);
notifications.error(m.macros_failed_update({ error: error.message }));
} else {
notifications.error("Failed to update macro");
notifications.error(m.macros_failed_update());
}
} finally {
setIsUpdating(false);
@ -76,13 +71,13 @@ export default function SettingsMacrosEditRoute() {
try {
const updatedMacros = normalizeSortOrders(macros.filter(m => m.id !== macro.id));
await saveMacros(updatedMacros);
notifications.success(`Macro "${macro.name}" deleted successfully`);
notifications.success(m.macros_deleted_success({ name: macro.name }));
navigate("../macros");
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to delete macro: ${error.message}`);
notifications.error(m.macros_failed_delete_error({ error: error.message }));
} else {
notifications.error("Failed to delete macro");
notifications.error(m.macros_failed_delete());
}
} finally {
setIsDeleting(false);
@ -95,13 +90,13 @@ export default function SettingsMacrosEditRoute() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<SettingsPageHeader
title="Edit Macro"
description="Modify your keyboard macro"
title={m.macros_edit_title()}
description={m.macros_edit_description()}
/>
<Button
size="SM"
theme="light"
text="Delete Macro"
className="text-red-500 dark:text-red-400"
LeadingIcon={LuTrash2}
onClick={() => setShowDeleteConfirm(true)}
@ -118,10 +113,10 @@ export default function SettingsMacrosEditRoute() {
<ConfirmDialog
open={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
title="Delete Macro"
description="Are you sure you want to delete this macro? This action cannot be undone."
title={m.macros_delete_macro()}
description={m.macros_delete_confirm()}
variant="danger"
confirmText={isDeleting ? "Deleting" : "Delete"}
confirmText={isDeleting ? m.macros_deleting() : m.delete()}
onConfirm={() => {
handleDeleteMacro();
setShowDeleteConfirm(false);

View File

@ -11,23 +11,18 @@ import {
LuCommand,
} from "react-icons/lu";
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { Button } from "@/components/Button";
import EmptyCard from "@/components/EmptyCard";
import Card from "@/components/Card";
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros";
import { KeySequence, useMacrosStore, generateMacroId } from "@hooks/stores";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { Button } from "@components/Button";
import Card from "@components/Card";
import { ConfirmDialog } from "@components/ConfirmDialog";
import EmptyCard from "@components/EmptyCard";
import LoadingSpinner from "@components/LoadingSpinner";
import notifications from "@/notifications";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import LoadingSpinner from "@/components/LoadingSpinner";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({
...macro,
sortOrder: index + 1,
}));
};
import { normalizeSortOrders } from "@/utils";
import { MAX_TOTAL_MACROS, COPY_SUFFIX, DEFAULT_DELAY } from "@/constants/macros";
import { m } from "@localizations/messages.js";
export default function SettingsMacrosRoute() {
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
@ -35,7 +30,7 @@ export default function SettingsMacrosRoute() {
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [macroToDelete, setMacroToDelete] = useState<KeySequence | null>(null);
const { selectedKeyboard } = useKeyboardLayout();
const { selectedKeyboard } = useKeyboardLayout();
const isMaxMacrosReached = useMemo(
() => macros.length >= MAX_TOTAL_MACROS,
@ -51,12 +46,12 @@ export default function SettingsMacrosRoute() {
const handleDuplicateMacro = useCallback(
async (macro: KeySequence) => {
if (!macro?.id || !macro?.name) {
notifications.error("Invalid macro data");
notifications.error(m.macros_invalid_data());
return;
}
if (isMaxMacrosReached) {
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
notifications.error(m.macros_maximum_macros_reached({ maximum: MAX_TOTAL_MACROS }));
return;
}
@ -71,12 +66,12 @@ export default function SettingsMacrosRoute() {
try {
await saveMacros(normalizeSortOrders([...macros, newMacroCopy]));
notifications.success(`Macro "${newMacroCopy.name}" duplicated successfully`);
notifications.success(m.macros_duplicated_success({ name: newMacroCopy.name }));
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to duplicate macro: ${error.message}`);
notifications.error(m.macros_failed_duplicate_error({ error: error.message || m.unknown_error() }));
} else {
notifications.error("Failed to duplicate macro");
notifications.error(m.macros_failed_duplicate());
}
} finally {
setActionLoadingId(null);
@ -88,7 +83,7 @@ export default function SettingsMacrosRoute() {
const handleMoveMacro = useCallback(
async (index: number, direction: "up" | "down", macroId: string) => {
if (!Array.isArray(macros) || macros.length === 0) {
notifications.error("No macros available");
notifications.error(m.macros_no_macros_available());
return;
}
@ -103,12 +98,12 @@ export default function SettingsMacrosRoute() {
const updatedMacros = normalizeSortOrders(newMacros);
await saveMacros(updatedMacros);
notifications.success("Macro order updated successfully");
notifications.success(m.macros_order_updated());
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to reorder macros: ${error.message}`);
notifications.error(m.macros_failed_reorder_error({ error: error.message || m.unknown_error() }));
} else {
notifications.error("Failed to reorder macros");
notifications.error(m.macros_failed_reorder());
}
} finally {
setActionLoadingId(null);
@ -126,14 +121,14 @@ export default function SettingsMacrosRoute() {
macros.filter(m => m.id !== macroToDelete.id),
);
await saveMacros(updatedMacros);
notifications.success(`Macro "${macroToDelete.name}" deleted successfully`);
notifications.success(m.macros_deleted_success({ name: macroToDelete.name }));
setShowDeleteConfirm(false);
setMacroToDelete(null);
} catch (error: unknown) {
if (error instanceof Error) {
notifications.error(`Failed to delete macro: ${error.message}`);
notifications.error(m.macros_failed_delete_error({ error: error.message || m.unknown_error() }));
} else {
notifications.error("Failed to delete macro");
notifications.error(m.macros_failed_delete());
}
} finally {
setActionLoadingId(null);
@ -153,7 +148,7 @@ export default function SettingsMacrosRoute() {
onClick={() => handleMoveMacro(index, "up", macro.id)}
disabled={index === 0 || actionLoadingId === macro.id}
LeadingIcon={LuArrowUp}
aria-label={`Move ${macro.name} up`}
aria-label={m.macros_aria_move_up({ name: macro.name })}
/>
<Button
size="XS"
@ -161,7 +156,7 @@ export default function SettingsMacrosRoute() {
onClick={() => handleMoveMacro(index, "down", macro.id)}
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
LeadingIcon={LuArrowDown}
aria-label={`Move ${macro.name} down`}
aria-label={m.macros_aria_move_down({ name: macro.name })}
/>
</div>
@ -180,7 +175,7 @@ export default function SettingsMacrosRoute() {
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
{(Array.isArray(step.modifiers) &&
step.modifiers.length > 0) ||
(Array.isArray(step.keys) && step.keys.length > 0) ? (
(Array.isArray(step.keys) && step.keys.length > 0) ? (
<>
{Array.isArray(step.modifiers) &&
step.modifiers.map((modifier, idx) => (
@ -189,10 +184,7 @@ export default function SettingsMacrosRoute() {
{selectedKeyboard.modifierDisplayMap[modifier] || modifier}
</span>
{idx < step.modifiers.length - 1 && (
<span className="text-slate-400 dark:text-slate-600">
{" "}
+{" "}
</span>
<span className="text-slate-400 dark:text-slate-600">&nbsp;+&nbsp;</span>
)}
</Fragment>
))}
@ -201,10 +193,7 @@ export default function SettingsMacrosRoute() {
step.modifiers.length > 0 &&
Array.isArray(step.keys) &&
step.keys.length > 0 && (
<span className="text-slate-400 dark:text-slate-600">
{" "}
+{" "}
</span>
<span className="text-slate-400 dark:text-slate-600">&nbsp;+&nbsp;</span>
)}
{Array.isArray(step.keys) &&
@ -214,17 +203,14 @@ export default function SettingsMacrosRoute() {
{selectedKeyboard.keyDisplayMap[key] || key}
</span>
{idx < step.keys.length - 1 && (
<span className="text-slate-400 dark:text-slate-600">
{" "}
+{" "}
</span>
<span className="text-slate-400 dark:text-slate-600">&nbsp;+&nbsp;</span>
)}
</Fragment>
))}
</>
) : (
<span className="font-medium text-slate-500 dark:text-slate-400">
Delay only
{m.macros_delay_only()}
</span>
)}
{step.delay !== DEFAULT_DELAY && (
@ -251,7 +237,7 @@ export default function SettingsMacrosRoute() {
setShowDeleteConfirm(true);
}}
disabled={actionLoadingId === macro.id}
aria-label={`Delete macro ${macro.name}`}
aria-label={m.macros_aria_delete({ name: macro.name })}
/>
<Button
size="XS"
@ -259,16 +245,16 @@ export default function SettingsMacrosRoute() {
LeadingIcon={LuCopy}
onClick={() => handleDuplicateMacro(macro)}
disabled={actionLoadingId === macro.id}
aria-label={`Duplicate macro ${macro.name}`}
aria-label={m.macros_aria_duplicate({ name: macro.name })}
/>
<Button
size="XS"
theme="light"
LeadingIcon={LuPenLine}
text="Edit"
text={m.macros_edit_button()}
onClick={() => navigate(`${macro.id}/edit`)}
disabled={actionLoadingId === macro.id}
aria-label={`Edit macro ${macro.name}`}
aria-label={m.macros_aria_edit({ name: macro.name })}
/>
</div>
</div>
@ -281,10 +267,10 @@ export default function SettingsMacrosRoute() {
setShowDeleteConfirm(false);
setMacroToDelete(null);
}}
title="Delete Macro"
description={`Are you sure you want to delete "${macroToDelete?.name}"? This action cannot be undone.`}
title={m.macros_confirm_delete_title()}
description={m.macros_confirm_delete_description({ name: macroToDelete?.name || "" })}
variant="danger"
confirmText={actionLoadingId === macroToDelete?.id ? "Deleting..." : "Delete"}
confirmText={actionLoadingId === macroToDelete?.id ? m.macros_confirm_deleting() : m.macros_delete_confirm_button()}
onConfirm={handleDeleteMacro}
isConfirming={actionLoadingId === macroToDelete?.id}
/>
@ -309,18 +295,18 @@ export default function SettingsMacrosRoute() {
<div className="space-y-4">
<div className="flex items-center justify-between">
<SettingsPageHeader
title="Keyboard Macros"
description={`Combine keystrokes into a single action for faster workflows.`}
title={m.macros_title()}
description={m.macros_add_new()}
/>
{macros.length > 0 && (
<div className="flex items-center pl-2">
<Button
size="SM"
theme="primary"
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
text={isMaxMacrosReached ? m.macros_max_reached() : m.macros_add_new_macro()}
onClick={() => navigate("add")}
disabled={isMaxMacrosReached}
aria-label="Add new macro"
aria-label={m.macros_aria_add_new()}
/>
</div>
)}
@ -330,7 +316,7 @@ export default function SettingsMacrosRoute() {
{loading && macros.length === 0 ? (
<EmptyCard
IconElm={LuCommand}
headline="Loading macros..."
headline={m.macros_loading()}
BtnElm={
<div className="my-2 flex flex-col items-center space-y-2 text-center">
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
@ -340,16 +326,16 @@ export default function SettingsMacrosRoute() {
) : macros.length === 0 ? (
<EmptyCard
IconElm={LuCommand}
headline="Create Your First Macro"
description="Combine keystrokes into a single action"
headline={m.macros_create_first_headline()}
description={m.macros_create_first_description()}
BtnElm={
<Button
size="SM"
theme="primary"
text="Add New Macro"
text={m.macros_add_new_macro()}
onClick={() => navigate("add")}
disabled={isMaxMacrosReached}
aria-label="Add new macro"
aria-label={m.macros_aria_add_new()}
/>
}
/>

View File

@ -1,3 +1,5 @@
import { KeySequence } from "@hooks/stores";
export const formatters = {
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
new Intl.DateTimeFormat("en-US", {
@ -243,3 +245,10 @@ export function isChromeOS() {
/* ChromeOS sets navigator.platform to Linux :/ */
return !!navigator.userAgent.match(" CrOS ");
}
export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
return macros.map((macro, index) => ({
...macro,
sortOrder: index + 1,
}));
};