standalone.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /*
  2. The MIT License (MIT)
  3. Copyright (c) 2014-2021 Nikolai Suslov and the Krestianstvo.org project contributors. (https://github.com/NikolaySuslov/livecodingspace/blob/master/LICENSE.md)
  4. */
  5. import { createSignal, onCleanup, createState, createEffect } from "/lib/ui/solid-js/dist/solid.js";
  6. import { render, For } from "/lib/ui/solid-js/web/dist/web.js";
  7. import h from "/lib/ui/solid-js/h/dist/h.js";
  8. import { styled, css, createGlobalStyles } from "/lib/ui/solid-js/solid-styled-components/src/index.js";
  9. class Standalone {
  10. constructor(userName, worldName) {
  11. console.log("standalone app constructor");
  12. //setBasePath('/lib/ui/shoelace');
  13. //this.entry = entry;
  14. this.userName = userName;
  15. this.worldName = worldName;
  16. this.worlds = {};
  17. this.instances = {};
  18. if (!_app.isLuminary) {
  19. // this.initReflectorConnection();
  20. }
  21. this.initApp();
  22. }
  23. get containerClass() {
  24. return css({
  25. "align-items": "center",
  26. display: "flex",
  27. "justify-content": "center"
  28. })
  29. }
  30. styledDiv() {
  31. return styled("div")`
  32. color: red;
  33. font-size: 32px;
  34. padding: 5px;
  35. border: 2px solid black;
  36. background-color: white;
  37. `;
  38. }
  39. get GlobalStyles() {
  40. return createGlobalStyles`
  41. html,
  42. body {
  43. background: white;
  44. margin: 10px;
  45. min-height: 94vh;
  46. display: flex;
  47. flex-direction: column;
  48. }
  49. `;
  50. }
  51. get CardInstance() {
  52. const card = props => h("sl-card", {
  53. class: "card-overview", style: {
  54. "box-shadow": "var(--sl-shadow-medium)"
  55. }
  56. }, [
  57. h("sl-qr-code", {
  58. value: props.url, label: "Scan this code to connect!", size: "150", style: {
  59. display: "flex",
  60. "justify-content": "center",
  61. "align-items": "center"
  62. }
  63. }),
  64. h("p"),
  65. h("small", props.instanceID),
  66. h("div", { slot: "footer" }, [
  67. h("sl-button", {
  68. type: "primary", "pill": true, style: {
  69. "margin-right": "1rem"
  70. },
  71. href: props.url,
  72. target: "_blank"
  73. //onClick: () => { this.goToIndexWorld(props.url)} //href: props.url
  74. }, "Connect", [
  75. h("sl-badge", { pill: true, pulse: true, type: "warning" }, props.clients)
  76. ]),
  77. h("sl-tooltip", { content: "Settings" }, [
  78. h("sl-icon-button", { name: "gear" })]) //disabled: true
  79. ])
  80. ])
  81. return card
  82. }
  83. get Card() {
  84. let self = this;
  85. const card = props => h("sl-card", {
  86. class: "card-overview", style: {
  87. "box-shadow": "var(--sl-shadow-x-large)",
  88. "max-width": "400px"
  89. }
  90. }, [
  91. h("img", {
  92. slot: "image",
  93. src: "/defaults/worlds/concert/webimg.jpg",
  94. alt: ""
  95. }),
  96. h("strong", "THIS IS NOT A CONCERT"),
  97. h("br"),
  98. h("small", "collaborative performance"),
  99. h("div", { slot: "footer" }, [
  100. h("sl-button", {
  101. type: "primary", "pill": {}, style: {
  102. "margin-right": "1rem"
  103. },
  104. href: props.url,
  105. target: "_blank"
  106. //onClick: () => { self.goToIndexWorld(props.url) },
  107. }, "Start new"),
  108. h("div", [
  109. h("sl-tooltip", { content: "Info" }, [
  110. h("sl-icon-button", { name: "info-circle" })
  111. ]),
  112. h("sl-tooltip", { content: "Settings" }, [
  113. h("sl-icon-button", { name: "gear" })
  114. ])
  115. ])
  116. ])
  117. ])
  118. return card
  119. //return (props) => h("h1", () => props.label, props.children);
  120. }
  121. goToIndexWorld(path) {
  122. window.location.href = path;
  123. }
  124. get Label() {
  125. return (props) => h("h1", () => props.label, props.children);
  126. }
  127. initApp() {
  128. let self = this;
  129. const App = () => {
  130. const [state, setState] = createState({ instances: [] });
  131. //const [count, setCount] = createSignal(0);
  132. // createEffect(() => {
  133. // console.log("instances:", () => state.instances);
  134. // });
  135. self.initReflectorConnection(setState)
  136. return [
  137. h(self.GlobalStyles),
  138. //self.containerClass
  139. h('header', {}, [
  140. h('sl-details', {
  141. summary: "About", open: false, style: {
  142. "max-width": "800px"
  143. }
  144. }, [
  145. h("a", { class: "link-in-text", href: "https://browsersound.com/" }, "BROWSER 2021"),
  146. h("h4", ["A Festival of Web-based Music | June 11th-13th"]),
  147. h("strong", "Delia Ramos Rodríguez and Nikolay Suslov"),
  148. h("p", "Another perspective to what we normally (don't) see. The bowing after coming to the stage; the tuning of the instrument; the playing itself; the applause (?)."),
  149. h("p", "As always, but different."),
  150. h("p", "The art work is presented in the form of interactive, multi-user, collaborative p2p web application. Application can be run on any desktop or mobile Web Browser. The audience collaboratively explores the artwork inside virtual canvas space within multi-contextual / conceptual creative layers by touching the virtual objects. These layers are not visible by default. Interaction is based on applying or viewing through some sort of \"filters\" (augmenting reality in virtual reality). Several participants can personally or collaboratively explore the hidden layers, as well as experimenting with the artwork through these layers without breaking the original artwork."),
  151. h("p", "For the implementation Open Source frameworks for Web Browser were used, especially LiveCoding.space SDK. During the development of the project there was implemented multi-user synchronization support for several open source frameworks across web browsers: multi-user 2D canvas in Two.js, synchronized Transport object in Tone.js. Also backported a video synchronization solution from Croquet V onto Virtual World Framework. MediaPipe and Hand.js were used for offline extracting of the body motion from the video performance. The project can be built from the source code and run locally without the need of internet connection, as it has no dependencies on any cloud services. The source code will be available on the project site soon.")
  152. ])
  153. ]),
  154. h('main', {}, [
  155. h(self.Card, { url: "https://localhost:3007/" + self.userName + "/" + self.worldName }),
  156. h(For, { each: () => state.instances }, (instance) =>
  157. h(self.CardInstance, { url: instance.url, clients: instance.clients, instanceID: instance.instanceID })
  158. )
  159. ]),
  160. h('footer', {
  161. style: {
  162. "margin-top": "auto"
  163. }
  164. }, [
  165. h("small", { style: { color: "var(--sl-color-gray-500" } }, [
  166. "Made with ",
  167. h("a", { class: "link-in-text", href: "https://livecoding.space" }, "LiveCoding.space"), //type: "text", size:"medium",
  168. " 2021"
  169. ])
  170. ])
  171. ]
  172. };
  173. render(App, document.body); //document.getElementById("root")
  174. }
  175. initReflectorConnection(fun) {
  176. let self = this;
  177. this.options = {
  178. query: 'pathname=' + window.location.pathname.slice(1,
  179. window.location.pathname.lastIndexOf("/")),
  180. secure: window.location.protocol === "https:",
  181. reconnection: false,
  182. path: '',
  183. transports: ['websocket']
  184. }
  185. //window.location.host
  186. var socket = io.connect(window._app.reflectorHost, this.options);
  187. const parse = (msg) => {
  188. //self.setCount(msg)
  189. //fun("instance", msg)
  190. this.parseOnlineData(fun, msg)
  191. }
  192. socket.on('getWebAppUpdate', msg => parse.call(this, msg));
  193. socket.on("connect", function () {
  194. let noty = new Noty({
  195. text: 'Connected to Reflector!',
  196. timeout: 2000,
  197. theme: 'mint',
  198. layout: 'bottomRight',
  199. type: 'success'
  200. });
  201. noty.show();
  202. })
  203. socket.on('connect_error', function (err) {
  204. console.log(err);
  205. var errDiv = document.createElement("div");
  206. errDiv.innerHTML = "<div class='vwf-err' style='z-index: 10; position: absolute; top: 80px; right: 50px'>Connection error to Reflector!" + err + "</div>";
  207. document.querySelector('body').appendChild(errDiv);
  208. let noty = new Noty({
  209. text: 'Connection error to Reflector! ' + err,
  210. theme: 'mint',
  211. layout: 'bottomRight',
  212. type: 'error'
  213. });
  214. noty.show();
  215. });
  216. }
  217. parseOnlineData(fun, data) {
  218. let self = this;
  219. let parcedData = _app.parseAppInstancesData(data);
  220. let worldInfo = parcedData[self.worldName];
  221. if (worldInfo) {
  222. const instances = Object.entries(worldInfo).map(el => {
  223. let url = el[0].split('/');
  224. return {
  225. clients: el[1].clients,
  226. user: el[1].user,
  227. url: window.location.href + el[1].user + '/' + url[1] + '?k=' + url[3],
  228. instanceID: url[3]
  229. }
  230. });
  231. fun("instances", instances);
  232. } else {
  233. fun("instances", []);
  234. }
  235. //this.initView(Object.values(worldInfo))
  236. }
  237. }
  238. export { Standalone }