From 239fd7804efff182c4c285f7994bb4acd2695539 Mon Sep 17 00:00:00 2001 From: Francesco Saverio Cannizzaro Date: Sat, 28 Sep 2024 16:05:34 +0200 Subject: [PATCH 1/4] add-stream-deck-deeplink-selection --- config/i18n.json | 3 + src/app/inventory/DraggableInventoryItem.tsx | 13 ---- src/app/item-actions/ItemAccessoryButtons.tsx | 20 ++++++ src/app/loadout/LoadoutView.tsx | 13 +--- src/app/loadout/LoadoutsRow.tsx | 32 +++++++++- src/app/loadout/ingame/InGameLoadoutStrip.tsx | 35 ++++++----- .../OpenOnStreamDeckButton.m.scss | 11 ++++ .../OpenOnStreamDeckButton.m.scss.d.ts | 8 +++ .../OpenOnStreamDeckButton.tsx | 29 +++++++++ .../StreamDeckButton/StreamDeckButton.tsx | 2 +- src/app/stream-deck/stream-deck.ts | 2 +- src/app/stream-deck/useStreamDeckSelection.ts | 63 +++++++------------ src/app/stream-deck/util/authorization.ts | 2 +- src/locale/en.json | 2 + 14 files changed, 148 insertions(+), 87 deletions(-) create mode 100644 src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss create mode 100644 src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss.d.ts create mode 100644 src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.tsx diff --git a/config/i18n.json b/config/i18n.json index 2a31b24872..afbdf3cd87 100644 --- a/config/i18n.json +++ b/config/i18n.json @@ -770,6 +770,7 @@ "Redo": "Redo", "RestoreAllItems": "All Items", "Save": "Save", + "OpenOnStreamDeck": "Open on Stream Deck", "SaveLoadout": "Save Loadout", "UpdateLoadout": "Update Loadout", "SaveAsDIM": "Save as DIM Loadout", @@ -950,6 +951,7 @@ "Rewards": "Rewards:", "SendToVault": "Send to Vault", "Store": "Pull to:", + "OpenOnStreamDeck": "Open on Stream Deck", "StoreWithName": "Pull to {{character}}", "Subtitle": { "Type": "{{classType}} {{typeName}}", @@ -1296,6 +1298,7 @@ "Others": "{{count}}x Ghost Projection" }, "StreamDeck": { + "name": "Stream Deck", "Tooltip": { "Title": "DIM Stream Deck Plugin", "Version": "Version:", diff --git a/src/app/inventory/DraggableInventoryItem.tsx b/src/app/inventory/DraggableInventoryItem.tsx index 614a8cb397..4b609d2071 100644 --- a/src/app/inventory/DraggableInventoryItem.tsx +++ b/src/app/inventory/DraggableInventoryItem.tsx @@ -1,7 +1,5 @@ import { hideItemPopup } from 'app/item-popup/item-popup'; -import { useStreamDeckSelection } from 'app/stream-deck/stream-deck'; import clsx from 'clsx'; -import { BucketHashes } from 'data/d2/generated-enums'; import React from 'react'; import { useDrag } from 'react-dnd'; import styles from './DraggableInventoryItem.m.scss'; @@ -16,16 +14,6 @@ interface Props { let dragTimeout: number | null = null; export default function DraggableInventoryItem({ children, item }: Props) { - const selectionProps = $featureFlags.elgatoStreamDeck - ? // eslint-disable-next-line - useStreamDeckSelection({ - type: 'item', - item, - isSubClass: item.bucket.hash === BucketHashes.Subclass, - equippable: !item.notransfer, - }) - : undefined; - const canDrag = (!item.location.inPostmaster || item.destinyVersion === 2) && item.notransfer ? item.equipment @@ -63,7 +51,6 @@ export default function DraggableInventoryItem({ children, item }: Props) { return (
+ import( + /* webpackChunkName: "send-to-stream-deck-button" */ 'app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton' + ), +); + /** * "Accessory" buttons like tagging, locking, comparing, adding to loadout. Displayed separately on mobile, but together with the move actions on desktop. */ @@ -24,6 +34,11 @@ export default function ItemAccessoryButtons({ showLabel: boolean; actionsModel: ItemActionsModel; }) { + const streamDeckEnabled = $featureFlags.elgatoStreamDeck + ? // eslint-disable-next-line react-hooks/rules-of-hooks + useSelector(streamDeckEnabledSelector) + : false; + return ( <> {actionsModel.taggable && ( @@ -43,6 +58,11 @@ export default function ItemAccessoryButtons({ {actionsModel.infusable && ( )} + {streamDeckEnabled && ( + + + + )} ); } diff --git a/src/app/loadout/LoadoutView.tsx b/src/app/loadout/LoadoutView.tsx index 53bf6653f0..892142e797 100644 --- a/src/app/loadout/LoadoutView.tsx +++ b/src/app/loadout/LoadoutView.tsx @@ -14,7 +14,6 @@ import { Loadout, LoadoutItem, ResolvedLoadoutItem } from 'app/loadout-drawer/lo import { getLight } from 'app/loadout-drawer/loadout-utils'; import AppIcon from 'app/shell/icons/AppIcon'; import { useIsPhonePortrait } from 'app/shell/selectors'; -import { useStreamDeckSelection } from 'app/stream-deck/stream-deck'; import { count, filterMap } from 'app/utils/collections'; import { emptyObject } from 'app/utils/empty'; import { itemCanBeEquippedBy } from 'app/utils/item-utils'; @@ -118,18 +117,8 @@ export default function LoadoutView({ ); const power = loadoutPower(store, categories); - const selectionProps = $featureFlags.elgatoStreamDeck - ? // eslint-disable-next-line - useStreamDeckSelection({ - type: 'loadout', - loadout, - store, - equippable: !hideShowModPlacements, - }) - : undefined; - return ( -
+

{loadout.classType === DestinyClass.Unknown && ( diff --git a/src/app/loadout/LoadoutsRow.tsx b/src/app/loadout/LoadoutsRow.tsx index fb4e61d26b..8e129985d3 100644 --- a/src/app/loadout/LoadoutsRow.tsx +++ b/src/app/loadout/LoadoutsRow.tsx @@ -7,6 +7,7 @@ import { editLoadout } from 'app/loadout-drawer/loadout-events'; import { Loadout } from 'app/loadout-drawer/loadout-types'; import { AppIcon, deleteIcon } from 'app/shell/icons'; import { useThunkDispatch } from 'app/store/thunk-dispatch'; +import { useStreamDeckSelection } from 'app/stream-deck/stream-deck'; import _ from 'lodash'; import { ReactNode, memo, useMemo } from 'react'; import LoadoutView from './LoadoutView'; @@ -31,6 +32,16 @@ export default memo(function LoadoutRow({ }) { const dispatch = useThunkDispatch(); + const streamDeckDeepLink = $featureFlags.elgatoStreamDeck + ? // eslint-disable-next-line + useStreamDeckSelection({ + type: 'loadout', + loadout, + store, + equippable, + }) + : undefined; + const actionButtons = useMemo(() => { const handleDeleteClick = () => dispatch(deleteLoadout(loadout.id)); @@ -64,6 +75,16 @@ export default memo(function LoadoutRow({ ); } + if (streamDeckDeepLink) { + actionButtons.push( + + + , + ); + } + if (saved) { actionButtons.push( @@ -84,7 +105,16 @@ export default memo(function LoadoutRow({ } return actionButtons; - }, [dispatch, equippable, loadout, onShare, onSnapshotInGameLoadout, saved, store]); + }, [ + dispatch, + equippable, + loadout, + onShare, + onSnapshotInGameLoadout, + saved, + store, + streamDeckDeepLink, + ]); return ( window.open(streamDeckDeepLink), + }, { key: 'delete', content: ( @@ -174,18 +188,9 @@ function InGameLoadoutTile({ ); } - const selectionProps = $featureFlags.elgatoStreamDeck - ? // eslint-disable-next-line - useStreamDeckSelection({ - type: 'in-game-loadout', - equippable: true, - loadout: gameLoadout, - }) - : undefined; - const loadoutIcon = (
onShowDetails(gameLoadout)}> -
+
{' '}
- {selectionProps?.ref ? ( - loadoutIcon - ) : ( - - {loadoutIcon} - - )} + + {loadoutIcon} +
); diff --git a/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss b/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss new file mode 100644 index 0000000000..7ad7c53782 --- /dev/null +++ b/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss @@ -0,0 +1,11 @@ +.icon { + margin-right: 8px; +} + +.link { + text-decoration: none; +} + +.link span { + padding-right: 8px; +} diff --git a/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss.d.ts b/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss.d.ts new file mode 100644 index 0000000000..91eccdd0ee --- /dev/null +++ b/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.m.scss.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'icon': string; + 'link': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.tsx b/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.tsx new file mode 100644 index 0000000000..69e5c7a4f9 --- /dev/null +++ b/src/app/stream-deck/OpenOnStreamDeckButton/OpenOnStreamDeckButton.tsx @@ -0,0 +1,29 @@ +import { t } from 'app/i18next-t'; +import { DimItem } from 'app/inventory/item-types'; +import ActionButton from 'app/item-actions/ActionButton'; +import { BucketHashes } from 'data/d2/generated-enums'; +import streamDeckIcon from 'images/streamDeck.svg'; +import { useStreamDeckSelection } from '../stream-deck'; +import styles from './OpenOnStreamDeckButton.m.scss'; + +export default function OpenOnStreamDeckButton({ item, label }: { item: DimItem; label: boolean }) { + const deepLink = useStreamDeckSelection({ + type: 'item', + item, + isSubClass: item.bucket.hash === BucketHashes.Subclass, + equippable: !item.notransfer, + }); + + if (!deepLink) { + return null; + } + + return ( + + null}> + + {label && {t('MovePopup.OpenOnStreamDeck')}} + + + ); +} diff --git a/src/app/stream-deck/StreamDeckButton/StreamDeckButton.tsx b/src/app/stream-deck/StreamDeckButton/StreamDeckButton.tsx index 67800bb34e..73f629e527 100644 --- a/src/app/stream-deck/StreamDeckButton/StreamDeckButton.tsx +++ b/src/app/stream-deck/StreamDeckButton/StreamDeckButton.tsx @@ -91,7 +91,7 @@ function StreamDeckButton() { className={styles.streamDeckButton} title={t('StreamDeck.Tooltip.Title')} > - + {error ? (
diff --git a/src/app/stream-deck/stream-deck.ts b/src/app/stream-deck/stream-deck.ts index 75157c1ca1..5494a2f046 100644 --- a/src/app/stream-deck/stream-deck.ts +++ b/src/app/stream-deck/stream-deck.ts @@ -31,4 +31,4 @@ export const startStreamDeckConnection = () => lazyLoaded.start!(); export const stopStreamDeckConnection = () => lazyLoaded.stop!(); export const useStreamDeckSelection: UseStreamDeckSelectionFn = (...args) => - lazyLoaded.useSelection?.(...args) ?? {}; + lazyLoaded.useSelection?.(...args); diff --git a/src/app/stream-deck/useStreamDeckSelection.ts b/src/app/stream-deck/useStreamDeckSelection.ts index 180eb6f966..2085af96ab 100644 --- a/src/app/stream-deck/useStreamDeckSelection.ts +++ b/src/app/stream-deck/useStreamDeckSelection.ts @@ -3,14 +3,14 @@ import { DimItem } from 'app/inventory/item-types'; import { DimStore } from 'app/inventory/store-types'; import { InGameLoadout, Loadout } from 'app/loadout-drawer/loadout-types'; import { d2ManifestSelector } from 'app/manifest/selectors'; -import { useThunkDispatch } from 'app/store/thunk-dispatch'; -import { RootState, ThunkResult } from 'app/store/types'; +import store from 'app/store/store'; +import { RootState } from 'app/store/types'; import { DamageType, DestinyClass } from 'bungie-api-ts/destiny2'; import { BucketHashes } from 'data/d2/generated-enums'; -import { useCallback } from 'react'; -import { useDrag } from 'react-dnd'; +import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { streamDeckSelectionSelector } from './selectors'; +import { STREAM_DECK_DEEP_LINK } from './util/authorization'; import { streamDeckClearId } from './util/packager'; export type StreamDeckSelectionOptions = ( @@ -49,10 +49,8 @@ const toSelection = (data: StreamDeckSelectionOptions, state: RootState) => { loadout: loadout.id, label: loadout.name, character: loadout.characterId, - inGameIcon: { - icon: loadout.icon, - background: loadout.colorIcon, - }, + 'inGameIcon.icon': loadout.icon, + 'inGameIcon.background': loadout.colorIcon, }; } case 'loadout': { @@ -77,7 +75,7 @@ const toSelection = (data: StreamDeckSelectionOptions, state: RootState) => { icon: item.icon, overlay: item.iconOverlay, isExotic: item.isExotic, - isSubClass: data.isSubClass, + isSubClass: data.isSubClass ?? false, isCrafted: item.crafted, element: item.element?.enumValue === DamageType.Kinetic @@ -88,48 +86,31 @@ const toSelection = (data: StreamDeckSelectionOptions, state: RootState) => { } }; -const setDataTransfer = - (e: React.DragEvent, data: StreamDeckSelectionOptions): ThunkResult => - async (_, getState) => { - const state = getState(); - e.dataTransfer.setData('text/plain', JSON.stringify(toSelection(data, state))); - }; - export type UseStreamDeckSelectionArgs = StreamDeckSelectionOptions & { equippable: boolean; isSubClass?: boolean; }; -interface UseStreamDeckSelectionReturn { - ref?: React.Ref; - onDragStart?: React.DragEventHandler; -} - -const useSelection = ({ - equippable, - ...props -}: UseStreamDeckSelectionArgs): UseStreamDeckSelectionReturn => { - const dispatch = useThunkDispatch(); +const useSelection = ({ equippable, ...props }: UseStreamDeckSelectionArgs): string | undefined => { const type = props.type === 'item' ? 'item' : 'loadout'; const selection = useSelector(streamDeckSelectionSelector); - const canDrag = (equippable || props.isSubClass) && selection === type; - const [_coll, dragRef] = useDrag(() => ({ - type, - })); + const canSelect = (equippable || props.isSubClass) && selection === type; - const onDragStart = useCallback( - (e: React.DragEvent) => dispatch(setDataTransfer(e, props)), - [dispatch, props], - ); + const href = useMemo(() => { + const state = store.getState(); + const query = new URLSearchParams(); + const params = toSelection(props, state); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + query.set(key, value as string); + } + } + return `${STREAM_DECK_DEEP_LINK}/selection?${query.toString()}`; + }, [props]); - if (canDrag) { - return { - ref: dragRef, - onDragStart: onDragStart, - }; + if (canSelect) { + return href; } - - return {}; }; export type UseStreamDeckSelectionFn = typeof useSelection; diff --git a/src/app/stream-deck/util/authorization.ts b/src/app/stream-deck/util/authorization.ts index 6958698b37..17a820c7fc 100644 --- a/src/app/stream-deck/util/authorization.ts +++ b/src/app/stream-deck/util/authorization.ts @@ -2,7 +2,7 @@ import { ThunkResult } from 'app/store/types'; import { streamDeckAuthorization } from '../actions'; import { startStreamDeckConnection, stopStreamDeckConnection } from '../stream-deck'; -const STREAM_DECK_DEEP_LINK = 'streamdeck://plugins/message/com.dim.streamdeck'; +export const STREAM_DECK_DEEP_LINK = 'streamdeck://plugins/message/com.dim.streamdeck'; export function streamDeckAuthorizationInit(): ThunkResult { return async (dispatch) => { diff --git a/src/locale/en.json b/src/locale/en.json index b07e5ef3e4..f56be53e73 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -801,6 +801,7 @@ "OnWrongCharacterWarning": "This character's most powerful armor is on another character. To count toward the Power of drops, powerful, and pinnacle rewards, armor must be on this character or in the Vault.", "OnlyItems": "Only equippable items, materials, and consumables can be added to a loadout.", "OpenInOptimizer": "Optimize Armor", + "OpenOnStreamDeck": "Open on Stream Deck", "PickArmor": "Pick Armor", "PickMods": "Add armor mods", "PullFromPostmaster": "Collect Postmaster", @@ -926,6 +927,7 @@ }, "MissingSockets": "Perk and mod details are unavailable while Bungie is updating their services. It will return when they are done, usually in a few hours.", "Notes": "Notes:", + "OpenOnStreamDeck": "Open on Stream Deck", "OverviewTab": "Overview", "Owned": "This item is in your inventory.", "OwnedMod": "This mod is in your modifications inventory.", From 9cd5e59305316d6b2b2ae94c35d3554da7a9fdcf Mon Sep 17 00:00:00 2001 From: Francesco Saverio Cannizzaro Date: Sat, 28 Sep 2024 17:39:11 +0200 Subject: [PATCH 2/4] remove unused class --- src/app/loadout/LoadoutView.m.scss | 4 ---- src/app/loadout/LoadoutView.m.scss.d.ts | 1 - 2 files changed, 5 deletions(-) diff --git a/src/app/loadout/LoadoutView.m.scss b/src/app/loadout/LoadoutView.m.scss index ce70974dbb..d9a7044c6f 100644 --- a/src/app/loadout/LoadoutView.m.scss +++ b/src/app/loadout/LoadoutView.m.scss @@ -109,7 +109,3 @@ .artifactMods { --item-size: 40px; } - -.disableEvents > div { - pointer-events: none; -} diff --git a/src/app/loadout/LoadoutView.m.scss.d.ts b/src/app/loadout/LoadoutView.m.scss.d.ts index fdbf1e38cf..b1d85f2451 100644 --- a/src/app/loadout/LoadoutView.m.scss.d.ts +++ b/src/app/loadout/LoadoutView.m.scss.d.ts @@ -5,7 +5,6 @@ interface CssExports { 'artifactMods': string; 'classIcon': string; 'contents': string; - 'disableEvents': string; 'fadeIn': string; 'finding': string; 'findings': string; From ab8028c2957d4b79bd8f9815d8fdb9715659981d Mon Sep 17 00:00:00 2001 From: Francesco Saverio Cannizzaro Date: Fri, 18 Oct 2024 09:54:48 +0200 Subject: [PATCH 3/4] update min plugin version --- src/app/stream-deck/util/version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/stream-deck/util/version.ts b/src/app/stream-deck/util/version.ts index 31e2d88811..d4f677e006 100644 --- a/src/app/stream-deck/util/version.ts +++ b/src/app/stream-deck/util/version.ts @@ -1,4 +1,4 @@ -export const STREAM_DECK_MINIMUM_VERSION = '3.0.0'; +export const STREAM_DECK_MINIMUM_VERSION = '3.1.0'; const [minMajor, minMinor, minPatch] = STREAM_DECK_MINIMUM_VERSION.split('.'); From 66373016c28130a8223049344afdb80247789b13 Mon Sep 17 00:00:00 2001 From: Francesco Saverio Cannizzaro Date: Mon, 28 Oct 2024 18:15:50 +0100 Subject: [PATCH 4/4] fix sd button key duplicate --- src/app/loadout/LoadoutsRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/loadout/LoadoutsRow.tsx b/src/app/loadout/LoadoutsRow.tsx index f53afa339d..1c8007b4d8 100644 --- a/src/app/loadout/LoadoutsRow.tsx +++ b/src/app/loadout/LoadoutsRow.tsx @@ -85,7 +85,7 @@ export default memo(function LoadoutRow({ if (streamDeckDeepLink) { actionButtons.push( - ,