├── .env.development ├── .env.production ├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── config └── contract.cse2j ├── contracts ├── ContractExample.cce └── output │ └── ContractExample │ ├── interface │ ├── HTML │ │ └── HomePage.html │ └── mapping │ │ └── ContractExample.cse2j │ └── programming │ ├── SIMPL │ └── ContractExample.chd │ └── SIMPLSharp │ └── ContractExample │ ├── ComponentMediator.g.cs │ ├── Contract.g.cs │ ├── HomePage.g.cs │ └── UIEventArgs.g.cs ├── index.html ├── package-lock.json ├── package.json ├── src ├── App.vue ├── assets │ ├── css │ │ └── App.css │ └── images │ │ ├── favicon.ico │ │ ├── screenshot_narrow.jpg │ │ ├── screenshot_wide.jpg │ │ └── vite.png ├── composables │ └── useWebXPanel.ts ├── globals.d.ts ├── main.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.env.development: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_ENV=production -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | archive 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CH5 Vite+VUE3+TypeScript SPA project demo 2 | 3 | This project is a minimal demonstration of turning a Vite project with Vue 3 + TypeScript acting as a single page application into a CH5 project. 4 | 5 | ## Usage 6 | Run `npm i` to install all dependencies. 7 | 8 | Run the `build:prod` script to build the project for production, or `build:dev` to build for development mode which exposes Eruda and sourcemaps. 9 | 10 | Run the `archive` script to package the contents of the /dist/ directory into a .ch5z that can be loaded onto a control system or panel. 11 | 12 | Run the `deploy:xpanel` script to upload the .ch5z to the control system as a WebXPanel. Adjust the IP address to match your control system. 13 | 14 | Run the `deploy:panel` script to upload the .ch5z to a touch panel as local project. Adjust the IP address to match your panel. 15 | 16 | ## Requirements 17 | - You must have Node.js 20.04.0 or higher and NPM 9.7.2 or higher. For more information see [System Requirements](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/QS-System-Requirements.htm) 18 | - The control system must have SSL and authentication enabled. For more information see [Control System Configuration](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/Platforms/X-CS-Settings.htm) 19 | - At the time of writing CH5 projects are only supported on 3 and 4-series processors (including VC-4), TST-1080, X60, and X70 panels, and the Crestron One app. For more information see [System Requirements](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/QS-System-Requirements.htm) 20 | 21 | ## Authentication 22 | Historically authenticating a CH5 session is handled by a redirect initiated by the WebXPanel library to the processor/server authentication service. However since CH5 2.8.0 an authentication token can be created on the processor/server instead of requiring manual user input for authentication. For processors this is handled via the ```websockettoken generate``` command. On VirtualControl servers the token is generated in the [web interface](https://docs.crestron.com/en-us/8912/content/topics/configuration/Web-Configuration.htm?#Tokens) 23 | 24 | ## The entry point 25 | The entry point is where the Crestron libraries (UMD) will be loaded into the application. In this demo index.html is treated as the entry point for the Crestron libraries. 26 | 27 | ## Contracts 28 | A [contract](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/CE-Overview.htm) is a document that defines how the elements in a UI will interact with a control system program. The Contract Editor outputs programming files for SIMPL Windows and C#, as well as an interface (.cse2j) file for a UI project to reference to interact with said programming files. This allows a UI and program to use descriptive names in a program; so rather than "digital join 1" a UI can reference "HomePage.Lighting.AllOff", which is much easier to read and understand at a glance. 29 | 30 | A contract interface (.cse2j) file can be used at both build and run time. At build time the contract file is referenced by the CH5 `archive` script (see [package.json](package.json)), while at run time it must be placed in `/config/contract.cse2j` (named exactly) at the root level of the served files. 31 | 32 | ## CH5 Web Components 33 | The Crestron HTML5 library (CH5, CrComLib) includes [purpose built HTML5 tags](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/UI-QS-Common-Attribute-Property.htm) (web components) that can be included in HTML markup. These web components include things like `ch5-button` and `ch5-dpad`. In an HTML5 project that uses a standard framework such as Vue these components are not generally necessary and aren't as flexible as Vue's SFC. However, there are some components that cannot be reproduced in Vue such as the `ch5-video` for displaying RTSP streams on Crestron touchpanels and the Crestron One app. For that reason this project does support the use of CH5 components. The [globals.d.ts](/src/globals.d.ts) file contains bindings to map the CH5 web components to the Vue GlobalComponents interface so they can be used without error and maintain type safety. 34 | 35 | ## Progressive Web App (PWA) 36 | Progressive web apps are HTML5 apps that can be "installed" on devices from browsers onto PC/Mac/iOS/Android devices. Browsers require strict security to perform this - so the server must serve the project via HTTPS and the server certificate must be signed by a CA that is trusted by the client device. See the wiki for this demo which details [how to create a certificate chain](https://github.com/jphillipsCrestron/ch5-vue-ts-template/wiki/Creating-a-certificate-chain-(for-PWA)). PWA's are defined via a manifest JSON file which describes the PWA, and a service worker js file that is responsible for caching files for later retrieval in the event that the device loses network connectivity to the server so the user will still see the project. 37 | This demo uses the [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) package to generate a manifest and service worker with the parameters defined in the [vite.config.ts](vite.config.ts) file, which goes into more detail about each parameter and requirements. For more information on PWA's in general, refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps). 38 | 39 | ### Initialize the WebXPanel library if running in a browser: 40 | ```ts 41 | const webXPanelConfig = { 42 | ipId: '0x03', 43 | host: '0.0.0.0', 44 | roomId: '', 45 | authToken: '' 46 | }; 47 | 48 | useWebXPanel(webXPanelConfig); 49 | ``` 50 | 51 | ### Receive data via joins from the control system: 52 | ```ts 53 | onMounted(() => { 54 | const d1Id = window.CrComLib.subscribeState('b', '1', (value: boolean) => digitalState.value = value); 55 | const a1Id = window.CrComLib.subscribeState('n', '1', (value: number) => analogState.value = value); 56 | const s1Id = window.CrComLib.subscribeState('s', '1', (value: string) => serialState.value = value); 57 | 58 | // Contracts 59 | const dc1Id = window.CrComLib.subscribeState('b', 'HomePage.DigitalState', (value: boolean) => digitalContractState.value = value); 60 | const ac1Id = window.CrComLib.subscribeState('n', 'HomePage.AnalogState', (value: number) => analogContractState.value = value); 61 | const sc1Id = window.CrComLib.subscribeState('s', 'HomePage.StringState', (value: string) => serialContractState.value = value); 62 | 63 | onUnmounted(() => { 64 | window.CrComLib.unsubscribeState('b', '1', d1Id); 65 | window.CrComLib.unsubscribeState('n', '1', a1Id); 66 | window.CrComLib.unsubscribeState('s', '1', s1Id); 67 | 68 | // Contracts 69 | window.CrComLib.unsubscribeState('b', 'HomePage.DigitalState', dc1Id); 70 | window.CrComLib.unsubscribeState('n', 'HomePage.AnalogState', ac1Id); 71 | window.CrComLib.unsubscribeState('s', 'HomePage.StringState', sc1Id); 72 | }); 73 | }); 74 | ``` 75 | 76 | ### Send data via joins to the control system: 77 | ```ts 78 | const sendDigital = (value: boolean) => window.CrComLib.publishEvent('b', '1', value); 79 | const sendAnalog = (value: number) => window.CrComLib.publishEvent('n', '1', value); 80 | const sendSerial = (value: string) => window.CrComLib.publishEvent('s', '1', value); 81 | 82 | // Contracts 83 | const sendDigitalContract = (value: boolean) => window.CrComLib.publishEvent('b', 'HomePage.DigitalEvent', value); 84 | const sendAnalogContract = (value: number) => window.CrComLib.publishEvent('n', 'HomePage.AnalogEvent', value); 85 | const sendSerialContract = (value: string) => window.CrComLib.publishEvent('s', 'HomePage.StringEvent', value); 86 | ``` 87 | 88 | ### Using CH5 components 89 | ```tsx 90 | // Import a CH5 theme (coming from the @crestron/ch5-theme package) for CH5 CSS (required when a CH5 component is in use) 91 | // If not using a CH5 component in the project then do not import the CSS as that adds ~2mb to the bundle size. 92 | // In this project we do this in /src/main.ts 93 | import '@crestron/ch5-theme/output/themes/light-theme.css' 94 | 95 | // In App.vue 96 | 108 | ``` -------------------------------------------------------------------------------- /config/contract.cse2j: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ContractExample", 3 | "timestamp": "2025-02-08 18:53:21.937", 4 | "version": "1.0.0.0", 5 | "schema_version": 1, 6 | "extra_value": "", 7 | "signals": { 8 | "states": { 9 | "boolean": { 10 | "1": { 11 | "1": "HomePage.DigitalState" 12 | } 13 | }, 14 | "numeric": { 15 | "1": { 16 | "1": "HomePage.AnalogState" 17 | } 18 | }, 19 | "string": { 20 | "1": { 21 | "1": "HomePage.StringState" 22 | } 23 | } 24 | }, 25 | "events": { 26 | "boolean": { 27 | "HomePage.DigitalEvent": { 28 | "joinId": 1, 29 | "smartObjectId": 1 30 | } 31 | }, 32 | "numeric": { 33 | "HomePage.AnalogEvent": { 34 | "joinId": 1, 35 | "smartObjectId": 1 36 | } 37 | }, 38 | "string": { 39 | "HomePage.StringEvent": { 40 | "joinId": 1, 41 | "smartObjectId": 1 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /contracts/ContractExample.cce: -------------------------------------------------------------------------------- 1 | { 2 | "Errors": [], 3 | "id": "_75toa934f", 4 | "name": "ContractExample", 5 | "description": "", 6 | "company": "", 7 | "client": "", 8 | "author": "", 9 | "version": "1.0.0.0", 10 | "schemaVersion": 1, 11 | "subContractLinks": [], 12 | "subContracts": [], 13 | "specifications": [ 14 | { 15 | "Errors": [], 16 | "parentId": "_75toa934f", 17 | "id": "_8to979t1b", 18 | "componentId": "_o3mh37scx", 19 | "instanceName": "HomePage", 20 | "numberOfInstances": 1 21 | } 22 | ], 23 | "components": [ 24 | { 25 | "Errors": [], 26 | "parentId": "_75toa934f", 27 | "id": "_o3mh37scx", 28 | "name": "HomePage", 29 | "commands": [ 30 | { 31 | "Errors": [], 32 | "name": "DigitalState", 33 | "siblingId": "_gtyas22m4", 34 | "dataType": 1, 35 | "notes": "", 36 | "id": "_wo3oj1nlo", 37 | "parentId": "_o3mh37scx", 38 | "attributeType": 0 39 | }, 40 | { 41 | "Errors": [], 42 | "name": "AnalogState", 43 | "siblingId": "_o9mi9nv5f", 44 | "dataType": 2, 45 | "notes": "", 46 | "id": "_eb33pqpd8", 47 | "parentId": "_o3mh37scx", 48 | "attributeType": 0 49 | }, 50 | { 51 | "Errors": [], 52 | "name": "StringState", 53 | "siblingId": "_ac100j0e2", 54 | "dataType": 3, 55 | "notes": "", 56 | "id": "_x62f89age", 57 | "parentId": "_o3mh37scx", 58 | "attributeType": 0 59 | } 60 | ], 61 | "feedbacks": [ 62 | { 63 | "Errors": [], 64 | "name": "DigitalEvent", 65 | "siblingId": "_wo3oj1nlo", 66 | "dataType": 1, 67 | "notes": "", 68 | "id": "_gtyas22m4", 69 | "parentId": "_o3mh37scx", 70 | "attributeType": 1 71 | }, 72 | { 73 | "Errors": [], 74 | "name": "AnalogEvent", 75 | "siblingId": "_eb33pqpd8", 76 | "dataType": 2, 77 | "notes": "", 78 | "id": "_o9mi9nv5f", 79 | "parentId": "_o3mh37scx", 80 | "attributeType": 1 81 | }, 82 | { 83 | "Errors": [], 84 | "name": "StringEvent", 85 | "siblingId": "_x62f89age", 86 | "dataType": 3, 87 | "notes": "", 88 | "id": "_ac100j0e2", 89 | "parentId": "_o3mh37scx", 90 | "attributeType": 1 91 | } 92 | ], 93 | "specifications": [] 94 | } 95 | ], 96 | "allComponentsForAllContracts": [] 97 | } -------------------------------------------------------------------------------- /contracts/output/ContractExample/interface/HTML/HomePage.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /contracts/output/ContractExample/interface/mapping/ContractExample.cse2j: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ContractExample", 3 | "timestamp": "2025-02-08 18:53:21.937", 4 | "version": "1.0.0.0", 5 | "schema_version": 1, 6 | "extra_value": "", 7 | "signals": { 8 | "states": { 9 | "boolean": { 10 | "1": { 11 | "1": "HomePage.DigitalState" 12 | } 13 | }, 14 | "numeric": { 15 | "1": { 16 | "1": "HomePage.AnalogState" 17 | } 18 | }, 19 | "string": { 20 | "1": { 21 | "1": "HomePage.StringState" 22 | } 23 | } 24 | }, 25 | "events": { 26 | "boolean": { 27 | "HomePage.DigitalEvent": { 28 | "joinId": 1, 29 | "smartObjectId": 1 30 | } 31 | }, 32 | "numeric": { 33 | "HomePage.AnalogEvent": { 34 | "joinId": 1, 35 | "smartObjectId": 1 36 | } 37 | }, 38 | "string": { 39 | "HomePage.StringEvent": { 40 | "joinId": 1, 41 | "smartObjectId": 1 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /contracts/output/ContractExample/programming/SIMPL/ContractExample.chd: -------------------------------------------------------------------------------- 1 | [ 2 | ObjTp=FSgntr 3 | Sgntr=CHD 4 | RelVrs=1 5 | Schema=1 6 | ContractEditor=1.0.0.0 7 | ] 8 | [ 9 | ObjTp=Hd 10 | Schema=1 11 | ProjectFile=ContractExample 12 | ContractID=_75toa934f 13 | CEProjectVer=1.0.0.0 14 | DateTimeUTC=2025-02-08 18:53:21.946 15 | ] 16 | [ 17 | ObjTp=Symbol 18 | Name=HomePage 19 | SmplCName=_8to979t1b1 20 | Hint=HomePage (Control Join Id 1) 21 | Code=1 22 | SMWRev=4.13.00 23 | Expand=expand_randomly 24 | MinVariableInputs=1 25 | MaxVariableInputs=1 26 | MinVariableOutputs=1 27 | MaxVariableOutputs=1 28 | MinVariableInputsList2=1 29 | MaxVariableInputsList2=1 30 | MinVariableOutputsList2=1 31 | MaxVariableOutputsList2=1 32 | MinVariableInputsList3=1 33 | MaxVariableInputsList3=1 34 | MinVariableOutputsList3=1 35 | MaxVariableOutputsList3=1 36 | NumFixedParams=1 37 | ParamCue1=ControlJoinId 38 | ParamSigType1=UI_RO_String 39 | ControlJoinId=1d 40 | MPp=1 41 | Pp1=1 42 | ChdH=1 43 | Render=8 44 | InputCue1=DigitalState 45 | InputSigType1=Digital 46 | SmplCInputCue1=_wo3oj1nlo 47 | InputList2Cue1=AnalogState 48 | InputList2SigType1=Analog 49 | SmplCInputList2Cue1=_eb33pqpd8 50 | InputList3Cue1=StringState 51 | InputList3SigType1=Serial 52 | SmplCInputList3Cue1=_x62f89age 53 | OutputCue1=DigitalEvent 54 | OutputSigType1=Digital 55 | SmplCOutputCue1=_gtyas22m4 56 | OutputList2Cue1=AnalogEvent 57 | OutputList2SigType1=Analog 58 | SmplCOutputList2Cue1=_o9mi9nv5f 59 | OutputList3Cue1=StringEvent 60 | OutputList3SigType1=Serial 61 | SmplCOutputList3Cue1=_ac100j0e2 62 | ] 63 | [ 64 | ObjTp=Dp 65 | Tp=1 66 | HD=TRUE 67 | NF=1 68 | DNF=1 69 | EncFmt=0 70 | DVLF=1 71 | Sgn=0 72 | H=1 73 | DV=1d 74 | ] 75 | [ 76 | ObjTp=CHD 77 | H=1 78 | ChdCode=1 79 | ParentChdFolder=0 80 | ] 81 | -------------------------------------------------------------------------------- /contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/ComponentMediator.g.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using Crestron.SimplSharpPro; 5 | 6 | namespace ContractExample 7 | { 8 | internal class ComponentMediator : IDisposable 9 | { 10 | #region Members 11 | 12 | private readonly IList _smartObjects; 13 | private IList SmartObjects { get { return _smartObjects; } } 14 | 15 | private readonly Dictionary> _booleanOutputs; 16 | private Dictionary> BooleanOutputs { get { return _booleanOutputs; } } 17 | 18 | private readonly Dictionary> _numericOutputs; 19 | private Dictionary> NumericOutputs { get { return _numericOutputs; } } 20 | 21 | private readonly Dictionary> _stringOutputs; 22 | private Dictionary> StringOutputs { get { return _stringOutputs; } } 23 | 24 | #endregion 25 | 26 | #region Construction & Initialization 27 | 28 | public ComponentMediator() 29 | { 30 | _smartObjects = new List(); 31 | 32 | _booleanOutputs = new Dictionary>(); 33 | _numericOutputs = new Dictionary>(); 34 | _stringOutputs = new Dictionary>(); 35 | } 36 | 37 | public void HookSmartObjectEvents(SmartObject smartObject) 38 | { 39 | SmartObjects.Add(smartObject); 40 | smartObject.SigChange += SmartObject_SigChange; 41 | } 42 | public void UnHookSmartObjectEvents(SmartObject smartObject) 43 | { 44 | SmartObjects.Remove(smartObject); 45 | smartObject.SigChange -= SmartObject_SigChange; 46 | } 47 | 48 | #endregion 49 | 50 | #region Smart Object Event Handler 51 | 52 | private string GetKey(uint smartObjectId, uint join) 53 | { 54 | return smartObjectId.ToString(CultureInfo.InvariantCulture) + "." + join.ToString(CultureInfo.InvariantCulture); 55 | } 56 | 57 | internal void ConfigureBooleanEvent(uint controlJoinId, uint join, Action action) 58 | { 59 | string key = GetKey(controlJoinId, join); 60 | if (BooleanOutputs.ContainsKey(key)) 61 | BooleanOutputs[key] = action; 62 | else 63 | BooleanOutputs.Add(key, action); 64 | } 65 | internal void ConfigureNumericEvent(uint controlJoinId, uint join, Action action) 66 | { 67 | string key = GetKey(controlJoinId, join); 68 | if (NumericOutputs.ContainsKey(key)) 69 | NumericOutputs[key] = action; 70 | else 71 | NumericOutputs.Add(key, action); 72 | 73 | } 74 | internal void ConfigureStringEvent(uint controlJoinId, uint join, Action action) 75 | { 76 | string key = GetKey(controlJoinId, join); 77 | if (StringOutputs.ContainsKey(key)) 78 | StringOutputs[key] = action; 79 | else 80 | StringOutputs.Add(key, action); 81 | } 82 | 83 | private void SmartObject_SigChange(GenericBase currentDevice, SmartObjectEventArgs args) 84 | { 85 | try 86 | { 87 | Dictionary> signals = null; 88 | switch (args.Sig.Type) 89 | { 90 | case eSigType.Bool: 91 | signals = BooleanOutputs; 92 | break; 93 | case eSigType.UShort: 94 | signals = NumericOutputs; 95 | break; 96 | case eSigType.String: 97 | signals = StringOutputs; 98 | break; 99 | } 100 | 101 | //Resolve and invoke the corresponding method 102 | Action action; 103 | string key = GetKey(args.SmartObjectArgs.ID, args.Sig.Number); 104 | if (signals != null && 105 | signals.TryGetValue(key, out action) && 106 | action != null) 107 | action.Invoke(args); 108 | } 109 | catch 110 | { 111 | } 112 | } 113 | 114 | #endregion 115 | 116 | #region IDisposable 117 | 118 | private bool IsDisposed { get; set; } 119 | 120 | public void Dispose() 121 | { 122 | if (IsDisposed) 123 | return; 124 | 125 | IsDisposed = true; 126 | 127 | for (int i = 0; i < SmartObjects.Count; i++) 128 | { 129 | SmartObjects[i].SigChange -= SmartObject_SigChange; 130 | } 131 | } 132 | 133 | #endregion 134 | } 135 | } -------------------------------------------------------------------------------- /contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/Contract.g.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Crestron.SimplSharpPro.DeviceSupport; 5 | using Crestron.SimplSharpPro; 6 | 7 | namespace ContractExample 8 | { 9 | /// 10 | /// Common Interface for Root Contracts. 11 | /// 12 | public interface IContract 13 | { 14 | object UserObject { get; set; } 15 | void AddDevice(BasicTriListWithSmartObject device); 16 | void RemoveDevice(BasicTriListWithSmartObject device); 17 | } 18 | 19 | public class Contract : IContract, IDisposable 20 | { 21 | #region Components 22 | 23 | private ComponentMediator ComponentMediator { get; set; } 24 | 25 | public ContractExample.IHomePage HomePage { get { return (ContractExample.IHomePage)InternalHomePage; } } 26 | private ContractExample.HomePage InternalHomePage { get; set; } 27 | 28 | #endregion 29 | 30 | #region Construction and Initialization 31 | 32 | public Contract() 33 | : this(new List().ToArray()) 34 | { 35 | } 36 | 37 | public Contract(BasicTriListWithSmartObject device) 38 | : this(new [] { device }) 39 | { 40 | } 41 | 42 | public Contract(BasicTriListWithSmartObject[] devices) 43 | { 44 | if (devices == null) 45 | throw new ArgumentNullException("Devices is null"); 46 | 47 | ComponentMediator = new ComponentMediator(); 48 | 49 | InternalHomePage = new ContractExample.HomePage(ComponentMediator, 1); 50 | 51 | for (int index = 0; index < devices.Length; index++) 52 | { 53 | AddDevice(devices[index]); 54 | } 55 | } 56 | 57 | #endregion 58 | 59 | #region Standard Contract Members 60 | 61 | public object UserObject { get; set; } 62 | 63 | public void AddDevice(BasicTriListWithSmartObject device) 64 | { 65 | InternalHomePage.AddDevice(device); 66 | } 67 | 68 | public void RemoveDevice(BasicTriListWithSmartObject device) 69 | { 70 | InternalHomePage.RemoveDevice(device); 71 | } 72 | 73 | #endregion 74 | 75 | #region IDisposable 76 | 77 | public bool IsDisposed { get; set; } 78 | 79 | public void Dispose() 80 | { 81 | if (IsDisposed) 82 | return; 83 | 84 | IsDisposed = true; 85 | 86 | InternalHomePage.Dispose(); 87 | ComponentMediator.Dispose(); 88 | } 89 | 90 | #endregion 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/HomePage.g.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Crestron.SimplSharpPro.DeviceSupport; 5 | using Crestron.SimplSharpPro; 6 | 7 | namespace ContractExample 8 | { 9 | public interface IHomePage 10 | { 11 | object UserObject { get; set; } 12 | 13 | event EventHandler DigitalEvent; 14 | event EventHandler AnalogEvent; 15 | event EventHandler StringEvent; 16 | 17 | void DigitalState(HomePageBoolInputSigDelegate callback); 18 | void AnalogState(HomePageUShortInputSigDelegate callback); 19 | void StringState(HomePageStringInputSigDelegate callback); 20 | 21 | } 22 | 23 | public delegate void HomePageBoolInputSigDelegate(BoolInputSig boolInputSig, IHomePage homePage); 24 | public delegate void HomePageUShortInputSigDelegate(UShortInputSig uShortInputSig, IHomePage homePage); 25 | public delegate void HomePageStringInputSigDelegate(StringInputSig stringInputSig, IHomePage homePage); 26 | 27 | internal class HomePage : IHomePage, IDisposable 28 | { 29 | #region Standard CH5 Component members 30 | 31 | private ComponentMediator ComponentMediator { get; set; } 32 | 33 | public object UserObject { get; set; } 34 | 35 | public uint ControlJoinId { get; private set; } 36 | 37 | private IList _devices; 38 | public IList Devices { get { return _devices; } } 39 | 40 | #endregion 41 | 42 | #region Joins 43 | 44 | private static class Joins 45 | { 46 | internal static class Booleans 47 | { 48 | public const uint DigitalEvent = 1; 49 | 50 | public const uint DigitalState = 1; 51 | } 52 | internal static class Numerics 53 | { 54 | public const uint AnalogEvent = 1; 55 | 56 | public const uint AnalogState = 1; 57 | } 58 | internal static class Strings 59 | { 60 | public const uint StringEvent = 1; 61 | 62 | public const uint StringState = 1; 63 | } 64 | } 65 | 66 | #endregion 67 | 68 | #region Construction and Initialization 69 | 70 | internal HomePage(ComponentMediator componentMediator, uint controlJoinId) 71 | { 72 | ComponentMediator = componentMediator; 73 | Initialize(controlJoinId); 74 | } 75 | 76 | private void Initialize(uint controlJoinId) 77 | { 78 | ControlJoinId = controlJoinId; 79 | 80 | _devices = new List(); 81 | 82 | ComponentMediator.ConfigureBooleanEvent(controlJoinId, Joins.Booleans.DigitalEvent, onDigitalEvent); 83 | ComponentMediator.ConfigureNumericEvent(controlJoinId, Joins.Numerics.AnalogEvent, onAnalogEvent); 84 | ComponentMediator.ConfigureStringEvent(controlJoinId, Joins.Strings.StringEvent, onStringEvent); 85 | 86 | } 87 | 88 | public void AddDevice(BasicTriListWithSmartObject device) 89 | { 90 | Devices.Add(device); 91 | ComponentMediator.HookSmartObjectEvents(device.SmartObjects[ControlJoinId]); 92 | } 93 | 94 | public void RemoveDevice(BasicTriListWithSmartObject device) 95 | { 96 | Devices.Remove(device); 97 | ComponentMediator.UnHookSmartObjectEvents(device.SmartObjects[ControlJoinId]); 98 | } 99 | 100 | #endregion 101 | 102 | #region CH5 Contract 103 | 104 | public event EventHandler DigitalEvent; 105 | private void onDigitalEvent(SmartObjectEventArgs eventArgs) 106 | { 107 | EventHandler handler = DigitalEvent; 108 | if (handler != null) 109 | handler(this, UIEventArgs.CreateEventArgs(eventArgs)); 110 | } 111 | 112 | 113 | public void DigitalState(HomePageBoolInputSigDelegate callback) 114 | { 115 | for (int index = 0; index < Devices.Count; index++) 116 | { 117 | callback(Devices[index].SmartObjects[ControlJoinId].BooleanInput[Joins.Booleans.DigitalState], this); 118 | } 119 | } 120 | 121 | public event EventHandler AnalogEvent; 122 | private void onAnalogEvent(SmartObjectEventArgs eventArgs) 123 | { 124 | EventHandler handler = AnalogEvent; 125 | if (handler != null) 126 | handler(this, UIEventArgs.CreateEventArgs(eventArgs)); 127 | } 128 | 129 | 130 | public void AnalogState(HomePageUShortInputSigDelegate callback) 131 | { 132 | for (int index = 0; index < Devices.Count; index++) 133 | { 134 | callback(Devices[index].SmartObjects[ControlJoinId].UShortInput[Joins.Numerics.AnalogState], this); 135 | } 136 | } 137 | 138 | public event EventHandler StringEvent; 139 | private void onStringEvent(SmartObjectEventArgs eventArgs) 140 | { 141 | EventHandler handler = StringEvent; 142 | if (handler != null) 143 | handler(this, UIEventArgs.CreateEventArgs(eventArgs)); 144 | } 145 | 146 | 147 | public void StringState(HomePageStringInputSigDelegate callback) 148 | { 149 | for (int index = 0; index < Devices.Count; index++) 150 | { 151 | callback(Devices[index].SmartObjects[ControlJoinId].StringInput[Joins.Strings.StringState], this); 152 | } 153 | } 154 | 155 | #endregion 156 | 157 | #region Overrides 158 | 159 | public override int GetHashCode() 160 | { 161 | return (int)ControlJoinId; 162 | } 163 | 164 | public override string ToString() 165 | { 166 | return string.Format("Contract: {0} Component: {1} HashCode: {2} {3}", "HomePage", GetType().Name, GetHashCode(), UserObject != null ? "UserObject: " + UserObject : null); 167 | } 168 | 169 | #endregion 170 | 171 | #region IDisposable 172 | 173 | public bool IsDisposed { get; set; } 174 | 175 | public void Dispose() 176 | { 177 | if (IsDisposed) 178 | return; 179 | 180 | IsDisposed = true; 181 | 182 | DigitalEvent = null; 183 | AnalogEvent = null; 184 | StringEvent = null; 185 | } 186 | 187 | #endregion 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/UIEventArgs.g.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Crestron.SimplSharpPro; 3 | using Crestron.SimplSharpPro.DeviceSupport; 4 | 5 | namespace ContractExample 6 | { 7 | public class UIEventArgs : EventArgs 8 | { 9 | public BasicTriListWithSmartObject Device { get; internal set; } 10 | public SigEventArgs SigArgs { get; internal set; } 11 | 12 | internal static UIEventArgs CreateEventArgs(SmartObjectEventArgs eventArgs) 13 | { 14 | return new UIEventArgs 15 | { 16 | Device = (BasicTriListWithSmartObject)eventArgs.SmartObjectArgs.Device, 17 | SigArgs = eventArgs 18 | }; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Vite + Vue + TS 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ch5-vue-ts-template", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build:dev": "vite build --mode development", 10 | "build:prod": "vite build --mode production", 11 | "archive": "ch5-cli archive -p ch5-vue-ts-template -d dist -o archive -c ./contracts/output/ContractExample/interface/mapping/ContractExample.cse2j", 12 | "deploy:mobile": "ch5-cli deploy -p -H 0.0.0.0 -t mobile archive/ch5-vue-ts-template.ch5z", 13 | "deploy:xpanel": "ch5-cli deploy -p -H 0.0.0.0 -t web archive/ch5-vue-ts-template.ch5z", 14 | "deploy:panel": "ch5-cli deploy -p -H 0.0.0.0 -t touchscreen archive/ch5-vue-ts-template.ch5z --slow-mode", 15 | "lint": "eslint . --ext .ts,.vue --report-unused-disable-directives --max-warnings 0", 16 | "preview": "vite preview" 17 | }, 18 | "dependencies": { 19 | "@crestron/ch5-crcomlib": "^2.11.2", 20 | "@crestron/ch5-theme": "^2.11.2", 21 | "@crestron/ch5-webxpanel": "^2.8.0", 22 | "eruda": "^3.4.1", 23 | "vue": "^3.5.13" 24 | }, 25 | "devDependencies": { 26 | "@crestron/ch5-shell-utilities-cli": "^2.11.2", 27 | "@crestron/ch5-utilities-cli": "^2.0.0", 28 | "@types/node": "^22.13.1", 29 | "@vitejs/plugin-vue": "^5.2.1", 30 | "@vue/tsconfig": "^0.7.0", 31 | "typescript": "~5.7.2", 32 | "vite": "^6.1.0", 33 | "vite-plugin-pwa": "^0.21.1", 34 | "vite-plugin-singlefile": "^2.1.0", 35 | "vite-plugin-static-copy": "^2.2.0", 36 | "vue-tsc": "^2.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 105 | 106 | -------------------------------------------------------------------------------- /src/assets/css/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap'); 2 | 3 | * { 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | body { 8 | background-color: #121212; 9 | font-family: 'Roboto', sans-serif; 10 | display: flex; 11 | flex-direction: row; 12 | justify-content: center; 13 | overflow: hidden; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | .controlGroupWrapper { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .controlGroup { 25 | width: 25%; 26 | height: 80%; 27 | padding: 1rem; 28 | background-color: #696969; 29 | border-radius: 15px; 30 | padding: 1rem; 31 | display: flex; 32 | flex-direction: column; 33 | align-items: center; 34 | justify-content: center; 35 | margin-top: 1rem; 36 | margin-bottom: 1rem; 37 | } 38 | 39 | .controlGroup .btn { 40 | /* margin: 1rem; */ 41 | outline: none; 42 | background-color: #0895e7; 43 | color: #ffffff; 44 | border: 0; 45 | border-radius: 10px; 46 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.1); 47 | padding: 14px 40px; 48 | font-size: 16px; 49 | } 50 | 51 | .controlGroup .btn:active { 52 | background-color: #71a0bb; 53 | } 54 | 55 | .controlGroup p { 56 | color: #ffffff; 57 | font-size: 2rem; 58 | } 59 | 60 | .controlgroup #analogSlider { 61 | margin: 1rem; 62 | appearance: none; 63 | width: 95%; 64 | height: 1.5rem; 65 | background-color: #d3d3d3; 66 | border-radius: 15px; 67 | outline: none; 68 | } 69 | 70 | #analogSlider::-webkit-slider-thumb { 71 | -webkit-appearance: none; 72 | appearance: none; 73 | width: 2rem; 74 | height: 2rem; 75 | background: #0895e7; 76 | border-radius: 50%; 77 | cursor: pointer; 78 | } 79 | 80 | #analogSlider::-moz-range-thumb { 81 | width: 2rem; 82 | height: 2rem; 83 | background: #0895e7; 84 | border-radius: 50%; 85 | cursor: pointer; 86 | } 87 | 88 | .controlGroup #currentSerialValue { 89 | background-color: #d3d3d3; 90 | border: 3px solid #d3d3d3; 91 | padding: 0.1rem; 92 | border-radius: 15px; 93 | margin: 1rem; 94 | width: 95%; 95 | height: 3rem; 96 | line-height: 2.5rem; 97 | outline: none; 98 | font-size: 2rem; 99 | } 100 | 101 | @media (max-width: 1200px) { 102 | #controlGroupWrapper { 103 | flex-direction: column; 104 | align-items: center; 105 | justify-content: space-evenly; 106 | } 107 | 108 | .controlGroup { 109 | width: 80%; 110 | height: 25%; 111 | } 112 | } 113 | 114 | @media (min-width: 1201px) { 115 | #controlGroupWrapper { 116 | flex-direction: row; 117 | align-items: center; 118 | justify-content: center; 119 | } 120 | 121 | .controlGroup { 122 | width: 25%; 123 | height: 80%; 124 | margin: 1rem; 125 | } 126 | 127 | body { 128 | height: 800px; 129 | } 130 | } -------------------------------------------------------------------------------- /src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jphillipsCrestron/ch5-vue-ts-template/d0fd9ef6e901f900bd846c64c9d60d14d5a21fbe/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/screenshot_narrow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jphillipsCrestron/ch5-vue-ts-template/d0fd9ef6e901f900bd846c64c9d60d14d5a21fbe/src/assets/images/screenshot_narrow.jpg -------------------------------------------------------------------------------- /src/assets/images/screenshot_wide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jphillipsCrestron/ch5-vue-ts-template/d0fd9ef6e901f900bd846c64c9d60d14d5a21fbe/src/assets/images/screenshot_wide.jpg -------------------------------------------------------------------------------- /src/assets/images/vite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jphillipsCrestron/ch5-vue-ts-template/d0fd9ef6e901f900bd846c64c9d60d14d5a21fbe/src/assets/images/vite.png -------------------------------------------------------------------------------- /src/composables/useWebXPanel.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from 'vue'; 2 | 3 | interface WebXPanelConfig { 4 | host: string; 5 | ipId: string; 6 | roomId?: string; 7 | authToken?: string; 8 | } 9 | 10 | export default function useWebXPanel(params: WebXPanelConfig) { 11 | const isActive = ref(false); 12 | 13 | onMounted(() => { 14 | const initWebXPanel = async () => { 15 | const { WebXPanel, isActive: active, WebXPanelEvents, WebXPanelConfigParams } = window.WebXPanel.getWebXPanel(!window.WebXPanel.runsInContainerApp()); 16 | 17 | isActive.value = active; 18 | 19 | const config: Partial = params; 20 | 21 | if (isActive.value) { 22 | console.log("Initializing XPanel with config: ", JSON.stringify(params)); 23 | 24 | WebXPanel.initialize(config); 25 | 26 | const connectWsListener = () => { 27 | console.log("WebXPanel websocket connection success"); 28 | }; 29 | 30 | const errorWsListener = ({ detail }: any) => { 31 | console.log(`WebXPanel websocket connection error: ${JSON.stringify(detail)}`); 32 | }; 33 | 34 | const connectCipListener = () => { 35 | console.log("WebXPanel CIP connection success"); 36 | }; 37 | 38 | const authenticationFailedListener = ({ detail }: any) => { 39 | console.log(`WebXPanel authentication failed: ${JSON.stringify(detail)}`); 40 | }; 41 | 42 | const notAuthorizedListener = ({ detail }: any) => { 43 | console.log(`WebXPanel not authorized: ${JSON.stringify(detail)}`); 44 | window.location = detail.redirectTo; 45 | }; 46 | 47 | const disconnectWsListener = ({ detail }: any) => { 48 | console.log(`WebXPanel websocket connection lost: ${JSON.stringify(detail)}`); 49 | }; 50 | 51 | const disconnectCipListener = ({ detail }: any) => { 52 | console.log(`WebXPanel CIP connection lost: ${JSON.stringify(detail)}`); 53 | }; 54 | 55 | // Adding event listeners 56 | window.addEventListener(WebXPanelEvents.CONNECT_WS, connectWsListener); 57 | window.addEventListener(WebXPanelEvents.ERROR_WS, errorWsListener); 58 | window.addEventListener(WebXPanelEvents.CONNECT_CIP, connectCipListener); 59 | window.addEventListener(WebXPanelEvents.AUTHENTICATION_FAILED, authenticationFailedListener); 60 | window.addEventListener(WebXPanelEvents.NOT_AUTHORIZED, notAuthorizedListener); 61 | window.addEventListener(WebXPanelEvents.DISCONNECT_WS, disconnectWsListener); 62 | window.addEventListener(WebXPanelEvents.DISCONNECT_CIP, disconnectCipListener); 63 | 64 | // Cleanup function 65 | onUnmounted(() => { 66 | window.removeEventListener(WebXPanelEvents.CONNECT_WS, connectWsListener); 67 | window.removeEventListener(WebXPanelEvents.ERROR_WS, errorWsListener); 68 | window.removeEventListener(WebXPanelEvents.CONNECT_CIP, connectCipListener); 69 | window.removeEventListener(WebXPanelEvents.AUTHENTICATION_FAILED, authenticationFailedListener); 70 | window.removeEventListener(WebXPanelEvents.NOT_AUTHORIZED, notAuthorizedListener); 71 | window.removeEventListener(WebXPanelEvents.DISCONNECT_WS, disconnectWsListener); 72 | window.removeEventListener(WebXPanelEvents.DISCONNECT_CIP, disconnectCipListener); 73 | }); 74 | } 75 | }; 76 | 77 | initWebXPanel(); 78 | }); 79 | 80 | return { isActive }; 81 | } -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { ICh5AnimationAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-animation/interfaces/i-ch5-animation-attributes'; 2 | import { ICh5BackgroundAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-background/interfaces'; 3 | import { ICh5ButtonAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-button/interfaces'; 4 | import { ICh5ButtonListAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-button-list/interfaces/i-ch5-button-list-attributes'; 5 | import { ICh5ColorChipAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-color-chip/interfaces/i-ch5-color-chip-attributes'; 6 | import { ICh5ColorPickerAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-color-picker/interfaces/i-ch5-color-picker-attributes'; 7 | import { ICh5DpadAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-dpad/interfaces/i-ch5-dpad-attributes'; 8 | import { ICh5DateTimeAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-datetime/interfaces/i-ch5-datetime-attributes'; 9 | import { ICh5FormAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-form/interfaces/i-ch5-form-attributes'; 10 | import { ICh5ImageAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-image/interfaces/i-ch5-image-attributes'; 11 | import { ICh5ImportHtmlSnippetAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-import-htmlsnippet/interfaces/i-ch5-import-htmlsnippet-attributes'; 12 | import { ICh5KeypadAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-keypad/interfaces/i-ch5-keypad-attributes'; 13 | import { ICh5JoinToTextBooleanAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-boolean/interfaces'; 14 | import { ICh5JoinToTextNumericAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-numeric/interfaces/i-ch5-jointotext-numeric-attributes'; 15 | import { ICh5JoinToTextStringAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-string/interfaces/i-ch5-jointotext-string-attributes'; 16 | import { ICh5ListAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-list/interfaces'; 17 | import { ICh5ModalDialogAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-modal-dialog/interfaces/i-ch5-modal-dialog-attributes'; 18 | import { ICh5OverlayPanelAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-overlay-panel/interfaces'; 19 | import { ICh5QrCodeAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-qrcode/interfaces/i-ch5-qrcode-attributes'; 20 | import { ICh5SegmentedGaugeAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-segmented-gauge/interfaces/i-ch5-segmented-gauge-attributes'; 21 | import { ICh5SelectAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-select/interfaces'; 22 | import { ICh5SelectOptionAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-select-option/interfaces/i-ch5-select-option-attributes'; 23 | import { ICh5SignalLevelGaugeAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-signal-level-gauge/interfaces/i-ch5-signal-level-gauge-attributes'; 24 | import { ICh5SliderAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-slider/interfaces'; 25 | import { ICh5SpinnerAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-spinner/interfaces'; 26 | import { ICh5SubpageReferenceListAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-subpage-reference-list/interfaces/i-ch5-subpage-reference-list-attributes'; 27 | import { ICh5TabButtonAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-tab-button/interfaces/i-ch5-tab-button-attributes'; 28 | import { ICh5TemplateAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-template/interfaces/i-ch5-template-attributes'; 29 | import { ICh5TextAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-text/interfaces/i-ch5-text-attributes'; 30 | import { ICh5TextInputAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-textinput/interfaces'; 31 | import { ICh5ToggleAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-toggle/interfaces/i-ch5-toggle-attributes'; 32 | import { ICh5TriggerViewAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-triggerview/interfaces/i-ch5-triggerview-attributes'; 33 | import { ICh5VideoAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-video/interfaces'; 34 | import { ICh5VideoSwitcherAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-video-switcher/interfaces'; 35 | import { ICh5WifiSignalLevelGaugeAttributes } from '@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-wifi-signal-level-gauge/interfaces/i-ch5-wifi-signal-level-gauge-attributes'; 36 | 37 | declare global { 38 | interface Window { 39 | CrComLib: typeof import("@crestron/ch5-crcomlib/build_bundles/umd/@types/index"); 40 | WebXPanel: typeof import("@crestron/ch5-webxpanel/dist/types/index"); 41 | } 42 | } 43 | 44 | declare module '@vue/runtime-core' { 45 | export interface GlobalComponents { 46 | 'ch5-animation': DefineComponent>; 47 | 'ch5-background': DefineComponent>; 48 | 'ch5-button': DefineComponent>; 49 | 'ch5-button-list': DefineComponent>; 50 | 'ch5-color-chip': DefineComponent>; 51 | 'ch5-color-picker': DefineComponent>; 52 | 'ch5-dpad': DefineComponent>; 53 | 'ch5-datetime': DefineComponent>; 54 | 'ch5-form': DefineComponent>; 55 | 'ch5-image': DefineComponent>; 56 | 'ch5-import-htmlsnippet': DefineComponent>; 57 | 'ch5-keypad': DefineComponent>; 58 | 'ch5-jointottext-boolean': DefineComponent>; 59 | 'ch5-jointottext-numeric': DefineComponent>; 60 | 'ch5-jointottext-string': DefineComponent>; 61 | 'ch5-list': DefineComponent>; 62 | 'ch5-modal-dialog': DefineComponent>; 63 | 'ch5-overlay-panel': DefineComponent>; 64 | 'ch5-qrcode': DefineComponent>; 65 | 'ch5-segmented-gauge': DefineComponent>; 66 | 'ch5-select': DefineComponent>; 67 | 'ch5-select-option': DefineComponent>; 68 | 'ch5-signal-level-gauge': DefineComponent>; 69 | 'ch5-slider': DefineComponent>; 70 | 'ch5-spinner': DefineComponent>; 71 | 'ch5-subpage-reference-list': DefineComponent>; 72 | 'ch5-tab-button': DefineComponent>; 73 | 'ch5-template': DefineComponent>; 74 | 'ch5-text': DefineComponent>; 75 | 'ch5-textinput': DefineComponent>; 76 | 'ch5-toggle': DefineComponent>; 77 | 'ch5-triggerview': DefineComponent>; 78 | 'ch5-video': DefineComponent>; 79 | 'ch5-video-switcher': DefineComponent>; 80 | 'ch5-wifi-signal-level-gauge': DefineComponent>; 81 | } 82 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | // Uncomment the below line if you are using CH5 components. 3 | // import '@crestron/ch5-theme/output/themes/light-theme.css' // Crestron CSS. @crestron/ch5-theme/output/themes shows the other themes that can be used. 4 | import App from './App.vue'; 5 | 6 | // Initialize eruda for panel/app debugging capabilities (in dev mode only) 7 | if (import.meta.env.VITE_APP_ENV === 'development') { 8 | import('eruda').then(({ default: eruda }) => { 9 | eruda.init(); 10 | }); 11 | } 12 | 13 | createApp(App).mount('#app'); -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | 6 | /* Linting */ 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedSideEffectImports": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { viteSingleFile } from 'vite-plugin-singlefile'; 4 | import { viteStaticCopy } from 'vite-plugin-static-copy'; 5 | import { VitePWA } from 'vite-plugin-pwa'; 6 | 7 | // https://vite.dev/config/ 8 | export default (config: { mode: string; }) => { 9 | const env = loadEnv(config.mode, process.cwd()); 10 | const isDevelopment = env.VITE_APP_ENV === 'development'; 11 | 12 | return defineConfig({ 13 | plugins: [ 14 | vue(), 15 | viteSingleFile(), 16 | viteStaticCopy({ 17 | targets: [ 18 | { 19 | src: 'node_modules/@crestron/ch5-crcomlib/build_bundles/umd/cr-com-lib.js', 20 | dest: '' 21 | }, 22 | { 23 | src: 'node_modules/@crestron/ch5-webxpanel/dist/umd/index.js', 24 | dest: '' 25 | }, 26 | { 27 | src: 'node_modules/@crestron/ch5-webxpanel/dist/umd/d4412f0cafef4f213591.worker.js', 28 | dest: '' 29 | }, 30 | { 31 | src: 'src/assets/images/favicon.ico', 32 | dest: 'assets/' 33 | }, 34 | { 35 | src: 'src/assets/images/screenshot_narrow.jpg', 36 | dest: 'assets/' 37 | }, 38 | { 39 | src: 'src/assets/images/screenshot_wide.jpg', 40 | dest: 'assets/' 41 | }, 42 | { 43 | src: 'src/assets/images/vite.png', 44 | dest: 'assets/' 45 | } 46 | ] 47 | }), 48 | // Note that browsers will only allow PWA service workers to work if the server 49 | // presents a certificate that is signed by a CA that is trusted by the client machine. 50 | VitePWA({ 51 | base: '/ch5-vue-ts-template/', 52 | registerType: 'autoUpdate', 53 | workbox: { 54 | globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg}'], // Pattern to match precached files to return to the client when offline 55 | runtimeCaching: [ 56 | { 57 | urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, // Example for caching Google Fonts (this demo uses Google Roboto in the App.css) 58 | handler: 'CacheFirst', 59 | options: { 60 | cacheName: 'google-fonts', 61 | expiration: { 62 | maxEntries: 10, 63 | maxAgeSeconds: 60 * 60 * 24 * 365, // Cache for a year 64 | }, 65 | cacheableResponse: { 66 | statuses: [0, 200], 67 | }, 68 | }, 69 | }, 70 | // If there are other resources in the project that may be pulled at runtime (and not stored in the project at build time) add caching for them here. 71 | // Refer to the above entry as an example. 72 | ], 73 | }, 74 | manifest: { 75 | // The start_url is the URL to the app index.html on the server 76 | start_url: "https://0.0.0.0/ch5-vue-ts-template/index.html", 77 | // The ID is a unique identifier for the PWA. 78 | // Common practice is to use the domain or subdomain of the app on its server. 79 | id: "/ch5-vue-ts-template/", 80 | name: 'Crestron CH5 Vue PWA', 81 | // Keep the short name 12 characters or less for mobile devices 82 | short_name: 'CH5 Vue PWA', 83 | description: 'A Crestron CH5 project using Vue, Vite, and PWA features', 84 | theme_color: '#696969', 85 | background_color: '#121212', 86 | // The display/_override is to hide browser/OS controls, making the app full screen 87 | display: 'standalone', 88 | display_override: ["window-controls-overlay","standalone"], 89 | lang: 'en', 90 | // There should be at least one icon, at a minimum of 144x144px. 91 | // The src property is relative to the index.html path at runtime. 92 | // Since the VitePWA `base` property is set to /ch5-vue-ts-template/ 93 | // the path will resolve to https://ip/ch5-vue-ts-template/assets/vite.png 94 | icons: [ 95 | { 96 | src: './assets/vite.png', 97 | sizes: '800x800', 98 | type: 'image/png', 99 | } 100 | ], 101 | // There should be at least two screenshots - one wide (landscape) and one narrow (portrait). These cannot be SVG 102 | // The src property is relative to the index.html path at runtime. 103 | // Since the VitePWA `base` property is set to /ch5-vue-ts-template/ 104 | // the path will resolve to https://ip/ch5-vue-ts-template/assets/screenshot_something.png 105 | screenshots: [ 106 | { 107 | src: './assets/screenshot_wide.jpg', 108 | sizes: '1024x593', 109 | form_factor: 'wide', 110 | type: 'image/jpeg' 111 | }, 112 | { 113 | src: './assets/screenshot_narrow.jpg', 114 | sizes: '540x720', 115 | form_factor: 'narrow', 116 | type: 'image/jpeg' 117 | } 118 | ] 119 | }, 120 | }), 121 | ], 122 | base: './', 123 | build: { 124 | sourcemap: isDevelopment, 125 | }, 126 | }); 127 | }; --------------------------------------------------------------------------------