Skip to content

Commit

Permalink
Merge pull request #59 from lonevvolf/pwa-update
Browse files Browse the repository at this point in the history
PWA Updates
  • Loading branch information
cracrayol authored Aug 29, 2024
2 parents 15342fc + 50a0a57 commit a8620ca
Show file tree
Hide file tree
Showing 16 changed files with 6,048 additions and 1,505 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ project-aon
# Tests result
tests_log.txt
ctrf-report.json
www/sw.js
www/sw.js.LICENSE.txt
www/sw.js.map
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ npm run lint

A "guide" to develop new books can be found at [doc/README-developing.md](doc/README-developing.md)

### Progressive Web App (PWA) Development

Kai Chronicles has been extended to act as a PWA. It can be added to the home screen of a mobile phone, or as an app in Windows. The app will continue to function offline due to caching of assets, though the books are not predownloaded, so starting a New Game will fail unless you have already started the book before.

Of course, asset caching complicates development, not least because webpack-dev-server doesn't play nice with the precaching mechanism (https://github.com/GoogleChrome/workbox/issues/1790). Therefore, the caching and PWA functionality should be disabled while developing.

If you want to specifically develop PWA features:
1) Comment out:
`if (environment !== EnvironmentType.Development)`
in app.ts
2) Make your changes to the app
3) Run:
`npm run predist`
to regenerate the service worker code
4) Run a dedicated http server other than webpack-dev-server:
`npx http-server ./www`
5) Open the app at:
`http://localhost:8080`

After finishing, be sure to reverse the change from step 1 before committing changes. Hopefully this process can be improved in the future, but there shouldn't be frequent changes to the PWA functionality.

### Tests

Tests are run with Selenium Web Driver and Jest. Currently tests will run only with Chrome, and Selenium will need a "browser driver". See https://www.selenium.dev/documentation/en/webdriver/driver_requirements for installation instructions. Tests are located at src/ts/tests. Be sure Typescript for node.js is compiled before running tests:
Expand Down
7,289 changes: 5,806 additions & 1,483 deletions package-lock.json

Large diffs are not rendered by default.

31 changes: 17 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,35 @@
"devDependencies": {
"@types/bootstrap": "^3.3.42",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.5.0",
"@types/jest": "^29.5.12",
"@types/jquery": "^3.5.14",
"@types/selenium-webdriver": "4.1.22",
"@types/toastr": "^2.1.39",
"@types/webpack-env": "^1.16.4",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"eslint": "^8.15.0",
"@types/toastr": "^2.1.43",
"@types/webpack-env": "^1.18.5",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"eslint": "^9.9.1",
"fs-extra": "^10.1.0",
"jest": "^27.5.1",
"jest": "^29.7.0",
"jest-ctrf-json-reporter": "^0.0.9",
"jest-environment-jsdom": "^29.7.0",
"jquery": "^3.6.0",
"klaw-sync": "^6.0.0",
"preprocess": "^3.2.0",
"selenium-webdriver": "^4.1.2",
"simple-git": "^3.7.1",
"terser-webpack-plugin": "^5.3.1",
"ts-jest": "^27.1.3",
"terser-webpack-plugin": "^5.3.10",
"ts-jest": "^29.2.5",
"ts-loader": "^9.3.0",
"typescript": "^4.6.4",
"webpack": "^5.72.1",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.0"
"typescript": "^5.5.4",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"workbox-webpack-plugin": "^7.1.0",
"workbox-window": "^7.1.0"
},
"scripts": {
"predownloaddata": "tsc src/ts/scripts/downloadProjectAonData.ts --outdir src/js",
"predownloaddata": "tsc src/ts/scripts/downloadProjectAonData.ts --lib es5 --target es5 --outdir src/js",
"downloaddata": "node src/js/scripts/downloadProjectAonData.js",
"serve": "webpack serve --config webpack.config.js",
"lint": "eslint -c .eslintrc.js --ext .ts src",
Expand Down
9 changes: 8 additions & 1 deletion src/ts/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { views, state, template, routing, declareCommonHelpers, mechanicsEngine } from ".";
import { views, state, template, routing, declareCommonHelpers, mechanicsEngine, pwa } from ".";

/** Execution enviroment type */
export enum EnvironmentType {
Expand Down Expand Up @@ -30,6 +30,13 @@ export class App {
/** Web application setup */
public static run(environment: string) {

// PWA app setup (ServiceWorker)
// Service worker is disabled in webpack-dev-server: https://github.com/GoogleChrome/workbox/issues/1790
if (environment !== EnvironmentType.Development)
{
pwa.registerServiceWorker();
}

// Declare helper functions in common.ts
declareCommonHelpers();

Expand Down
3 changes: 2 additions & 1 deletion src/ts/controller/mechanics/specialSections/book3sect88.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const book3sect88 = {
return Combat.prototype.nextTurnAsync.call(this)
.then((turn: CombatTurn) => {
// Check the bite:
if ( turn.loneWolf > 0 && turn.loneWolf !== COMBATTABLE_DEATH ) {
if ( (typeof turn.loneWolf === "number" && turn.loneWolf > 0)
|| ( typeof turn.loneWolf === "string" && turn.loneWolf !== COMBATTABLE_DEATH )) {
const biteRandomValue = randomTable.getRandomValue();
turn.playerLossText = "(" + turn.playerLossText + ")";
turn.playerLossText += `Random: ${biteRandomValue}`;
Expand Down
1 change: 1 addition & 0 deletions src/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from "./views";
export * from "./app";
export * from "./routing";
export * from "./common";
export * from "./pwa";

export * from "./controller/mainMenuController";
export * from "./controller/privacyController";
Expand Down
82 changes: 82 additions & 0 deletions src/ts/pwa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Workbox } from "workbox-window";

export class pwa {
public static isOnline = true;
private static wb: Workbox = null;

private static promptForUpdate() : void {
toastr.info("A new version of the app is available. Click here to update.",
"App Update Available",
{
timeOut: 0,
extendedTimeOut: 0,
onclick : () =>
{
pwa.messageSkipWaiting();
},
});
}

public static messageSkipWaiting() {
this.wb.messageSkipWaiting();
}

public static showConnectivityStatus() : void {
window.addEventListener("online", () => {
toastr.info("Your Internet connection was restored.");
pwa.isOnline = true;
});

window.addEventListener("offline", () => {
toastr.warning("Your Internet connection was lost.");
pwa.isOnline = false;
});
}

public static registerServiceWorker() {
if ("serviceWorker" in window.navigator) {
try {
this.wb = new Workbox("/sw.js");

const showSkipWaitingPrompt = async (event) => {
// Assuming the user accepted the update, set up a listener
// that will reload the page as soon as the previously waiting
// service worker has taken control.
this.wb.addEventListener('controlling', () => {
// At this point, reloading will ensure that the current
// tab is loaded under the control of the new service worker.
// Depending on your web app, you may want to auto-save or
// persist transient state before triggering the reload.
window.location.reload();
});

// When `event.wasWaitingBeforeRegister` is true, a previously
// updated service worker is still waiting.
// You may want to customize the UI prompt accordingly.

// This code assumes your app has a promptForUpdate() method,
// which returns true if the user wants to update.
// Implementing this is app-specific; some examples are:
// https://open-ui.org/components/alert.research or
// https://open-ui.org/components/toast.research
this.promptForUpdate();
};

// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
this.wb.addEventListener('waiting', (event) => {
showSkipWaitingPrompt(event);
});

window.addEventListener('load', () => {
this.showConnectivityStatus();
});

this.wb.register();

} catch (error) {
console.error(`Registration failed with ${error}`);
}
}
}
}
2 changes: 1 addition & 1 deletion src/ts/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const routing = {
template.collapseMenu();

// This will fire the onHashChange callback:
location.hash = route;
window.location.hash = route;

} catch (e) {
mechanicsEngine.debugWarning(e);
Expand Down
2 changes: 1 addition & 1 deletion src/ts/scripts/downloadProjectAonData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BookData } from "./bookData";
import simpleGit, {SimpleGit, SimpleGitProgressEvent} from 'simple-git';

/*
Dowload Project Aon book data
Download Project Aon book data
Command line parameters:
1) Book index (1-based). If it does not exists, the "www/data/projectAon" will be re-created and all books will be downloaded
*/
Expand Down
63 changes: 63 additions & 0 deletions src/ts/sw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// <reference lib="webworker" />
declare const self: ServiceWorkerGlobalScope;

import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
import { registerRoute, Route } from 'workbox-routing';
import { NetworkFirst, CacheFirst } from 'workbox-strategies';

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: __WB_MANIFEST is a placeholder filled by workbox-webpack-plugin with the list of dependencies to be cached
precacheAndRoute(self.__WB_MANIFEST);

// A new route that matches same-origin image/stylesheet requests and handles
// them with the cache-first, falling back to network strategy:
const imageRoute = new Route(({ request }) => {
return request.destination === 'image' || request.destination === 'style'
}, new CacheFirst({
cacheName: 'imagesAndStyles'
}));

// Register the new route
registerRoute(imageRoute);

// A new route that matches .html/.xml and handles
// them with the network-first, falling back to cache strategy:
const htmlAndXmlRoute = new Route(({ request }) => {
return request.destination === 'document'
}, new NetworkFirst({
cacheName: 'htmlAndMechanics'
}));

// Register the new route
registerRoute(htmlAndXmlRoute);

// A new route that matches fonts and handles
// them with the cache-first, falling back to cache strategy:
const fontsRoute = new Route(({ request }) => {
return request.destination === 'font'
}, new CacheFirst({
cacheName: 'fonts'
}));

// Register the new route
registerRoute(fontsRoute);

// A new route that matches javascript and handles
// them with the cache-first, falling back to cache strategy:
const extJsRoute = new Route(({ request }) => {
return request.destination === 'script'
}, new CacheFirst({
cacheName: 'extJs'
}));

// Register the new route
registerRoute(extJsRoute);

addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});



16 changes: 15 additions & 1 deletion src/ts/views/newGameView.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { projectAon, translations, newGameController, state } from "..";
import { projectAon, translations, newGameController, state, pwa } from "..";

/**
* The new game view API
Expand Down Expand Up @@ -36,6 +36,20 @@ export const newGameView = {
state.manualRandomTable = ($("#newgame-randomtable").val() === "manual");
});

if (pwa.isOnline) {
$("#newgame-offline-warning").hide();
} else {
$("#newgame-offline-warning").show();
}

window.addEventListener("online", () => {
$("#newgame-offline-warning").hide();
});

window.addEventListener("offline", () => {
$("#newgame-offline-warning").show();
});

// Set the first book as selected:
newGameController.selectedBookChanged(1);
},
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"sourceMap": true,
"rootDir" : "src/ts",
"target": "es5",
"lib": ["dom"]
"lib": ["WebWorker", "es5"]
},

"include": [
Expand Down
23 changes: 22 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const path = require('path');
const TerserPlugin = require("terser-webpack-plugin");
const {InjectManifest} = require('workbox-webpack-plugin');

module.exports = {
mode: 'development',
Expand All @@ -9,6 +10,17 @@ module.exports = {
static: './www',
port: 3000,
hot: false,
client: {
overlay: {
// This is a terrible workaround for the annoying message from Workbox, but other solutions to suppress it have not yet worked
warnings: (warning) => {
if (warning.message.startsWith('InjectManifest has been called multiple times')) {
return false;
}
return true;
},
}
}
},
module: {
rules: [
Expand All @@ -31,5 +43,14 @@ module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
}
},
plugins: [
new InjectManifest({
swSrc: '/src/ts/sw.ts',
swDest: '../sw.js',
include: [
/kai\.js$/
]
}),
]
};
2 changes: 1 addition & 1 deletion www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">

<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="Kai Chronicles, a player for Lone Wolf game books">
Expand Down
4 changes: 4 additions & 0 deletions www/views/newGame.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ <h1 data-translation="newGame">New game</h1>
Start game
</button>

<br />
<div class="alert alert-danger" id="newgame-offline-warning" style="display:none">
WARNING: You are currently offline. The download of a new book will fail until your connection has been restored.
</div>
</form>
</div>

Expand Down

0 comments on commit a8620ca

Please sign in to comment.