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_description": "The video stream from the JetKVM to the client.",
"connection_stats_video": "Video", "connection_stats_video": "Video",
"continue": "Continue", "continue": "Continue",
"creating_peer_connection": "Creating peer connection...", "creating_peer_connection": "Creating peer connection",
"dc_power_control_current_unit": "A", "dc_power_control_current_unit": "A",
"dc_power_control_current": "Current", "dc_power_control_current": "Current",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}", "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_description": "Control your DC Power extension",
"extensions_dc_power_control": "DC Power Control", "extensions_dc_power_control": "DC Power Control",
"extensions_popover_extensions": "Extensions", "extensions_popover_extensions": "Extensions",
"gathering_ice_candidates": "Gathering ICE candidates...", "gathering_ice_candidates": "Gathering ICE candidates",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
"general_auto_update_description": "Automatically update the device to the latest version", "general_auto_update_description": "Automatically update the device to the latest version",
"general_auto_update_error": "Failed to set auto-update: {error}", "general_auto_update_error": "Failed to set auto-update: {error}",
@ -258,7 +258,7 @@
"general_update_background_button": "Update in Background", "general_update_background_button": "Update in Background",
"general_update_check_again_button": "Check Again", "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_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_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_completed_title": "Update Completed Successfully",
"general_update_error_description": "An error occurred while updating your device. Please try again later.", "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_error_title": "Update Error",
"general_update_later_button": "Do it later", "general_update_later_button": "Do it later",
"general_update_now_button": "Update Now", "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_awaiting_reboot": "Awaiting reboot",
"general_update_status_downloading": "Downloading {update_type} update...", "general_update_status_downloading": "Downloading {update_type} update",
"general_update_status_fetching": "Fetching update information...", "general_update_status_fetching": "Fetching update information",
"general_update_status_installing": "Installing {update_type} update...", "general_update_status_installing": "Installing {update_type} update",
"general_update_status_verifying": "Verifying {update_type} update...", "general_update_status_verifying": "Verifying {update_type} update",
"general_update_system_type": "System", "general_update_system_type": "System",
"general_update_system_update_title": "Linux System Update", "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.", "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_step_search_for_key": "Search for key…",
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.", "macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
"macro_steps_label": "Steps", "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_not_supported": "Metric not supported",
"metric_waiting_for_data": "Waiting for data…", "metric_waiting_for_data": "Waiting for data…",
"mount_add_file_to_get_started": "Add a file to get started", "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 { useState } from "react";
import { useNavigate } from "react-router";
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores"; import { KeySequence, useMacrosStore, generateMacroId } from "@hooks/stores";
import { SettingsPageHeader } from "@/components/SettingsPageheader"; import { MacroForm } from "@components/MacroForm";
import { MacroForm } from "@/components/MacroForm"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { DEFAULT_DELAY } from "@/constants/macros"; import { DEFAULT_DELAY } from "@/constants/macros";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { normalizeSortOrders } from "@/utils";
import { m } from "@localizations/messages.js";
export default function SettingsMacrosAddRoute() { export default function SettingsMacrosAddRoute() {
const { macros, saveMacros } = useMacrosStore(); const { macros, saveMacros } = useMacrosStore();
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
return macros.map((macro, index) => ({
...macro,
sortOrder: index + 1,
}));
};
const handleAddMacro = async (macro: Partial<KeySequence>) => { const handleAddMacro = async (macro: Partial<KeySequence>) => {
setIsSaving(true); setIsSaving(true);
try { try {
@ -30,13 +25,13 @@ export default function SettingsMacrosAddRoute() {
}; };
await saveMacros(normalizeSortOrders([...macros, newMacro])); await saveMacros(normalizeSortOrders([...macros, newMacro]));
notifications.success(`Macro "${newMacro.name}" created successfully`); notifications.success(m.macros_created_success({name: newMacro.name}));
navigate("../"); navigate("../");
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { 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 { } else {
notifications.error("Failed to create macro"); notifications.error(m.macros_failed_create());
} }
} finally { } finally {
setIsSaving(false); setIsSaving(false);
@ -46,8 +41,8 @@ export default function SettingsMacrosAddRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Add New Macro" title={m.macros_add_new()}
description="Create a new keyboard macro" description={m.macros_add_description()}
/> />
<MacroForm <MacroForm
initialData={{ initialData={{
@ -60,4 +55,4 @@ export default function SettingsMacrosAddRoute() {
/> />
</div> </div>
); );
} }

View File

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

View File

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