From c1dda4bacca2a9525767dc8df411cc7b95d3be96 Mon Sep 17 00:00:00 2001 From: Rawrington Date: Tue, 10 Sep 2019 00:22:09 +0100 Subject: [PATCH] Uploaded missing file, small refactoring, OverlayPlugin support (hopefully) --- src/ACTListener.js | 43 ++++++++++++ src/ACTWebsocket.js | 30 --------- src/Action.js | 27 +++++--- src/App.js | 156 +++++++++++++++++++++---------------------- src/Config.js | 0 src/Rotation.js | 33 +++++++++ src/css/Action.css | 10 +++ src/css/App.css | 17 +++++ src/css/Rotation.css | 32 +++++++++ src/index.js | 15 ++--- src/serviceWorker.js | 135 ------------------------------------- 11 files changed, 233 insertions(+), 265 deletions(-) create mode 100644 src/ACTListener.js delete mode 100644 src/ACTWebsocket.js create mode 100644 src/Config.js create mode 100644 src/Rotation.js create mode 100644 src/css/Rotation.css delete mode 100644 src/serviceWorker.js diff --git a/src/ACTListener.js b/src/ACTListener.js new file mode 100644 index 0000000..de89d63 --- /dev/null +++ b/src/ACTListener.js @@ -0,0 +1,43 @@ +const getHost = () => /HOST_PORT=(wss?:\/\/.+)/.exec(window.location.search) + +export default function listenToACT(callback) { + if (!getHost()) return listenOverlayPlugin(callback) + return listenActWebSocket(callback) +} + +function listenActWebSocket(callback) { + const url = new URLSearchParams(window.location.search) + const wsUri = `${url.get("HOST_PORT")}BeforeLogLineRead` || undefined + const ws = new WebSocket(wsUri) + ws.onerror = () => ws.close() + ws.onclose = () => + setTimeout(() => { + listenActWebSocket(callback) + }, 1000) + ws.onmessage = function(e, m) { + if (e.data === ".") return ws.send(".") + + const obj = JSON.parse(e.data) + if (obj.msgtype === "SendCharName") { + return callback(obj.msg) + } else if (obj.msgtype === "Chat") { + return callback(...obj.msg.split("|")) + } + } + + return () => { + ws.close() + } +} + +function listenOverlayPlugin(callback) { + const listener = e => { + callback(...e.detail) + } + + document.addEventListener("onLogLine", listener) + + return () => { + document.removeEventListener("onLogLine", listener) + } +} diff --git a/src/ACTWebsocket.js b/src/ACTWebsocket.js deleted file mode 100644 index d4c393e..0000000 --- a/src/ACTWebsocket.js +++ /dev/null @@ -1,30 +0,0 @@ -const handleCodes = new Set([ - '00', - '01', - '02', - '21', - '22', - '33' -]) - -export default function listenActWebSocket( callback ) { - const url = new URLSearchParams(window.location.search) - const wsUri = `${url.get("HOST_PORT")}BeforeLogLineRead` || undefined - const ws = new WebSocket(wsUri) - ws.onerror = () => ws.close() - ws.onclose = () => setTimeout(() => { listenActWebSocket( callback ) }, 1000) - ws.onmessage = function(e, m) { - if (e.data === ".") return ws.send(".") //PING - - const obj = JSON.parse(e.data); - if (obj.msgtype === "SendCharName") { - return callback(obj.msg, null) - } else if (obj.msgtype === "Chat") { - const code = obj.msg.substring(0, 2) //first 2 numbers POG - - if (handleCodes.has(code)) return callback(obj.msg, code) //NetworkAbility or NetworkAoeAbility - } - } - - return ws -} diff --git a/src/Action.js b/src/Action.js index d24d979..54a4a4a 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,10 +1,10 @@ -import React from 'react' -import './css/Action.css' +import React from "react" +import "./css/Action.css" const gcdOverrides = new Set([ 15997, //standard step 15998, //technical step - 15999, + 15999, 16000, 16001, 16002, //step actions @@ -28,13 +28,13 @@ const ogcdOverrides = new Set([ export default function Action({ actionId, additionalClasses }) { const [apiData, setApiData] = React.useState() - + React.useEffect(() => { let current = true void (async () => { - const data = await ( - await fetch(`https://xivapi.com/Action/${actionId}`, { mode: 'cors' }) - ).json() + const data = await (await fetch(`https://xivapi.com/Action/${actionId}`, { + mode: "cors" + })).json() if (current) { setApiData(data) } @@ -44,16 +44,21 @@ export default function Action({ actionId, additionalClasses }) { current = false } }, [actionId]) - + if (apiData === undefined || !apiData.Icon) { return null } - + return ( {apiData.Name ) } diff --git a/src/App.js b/src/App.js index a8a8d8a..9bc1166 100644 --- a/src/App.js +++ b/src/App.js @@ -1,100 +1,93 @@ -import React from 'react' -import listenActWebSocket from './ACTWebsocket' -import './css/App.css' -import Action from './Action' -import RotationContainer from './Rotation' -import ReactDOM from 'react-dom' +import React from "react" +import listenToACT from "./ACTListener" +import "./css/App.css" +import Action from "./Action" +import RotationContainer from "./Rotation" +import ReactDOM from "react-dom" + +const handleCodes = new Set(["00", "01", "02", "21", "22", "33"]) export default function App() { - // NOTE: unlike class state, useState doesn't do object merging; instead, it directly holds values const [actionList, setActionList] = React.useState([]) const [encounterList, setEncounterList] = React.useState([]) React.useEffect(() => { - - // These values are only used internally by the handler, - // we don't need to notify React that they were updated, - // or keep their updates synchronized with actionList updates. - // - // This means we don't have to keep them in State (or Reducer)! let selfId - let lastTimestamp = '' + let lastTimestamp = "" let lastAction = -1 - let currentZone = 'Unknown' + let currentZone = "Unknown" - // we need keys to persist for each push, even if we shorten the array later, - // so we store the key with the action; can't just use array index due to CSS let lastKey = 1 - // listenActWebSocket should be changed to return the websocket, - // and this effect should return a function that disconnects the websocket - // - // like "return () => { ws.close() }" - let ws = listenActWebSocket((data, code) => { - const openNewEncounter = (timestamp) => { + let closeFn = listenToACT((...logSplit) => { + const openNewEncounter = () => { setEncounterList(encounterList => { - if(encounterList[0] && encounterList[0].rotation && encounterList[0].rotation.length <= 0) { + if ( + encounterList[0] && + encounterList[0].rotation && + encounterList[0].rotation.length <= 0 + ) { encounterList.shift() } - + encounterList.unshift({ name: currentZone, rotation: [] }) - - return encounterList.slice(0,3) + + return encounterList.slice(0, 3) }) } - - if (data.charID) { - selfId = data.charID + + if (logSplit.length === 1 && logSplit[0].charID) { + selfId = logSplit[0].charID openNewEncounter() return } - - switch(code) { - case '00': - const [, , refCode, , message] = data.split('|') - if(refCode === '0038' && message === 'end') openNewEncounter() + + const [ + logCode, + logTimestamp, + logParameter1, + logParameter2, + logParameter3 + ] = logSplit + + if (!handleCodes.has(logCode)) return + + switch (logCode) { + case "00": + if (logParameter1 === "0038" && logParameter3 === "end") + openNewEncounter() return - case '01': - const [, , , zoneName] = data.split('|') - currentZone = zoneName + case "01": + currentZone = logParameter2 return - case '02': - const [, , logCharIdHex] = data.split('|') - selfId = parseInt(logCharIdHex, 16) + case "02": + selfId = parseInt(logParameter1, 16) openNewEncounter() return - case '33': - const [, , , controlCode] = data.split('|') - if(controlCode === '40000012' || controlCode === '40000010') openNewEncounter() + case "33": + if (logParameter2 === "40000012" || logParameter2 === "40000010") + openNewEncounter() return default: break } - - //if it's not any of these it must be Network(AOE)Ability, the bulk of what we want to handle if (selfId === undefined) return - const [, logTimestamp, logCharIdHex, , logActionIdHex] = data.split('|') + if (parseInt(logParameter1, 16) !== selfId) return - // microoptimization: since selfId updates way less often, - // save selfId as data.charID.toString(16), that way you don't need to - // parse logCharIdHex every time - if (parseInt(logCharIdHex, 16) !== selfId) return - - // we do a mathematical comparison with action though so can't optimize this away - const action = parseInt(logActionIdHex, 16) + const action = parseInt(logParameter3, 16) if ( action <= 8 || (logTimestamp === lastTimestamp && action === lastAction) ) return - - if((Date.now() - Date.parse(lastTimestamp)) > 120000) openNewEncounter()//last action > 120s ago + + if (Date.now() - Date.parse(lastTimestamp) > 120000) openNewEncounter() //last action > 120s ago lastTimestamp = logTimestamp lastAction = action @@ -102,48 +95,55 @@ export default function App() { const key = (lastKey % 256) + 1 lastKey = key + // This is pretty silly but it's the neatest way to handle the updates going + // out at the same time, without finding some way to merge the action lists.... ReactDOM.unstable_batchedUpdates(() => { setActionList(actionList => actionList.concat({ action, key })) setEncounterList(encounterList => { - if(!encounterList[0]) { + if (!encounterList[0]) { encounterList[0] = { name: currentZone, rotation: [] } } - - encounterList[0].rotation.push( action ) - + + encounterList[0].rotation.push(action) + return encounterList }) }) - // This _probably_ should be done as a separate React.useEffect instead, - // which runs as an effect whenever the value of actionList changes. - // The problem there is, it would have to detect whether the list grew - // since the last time it was called, otherwise it'd react (heh) to its own - // updates. - // - // Easier to pair it with the previous set. setTimeout(() => { setActionList(actionList => actionList.slice(1)) }, 10000) }) - - return () => { ws.close() } + + return () => { + closeFn() + } }, []) return ( -
-
- {actionList.map(({ action, key }) => ( - + <> +
+
+ {actionList.map(({ action, key }) => ( + + ))} +
+ {encounterList.map((encounter, i) => ( + ))}
- {encounterList.map((encounter, i) => ( - - ))} -
+ ) } - diff --git a/src/Config.js b/src/Config.js new file mode 100644 index 0000000..e69de29 diff --git a/src/Rotation.js b/src/Rotation.js new file mode 100644 index 0000000..9d6e7e9 --- /dev/null +++ b/src/Rotation.js @@ -0,0 +1,33 @@ +import React from "react" +import "./css/Rotation.css" +import Action from "./Action" + +export default function RotationContainer({ encounterId, name, actionList }) { + const [open, setOpen] = React.useState(false) + + return ( + <> + + + + ) +} + +function RotationContents({ expanded, actionList }) { + if (!expanded) return null + + return ( +
+ {actionList.map((action, i) => ( + + ))} +
+ ) +} diff --git a/src/css/Action.css b/src/css/Action.css index 198cfc1..e104d6f 100644 --- a/src/css/Action.css +++ b/src/css/Action.css @@ -16,6 +16,16 @@ vertical-align: top; } +.action-rotation.gcd { + margin-left: 1em; +} + +.action-rotation.gcd::before { + content: '\25B6'; /* Unicode character for right arrow */ + font-size: 13px; + color: rgba(255, 255, 255, 0.5); +} + @keyframes action-move { from { transform: translateX(calc(100vw - 3rem)); diff --git a/src/css/App.css b/src/css/App.css index c1f8e47..b67b2fd 100644 --- a/src/css/App.css +++ b/src/css/App.css @@ -1,3 +1,20 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; +} + +html { + height: 100vh; + overflow: hidden; + background-image: none; + background-repeat: no-repeat; +} + .actions { padding-top: 1rem; background: linear-gradient(180deg, diff --git a/src/css/Rotation.css b/src/css/Rotation.css new file mode 100644 index 0000000..2016f5a --- /dev/null +++ b/src/css/Rotation.css @@ -0,0 +1,32 @@ +.rotation-list { + padding: 0 18px; + background-color: rgba(80, 80, 80, 0.3); + display: block; +} + +.rotation-button { + background-color: rgba(100, 100, 100, 0.3); + color: rgba(255, 255, 255, 0.5); + padding: 0.1em; + width: 100vw; + border: none; + outline: none; + transition: 0.4s; + display: block; +} + +.rotation-button.expanded, .rotation-button:hover { + background-color: rgba(120, 120, 120, 0.3); +} + +.rotation-button:after { + content: '\25BC'; + font-size: 13px; + color: rgb(119,119,119); + float: right; + margin-left: 5px; +} + +.rotation-button.expanded:after { + content: "\25B2"; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0cd1302..b3d0515 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,5 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './css/index.css'; -import App from './App'; -import * as serviceWorker from './serviceWorker'; +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' -ReactDOM.render(, document.getElementById('root')); - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister(); +ReactDOM.render(, document.getElementById('root')) diff --git a/src/serviceWorker.js b/src/serviceWorker.js deleted file mode 100644 index f8c7e50..0000000 --- a/src/serviceWorker.js +++ /dev/null @@ -1,135 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.1/8 is considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' - ); - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - }); -} - -function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } -}