Skip to content

Commit

Permalink
Adds support for Stream Deck Neo
Browse files Browse the repository at this point in the history
  • Loading branch information
zahnooo committed Apr 18, 2024
1 parent cc83e64 commit 0548b1c
Show file tree
Hide file tree
Showing 31 changed files with 720 additions and 139 deletions.
23 changes: 23 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

# Change Log
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.0.2-alpha] - 2024-04-18

This release adds support for the newest member in the Stream Deck family: Stream Deck Neo.

Make sure that you also update Stream Deck Connect which adds device support for Neo in the driver.

### Added
- Simulator for Neo

### Changed
- Example App now includes examples for Stream Deck Neo's new info panel and touch keys
- Updated Readme and documentation

## [0.0.1-alpha] - 2024-03-04

Initial alpha release.
63 changes: 59 additions & 4 deletions Documentation/Layout/Animated.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ struct AnimatedStreamDeckLayout {
MyKeyView()
}
} windowArea: {
StreamDeckDialAreaLayout { _ in
// To react to state changes within each StreamDeckDialView, extract the view, just as you normally would in SwiftUI
// Example:
MyDialView()
// To react to state changes within each view, extract the view, just as you normally would in SwiftUI
// Example:
if streamDeck.info.product == .plus {
StreamDeckDialAreaLayout { _ in
MyDialView()
}
} else if streamDeck.info.product == .neo {
MyNeoPanelView()
}
}
}
Expand Down Expand Up @@ -153,6 +157,57 @@ struct AnimatedStreamDeckLayout {
}
}
}

@StreamDeckView
struct MyNeoPanelView {

@State private var offset: Double = 0
@State private var targetOffset: Double = 0

@State private var date: Date = .now

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

var streamDeckBody: some View {
// Use StreamDeckNeoPanelLayout for Stream Deck Neo
StreamDeckNeoPanelLayout { touched in
targetOffset -= touched ? 50 : 0
} rightTouch: { touched in
targetOffset += touched ? 50 : 0
} panel: {
VStack {
Text(date.formatted(date: .complete, time: .omitted))
Text(date.formatted(date: .omitted, time: .standard)).bold().monospaced()
}
.offset(x: offset)
}
.background(Color(white: Double(1) / 5 + 0.5))
.onReceive(timer, perform: { _ in
date = .now
})
.task(id: targetOffset) {
// Animate the change of the offset by applying different position values over time
// Calculate three values in between the current offset and the target offset

func calculateCenter(_ offsetA: Double, _ offsetB: Double) -> Double {
return (offsetA + offsetB) / 2
}
let currentOffset = offset
let centerOffset = calculateCenter(currentOffset, targetOffset)
let firstQuarterOffset = calculateCenter(currentOffset, centerOffset)
let thirdQuarterOffset = calculateCenter(currentOffset, targetOffset)

func apply(_ offset: Double) async {
guard !Task.isCancelled else { return }
self.offset = offset
try? await Task.sleep(for: .milliseconds(50))
}
for position in [firstQuarterOffset, centerOffset, thirdQuarterOffset, targetOffset] {
await apply(position)
}
}
}
}

}
```
60 changes: 41 additions & 19 deletions Documentation/Layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ The general structure of `StreamDeckLayout` is as follows:
StreamDeckLayout
└───keyArea: StreamDeckKeyAreaLayout
│ └───StreamDeckKeyView
└───windowArea: StreamDeckDialAreaLayout
└───StreamDeckDialView
└───windowArea:
└───StreamDeckDialAreaLayout
└───StreamDeckDialView
└───StreamDeckNeoPanelLayout
```

<figure>
Expand All @@ -23,7 +25,7 @@ StreamDeckLayout
</figure>

{% hint style="info" %}
The window area is only available for the Stream Deck + and will be ignored for other device types.
The window area is only available for the Stream Deck + and Stream Deck Neo, and will be ignored for other device types. For the Stream Deck Neo, the window area corresponds to the info panel only, excluding the touch buttons.
{% endhint %}

