2022-07-19 09:05:42 +00:00
|
|
|
import { Notice, Platform } from "obsidian";
|
2022-06-06 16:29:14 +00:00
|
|
|
import { Fragment, h } from "preact";
|
2022-07-11 22:07:28 +00:00
|
|
|
import t from "src/l10n";
|
2022-06-06 16:29:14 +00:00
|
|
|
import { CommandIconPair } from "src/types";
|
2022-07-11 22:07:28 +00:00
|
|
|
import { getCommandFromId, ObsidianIcon } from "src/util";
|
2022-07-12 09:34:00 +00:00
|
|
|
import MobileModifyModal from "../mobileModifyModal";
|
2022-06-13 09:31:03 +00:00
|
|
|
import ChangeableText from "./ChangeableText";
|
2022-11-29 20:04:11 +00:00
|
|
|
import { ColorPicker } from "./ColorPicker";
|
2024-10-11 01:16:41 +00:00
|
|
|
import CommanderPlugin from "src/main";
|
2022-06-06 16:29:14 +00:00
|
|
|
|
|
|
|
|
interface CommandViewerProps {
|
2024-10-11 01:16:41 +00:00
|
|
|
plugin: CommanderPlugin;
|
2022-06-06 16:29:14 +00:00
|
|
|
pair: CommandIconPair;
|
|
|
|
|
handleRemove: () => void;
|
|
|
|
|
handleUp: () => void;
|
|
|
|
|
handleDown: () => void;
|
2022-06-13 09:31:03 +00:00
|
|
|
handleNewIcon: () => void;
|
2026-06-14 00:55:23 +00:00
|
|
|
handleRename: (_name: string) => void;
|
|
|
|
|
handleModeChange: (_mode?: string) => void;
|
|
|
|
|
handleColorChange: (_color?: string) => void;
|
2022-12-20 21:24:35 +00:00
|
|
|
sortable?: boolean;
|
2022-06-06 16:29:14 +00:00
|
|
|
}
|
|
|
|
|
|
2022-07-11 22:07:28 +00:00
|
|
|
export default function CommandComponent({
|
2024-10-11 01:16:41 +00:00
|
|
|
plugin,
|
2022-07-11 22:07:28 +00:00
|
|
|
pair,
|
|
|
|
|
handleRemove,
|
|
|
|
|
handleDown,
|
|
|
|
|
handleUp,
|
|
|
|
|
handleNewIcon,
|
|
|
|
|
handleRename,
|
|
|
|
|
handleModeChange,
|
2022-11-29 20:04:11 +00:00
|
|
|
handleColorChange,
|
2022-12-20 21:24:35 +00:00
|
|
|
sortable = true,
|
2022-07-11 22:07:28 +00:00
|
|
|
}: CommandViewerProps): h.JSX.Element {
|
2024-10-11 01:16:41 +00:00
|
|
|
const cmd = getCommandFromId(pair.id, plugin);
|
2022-06-06 16:29:14 +00:00
|
|
|
if (!cmd) {
|
2022-11-29 20:04:11 +00:00
|
|
|
return (
|
|
|
|
|
<Fragment>
|
|
|
|
|
{Platform.isDesktop && (
|
|
|
|
|
<div className="setting-item mod-toggle">
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="alert-triangle"
|
|
|
|
|
size={20}
|
|
|
|
|
className="cmdr-icon clickable-icon mod-warning"
|
|
|
|
|
/>
|
|
|
|
|
<div className="setting-item-info">
|
|
|
|
|
<div className="setting-item-name">{pair.name}</div>
|
|
|
|
|
<div className="setting-item-description">
|
|
|
|
|
{t(
|
|
|
|
|
"This Command is not available on this device."
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="setting-item-control">
|
|
|
|
|
<button
|
|
|
|
|
className="mod-warning"
|
|
|
|
|
style="display: flex"
|
|
|
|
|
onClick={handleRemove}
|
|
|
|
|
aria-label={t("Delete")}
|
|
|
|
|
>
|
|
|
|
|
<ObsidianIcon icon="lucide-trash" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2022-07-19 09:05:42 +00:00
|
|
|
</div>
|
2022-11-29 20:04:11 +00:00
|
|
|
)}
|
|
|
|
|
{Platform.isMobile && (
|
|
|
|
|
<div
|
|
|
|
|
className="mobile-option-setting-item"
|
|
|
|
|
onClick={(): void => {
|
|
|
|
|
new Notice(
|
|
|
|
|
t(
|
|
|
|
|
"This Command is not available on this device."
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}}
|
2022-07-19 09:05:42 +00:00
|
|
|
>
|
2022-11-29 20:04:11 +00:00
|
|
|
<span
|
|
|
|
|
className="mobile-option-setting-item-remove-icon"
|
|
|
|
|
onClick={handleRemove}
|
|
|
|
|
>
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="minus-with-circle"
|
|
|
|
|
size={22}
|
|
|
|
|
style={{ color: "var(--text-error)" }}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mobile-option-setting-item-option-icon mod-warning">
|
|
|
|
|
<ObsidianIcon icon={"alert-triangle"} size={22} />
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mobile-option-setting-item-name">
|
|
|
|
|
{pair.name}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Fragment>
|
|
|
|
|
);
|
2022-06-06 16:29:14 +00:00
|
|
|
}
|
|
|
|
|
const owningPluginID = cmd.id.split(":").first();
|
2024-10-11 01:16:41 +00:00
|
|
|
const owningPlugin = plugin.app.plugins.manifests[owningPluginID!];
|
2022-06-06 16:29:14 +00:00
|
|
|
const isInternal = !owningPlugin;
|
2022-07-11 22:07:28 +00:00
|
|
|
const isChecked =
|
lint: replace remaining any-typed casts with proper types
Resolves the rest of the no-unsafe-* family by giving TypeScript
enough information instead of suppressing it:
- Extend the Obsidian module augmentation (types.ts) with
App.commands.removeCommand and Vault.getConfig so util.tsx and
main.ts no longer need @ts-ignore around them.
- Cast event.target to the concrete HTMLElement subtype at each
DOM event handler that reads .value/.checked/.offsetWidth
(ChangeableText, MacroBuilder, commandComponent) instead of
@ts-ignore/@ts-expect-error.
- Cast JSON.parse() results to their known shape (MacroBuilder's
macro clone, locales.test.ts's loadJson, main.ts's loadSettings)
instead of letting `any` flow through.
- Drop menuManager's unusual `this: (_item: MenuItem) => void`
parameter and the .call(this, ...) it required - the returned
closure never reads `this`, so this was both unnecessary and,
under this project's non-strict bind/call/apply typing, exactly
why the arguments passed through untyped. Calling the method
directly is equivalent and fully typed.
- Same root cause for MacroBuilderModal's this.close.bind(this) and
executeMacro.test.ts's .bind() - replaced with equivalent
non-bind forms.
- confirmDeleteModal/mobileModifyModal: inline the h(...) call into
render() instead of round-tripping through an intermediately
VNode-typed field, which is where preact's h() lost prop typing.
- vitest.config.ts-style import.meta.dirname swapped for
fileURLToPath(import.meta.url) in eslint.config.mts, since the
project's TS lib doesn't type import.meta.dirname.
Type-level only; no behavior change beyond what's already covered
by the two preceding commits.
2026-07-11 13:43:37 +00:00
|
|
|
(Object.prototype.hasOwnProperty.call(cmd, "checkCallback") as boolean) ||
|
|
|
|
|
(Object.prototype.hasOwnProperty.call(cmd, "editorCheckCallback") as boolean);
|
2022-06-06 16:29:14 +00:00
|
|
|
|
2022-07-04 18:59:29 +00:00
|
|
|
const modeIcon = getModeIcon(pair.mode);
|
2022-07-11 22:07:28 +00:00
|
|
|
const modeName = pair.mode.match(/desktop|mobile|any/)
|
|
|
|
|
? pair.mode[0].toUpperCase() + pair.mode.substring(1)
|
|
|
|
|
: t("This device");
|
2022-06-06 16:29:14 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Fragment>
|
2022-07-11 22:07:28 +00:00
|
|
|
{Platform.isDesktop && (
|
|
|
|
|
<div className="setting-item mod-toggle">
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon={pair.icon}
|
|
|
|
|
size={20}
|
|
|
|
|
aria-label={t("Choose new")}
|
|
|
|
|
onClick={handleNewIcon}
|
|
|
|
|
className="cmdr-icon clickable-icon"
|
|
|
|
|
/>
|
|
|
|
|
<div className="setting-item-info">
|
|
|
|
|
<div className="setting-item-name">
|
|
|
|
|
<ChangeableText
|
2022-07-23 13:32:41 +00:00
|
|
|
ariaLabel={t("Double click to rename")}
|
2022-07-11 22:07:28 +00:00
|
|
|
handleChange={({ target }): void => {
|
lint: replace remaining any-typed casts with proper types
Resolves the rest of the no-unsafe-* family by giving TypeScript
enough information instead of suppressing it:
- Extend the Obsidian module augmentation (types.ts) with
App.commands.removeCommand and Vault.getConfig so util.tsx and
main.ts no longer need @ts-ignore around them.
- Cast event.target to the concrete HTMLElement subtype at each
DOM event handler that reads .value/.checked/.offsetWidth
(ChangeableText, MacroBuilder, commandComponent) instead of
@ts-ignore/@ts-expect-error.
- Cast JSON.parse() results to their known shape (MacroBuilder's
macro clone, locales.test.ts's loadJson, main.ts's loadSettings)
instead of letting `any` flow through.
- Drop menuManager's unusual `this: (_item: MenuItem) => void`
parameter and the .call(this, ...) it required - the returned
closure never reads `this`, so this was both unnecessary and,
under this project's non-strict bind/call/apply typing, exactly
why the arguments passed through untyped. Calling the method
directly is equivalent and fully typed.
- Same root cause for MacroBuilderModal's this.close.bind(this) and
executeMacro.test.ts's .bind() - replaced with equivalent
non-bind forms.
- confirmDeleteModal/mobileModifyModal: inline the h(...) call into
render() instead of round-tripping through an intermediately
VNode-typed field, which is where preact's h() lost prop typing.
- vitest.config.ts-style import.meta.dirname swapped for
fileURLToPath(import.meta.url) in eslint.config.mts, since the
project's TS lib doesn't type import.meta.dirname.
Type-level only; no behavior change beyond what's already covered
by the two preceding commits.
2026-07-11 13:43:37 +00:00
|
|
|
handleRename(
|
|
|
|
|
(target as HTMLInputElement | null)?.value ?? ""
|
|
|
|
|
);
|
2022-11-29 20:04:11 +00:00
|
|
|
}}
|
2022-07-11 22:07:28 +00:00
|
|
|
value={pair.name}
|
|
|
|
|
/>
|
|
|
|
|
{pair.name !== cmd.name && (
|
|
|
|
|
<span style="margin-left: .8ex">
|
|
|
|
|
({cmd.name})
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="setting-item-description">
|
|
|
|
|
{t(
|
|
|
|
|
"Added by {{plugin_name}}.".replace(
|
|
|
|
|
"{{plugin_name}}",
|
|
|
|
|
isInternal ? "Obsidian" : owningPlugin.name
|
|
|
|
|
)
|
|
|
|
|
)}{" "}
|
|
|
|
|
{isChecked
|
|
|
|
|
? t(
|
2022-11-29 20:04:11 +00:00
|
|
|
"Warning: This is a checked Command, meaning it might not run under every circumstance."
|
2026-06-14 00:55:23 +00:00
|
|
|
)
|
2022-07-11 22:07:28 +00:00
|
|
|
: ""}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="setting-item-control">
|
2022-11-29 20:04:11 +00:00
|
|
|
<ColorPicker
|
|
|
|
|
initialColor={pair.color ?? "#000"}
|
|
|
|
|
onChange={handleColorChange}
|
|
|
|
|
/>
|
2022-12-20 21:24:35 +00:00
|
|
|
{sortable && (
|
|
|
|
|
<>
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="arrow-down"
|
|
|
|
|
className="setting-editor-extra-setting-button clickable-icon"
|
|
|
|
|
onClick={handleDown}
|
|
|
|
|
aria-label={t("Move down")}
|
|
|
|
|
/>
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="arrow-up"
|
|
|
|
|
className="setting-editor-extra-setting-button clickable-icon"
|
|
|
|
|
onClick={handleUp}
|
|
|
|
|
aria-label={t("Move up")}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2022-07-11 22:07:28 +00:00
|
|
|
<ObsidianIcon
|
|
|
|
|
icon={modeIcon}
|
|
|
|
|
className="setting-editor-extra-setting-button clickable-icon"
|
2022-07-12 09:34:00 +00:00
|
|
|
onClick={(): void => handleModeChange()}
|
2022-07-11 22:07:28 +00:00
|
|
|
aria-label={t(
|
|
|
|
|
"Change Mode (Currently: {{current_mode}})"
|
|
|
|
|
).replace("{{current_mode}}", modeName)}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
className="mod-warning"
|
|
|
|
|
style="display: flex"
|
|
|
|
|
onClick={handleRemove}
|
|
|
|
|
aria-label={t("Delete")}
|
|
|
|
|
>
|
|
|
|
|
<ObsidianIcon icon="lucide-trash" />
|
|
|
|
|
</button>
|
2022-06-13 09:31:03 +00:00
|
|
|
</div>
|
2022-06-06 16:29:14 +00:00
|
|
|
</div>
|
2022-07-11 22:07:28 +00:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{Platform.isMobile && (
|
|
|
|
|
<div className="mobile-option-setting-item">
|
|
|
|
|
<span
|
|
|
|
|
className="mobile-option-setting-item-remove-icon"
|
|
|
|
|
onClick={handleRemove}
|
|
|
|
|
>
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="minus-with-circle"
|
|
|
|
|
size={22}
|
|
|
|
|
style={{ color: "var(--text-error)" }}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mobile-option-setting-item-option-icon">
|
2022-07-13 19:47:57 +00:00
|
|
|
<ObsidianIcon
|
|
|
|
|
icon={pair.icon}
|
|
|
|
|
size={22}
|
|
|
|
|
onClick={(): void => {
|
2022-11-29 20:04:11 +00:00
|
|
|
new MobileModifyModal(
|
2024-10-11 01:16:41 +00:00
|
|
|
plugin,
|
2022-11-29 20:04:11 +00:00
|
|
|
pair,
|
|
|
|
|
handleRename,
|
|
|
|
|
handleNewIcon,
|
|
|
|
|
handleModeChange,
|
|
|
|
|
handleColorChange
|
|
|
|
|
).open();
|
2022-07-13 19:47:57 +00:00
|
|
|
}}
|
|
|
|
|
/>
|
2022-07-11 22:07:28 +00:00
|
|
|
</span>
|
2022-11-29 20:04:11 +00:00
|
|
|
<span
|
|
|
|
|
className="mobile-option-setting-item-name"
|
|
|
|
|
onClick={(): void => {
|
|
|
|
|
new MobileModifyModal(
|
2024-10-11 01:16:41 +00:00
|
|
|
plugin,
|
2022-11-29 20:04:11 +00:00
|
|
|
pair,
|
|
|
|
|
handleRename,
|
|
|
|
|
handleNewIcon,
|
|
|
|
|
handleModeChange,
|
|
|
|
|
handleColorChange
|
|
|
|
|
).open();
|
|
|
|
|
}}
|
|
|
|
|
>
|
2022-07-11 22:07:28 +00:00
|
|
|
{pair.name}
|
|
|
|
|
{pair.name !== cmd.name && (
|
2022-11-29 20:04:11 +00:00
|
|
|
<span className="cmdr-option-setting-name">
|
|
|
|
|
({cmd.name})
|
|
|
|
|
</span>
|
2022-07-11 22:07:28 +00:00
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mobile-option-setting-item-option-icon">
|
2022-12-20 21:24:35 +00:00
|
|
|
{sortable && (
|
|
|
|
|
<>
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="arrow-down"
|
|
|
|
|
className="clickable-icon"
|
|
|
|
|
onClick={handleDown}
|
|
|
|
|
/>
|
|
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="arrow-up"
|
|
|
|
|
className="clickable-icon"
|
|
|
|
|
onClick={handleUp}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2022-07-11 22:07:28 +00:00
|
|
|
<ObsidianIcon
|
|
|
|
|
icon="three-horizontal-bars"
|
|
|
|
|
className="clickable-icon"
|
2022-07-12 09:34:00 +00:00
|
|
|
onClick={(): void => {
|
2022-11-29 20:04:11 +00:00
|
|
|
new MobileModifyModal(
|
2024-10-11 01:16:41 +00:00
|
|
|
plugin,
|
2022-11-29 20:04:11 +00:00
|
|
|
pair,
|
|
|
|
|
handleRename,
|
|
|
|
|
handleNewIcon,
|
|
|
|
|
handleModeChange,
|
|
|
|
|
handleColorChange
|
|
|
|
|
).open();
|
2022-07-12 09:34:00 +00:00
|
|
|
}}
|
2022-07-11 22:07:28 +00:00
|
|
|
/>
|
|
|
|
|
</span>
|
2022-06-06 16:29:14 +00:00
|
|
|
</div>
|
2022-07-11 22:07:28 +00:00
|
|
|
)}
|
2022-06-06 16:29:14 +00:00
|
|
|
</Fragment>
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-07-04 18:59:29 +00:00
|
|
|
|
|
|
|
|
function getModeIcon(mode: string): string {
|
|
|
|
|
if (mode === "mobile") return "smartphone";
|
|
|
|
|
if (mode === "desktop") return "monitor";
|
2022-07-12 15:20:14 +00:00
|
|
|
if (mode === "any") return "cmdr-all-devices";
|
2022-07-04 18:59:29 +00:00
|
|
|
return "airplay";
|
|
|
|
|
}
|