Stuff.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import {CentreBoi, LeftBoi, RightBoi} from "./components/Bois";
|
||||
import {StatusContextProvider} from "./components/Contexts";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<StatusContextProvider>
|
||||
<LeftBoi/>
|
||||
<CentreBoi/>
|
||||
<RightBoi/>
|
||||
</StatusContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,44 @@
|
||||
.Boi {
|
||||
position: fixed;
|
||||
z-index: 99999999;
|
||||
color: #FFF;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding: 0.25em 0.5ch;
|
||||
font-size: 3vmax;
|
||||
line-height: 1.33em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.Boi-left {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.Boi-centre {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 5vmax;
|
||||
}
|
||||
|
||||
.Boi-centre-warning {
|
||||
margin-top: 1em;
|
||||
font-weight: 800;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.Boi-centre-info {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.Boi-centre-info td:first-child {
|
||||
padding-right: 1ch;
|
||||
}
|
||||
|
||||
.Boi-right {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 2vmax;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import React, {useContext, useEffect, useMemo, useState} from 'react';
|
||||
|
||||
import "./Bois.css";
|
||||
import {StatusContext} from "./Contexts";
|
||||
import {CalorieScore, CpmScore, DistanceScore, RpmScore, Timer} from "./Score";
|
||||
import calculateDiff from "../helpers/diff";
|
||||
import useKey from "../hooks/useKey";
|
||||
import {COLOR_VERY_BAD} from "../helpers/color";
|
||||
import {Milestones} from "./Milestones";
|
||||
|
||||
const Boi = ({type, children}) => {
|
||||
return (
|
||||
<div className={`Boi Boi-${type}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export const LeftBoi = () => {
|
||||
const {prevLongDiff, workoutStatus, program} = useContext(StatusContext);
|
||||
if (workoutStatus === null || program === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {minutes, seconds, calories, distance, rpm} = workoutStatus;
|
||||
const diff = calculateDiff(program, minutes, seconds, calories);
|
||||
const cpm = calories / (minutes + (seconds / 60));
|
||||
|
||||
return (
|
||||
<Boi type="left">
|
||||
<Timer minutes={minutes} seconds={seconds}/>
|
||||
<CalorieScore calories={calories} diff={diff} prevDiff={prevLongDiff}/>
|
||||
<DistanceScore distance={distance}/>
|
||||
<RpmScore rpm={rpm}/>
|
||||
<CpmScore cpm={cpm}/>
|
||||
</Boi>
|
||||
);
|
||||
};
|
||||
|
||||
export const CentreBoi = () => {
|
||||
const {
|
||||
state, program, setProgram, bike, setBike, bikes, programs,
|
||||
start, pause, stop, create, workoutStatus,
|
||||
} = useContext(StatusContext);
|
||||
const [options, setOptions] = useState(null);
|
||||
const [current, setCurrent] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (bikes === null && programs === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOptions(state === "bike" ? bikes : programs);
|
||||
}, [state, bike, program, bikes, programs]);
|
||||
|
||||
useKey("-", () => {
|
||||
if (options === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current > 0) {
|
||||
setCurrent(current - 1);
|
||||
} else {
|
||||
setCurrent(options.length - 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
useKey("+", () => {
|
||||
if (options === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current < options.length - 1) {
|
||||
setCurrent(current + 1);
|
||||
} else {
|
||||
setCurrent(0);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
useKey("Enter", () => {
|
||||
if (state === "bike") {
|
||||
setBike(options[current]);
|
||||
|
||||
return true;
|
||||
} else if (state === "program") {
|
||||
setProgram(options[current]);
|
||||
create(options[current]);
|
||||
|
||||
return true;
|
||||
} else if (state === "connected") {
|
||||
start();
|
||||
|
||||
return true;
|
||||
} else if (state === "started") {
|
||||
pause();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
useKey("Escape", () => {
|
||||
if (state === "connected") {
|
||||
stop();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (state === "started" && workoutStatus !== null) {
|
||||
const {minutes, seconds, calories} = workoutStatus;
|
||||
const diff = calculateDiff(program, minutes, seconds, calories);
|
||||
|
||||
if (diff < 0) {
|
||||
return (
|
||||
<Boi type="centre">
|
||||
<span className="Boi-centre-warning" style={{color: COLOR_VERY_BAD}}>
|
||||
{diff}
|
||||
</span>
|
||||
</Boi>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (bikes === null || programs === null) {
|
||||
return <Boi type="centre">Laster inn...</Boi>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Boi type="centre">
|
||||
{state === "stopped" && (
|
||||
<div className="Boi-centre-info">Inaktiv</div>
|
||||
)}
|
||||
{state === "bike" && (
|
||||
<div className="Boi-centre-info">
|
||||
Velg sykkel (+/-):
|
||||
<br/>
|
||||
<b>{options[current].name}</b>
|
||||
</div>
|
||||
)}
|
||||
{state === "program" && (
|
||||
<div className="Boi-centre-info">
|
||||
Velg program (+/-):
|
||||
<br/>
|
||||
<b>{options[current].name}</b>
|
||||
</div>
|
||||
)}
|
||||
{state === "connected" && (
|
||||
<table className="Boi-centre-info">
|
||||
<tr>
|
||||
<td>Enter:</td>
|
||||
<td><b>Start</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Esc:</td>
|
||||
<td><b>Avslutt</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
)}
|
||||
{state === "unknown" && (
|
||||
<div className="Boi-centre-info">Noe gikk galt</div>
|
||||
)}
|
||||
</Boi>
|
||||
);
|
||||
};
|
||||
|
||||
export const RightBoi = () => {
|
||||
const {milestones} = useContext(StatusContext);
|
||||
|
||||
if ((milestones || []).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Boi type="right">
|
||||
<Milestones milestones={milestones.sort((i, j) => j.minutes - i.minutes)}/>
|
||||
</Boi>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import React, {createContext, useEffect, useState} from "react";
|
||||
import {
|
||||
connectWorkout,
|
||||
createNewWorkout,
|
||||
fetchActiveWorkouts, fetchBikes, fetchPrograms,
|
||||
openWebsocket, pauseWorkout,
|
||||
startWorkout,
|
||||
stopWorkout
|
||||
} from "../hooks/net";
|
||||
import useMilestones from "../hooks/milestones";
|
||||
|
||||
export const StatusContext = createContext({
|
||||
bike: null,
|
||||
program: null,
|
||||
bikes: null,
|
||||
programs: null,
|
||||
workout: null,
|
||||
workoutStatus: null,
|
||||
setBike: null,
|
||||
setProgram: null,
|
||||
state: null,
|
||||
milestones: [],
|
||||
create: () => null,
|
||||
resume: () => null,
|
||||
start: () => null,
|
||||
pause: () => null,
|
||||
stop: () => null,
|
||||
});
|
||||
|
||||
export const StatusContextProvider = ({children}) => {
|
||||
const [bike, setBike] = useState(null);
|
||||
const [program, setProgram] = useState(null);
|
||||
const [bikes, setBikes] = useState(null);
|
||||
const [programs, setPrograms] = useState(null);
|
||||
const [workout, setWorkout] = useState(null);
|
||||
const [workoutStatus, setWorkoutStatus] = useState(null);
|
||||
const [state, setState] = useState("offline");
|
||||
const {milestones, prevDiff, prevLongDiff, msDispatch} = useMilestones();
|
||||
const [socket, setSocket] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (programs === null) {
|
||||
fetchPrograms().then(newPrograms => setPrograms(newPrograms));
|
||||
}
|
||||
}, [programs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bikes === null) {
|
||||
fetchBikes().then(newBikes => {
|
||||
setBikes(newBikes);
|
||||
|
||||
if (newBikes.length === 1) {
|
||||
setBike(newBikes[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [bikes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bike === null && program === null && workout === null) {
|
||||
resume();
|
||||
}
|
||||
}, [bike, program, workout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bikes === null || programs === null) {
|
||||
setState("loading");
|
||||
} else if (workout !== null) {
|
||||
setState(workout.state || "unknown");
|
||||
} else if (bike === null) {
|
||||
setState("bike");
|
||||
} else if (program === null) {
|
||||
setState("program");
|
||||
}
|
||||
}, [bike, program, workout, bikes, programs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workout === null) {
|
||||
socket.close();
|
||||
setSocket(null);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.onmessage = ({data}) => {
|
||||
onSocketMessage(workout, program, JSON.parse(data));
|
||||
};
|
||||
|
||||
return () => {
|
||||
socket.onmessage = null;
|
||||
}
|
||||
}, [socket, workout, program]);
|
||||
|
||||
function onSocketMessage(myWorkout, program, body) {
|
||||
if (typeof body.workout !== "undefined") {
|
||||
setWorkout({...body.workout, ...myWorkout});
|
||||
}
|
||||
|
||||
if (typeof body.workoutStatusBackfill !== "undefined") {
|
||||
body.workoutStatusBackfill.forEach(wsbf => {
|
||||
setWorkoutStatus(wsbf);
|
||||
msDispatch({type: "measure", payload: {...wsbf, program}});
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof body.workoutStatus !== "undefined") {
|
||||
setWorkoutStatus(body.workoutStatus);
|
||||
msDispatch({type: "measure", payload: {...body.workoutStatus, program}});
|
||||
}
|
||||
}
|
||||
|
||||
async function create(myProgram = null) {
|
||||
setWorkoutStatus(null);
|
||||
|
||||
let newWorkout = await createNewWorkout(bike, myProgram || program);
|
||||
|
||||
setWorkout(newWorkout);
|
||||
setWorkout(await connectWorkout(newWorkout));
|
||||
|
||||
setSocket(openWebsocket(newWorkout));
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
const activeWorkouts = await fetchActiveWorkouts();
|
||||
const count = activeWorkouts.length;
|
||||
const last = count > 0 ? activeWorkouts[count - 1] : null;
|
||||
|
||||
if (last !== null) {
|
||||
setProgram(last.program);
|
||||
setWorkout(last);
|
||||
setBike(last.bike);
|
||||
|
||||
setSocket(openWebsocket(last));
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
setWorkout(await startWorkout(workout));
|
||||
}
|
||||
|
||||
async function pause() {
|
||||
setWorkout(await pauseWorkout(workout));
|
||||
}
|
||||
|
||||
async function stop() {
|
||||
await stopWorkout(workout);
|
||||
|
||||
setBike(null);
|
||||
setProgram(null);
|
||||
setWorkout(null);
|
||||
msDispatch({type: "clear"});
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusContext.Provider value={{
|
||||
bike, program, workout, workoutStatus, state, milestones,
|
||||
setBike, setProgram,
|
||||
create, resume, start, pause, stop,
|
||||
programs, bikes,
|
||||
}}>
|
||||
{children}
|
||||
</StatusContext.Provider>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
.Milestones {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.Milestone-minutes,
|
||||
.Milestone-calories,
|
||||
.Milestone-diff {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.Milestone-calories {
|
||||
min-width: 4ch;
|
||||
}
|
||||
|
||||
.Milestone-diff {
|
||||
min-width: 3ch;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import {diffString} from "../helpers/diff";
|
||||
|
||||
import "./Milestones.css";
|
||||
import {colorByDiff} from "../helpers/color";
|
||||
|
||||
export const Milestones = ({milestones}) => (
|
||||
<table className="Milestone">
|
||||
{milestones.map(milestone => <Milestone {...milestone} />)}
|
||||
</table>
|
||||
);
|
||||
|
||||
const Milestone = ({minutes, calories, diff, prevDiff}) => (
|
||||
<tr style={{color: colorByDiff(diff, prevDiff)}}>
|
||||
<td className="Milestone-minutes">{minutes}</td>
|
||||
<td className="Milestone-calories">{calories}</td>
|
||||
<td className="Milestone-diff">{minutes % 5 === 0 ? diffString(diff) : ""}</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
.Score-value {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.Score-suffix {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.Timer-number {
|
||||
font-size: 125%;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import {COLOR_NEUTRAL, colorByDiff} from "../helpers/color";
|
||||
|
||||
import "./Score.css";
|
||||
|
||||
const Score = ({value, suffix = null, color = COLOR_NEUTRAL}) => (
|
||||
<div className="Score">
|
||||
<span className="Score-value" style={{color}}>{value}</span>
|
||||
{suffix !== null && (
|
||||
<>
|
||||
|
||||
<span className="Score-suffix">{suffix}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Timer = ({minutes, seconds}) => {
|
||||
function pad(number) {
|
||||
return number >= 10 ? `${number}` : `0${number}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Timer">
|
||||
<span className="Timer-number">{pad(minutes)}</span>
|
||||
{" : "}
|
||||
<span className="Timer-number">{pad(seconds)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalorieScore = ({calories, diff, prevDiff}) => (
|
||||
<Score value={calories} color={colorByDiff(diff, prevDiff)} suffix="kal"/>
|
||||
);
|
||||
|
||||
export const DistanceScore = ({distance}) => <Score value={distance.toFixed(1)} suffix="km"/>;
|
||||
|
||||
export const RpmScore = ({rpm}) => <Score value={rpm} suffix="rpm"/>;
|
||||
|
||||
export const CpmScore = ({cpm}) => <Score value={cpm.toFixed(1)} suffix="kpm"/>;
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
export const COLOR_VERY_BAD = "#F66";
|
||||
export const COLOR_BAD = "#FBB";
|
||||
export const COLOR_NEUTRAL = "#FFF";
|
||||
export const COLOR_GOOD = "#BFB";
|
||||
export const COLOR_VERY_GOOD = "#6F6";
|
||||
|
||||
export const colorByDiff = (diff, prevDiff) => {
|
||||
if (diff < 0) {
|
||||
return diff < prevDiff ? COLOR_VERY_BAD : COLOR_BAD;
|
||||
} else {
|
||||
return diff > prevDiff ? COLOR_VERY_GOOD : COLOR_GOOD;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export function diffString(diff) {
|
||||
return diff < 0 ? `${diff}` : `+${diff}`
|
||||
}
|
||||
|
||||
export default function calculateDiff(program, minutes, seconds, calories) {
|
||||
const {warmupMin, warmupCpm, cpm} = program;
|
||||
|
||||
let preWarmup = 0;
|
||||
if (warmupMin > 0) {
|
||||
// Pre-warmup
|
||||
const warmedUpMinutes = Math.min(minutes, warmupMin);
|
||||
const warmedUpSeconds = minutes >= warmupMin ? 0 : seconds;
|
||||
preWarmup = Math.round((warmupCpm * (warmedUpMinutes + (warmedUpSeconds / 60))));
|
||||
}
|
||||
|
||||
// Post-warmup
|
||||
const trainedMinutes = Math.max(0, minutes - warmupMin);
|
||||
const trainedSeconds = minutes >= warmupMin ? seconds : 0;
|
||||
const postWarmup = Math.round((cpm * (trainedMinutes + (trainedSeconds / 60))));
|
||||
|
||||
// Sum
|
||||
const target = Math.round(preWarmup + postWarmup);
|
||||
|
||||
return calories - target;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {useReducer} from "react";
|
||||
import calculateDiff from "../helpers/diff";
|
||||
|
||||
const DEFAULT = {
|
||||
milestones: [],
|
||||
prevDiff: 0,
|
||||
prevLongDiff: 0,
|
||||
};
|
||||
|
||||
function reducer(state, {type, payload}) {
|
||||
switch (type) {
|
||||
case "measure":
|
||||
let {prevDiff, prevLongDiff, milestones} = state;
|
||||
const {minutes, seconds, calories, program} = payload;
|
||||
|
||||
if (minutes === 0 || seconds !== 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const isFive = minutes % 5 === 0;
|
||||
const diff = calculateDiff(program, minutes, seconds, calories);
|
||||
let newMilestones = milestones.filter(m => m.minutes !== minutes);
|
||||
newMilestones.push({
|
||||
minutes, seconds, calories, diff,
|
||||
prevDiff: isFive ? prevDiff : prevLongDiff
|
||||
});
|
||||
|
||||
if (isFive) {
|
||||
newMilestones = newMilestones.filter(m => m.minutes % 5 === 0);
|
||||
prevLongDiff = diff;
|
||||
}
|
||||
prevDiff = diff;
|
||||
|
||||
return {prevLongDiff, prevDiff, milestones: newMilestones};
|
||||
case "clear":
|
||||
return {...DEFAULT};
|
||||
default:
|
||||
throw new Error(`Unknown type: ${type}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default function useMilestones() {
|
||||
const [state, msDispatch] = useReducer(reducer, {...DEFAULT});
|
||||
const {prevLongDiff, prevDiff, milestones} = state || {...DEFAULT};
|
||||
|
||||
return {milestones, prevDiff, prevLongDiff, msDispatch};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export async function fetchBikes() {
|
||||
return await get("/bike");
|
||||
}
|
||||
|
||||
export async function fetchPrograms() {
|
||||
return await get("/program");
|
||||
}
|
||||
|
||||
export async function fetchActiveWorkouts() {
|
||||
return await get("/workout?active=true");
|
||||
}
|
||||
|
||||
export async function createNewWorkout(bike, program) {
|
||||
const bikeId = bike.id;
|
||||
const programId = program.id;
|
||||
|
||||
return await post("/workout", {bikeId, programId});
|
||||
}
|
||||
|
||||
export async function connectWorkout(workout) {
|
||||
return await post(`/workout/${workout.id}/connect`);
|
||||
}
|
||||
|
||||
export async function stopWorkout(workout) {
|
||||
return await post(`/workout/${workout.id}/stop`);
|
||||
}
|
||||
|
||||
export async function startWorkout(workout) {
|
||||
return await post(`/workout/${workout.id}/start`);
|
||||
}
|
||||
|
||||
export async function pauseWorkout(workout) {
|
||||
return await post(`/workout/${workout.id}/pause`);
|
||||
}
|
||||
|
||||
export function openWebsocket(workout) {
|
||||
return new WebSocket(url(`/workout/${workout.id}/subscribe`, "ws"));
|
||||
}
|
||||
|
||||
function get(path) {
|
||||
return fetch(url(path), {
|
||||
method: "GET",
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
function post(path, data = {}) {
|
||||
return fetch(url(path), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
function url(path, prefix = "http") {
|
||||
return `${prefix}://127.0.0.1:9999/api${path}`;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {useEffect} from "react";
|
||||
|
||||
export default function useKey(keyName, callback) {
|
||||
useEffect(() => {
|
||||
const handler = function (ev) {
|
||||
if (ev.key === keyName) {
|
||||
if (callback()) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handler);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
ReactDOM.render(<App />, 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();
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,135 @@
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user