## Usage
Expand Down Expand Up @@ -59,23 +61,35 @@ struct StatelessStreamDeckLayout {
}.background(.purple)
} windowArea: {
// Define window area
// Use StreamDeckDialAreaLayout for rendering separate parts of the display
StreamDeckDialAreaLayout { dialIndex in
// Define content for each dial
// StreamDeckDialAreaLayout provides an index for each available dial,
// and StreamDeckDialView provides callbacks for the dial actions
// Example:
StreamDeckDialView { rotations in
print("dial rotated \(rotations)")
} press: { pressed in
print("pressed \(pressed)")
} touch: { location in
print("touched at \(location)")
} content: {
Text("\(dialIndex)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(white: Double(dialIndex) / 5 + 0.5))
if streamDeck.info.product == .plus {
// Use StreamDeckDialAreaLayout for Stream Deck +
StreamDeckDialAreaLayout { dialIndex in
// Define content for each dial
// StreamDeckDialAreaLayout provides an index for each available dial,
// and StreamDeckDialView provides callbacks for the dial actions
// Example:
StreamDeckDialView { rotations in
print("dial rotated \(rotations)")
} press: { pressed in
print("pressed \(pressed)")
} touch: { location in
print("touched at \(location)")
} content: {
Text("\(dialIndex)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(white: Double(dialIndex) / 5 + 0.5))
}
}
} else if streamDeck.info.product == .neo {
// Use StreamDeckNeoPanelLayout for Stream Deck Neo
StreamDeckNeoPanelLayout { touched in
print("left key touched \(touched)")
} rightTouch: { touched in
print("right key touched \(touched)")
} panel: {
Text("Info Panel")
}
.background(.yellow)
}
}.background(.indigo)
}
Expand Down Expand Up @@ -110,6 +124,14 @@ Depending on the device, the outcome will look like this:
<td><img src="../_images/layout_sd_plus.png"></td>
<td><img src="../_images/layout_sd_plus_device.png"></td>
</tr>
<tr>
<td>Neo</td>
<td>
<img src="../_images/layout_sd_neo.png">
<strong>Note:</strong> On the Stream Deck Neo device, you can not set images to the two touch key areas directly. However, you can change the appeareance of these by changing the background.
</td>
<td><img src="../_images/layout_sd_neo_device.png"></td>
</tr>
</table>


Expand Down
40 changes: 36 additions & 4 deletions Documentation/Layout/Stateful.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ struct StatefulStreamDeckLayout {
MyKeyView()
}
} windowArea: {
StreamDeckDialAreaLayout { _ in
// To react to state changes within each StreamDeckDialView, extract the view, just as you normally would in SwiftUI
// Example:
MyDialView()
// To react to state changes within each view, extract the view, just as you normally would in SwiftUI
// Example:
if streamDeck.info.product == .plus {
StreamDeckDialAreaLayout { _ in
MyDialView()
}
} else if streamDeck.info.product == .neo {
MyNeoPanelView()
}
}
}
Expand Down Expand Up @@ -85,6 +89,34 @@ struct StatefulStreamDeckLayout {
}
}
}

@StreamDeckView
struct MyNeoPanelView {

@State private var offset: Double = 0
@State private var date: Date = .now

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

var streamDeckBody: some View {
// Use StreamDeckNeoPanelLayout for Stream Deck Neo
StreamDeckNeoPanelLayout { touched in
offset -= touched ? 5 : 0
} rightTouch: { touched in
offset += touched ? 5 : 0
} panel: {
VStack {
Text(date.formatted(date: .complete, time: .omitted))
Text(date.formatted(date: .omitted, time: .standard)).bold().monospaced()
}
.offset(x: offset)
}
.background(Color(white: Double(1) / 5 + 0.5))
.onReceive(timer, perform: { _ in
date = .now
})
}
}

}

Expand Down
Binary file added Documentation/_images/layout_sd_neo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Documentation/_images/layout_sd_neo_device.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 9 additions & 7 deletions Example/Example App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct ContentView: View {
}

var sessionStateView: some View {
VStack {
VStack(spacing: 20) {
switch dataModel.selectedExample {
case .stateless: Text("1. Example - Stateless").font(.title).padding()
case .stateful: Text("2. Example - Stateful").font(.title).padding()
Expand All @@ -48,12 +48,6 @@ struct ContentView: View {
Text("Session State: \(stateDescription)")
if devices.isEmpty {
Text("Please connect a Stream Deck device!")
#if DEBUG
Text("or")
Button("Start the Stream Deck Simulator") {
StreamDeckSimulator.show(defaultStreamDeck: .mini)
}
#endif
} else {
ForEach(devices) { device in
VStack(alignment: .leading) {
Expand All @@ -75,8 +69,16 @@ struct ContentView: View {
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(StreamDeckSession.instance.$state) { stateDescription = $0.debugDescription }
.onReceive(StreamDeckSession.instance.$devices) { devices = $0 }
.overlay(alignment: .bottomTrailing) {
Button("Show Stream Deck Simulator") {
StreamDeckSimulator.show(streamDeck: .regular)
}
.buttonStyle(.borderedProminent)
.padding()
}
}
}

Expand Down
70 changes: 44 additions & 26 deletions Example/Example App/Examples/1_StatelessStreamDeckLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct StatelessStreamDeckLayout {
// and StreamDeckKeyView provides a callback for the key action
// Example:
StreamDeckKeyView { pressed in
print("pressed \(pressed)")
print("pressed \(pressed) at index \(keyIndex)")
} content: {
Text("\(keyIndex)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand All @@ -30,23 +30,35 @@ struct StatelessStreamDeckLayout {
}.background(.purple)
} windowArea: {
// Define window area
// Use StreamDeckDialAreaLayout for rendering separate parts of the display
StreamDeckDialAreaLayout { dialIndex in
// Define content for each dial
// StreamDeckDialAreaLayout provides an index for each available dial,
// and StreamDeckDialView provides callbacks for the dial actions
// Example:
StreamDeckDialView { rotations in
print("dial rotated \(rotations)")
} press: { pressed in
print("pressed \(pressed)")
} touch: { location in
print("touched at \(location)")
} content: {
Text("\(dialIndex)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(white: Double(dialIndex) / 5 + 0.5))
if streamDeck.info.product == .plus {
// Use StreamDeckDialAreaLayout for Stream Deck +
StreamDeckDialAreaLayout { dialIndex in
// Define content for each dial
// StreamDeckDialAreaLayout provides an index for each available dial,
// and StreamDeckDialView provides callbacks for the dial actions
// Example:
StreamDeckDialView { rotations in
print("dial rotated \(rotations)")
} press: { pressed in
print("pressed \(pressed)")
} touch: { location in
print("touched at \(location)")
} content: {
Text("\(dialIndex)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(white: Double(dialIndex) / 5 + 0.5))
}
}
} else if streamDeck.info.product == .neo {
// Use StreamDeckNeoPanelLayout for Stream Deck Neo
StreamDeckNeoPanelLayout { touched in
print("left key touched \(touched)")
} rightTouch: { touched in
print("right key touched \(touched)")
} panel: {
Text("Info Panel")
}
.background(.yellow)
}
}.background(.indigo)
}
Expand All @@ -55,18 +67,24 @@ struct StatelessStreamDeckLayout {

#if DEBUG

import StreamDeckSimulator
import StreamDeckSimulator

#Preview("Stream Deck +") {
StreamDeckSimulator.PreviewView(streamDeck: .plus) { device in
device.render(StatelessStreamDeckLayout())
}
#Preview("Stream Deck +") {
StreamDeckSimulator.PreviewView(streamDeck: .plus) { device in
device.render(StatelessStreamDeckLayout())
}
}

#Preview("Stream Deck Classic") {
StreamDeckSimulator.PreviewView(streamDeck: .regular) { device in
device.render(StatelessStreamDeckLayout())
}
#Preview("Stream Deck Classic") {
StreamDeckSimulator.PreviewView(streamDeck: .regular) { device in
device.render(StatelessStreamDeckLayout())
}
}

#Preview("Stream Deck Neo") {
StreamDeckSimulator.PreviewView(streamDeck: .neo) { device in
device.render(StatelessStreamDeckLayout())
}
}

#endif
Loading

0 comments on commit 0548b1c

Please sign in to comment.