tlater/nix-flake-rework #2
					 27 changed files with 19727 additions and 4397 deletions
				
			
		| 
						 | 
				
			
			@ -1,14 +1,17 @@
 | 
			
		|||
{
 | 
			
		||||
    "parser": "@typescript-eslint/parser",
 | 
			
		||||
    "plugins": [
 | 
			
		||||
        "@typescript-eslint"
 | 
			
		||||
    ],
 | 
			
		||||
    "env": {
 | 
			
		||||
        "es6": true,
 | 
			
		||||
        "browser": true,
 | 
			
		||||
        "jquery": true
 | 
			
		||||
    },
 | 
			
		||||
    "parserOptions": {
 | 
			
		||||
        "ecmaVersion": 6,
 | 
			
		||||
        "sourceType": "module"
 | 
			
		||||
    },
 | 
			
		||||
    "extends": "eslint:recommended",
 | 
			
		||||
    "extends": [
 | 
			
		||||
        "eslint:recommended",
 | 
			
		||||
        "plugin:@typescript-eslint/recommended"
 | 
			
		||||
    ],
 | 
			
		||||
    "rules": {
 | 
			
		||||
        "indent": [
 | 
			
		||||
            "error",
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +33,8 @@
 | 
			
		|||
            "off"
 | 
			
		||||
        ],
 | 
			
		||||
        "no-unused-vars": [
 | 
			
		||||
            "warn"
 | 
			
		||||
            "warn",
 | 
			
		||||
            { "argsIgnorePattern": "^_" }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +0,0 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    "posthtml-favicons": {
 | 
			
		||||
      outDir: "./dist/browser/",
 | 
			
		||||
      configuration: {
 | 
			
		||||
        appName: "tlater.net",
 | 
			
		||||
        appShortName: "tlater.net",
 | 
			
		||||
        appDescription: "tlater's home page",
 | 
			
		||||
        developerName: "Tristan Daniël Maat",
 | 
			
		||||
        developerURL: "https://tlater.net",
 | 
			
		||||
        dir: "auto",
 | 
			
		||||
        lang: "en-US",
 | 
			
		||||
        background: "#0f0f0f",
 | 
			
		||||
        theme_color: "#991dce",
 | 
			
		||||
        appleStatusBarStyle: "black-translucent",
 | 
			
		||||
        display: "browser",
 | 
			
		||||
        orientation: "any",
 | 
			
		||||
        start_url: "https://tlater.net",
 | 
			
		||||
        version: "1.0",
 | 
			
		||||
        icons: {
 | 
			
		||||
          favicons: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										26
									
								
								.posthtmlrc.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.posthtmlrc.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
{
 | 
			
		||||
  "plugins": {
 | 
			
		||||
    "posthtml-favicons": {
 | 
			
		||||
      "outDir": "./dist/browser/",
 | 
			
		||||
      "configuration": {
 | 
			
		||||
        "appName": "tlater.net",
 | 
			
		||||
        "appShortName": "tlater.net",
 | 
			
		||||
        "appDescription": "tlater's home page",
 | 
			
		||||
        "developerName": "Tristan Daniël Maat",
 | 
			
		||||
        "developerURL": "https://tlater.net",
 | 
			
		||||
        "dir": "auto",
 | 
			
		||||
        "lang": "en-US",
 | 
			
		||||
        "background": "#0f0f0f",
 | 
			
		||||
        "theme_color": "#991dce",
 | 
			
		||||
        "appleStatusBarStyle": "black-translucent",
 | 
			
		||||
        "display": "browser",
 | 
			
		||||
        "orientation": "any",
 | 
			
		||||
        "start_url": "https://tlater.net",
 | 
			
		||||
        "version": "1.0",
 | 
			
		||||
        "icons": {
 | 
			
		||||
          "favicons": true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										32
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
# tlaternet-templates
 | 
			
		||||
 | 
			
		||||
Website templates bundled with parcel.
 | 
			
		||||
 | 
			
		||||
## Updating
 | 
			
		||||
 | 
			
		||||
Firstly, updating the npm dependencies needs:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
npm update --package-lock-only
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
After that, the NixOS derivations can be updated by running
 | 
			
		||||
`generate.sh` in the `nix` subdirectory.
 | 
			
		||||
 | 
			
		||||
### Note
 | 
			
		||||
 | 
			
		||||
[Sharp](https://github.com/lovell/sharp/releases) is gloriously hard
 | 
			
		||||
to install, because it depends on the most cutting-edge patch version
 | 
			
		||||
of `vips`.
 | 
			
		||||
 | 
			
		||||
Check which version it needs in the `package.json` of sharp, and then
 | 
			
		||||
match this up which major sharp version matches up to which OS version
 | 
			
		||||
of vips.
 | 
			
		||||
 | 
			
		||||
Known versions:
 | 
			
		||||
 | 
			
		||||
| Sharp | vips       | NixOS |
 | 
			
		||||
| ----: | ---------: | ----: |
 | 
			
		||||
| 28.*  | 8.10.6     |       |
 | 
			
		||||
| 27.*  | 8.10.5     | 21.05 |
 | 
			
		||||
| 26.*  | 8.10.0     | 20.09 |
 | 
			
		||||
							
								
								
									
										28
									
								
								default.nix
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								default.nix
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
{ pkgs ? import <nixpkgs> {} }:
 | 
			
		||||
 | 
			
		||||
with pkgs;
 | 
			
		||||
 | 
			
		||||
# TODO: Once https://github.com/svanderburg/node2nix/issues/184 is
 | 
			
		||||
# fixed, use a proper nixos package instead of a tarball.
 | 
			
		||||
stdenv.mkDerivation {
 | 
			
		||||
  pname = "tlaternet-templates";
 | 
			
		||||
  version = "1.0";
 | 
			
		||||
  src = ./dist.tar.gz;
 | 
			
		||||
  installPhase = ''
 | 
			
		||||
    mkdir -p $out/srv/
 | 
			
		||||
    mv browser $out/srv/web
 | 
			
		||||
  '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# let
 | 
			
		||||
#   nodePackages = import ./node-packages.nix {
 | 
			
		||||
#     inherit pkgs;
 | 
			
		||||
#   };
 | 
			
		||||
 | 
			
		||||
# in
 | 
			
		||||
# nodePackages.package.override {
 | 
			
		||||
#   buildInputs = [ pkgs.nodePackages.node-gyp-build ];
 | 
			
		||||
#   postInstall = ''
 | 
			
		||||
#     npm run build-dist
 | 
			
		||||
#   '';
 | 
			
		||||
# }
 | 
			
		||||
							
								
								
									
										43
									
								
								flake.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								flake.lock
									
										
									
										generated
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
{
 | 
			
		||||
  "nodes": {
 | 
			
		||||
    "flake-utils": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1614513358,
 | 
			
		||||
        "narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
 | 
			
		||||
        "owner": "numtide",
 | 
			
		||||
        "repo": "flake-utils",
 | 
			
		||||
        "rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
        "owner": "numtide",
 | 
			
		||||
        "repo": "flake-utils",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1617271991,
 | 
			
		||||
        "narHash": "sha256-VdzjIQaT1FtycNid0iPSVr44Jrstoox6QRLrwCY1uG0=",
 | 
			
		||||
        "owner": "nixos",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "da7f4c4842520167f65c20ad75ecdbd14e27ae91",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
        "owner": "nixos",
 | 
			
		||||
        "ref": "nixos-20.09",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "root": {
 | 
			
		||||
      "inputs": {
 | 
			
		||||
        "flake-utils": "flake-utils",
 | 
			
		||||
        "nixpkgs": "nixpkgs"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "root": "root",
 | 
			
		||||
  "version": 7
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								flake.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								flake.nix
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,57 @@
 | 
			
		|||
{
 | 
			
		||||
  description = "tlater.net web server contents";
 | 
			
		||||
 | 
			
		||||
  inputs = {
 | 
			
		||||
    nixpkgs.url = "github:nixos/nixpkgs/nixos-20.09";
 | 
			
		||||
    flake-utils.url = "github:numtide/flake-utils";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  outputs = { self, nixpkgs, flake-utils }@inputs:
 | 
			
		||||
    flake-utils.lib.simpleFlake {
 | 
			
		||||
      inherit self nixpkgs;
 | 
			
		||||
      name = "tlaternet-templates";
 | 
			
		||||
 | 
			
		||||
      overlay = final: prev: {
 | 
			
		||||
        tlaternet-templates = rec {
 | 
			
		||||
          packages = rec {
 | 
			
		||||
            nodeEnv = prev.callPackage ./nix/override.nix { };
 | 
			
		||||
            templates = prev.callPackage ({ stdenv, nodejs, rename, ... }:
 | 
			
		||||
              let nodeDeps = nodeEnv.shell.nodeDependencies;
 | 
			
		||||
              in stdenv.mkDerivation {
 | 
			
		||||
                name = "tlaternet-templates";
 | 
			
		||||
                src = ./.;
 | 
			
		||||
 | 
			
		||||
                buildInputs = [ nodejs rename ];
 | 
			
		||||
                buildPhase = ''
 | 
			
		||||
                  ln -s ${nodeDeps}/lib/node_modules ./node_modules
 | 
			
		||||
                  export PATH="${nodeDeps}/bin:$PATH"
 | 
			
		||||
 | 
			
		||||
                  npm run build
 | 
			
		||||
                '';
 | 
			
		||||
 | 
			
		||||
                installPhase = ''
 | 
			
		||||
                  cp -r dist $out/
 | 
			
		||||
                '';
 | 
			
		||||
 | 
			
		||||
                postFixup = ''
 | 
			
		||||
                  rename 's/.html$/.html.hbs/' $out/browser/*.html
 | 
			
		||||
                '';
 | 
			
		||||
              }) { };
 | 
			
		||||
          };
 | 
			
		||||
          defaultPackage = packages.templates;
 | 
			
		||||
 | 
			
		||||
          devShell = prev.mkShell {
 | 
			
		||||
            buildInputs = with prev;
 | 
			
		||||
              with nodePackages; [
 | 
			
		||||
                nodejs
 | 
			
		||||
                node2nix
 | 
			
		||||
                prettier
 | 
			
		||||
 | 
			
		||||
                typescript
 | 
			
		||||
                typescript-language-server
 | 
			
		||||
              ];
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
#!/usr/bin/env nix-shell
 | 
			
		||||
#! nix-shell -i bash -p nodePackages.node2nix
 | 
			
		||||
set -euo pipefail
 | 
			
		||||
 | 
			
		||||
node2nix --input package.json \
 | 
			
		||||
         --output node-packages-generated.nix \
 | 
			
		||||
         --composition node-packages.nix
 | 
			
		||||
							
								
								
									
										17
									
								
								nix/default.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								nix/default.nix
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
# This file has been generated by node2nix 1.8.0. Do not edit!
 | 
			
		||||
 | 
			
		||||
{pkgs ? import <nixpkgs> {
 | 
			
		||||
    inherit system;
 | 
			
		||||
  }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs-12_x"}:
 | 
			
		||||
 | 
			
		||||
let
 | 
			
		||||
  nodeEnv = import ./node-env.nix {
 | 
			
		||||
    inherit (pkgs) stdenv python2 utillinux runCommand writeTextFile;
 | 
			
		||||
    inherit nodejs;
 | 
			
		||||
    libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null;
 | 
			
		||||
  };
 | 
			
		||||
in
 | 
			
		||||
import ./node-packages.nix {
 | 
			
		||||
  inherit (pkgs) fetchurl fetchgit;
 | 
			
		||||
  inherit nodeEnv;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								nix/generate.sh
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								nix/generate.sh
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
#!/usr/bin/env sh
 | 
			
		||||
 | 
			
		||||
node2nix -d -i ../package.json -l ../package-lock.json
 | 
			
		||||
							
								
								
									
										542
									
								
								nix/node-env.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										542
									
								
								nix/node-env.nix
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,542 @@
 | 
			
		|||
# This file originates from node2nix
 | 
			
		||||
 | 
			
		||||
{stdenv, nodejs, python2, utillinux, libtool, runCommand, writeTextFile}:
 | 
			
		||||
 | 
			
		||||
let
 | 
			
		||||
  python = if nodejs ? python then nodejs.python else python2;
 | 
			
		||||
 | 
			
		||||
  # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
 | 
			
		||||
  tarWrapper = runCommand "tarWrapper" {} ''
 | 
			
		||||
    mkdir -p $out/bin
 | 
			
		||||
 | 
			
		||||
    cat > $out/bin/tar <<EOF
 | 
			
		||||
    #! ${stdenv.shell} -e
 | 
			
		||||
    $(type -p tar) "\$@" --warning=no-unknown-keyword --delay-directory-restore
 | 
			
		||||
    EOF
 | 
			
		||||
 | 
			
		||||
    chmod +x $out/bin/tar
 | 
			
		||||
  '';
 | 
			
		||||
 | 
			
		||||
  # Function that generates a TGZ file from a NPM project
 | 
			
		||||
  buildNodeSourceDist =
 | 
			
		||||
    { name, version, src, ... }:
 | 
			
		||||
 | 
			
		||||
    stdenv.mkDerivation {
 | 
			
		||||
      name = "node-tarball-${name}-${version}";
 | 
			
		||||
      inherit src;
 | 
			
		||||
      buildInputs = [ nodejs ];
 | 
			
		||||
      buildPhase = ''
 | 
			
		||||
        export HOME=$TMPDIR
 | 
			
		||||
        tgzFile=$(npm pack | tail -n 1) # Hooks to the pack command will add output (https://docs.npmjs.com/misc/scripts)
 | 
			
		||||
      '';
 | 
			
		||||
      installPhase = ''
 | 
			
		||||
        mkdir -p $out/tarballs
 | 
			
		||||
        mv $tgzFile $out/tarballs
 | 
			
		||||
        mkdir -p $out/nix-support
 | 
			
		||||
        echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
 | 
			
		||||
      '';
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  includeDependencies = {dependencies}:
 | 
			
		||||
    stdenv.lib.optionalString (dependencies != [])
 | 
			
		||||
      (stdenv.lib.concatMapStrings (dependency:
 | 
			
		||||
        ''
 | 
			
		||||
          # Bundle the dependencies of the package
 | 
			
		||||
          mkdir -p node_modules
 | 
			
		||||
          cd node_modules
 | 
			
		||||
 | 
			
		||||
          # Only include dependencies if they don't exist. They may also be bundled in the package.
 | 
			
		||||
          if [ ! -e "${dependency.name}" ]
 | 
			
		||||
          then
 | 
			
		||||
              ${composePackage dependency}
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          cd ..
 | 
			
		||||
        ''
 | 
			
		||||
      ) dependencies);
 | 
			
		||||
 | 
			
		||||
  # Recursively composes the dependencies of a package
 | 
			
		||||
  composePackage = { name, packageName, src, dependencies ? [], ... }@args:
 | 
			
		||||
    builtins.addErrorContext "while evaluating node package '${packageName}'" ''
 | 
			
		||||
      DIR=$(pwd)
 | 
			
		||||
      cd $TMPDIR
 | 
			
		||||
 | 
			
		||||
      unpackFile ${src}
 | 
			
		||||
 | 
			
		||||
      # Make the base dir in which the target dependency resides first
 | 
			
		||||
      mkdir -p "$(dirname "$DIR/${packageName}")"
 | 
			
		||||
 | 
			
		||||
      if [ -f "${src}" ]
 | 
			
		||||
      then
 | 
			
		||||
          # Figure out what directory has been unpacked
 | 
			
		||||
          packageDir="$(find . -maxdepth 1 -type d | tail -1)"
 | 
			
		||||
 | 
			
		||||
          # Restore write permissions to make building work
 | 
			
		||||
          find "$packageDir" -type d -exec chmod u+x {} \;
 | 
			
		||||
          chmod -R u+w "$packageDir"
 | 
			
		||||
 | 
			
		||||
          # Move the extracted tarball into the output folder
 | 
			
		||||
          mv "$packageDir" "$DIR/${packageName}"
 | 
			
		||||
      elif [ -d "${src}" ]
 | 
			
		||||
      then
 | 
			
		||||
          # Get a stripped name (without hash) of the source directory.
 | 
			
		||||
          # On old nixpkgs it's already set internally.
 | 
			
		||||
          if [ -z "$strippedName" ]
 | 
			
		||||
          then
 | 
			
		||||
              strippedName="$(stripHash ${src})"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          # Restore write permissions to make building work
 | 
			
		||||
          chmod -R u+w "$strippedName"
 | 
			
		||||
 | 
			
		||||
          # Move the extracted directory into the output folder
 | 
			
		||||
          mv "$strippedName" "$DIR/${packageName}"
 | 
			
		||||
      fi
 | 
			
		||||
 | 
			
		||||
      # Unset the stripped name to not confuse the next unpack step
 | 
			
		||||
      unset strippedName
 | 
			
		||||
 | 
			
		||||
      # Include the dependencies of the package
 | 
			
		||||
      cd "$DIR/${packageName}"
 | 
			
		||||
      ${includeDependencies { inherit dependencies; }}
 | 
			
		||||
      cd ..
 | 
			
		||||
      ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
 | 
			
		||||
    '';
 | 
			
		||||
 | 
			
		||||
  pinpointDependencies = {dependencies, production}:
 | 
			
		||||
    let
 | 
			
		||||
      pinpointDependenciesFromPackageJSON = writeTextFile {
 | 
			
		||||
        name = "pinpointDependencies.js";
 | 
			
		||||
        text = ''
 | 
			
		||||
          var fs = require('fs');
 | 
			
		||||
          var path = require('path');
 | 
			
		||||
 | 
			
		||||
          function resolveDependencyVersion(location, name) {
 | 
			
		||||
              if(location == process.env['NIX_STORE']) {
 | 
			
		||||
                  return null;
 | 
			
		||||
              } else {
 | 
			
		||||
                  var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json");
 | 
			
		||||
 | 
			
		||||
                  if(fs.existsSync(dependencyPackageJSON)) {
 | 
			
		||||
                      var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON));
 | 
			
		||||
 | 
			
		||||
                      if(dependencyPackageObj.name == name) {
 | 
			
		||||
                          return dependencyPackageObj.version;
 | 
			
		||||
                      }
 | 
			
		||||
                  } else {
 | 
			
		||||
                      return resolveDependencyVersion(path.resolve(location, ".."), name);
 | 
			
		||||
                  }
 | 
			
		||||
              }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          function replaceDependencies(dependencies) {
 | 
			
		||||
              if(typeof dependencies == "object" && dependencies !== null) {
 | 
			
		||||
                  for(var dependency in dependencies) {
 | 
			
		||||
                      var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency);
 | 
			
		||||
 | 
			
		||||
                      if(resolvedVersion === null) {
 | 
			
		||||
                          process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n");
 | 
			
		||||
                      } else {
 | 
			
		||||
                          dependencies[dependency] = resolvedVersion;
 | 
			
		||||
                      }
 | 
			
		||||
                  }
 | 
			
		||||
              }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          /* Read the package.json configuration */
 | 
			
		||||
          var packageObj = JSON.parse(fs.readFileSync('./package.json'));
 | 
			
		||||
 | 
			
		||||
          /* Pinpoint all dependencies */
 | 
			
		||||
          replaceDependencies(packageObj.dependencies);
 | 
			
		||||
          if(process.argv[2] == "development") {
 | 
			
		||||
              replaceDependencies(packageObj.devDependencies);
 | 
			
		||||
          }
 | 
			
		||||
          replaceDependencies(packageObj.optionalDependencies);
 | 
			
		||||
 | 
			
		||||
          /* Write the fixed package.json file */
 | 
			
		||||
          fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
 | 
			
		||||
        '';
 | 
			
		||||
      };
 | 
			
		||||
    in
 | 
			
		||||
    ''
 | 
			
		||||
      node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"}
 | 
			
		||||
 | 
			
		||||
      ${stdenv.lib.optionalString (dependencies != [])
 | 
			
		||||
        ''
 | 
			
		||||
          if [ -d node_modules ]
 | 
			
		||||
          then
 | 
			
		||||
              cd node_modules
 | 
			
		||||
              ${stdenv.lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies}
 | 
			
		||||
              cd ..
 | 
			
		||||
          fi
 | 
			
		||||
        ''}
 | 
			
		||||
    '';
 | 
			
		||||
 | 
			
		||||
  # Recursively traverses all dependencies of a package and pinpoints all
 | 
			
		||||
  # dependencies in the package.json file to the versions that are actually
 | 
			
		||||
  # being used.
 | 
			
		||||
 | 
			
		||||
  pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args:
 | 
			
		||||
    ''
 | 
			
		||||
      if [ -d "${packageName}" ]
 | 
			
		||||
      then
 | 
			
		||||
          cd "${packageName}"
 | 
			
		||||
          ${pinpointDependencies { inherit dependencies production; }}
 | 
			
		||||
          cd ..
 | 
			
		||||
          ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
 | 
			
		||||
      fi
 | 
			
		||||
    '';
 | 
			
		||||
 | 
			
		||||
  # Extract the Node.js source code which is used to compile packages with
 | 
			
		||||
  # native bindings
 | 
			
		||||
  nodeSources = runCommand "node-sources" {} ''
 | 
			
		||||
    tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
 | 
			
		||||
    mv node-* $out
 | 
			
		||||
  '';
 | 
			
		||||
 | 
			
		||||
  # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty)
 | 
			
		||||
  addIntegrityFieldsScript = writeTextFile {
 | 
			
		||||
    name = "addintegrityfields.js";
 | 
			
		||||
    text = ''
 | 
			
		||||
      var fs = require('fs');
 | 
			
		||||
      var path = require('path');
 | 
			
		||||
 | 
			
		||||
      function augmentDependencies(baseDir, dependencies) {
 | 
			
		||||
          for(var dependencyName in dependencies) {
 | 
			
		||||
              var dependency = dependencies[dependencyName];
 | 
			
		||||
 | 
			
		||||
              // Open package.json and augment metadata fields
 | 
			
		||||
              var packageJSONDir = path.join(baseDir, "node_modules", dependencyName);
 | 
			
		||||
              var packageJSONPath = path.join(packageJSONDir, "package.json");
 | 
			
		||||
 | 
			
		||||
              if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored
 | 
			
		||||
                  console.log("Adding metadata fields to: "+packageJSONPath);
 | 
			
		||||
                  var packageObj = JSON.parse(fs.readFileSync(packageJSONPath));
 | 
			
		||||
 | 
			
		||||
                  if(dependency.integrity) {
 | 
			
		||||
                      packageObj["_integrity"] = dependency.integrity;
 | 
			
		||||
                  } else {
 | 
			
		||||
                      packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads.
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  if(dependency.resolved) {
 | 
			
		||||
                      packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided
 | 
			
		||||
                  } else {
 | 
			
		||||
                      packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories.
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  if(dependency.from !== undefined) { // Adopt from property if one has been provided
 | 
			
		||||
                      packageObj["_from"] = dependency.from;
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2));
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // Augment transitive dependencies
 | 
			
		||||
              if(dependency.dependencies !== undefined) {
 | 
			
		||||
                  augmentDependencies(packageJSONDir, dependency.dependencies);
 | 
			
		||||
              }
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if(fs.existsSync("./package-lock.json")) {
 | 
			
		||||
          var packageLock = JSON.parse(fs.readFileSync("./package-lock.json"));
 | 
			
		||||
 | 
			
		||||
          if(packageLock.lockfileVersion !== 1) {
 | 
			
		||||
             process.stderr.write("Sorry, I only understand lock file version 1!\n");
 | 
			
		||||
             process.exit(1);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if(packageLock.dependencies !== undefined) {
 | 
			
		||||
              augmentDependencies(".", packageLock.dependencies);
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
    '';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes
 | 
			
		||||
  reconstructPackageLock = writeTextFile {
 | 
			
		||||
    name = "addintegrityfields.js";
 | 
			
		||||
    text = ''
 | 
			
		||||
      var fs = require('fs');
 | 
			
		||||
      var path = require('path');
 | 
			
		||||
 | 
			
		||||
      var packageObj = JSON.parse(fs.readFileSync("package.json"));
 | 
			
		||||
 | 
			
		||||
      var lockObj = {
 | 
			
		||||
          name: packageObj.name,
 | 
			
		||||
          version: packageObj.version,
 | 
			
		||||
          lockfileVersion: 1,
 | 
			
		||||
          requires: true,
 | 
			
		||||
          dependencies: {}
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      function augmentPackageJSON(filePath, dependencies) {
 | 
			
		||||
          var packageJSON = path.join(filePath, "package.json");
 | 
			
		||||
          if(fs.existsSync(packageJSON)) {
 | 
			
		||||
              var packageObj = JSON.parse(fs.readFileSync(packageJSON));
 | 
			
		||||
              dependencies[packageObj.name] = {
 | 
			
		||||
                  version: packageObj.version,
 | 
			
		||||
                  integrity: "sha1-000000000000000000000000000=",
 | 
			
		||||
                  dependencies: {}
 | 
			
		||||
              };
 | 
			
		||||
              processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies);
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function processDependencies(dir, dependencies) {
 | 
			
		||||
          if(fs.existsSync(dir)) {
 | 
			
		||||
              var files = fs.readdirSync(dir);
 | 
			
		||||
 | 
			
		||||
              files.forEach(function(entry) {
 | 
			
		||||
                  var filePath = path.join(dir, entry);
 | 
			
		||||
                  var stats = fs.statSync(filePath);
 | 
			
		||||
 | 
			
		||||
                  if(stats.isDirectory()) {
 | 
			
		||||
                      if(entry.substr(0, 1) == "@") {
 | 
			
		||||
                          // When we encounter a namespace folder, augment all packages belonging to the scope
 | 
			
		||||
                          var pkgFiles = fs.readdirSync(filePath);
 | 
			
		||||
 | 
			
		||||
                          pkgFiles.forEach(function(entry) {
 | 
			
		||||
                              if(stats.isDirectory()) {
 | 
			
		||||
                                  var pkgFilePath = path.join(filePath, entry);
 | 
			
		||||
                                  augmentPackageJSON(pkgFilePath, dependencies);
 | 
			
		||||
                              }
 | 
			
		||||
                          });
 | 
			
		||||
                      } else {
 | 
			
		||||
                          augmentPackageJSON(filePath, dependencies);
 | 
			
		||||
                      }
 | 
			
		||||
                  }
 | 
			
		||||
              });
 | 
			
		||||
          }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      processDependencies("node_modules", lockObj.dependencies);
 | 
			
		||||
 | 
			
		||||
      fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
 | 
			
		||||
    '';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}:
 | 
			
		||||
    let
 | 
			
		||||
      forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
 | 
			
		||||
    in
 | 
			
		||||
    ''
 | 
			
		||||
        # Pinpoint the versions of all dependencies to the ones that are actually being used
 | 
			
		||||
        echo "pinpointing versions of dependencies..."
 | 
			
		||||
        source $pinpointDependenciesScriptPath
 | 
			
		||||
 | 
			
		||||
        # Patch the shebangs of the bundled modules to prevent them from
 | 
			
		||||
        # calling executables outside the Nix store as much as possible
 | 
			
		||||
        patchShebangs .
 | 
			
		||||
 | 
			
		||||
        # Deploy the Node.js package by running npm install. Since the
 | 
			
		||||
        # dependencies have been provided already by ourselves, it should not
 | 
			
		||||
        # attempt to install them again, which is good, because we want to make
 | 
			
		||||
        # it Nix's responsibility. If it needs to install any dependencies
 | 
			
		||||
        # anyway (e.g. because the dependency parameters are
 | 
			
		||||
        # incomplete/incorrect), it fails.
 | 
			
		||||
        #
 | 
			
		||||
        # The other responsibilities of NPM are kept -- version checks, build
 | 
			
		||||
        # steps, postprocessing etc.
 | 
			
		||||
 | 
			
		||||
        export HOME=$TMPDIR
 | 
			
		||||
        cd "${packageName}"
 | 
			
		||||
        runHook preRebuild
 | 
			
		||||
 | 
			
		||||
        ${stdenv.lib.optionalString bypassCache ''
 | 
			
		||||
          ${stdenv.lib.optionalString reconstructLock ''
 | 
			
		||||
            if [ -f package-lock.json ]
 | 
			
		||||
            then
 | 
			
		||||
                echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!"
 | 
			
		||||
                echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!"
 | 
			
		||||
                rm package-lock.json
 | 
			
		||||
            else
 | 
			
		||||
                echo "No package-lock.json file found, reconstructing..."
 | 
			
		||||
            fi
 | 
			
		||||
 | 
			
		||||
            node ${reconstructPackageLock}
 | 
			
		||||
          ''}
 | 
			
		||||
 | 
			
		||||
          node ${addIntegrityFieldsScript}
 | 
			
		||||
        ''}
 | 
			
		||||
 | 
			
		||||
        npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} rebuild
 | 
			
		||||
 | 
			
		||||
        if [ "''${dontNpmInstall-}" != "1" ]
 | 
			
		||||
        then
 | 
			
		||||
            # NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
 | 
			
		||||
            rm -f npm-shrinkwrap.json
 | 
			
		||||
 | 
			
		||||
            npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} install
 | 
			
		||||
        fi
 | 
			
		||||
    '';
 | 
			
		||||
 | 
			
		||||
  # Builds and composes an NPM package including all its dependencies
 | 
			
		||||
  buildNodePackage =
 | 
			
		||||
    { name
 | 
			
		||||
    , packageName
 | 
			
		||||
    , version
 | 
			
		||||
    , dependencies ? []
 | 
			
		||||
    , buildInputs ? []
 | 
			
		||||
    , production ? true
 | 
			
		||||
    , npmFlags ? ""
 | 
			
		||||
    , dontNpmInstall ? false
 | 
			
		||||
    , bypassCache ? false
 | 
			
		||||
    , reconstructLock ? false
 | 
			
		||||
    , preRebuild ? ""
 | 
			
		||||
    , dontStrip ? true
 | 
			
		||||
    , unpackPhase ? "true"
 | 
			
		||||
    , buildPhase ? "true"
 | 
			
		||||
    , ... }@args:
 | 
			
		||||
 | 
			
		||||
    let
 | 
			
		||||
      extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" ];
 | 
			
		||||
    in
 | 
			
		||||
    stdenv.mkDerivation ({
 | 
			
		||||
      name = "node_${name}-${version}";
 | 
			
		||||
      buildInputs = [ tarWrapper python nodejs ]
 | 
			
		||||
        ++ stdenv.lib.optional (stdenv.isLinux) utillinux
 | 
			
		||||
        ++ stdenv.lib.optional (stdenv.isDarwin) libtool
 | 
			
		||||
        ++ buildInputs;
 | 
			
		||||
 | 
			
		||||
      inherit nodejs;
 | 
			
		||||
 | 
			
		||||
      inherit dontStrip; # Stripping may fail a build for some package deployments
 | 
			
		||||
      inherit dontNpmInstall preRebuild unpackPhase buildPhase;
 | 
			
		||||
 | 
			
		||||
      compositionScript = composePackage args;
 | 
			
		||||
      pinpointDependenciesScript = pinpointDependenciesOfPackage args;
 | 
			
		||||
 | 
			
		||||
      passAsFile = [ "compositionScript" "pinpointDependenciesScript" ];
 | 
			
		||||
 | 
			
		||||
      installPhase = ''
 | 
			
		||||
        # Create and enter a root node_modules/ folder
 | 
			
		||||
        mkdir -p $out/lib/node_modules
 | 
			
		||||
        cd $out/lib/node_modules
 | 
			
		||||
 | 
			
		||||
        # Compose the package and all its dependencies
 | 
			
		||||
        source $compositionScriptPath
 | 
			
		||||
 | 
			
		||||
        ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
 | 
			
		||||
 | 
			
		||||
        # Create symlink to the deployed executable folder, if applicable
 | 
			
		||||
        if [ -d "$out/lib/node_modules/.bin" ]
 | 
			
		||||
        then
 | 
			
		||||
            ln -s $out/lib/node_modules/.bin $out/bin
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Create symlinks to the deployed manual page folders, if applicable
 | 
			
		||||
        if [ -d "$out/lib/node_modules/${packageName}/man" ]
 | 
			
		||||
        then
 | 
			
		||||
            mkdir -p $out/share
 | 
			
		||||
            for dir in "$out/lib/node_modules/${packageName}/man/"*
 | 
			
		||||
            do
 | 
			
		||||
                mkdir -p $out/share/man/$(basename "$dir")
 | 
			
		||||
                for page in "$dir"/*
 | 
			
		||||
                do
 | 
			
		||||
                    ln -s $page $out/share/man/$(basename "$dir")
 | 
			
		||||
                done
 | 
			
		||||
            done
 | 
			
		||||
        fi
 | 
			
		||||
 | 
			
		||||
        # Run post install hook, if provided
 | 
			
		||||
        runHook postInstall
 | 
			
		||||
      '';
 | 
			
		||||
    } // extraArgs);
 | 
			
		||||
 | 
			
		||||
  # Builds a development shell
 | 
			
		||||
  buildNodeShell =
 | 
			
		||||
    { name
 | 
			
		||||
    , packageName
 | 
			
		||||
    , version
 | 
			
		||||
    , src
 | 
			
		||||
    , dependencies ? []
 | 
			
		||||
    , buildInputs ? []
 | 
			
		||||
    , production ? true
 | 
			
		||||
    , npmFlags ? ""
 | 
			
		||||
    , dontNpmInstall ? false
 | 
			
		||||
    , bypassCache ? false
 | 
			
		||||
    , reconstructLock ? false
 | 
			
		||||
    , dontStrip ? true
 | 
			
		||||
    , unpackPhase ? "true"
 | 
			
		||||
    , buildPhase ? "true"
 | 
			
		||||
    , ... }@args:
 | 
			
		||||
 | 
			
		||||
    let
 | 
			
		||||
      extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
 | 
			
		||||
 | 
			
		||||
      nodeDependencies = stdenv.mkDerivation ({
 | 
			
		||||
        name = "node-dependencies-${name}-${version}";
 | 
			
		||||
 | 
			
		||||
        buildInputs = [ tarWrapper python nodejs ]
 | 
			
		||||
          ++ stdenv.lib.optional (stdenv.isLinux) utillinux
 | 
			
		||||
          ++ stdenv.lib.optional (stdenv.isDarwin) libtool
 | 
			
		||||
          ++ buildInputs;
 | 
			
		||||
 | 
			
		||||
        inherit dontStrip; # Stripping may fail a build for some package deployments
 | 
			
		||||
        inherit dontNpmInstall unpackPhase buildPhase;
 | 
			
		||||
 | 
			
		||||
        includeScript = includeDependencies { inherit dependencies; };
 | 
			
		||||
        pinpointDependenciesScript = pinpointDependenciesOfPackage args;
 | 
			
		||||
 | 
			
		||||
        passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
 | 
			
		||||
 | 
			
		||||
        installPhase = ''
 | 
			
		||||
          mkdir -p $out/${packageName}
 | 
			
		||||
          cd $out/${packageName}
 | 
			
		||||
 | 
			
		||||
          source $includeScriptPath
 | 
			
		||||
 | 
			
		||||
          # Create fake package.json to make the npm commands work properly
 | 
			
		||||
          cp ${src}/package.json .
 | 
			
		||||
          chmod 644 package.json
 | 
			
		||||
          ${stdenv.lib.optionalString bypassCache ''
 | 
			
		||||
            if [ -f ${src}/package-lock.json ]
 | 
			
		||||
            then
 | 
			
		||||
                cp ${src}/package-lock.json .
 | 
			
		||||
            fi
 | 
			
		||||
          ''}
 | 
			
		||||
 | 
			
		||||
          # Go to the parent folder to make sure that all packages are pinpointed
 | 
			
		||||
          cd ..
 | 
			
		||||
          ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
 | 
			
		||||
 | 
			
		||||
          ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
 | 
			
		||||
 | 
			
		||||
          # Expose the executables that were installed
 | 
			
		||||
          cd ..
 | 
			
		||||
          ${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
 | 
			
		||||
 | 
			
		||||
          mv ${packageName} lib
 | 
			
		||||
          ln -s $out/lib/node_modules/.bin $out/bin
 | 
			
		||||
        '';
 | 
			
		||||
      } // extraArgs);
 | 
			
		||||
    in
 | 
			
		||||
    stdenv.mkDerivation {
 | 
			
		||||
      name = "node-shell-${name}-${version}";
 | 
			
		||||
 | 
			
		||||
      buildInputs = [ python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
 | 
			
		||||
      buildCommand = ''
 | 
			
		||||
        mkdir -p $out/bin
 | 
			
		||||
        cat > $out/bin/shell <<EOF
 | 
			
		||||
        #! ${stdenv.shell} -e
 | 
			
		||||
        $shellHook
 | 
			
		||||
        exec ${stdenv.shell}
 | 
			
		||||
        EOF
 | 
			
		||||
        chmod +x $out/bin/shell
 | 
			
		||||
      '';
 | 
			
		||||
 | 
			
		||||
      # Provide the dependencies in a development shell through the NODE_PATH environment variable
 | 
			
		||||
      inherit nodeDependencies;
 | 
			
		||||
      shellHook = stdenv.lib.optionalString (dependencies != []) ''
 | 
			
		||||
        export NODE_PATH=${nodeDependencies}/lib/node_modules
 | 
			
		||||
        export PATH="${nodeDependencies}/bin:$PATH"
 | 
			
		||||
      '';
 | 
			
		||||
    };
 | 
			
		||||
in
 | 
			
		||||
{
 | 
			
		||||
  buildNodeSourceDist = stdenv.lib.makeOverridable buildNodeSourceDist;
 | 
			
		||||
  buildNodePackage = stdenv.lib.makeOverridable buildNodePackage;
 | 
			
		||||
  buildNodeShell = stdenv.lib.makeOverridable buildNodeShell;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12112
									
								
								nix/node-packages.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12112
									
								
								nix/node-packages.nix
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										16
									
								
								nix/override.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								nix/override.nix
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
{ pkgs ? import <nixpkgs> { inherit system; }, system ? builtins.currentSystem
 | 
			
		||||
}:
 | 
			
		||||
 | 
			
		||||
let nodePackages = import ./default.nix { inherit pkgs system; };
 | 
			
		||||
in nodePackages // {
 | 
			
		||||
  shell = nodePackages.shell.override {
 | 
			
		||||
    buildInputs = with pkgs; [
 | 
			
		||||
      pkgs.nodePackages.node-gyp-build
 | 
			
		||||
      vips
 | 
			
		||||
      glib
 | 
			
		||||
      pkgconfig
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    npmFlags = "--ignore-scripts";
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10241
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										10241
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										34
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										34
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -6,30 +6,46 @@
 | 
			
		|||
  "license": "MIT",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@babel/preset-env": "^7.10.4",
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^5.13.1",
 | 
			
		||||
    "@types/jquery": "^3.5.0",
 | 
			
		||||
    "@types/react": "^16.9.41",
 | 
			
		||||
    "@types/react-redux": "^7.1.9",
 | 
			
		||||
    "bootstrap": "^4.5.0",
 | 
			
		||||
    "classnames": "^2.2.6",
 | 
			
		||||
    "immutability-helper": "^3.1.1",
 | 
			
		||||
    "jquery": "^3.5.1",
 | 
			
		||||
    "jstransformer-markdown-it": "^2.1.0",
 | 
			
		||||
    "parcel": "^2.0.0-nightly.328",
 | 
			
		||||
    "popper.js": "^1.16.1",
 | 
			
		||||
    "posthtml-favicons": "^1.3.0",
 | 
			
		||||
    "pug": "^2.0.4",
 | 
			
		||||
    "react": "^16.13.1",
 | 
			
		||||
    "react-dom": "^16.13.1",
 | 
			
		||||
    "react-redux": "^7.2.0",
 | 
			
		||||
    "redux": "^4.0.5",
 | 
			
		||||
    "redux-act": "^1.8.0",
 | 
			
		||||
    "three": "^0.127.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/preset-env": "^7.10.4",
 | 
			
		||||
    "@babel/preset-typescript": "^7.13.0",
 | 
			
		||||
    "@parcel/transformer-pug": "2.0.0-beta.2",
 | 
			
		||||
    "@parcel/transformer-sass": "2.0.0-beta.2",
 | 
			
		||||
    "@parcel/validator-typescript": "2.0.0-beta.2",
 | 
			
		||||
    "@types/jquery": "^3.5.0",
 | 
			
		||||
    "@types/react": "^16.9.41",
 | 
			
		||||
    "@types/react-dom": "^17.0.3",
 | 
			
		||||
    "@types/react-redux": "^7.1.9",
 | 
			
		||||
    "@types/three": "^0.127.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^4.21.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^4.21.0",
 | 
			
		||||
    "eslint": "^7.23.0",
 | 
			
		||||
    "jstransformer-markdown-it": "^2.1.0",
 | 
			
		||||
    "parcel": "2.0.0-beta.2",
 | 
			
		||||
    "posthtml-favicons": "^1.3.0",
 | 
			
		||||
    "pug": "^3.0.2",
 | 
			
		||||
    "sass": "^1.26.10",
 | 
			
		||||
    "three": "^0.101.1",
 | 
			
		||||
    "typescript": "^4.2.4",
 | 
			
		||||
    "typescript-language-server": "^0.4.0"
 | 
			
		||||
  },
 | 
			
		||||
  "resolutions": {
 | 
			
		||||
    "sharp": "0.26.3"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "preinstall": "npx npm-force-resolutions",
 | 
			
		||||
    "build": "parcel build --no-autoinstall $(find src -path src/lib -prune -o -name '*.pug' -print)",
 | 
			
		||||
    "serve": "parcel serve --no-autoinstall $(find src -path src/lib -prune -o -name '*.pug' -print)",
 | 
			
		||||
    "watch": "parcel watch --no-autoinstall $(find src -path src/lib -prune -o -name '*.pug' -print)",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
with import <nixpkgs> {};
 | 
			
		||||
 | 
			
		||||
runCommand "www" {
 | 
			
		||||
  buildInputs = [
 | 
			
		||||
    python3
 | 
			
		||||
    nodejs
 | 
			
		||||
    nodePackages.prettier
 | 
			
		||||
  ];
 | 
			
		||||
} ""
 | 
			
		||||
							
								
								
									
										196
									
								
								src/index.ts
									
										
									
									
									
								
							
							
						
						
									
										196
									
								
								src/index.ts
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import $ from "jquery";
 | 
			
		||||
import jQuery from "jquery";
 | 
			
		||||
 | 
			
		||||
// Helpers
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -6,114 +6,114 @@ import $ from "jquery";
 | 
			
		|||
 * "Types" out a DOM element, emulating the way a human might.
 | 
			
		||||
 */
 | 
			
		||||
class Typer {
 | 
			
		||||
  private element: JQuery;
 | 
			
		||||
  private text: string;
 | 
			
		||||
  private cursor: boolean;
 | 
			
		||||
  private typed: number;
 | 
			
		||||
  private min: number;
 | 
			
		||||
  private max: number;
 | 
			
		||||
  private blink_tick: number;
 | 
			
		||||
  private blink_timeout: number;
 | 
			
		||||
  private end?: number;
 | 
			
		||||
    private element: JQuery;
 | 
			
		||||
    private text: string;
 | 
			
		||||
    private cursor: boolean;
 | 
			
		||||
    private typed: number;
 | 
			
		||||
    private min: number;
 | 
			
		||||
    private max: number;
 | 
			
		||||
    private blink_tick: number;
 | 
			
		||||
    private blink_timeout: number;
 | 
			
		||||
    private end?: number;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create the typer.
 | 
			
		||||
   * @param {HTMLElement} element - The element to type.
 | 
			
		||||
   * @param {number} blink - The time between cursor blinks.
 | 
			
		||||
   * @param {number} blink_timeout - How long the cursor should keep
 | 
			
		||||
   *                                 blinking for after the text
 | 
			
		||||
   *                                 finishes typing.
 | 
			
		||||
   */
 | 
			
		||||
  constructor(element: HTMLElement, blink: number, blink_timeout: number) {
 | 
			
		||||
    // Retrieve the current content and wipe it. We also make the
 | 
			
		||||
    // element visible if it was hidden.
 | 
			
		||||
    this.element = $(element);
 | 
			
		||||
    this.text = this.element.html();
 | 
			
		||||
    this.element.html("");
 | 
			
		||||
    this.element.css("visibility", "visible");
 | 
			
		||||
    /**
 | 
			
		||||
     * Create the typer.
 | 
			
		||||
     * @param {HTMLElement} element - The element to type.
 | 
			
		||||
     * @param {number} blink - The time between cursor blinks.
 | 
			
		||||
     * @param {number} blink_timeout - How long the cursor should keep
 | 
			
		||||
     *                                 blinking for after the text
 | 
			
		||||
     *                                 finishes typing.
 | 
			
		||||
     */
 | 
			
		||||
    constructor(element: HTMLElement, blink: number, blink_timeout: number) {
 | 
			
		||||
        // Retrieve the current content and wipe it. We also make the
 | 
			
		||||
        // element visible if it was hidden.
 | 
			
		||||
        this.element = $(element);
 | 
			
		||||
        this.text = this.element.html();
 | 
			
		||||
        this.element.html("");
 | 
			
		||||
        this.element.css("visibility", "visible");
 | 
			
		||||
 | 
			
		||||
    this.cursor = false;
 | 
			
		||||
    this.typed = 0;
 | 
			
		||||
        this.cursor = false;
 | 
			
		||||
        this.typed = 0;
 | 
			
		||||
 | 
			
		||||
    this.min = 20;
 | 
			
		||||
    this.max = 70;
 | 
			
		||||
    this.blink_tick = blink;
 | 
			
		||||
    this.blink_timeout = blink_timeout;
 | 
			
		||||
        this.min = 20;
 | 
			
		||||
        this.max = 70;
 | 
			
		||||
        this.blink_tick = blink;
 | 
			
		||||
        this.blink_timeout = blink_timeout;
 | 
			
		||||
 | 
			
		||||
    this.end = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start typing.
 | 
			
		||||
   */
 | 
			
		||||
  type() {
 | 
			
		||||
    this._type();
 | 
			
		||||
    this._blink();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Draw the current text line, i.e., anything that has been typed
 | 
			
		||||
   * so far, and a cursor if it is currently supposed to be on.
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _draw() {
 | 
			
		||||
    let text = this.text.slice(0, this.typed);
 | 
			
		||||
 | 
			
		||||
    if (this.cursor) {
 | 
			
		||||
      text += "\u2588";
 | 
			
		||||
        this.end = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.requestAnimationFrame(() => this.element.html(text));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Type the next character, and prepare to draw the next one. If
 | 
			
		||||
   * no new characters are to be drawn, set the end timestamp.
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _type() {
 | 
			
		||||
    this.typed += 1;
 | 
			
		||||
    this._draw();
 | 
			
		||||
 | 
			
		||||
    if (this.typed != this.text.length)
 | 
			
		||||
      setTimeout(this._type.bind(this), this._type_tick());
 | 
			
		||||
    else {
 | 
			
		||||
      this.end = Date.now();
 | 
			
		||||
    /**
 | 
			
		||||
     * Start typing.
 | 
			
		||||
     */
 | 
			
		||||
    type() {
 | 
			
		||||
        this._type();
 | 
			
		||||
        this._blink();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Make the cursor change blink status, and prepare for the next
 | 
			
		||||
   * blink.
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _blink() {
 | 
			
		||||
    this.cursor = !this.cursor;
 | 
			
		||||
    this._draw();
 | 
			
		||||
    /**
 | 
			
		||||
     * Draw the current text line, i.e., anything that has been typed
 | 
			
		||||
     * so far, and a cursor if it is currently supposed to be on.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _draw() {
 | 
			
		||||
        let text = this.text.slice(0, this.typed);
 | 
			
		||||
 | 
			
		||||
    // As long as we are typing, keep blinking
 | 
			
		||||
    if (this.typed != this.text.length)
 | 
			
		||||
      setTimeout(this._blink.bind(this), this.blink_tick);
 | 
			
		||||
    // Once typing ends, keep going for a little bit
 | 
			
		||||
    else if (Date.now() - this.end < this.blink_timeout)
 | 
			
		||||
      setTimeout(this._blink.bind(this), this.blink_tick);
 | 
			
		||||
    // Make sure we get rid of the cursor in the end
 | 
			
		||||
    else {
 | 
			
		||||
      this.cursor = true;
 | 
			
		||||
      setTimeout(this._blink.bind(this), this.blink_tick);
 | 
			
		||||
        if (this.cursor) {
 | 
			
		||||
            text += "\u2588";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        window.requestAnimationFrame(() => this.element.html(text));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Calculate a "human" time for the next character to type.
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _type_tick() {
 | 
			
		||||
    return Math.round(Math.random() * this.max) + this.min;
 | 
			
		||||
  }
 | 
			
		||||
    /**
 | 
			
		||||
     * Type the next character, and prepare to draw the next one. If
 | 
			
		||||
     * no new characters are to be drawn, set the end timestamp.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _type() {
 | 
			
		||||
        this.typed += 1;
 | 
			
		||||
        this._draw();
 | 
			
		||||
 | 
			
		||||
        if (this.typed != this.text.length)
 | 
			
		||||
            setTimeout(this._type.bind(this), this._type_tick());
 | 
			
		||||
        else {
 | 
			
		||||
            this.end = Date.now();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Make the cursor change blink status, and prepare for the next
 | 
			
		||||
     * blink.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _blink() {
 | 
			
		||||
        this.cursor = !this.cursor;
 | 
			
		||||
        this._draw();
 | 
			
		||||
 | 
			
		||||
        // As long as we are typing, keep blinking
 | 
			
		||||
        if (this.typed != this.text.length)
 | 
			
		||||
            setTimeout(this._blink.bind(this), this.blink_tick);
 | 
			
		||||
        // Once typing ends, keep going for a little bit
 | 
			
		||||
        else if (Date.now() - this.end < this.blink_timeout)
 | 
			
		||||
            setTimeout(this._blink.bind(this), this.blink_tick);
 | 
			
		||||
        // Make sure we get rid of the cursor in the end
 | 
			
		||||
        else {
 | 
			
		||||
            this.cursor = true;
 | 
			
		||||
            setTimeout(this._blink.bind(this), this.blink_tick);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Calculate a "human" time for the next character to type.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _type_tick() {
 | 
			
		||||
        return Math.round(Math.random() * this.max) + this.min;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => {
 | 
			
		||||
  let typer = new Typer($(".head-line .typed").get(0), 500, 3000);
 | 
			
		||||
  typer.type();
 | 
			
		||||
jQuery(($) => {
 | 
			
		||||
    const typer = new Typer($(".head-line .typed").get(0), 500, 3000);
 | 
			
		||||
    typer.type();
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
import $ from "jquery";
 | 
			
		||||
import jQuery from "jquery";
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => $("html").removeClass("no-js"));
 | 
			
		||||
jQuery(($) => $("html").removeClass("no-js"));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ html.no-js(lang="en")
 | 
			
		|||
    meta(name="description", content="tlater.net web server")
 | 
			
		||||
    meta(name="author", content="Tristan Daniël Maat")
 | 
			
		||||
    meta(name="viewport", content="width=device-width, initial-scale=1")
 | 
			
		||||
    link(rel="icon", href="src/icon.svg", type="image/x-icon")
 | 
			
		||||
    link(rel="icon", href="./icon.svg", type="image/x-icon")
 | 
			
		||||
    link(rel="stylesheet", href="~/src/lib/scss/main.scss")
 | 
			
		||||
    block stylesheets
 | 
			
		||||
    title="tlater.net"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,77 +18,77 @@ type MusicPlayerProps = {
 | 
			
		|||
  source?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MusicPlayer extends React.Component<MusicPlayerProps, {}> {
 | 
			
		||||
class MusicPlayer extends React.Component<MusicPlayerProps, State> {
 | 
			
		||||
  private audioState: AudioState;
 | 
			
		||||
 | 
			
		||||
  constructor(props: MusicPlayerProps) {
 | 
			
		||||
    super(props);
 | 
			
		||||
      super(props);
 | 
			
		||||
 | 
			
		||||
    let context = new AudioContext();
 | 
			
		||||
    let source = new Audio();
 | 
			
		||||
    let sourceNode = context.createMediaElementSource(source);
 | 
			
		||||
    let volume = context.createGain();
 | 
			
		||||
      const context = new AudioContext();
 | 
			
		||||
      const source = new Audio();
 | 
			
		||||
      const sourceNode = context.createMediaElementSource(source);
 | 
			
		||||
      const volume = context.createGain();
 | 
			
		||||
 | 
			
		||||
    sourceNode.connect(volume);
 | 
			
		||||
    volume.connect(context.destination);
 | 
			
		||||
      sourceNode.connect(volume);
 | 
			
		||||
      volume.connect(context.destination);
 | 
			
		||||
 | 
			
		||||
    this.audioState = {
 | 
			
		||||
      audioContext: context,
 | 
			
		||||
      audioSourceNode: sourceNode,
 | 
			
		||||
      audioSource: source,
 | 
			
		||||
      audioVolume: volume,
 | 
			
		||||
    };
 | 
			
		||||
      this.audioState = {
 | 
			
		||||
          audioContext: context,
 | 
			
		||||
          audioSourceNode: sourceNode,
 | 
			
		||||
          audioSource: source,
 | 
			
		||||
          audioVolume: volume,
 | 
			
		||||
      };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div id="player" style={{ height: "100%", width: "100%" }}>
 | 
			
		||||
        <Visualizer
 | 
			
		||||
          audioContext={this.audioState.audioContext}
 | 
			
		||||
          audioSource={this.audioState.audioSourceNode}
 | 
			
		||||
        />
 | 
			
		||||
        <Controls />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
      return (
 | 
			
		||||
          <div id="player" style={{ height: "100%", width: "100%" }}>
 | 
			
		||||
              <Visualizer
 | 
			
		||||
                  audioContext={this.audioState.audioContext}
 | 
			
		||||
                  audioSource={this.audioState.audioSourceNode}
 | 
			
		||||
              />
 | 
			
		||||
              <Controls />
 | 
			
		||||
          </div>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate() {
 | 
			
		||||
    let context = this.audioState.audioContext;
 | 
			
		||||
    let source = this.audioState.audioSource;
 | 
			
		||||
    let volume = this.audioState.audioVolume;
 | 
			
		||||
      const context = this.audioState.audioContext;
 | 
			
		||||
      const source = this.audioState.audioSource;
 | 
			
		||||
      const volume = this.audioState.audioVolume;
 | 
			
		||||
 | 
			
		||||
    // First, set the audio source (if it changed)
 | 
			
		||||
    if (this.props.source && source.src != this.props.source) {
 | 
			
		||||
      source.src = this.props.source;
 | 
			
		||||
    }
 | 
			
		||||
      // First, set the audio source (if it changed)
 | 
			
		||||
      if (this.props.source && source.src != this.props.source) {
 | 
			
		||||
          source.src = this.props.source;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    if (this.props.playing) {
 | 
			
		||||
      source
 | 
			
		||||
        .play()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          console.info("Started playing audio");
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error(`Could not play audio: ${error}`);
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
      source.pause();
 | 
			
		||||
    }
 | 
			
		||||
      if (this.props.playing) {
 | 
			
		||||
          source
 | 
			
		||||
              .play()
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                  console.info("Started playing audio");
 | 
			
		||||
              })
 | 
			
		||||
              .catch((error) => {
 | 
			
		||||
                  console.error(`Could not play audio: ${error}`);
 | 
			
		||||
              });
 | 
			
		||||
      } else {
 | 
			
		||||
          source.pause();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    if (!this.props.muted) {
 | 
			
		||||
      volume.gain.setValueAtTime(1, context.currentTime);
 | 
			
		||||
    } else {
 | 
			
		||||
      volume.gain.setValueAtTime(0, context.currentTime);
 | 
			
		||||
    }
 | 
			
		||||
      if (!this.props.muted) {
 | 
			
		||||
          volume.gain.setValueAtTime(1, context.currentTime);
 | 
			
		||||
      } else {
 | 
			
		||||
          volume.gain.setValueAtTime(0, context.currentTime);
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mapStateToProps(state: State): MusicPlayerProps {
 | 
			
		||||
  return {
 | 
			
		||||
    playing: state.musicState.playing,
 | 
			
		||||
    muted: state.musicState.muted,
 | 
			
		||||
    source: state.musicState.source,
 | 
			
		||||
  };
 | 
			
		||||
    return {
 | 
			
		||||
        playing: state.musicState.playing,
 | 
			
		||||
        muted: state.musicState.muted,
 | 
			
		||||
        source: state.musicState.source,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(MusicPlayer);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,50 +1,49 @@
 | 
			
		|||
import React from "react";
 | 
			
		||||
import Redux from "redux";
 | 
			
		||||
import { connect } from "react-redux";
 | 
			
		||||
 | 
			
		||||
import { State } from "../store";
 | 
			
		||||
import { Title, togglePlay } from "../store/music/types";
 | 
			
		||||
import { Title } from "../store/music/types";
 | 
			
		||||
import Indicator from "./indicator";
 | 
			
		||||
 | 
			
		||||
type ControlProps = {
 | 
			
		||||
  title: Title;
 | 
			
		||||
    title: Title;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Controls extends React.Component<ControlProps, {}> {
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div id="playerControls" className="container-fluid fixed-bottom">
 | 
			
		||||
        <div className="align-items-center row p-2">
 | 
			
		||||
          <Indicator></Indicator>
 | 
			
		||||
          <div
 | 
			
		||||
            id="playerText"
 | 
			
		||||
            className="text-justify text-truncate col-6 playerControlsContent"
 | 
			
		||||
          >
 | 
			
		||||
            {this.props.title.name} - {this.props.title.album}
 | 
			
		||||
          </div>
 | 
			
		||||
class Controls extends React.Component<ControlProps, State> {
 | 
			
		||||
    render() {
 | 
			
		||||
        return (
 | 
			
		||||
            <div id="playerControls" className="container-fluid fixed-bottom">
 | 
			
		||||
                <div className="align-items-center row p-2">
 | 
			
		||||
                    <Indicator></Indicator>
 | 
			
		||||
                    <div
 | 
			
		||||
                        id="playerText"
 | 
			
		||||
                        className="text-justify text-truncate col-6 playerControlsContent"
 | 
			
		||||
                    >
 | 
			
		||||
                        {this.props.title.name} - {this.props.title.album}
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
          {this.props.title.name === "Journey" &&
 | 
			
		||||
          this.props.title.artist === "Mseq" ? (
 | 
			
		||||
            <div id="copyrightNotice" className="col text-center">
 | 
			
		||||
              <a href="http://dig.ccmixter.org/files/Mseq/54702">Journey</a>
 | 
			
		||||
                    {this.props.title.name === "Journey" &&
 | 
			
		||||
                        this.props.title.artist === "Mseq" ? (
 | 
			
		||||
                            <div id="copyrightNotice" className="col text-center">
 | 
			
		||||
                                <a href="http://dig.ccmixter.org/files/Mseq/54702">Journey</a>
 | 
			
		||||
               by Mseq (c) copyright 2016 Licensed under a Creative
 | 
			
		||||
              Commons 
 | 
			
		||||
              <a href="http://creativecommons.org/licenses/by-nc/3.0/">
 | 
			
		||||
                Attribution Noncommercial (3.0)
 | 
			
		||||
              </a>
 | 
			
		||||
                                <a href="http://creativecommons.org/licenses/by-nc/3.0/">
 | 
			
		||||
                                    Attribution Noncommercial (3.0)
 | 
			
		||||
                                </a>
 | 
			
		||||
                license. Ft: Admiral Bob,Texas Radio Fish
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mapStateToProps(state: State): ControlProps {
 | 
			
		||||
  return {
 | 
			
		||||
    title: state.musicState.title,
 | 
			
		||||
  };
 | 
			
		||||
    return {
 | 
			
		||||
        title: state.musicState.title,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(Controls);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,59 +2,59 @@ import React from "react";
 | 
			
		|||
import { connect } from "react-redux";
 | 
			
		||||
import classNames from "classnames";
 | 
			
		||||
 | 
			
		||||
import { State } from "../store";
 | 
			
		||||
import { MusicState, togglePlay } from "../store/music/types";
 | 
			
		||||
import { Dispatch, State } from "../store";
 | 
			
		||||
import { togglePlay } from "../store/music/types";
 | 
			
		||||
 | 
			
		||||
type IndicatorProps = {
 | 
			
		||||
  muted: boolean;
 | 
			
		||||
  playing: boolean;
 | 
			
		||||
    muted: boolean;
 | 
			
		||||
    playing: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type IndicatorDispatch = {
 | 
			
		||||
  play: () => void;
 | 
			
		||||
    play: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Props = IndicatorProps & IndicatorDispatch;
 | 
			
		||||
 | 
			
		||||
class Indicator extends React.Component<Props, {}> {
 | 
			
		||||
  click() {
 | 
			
		||||
    this.props.play();
 | 
			
		||||
  }
 | 
			
		||||
class Indicator extends React.Component<Props, State> {
 | 
			
		||||
    click() {
 | 
			
		||||
        this.props.play();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    let classes = classNames({
 | 
			
		||||
      btn: true,
 | 
			
		||||
      "col-auto": true,
 | 
			
		||||
      fas: true,
 | 
			
		||||
      "fa-muted": this.props.muted,
 | 
			
		||||
      "fa-play": this.props.playing,
 | 
			
		||||
      "fa-pause": !this.props.playing,
 | 
			
		||||
    });
 | 
			
		||||
    render() {
 | 
			
		||||
        const classes = classNames({
 | 
			
		||||
            btn: true,
 | 
			
		||||
            "col-auto": true,
 | 
			
		||||
            fas: true,
 | 
			
		||||
            "fa-muted": this.props.muted,
 | 
			
		||||
            "fa-play": this.props.playing,
 | 
			
		||||
            "fa-pause": !this.props.playing,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        type="button"
 | 
			
		||||
        id="playerIndicator"
 | 
			
		||||
        onClick={this.click.bind(this)}
 | 
			
		||||
        className={classes}
 | 
			
		||||
      ></button>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
        return (
 | 
			
		||||
            <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                id="playerIndicator"
 | 
			
		||||
                onClick={this.click.bind(this)}
 | 
			
		||||
                className={classes}
 | 
			
		||||
            ></button>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mapStateToProps(state: State): IndicatorProps {
 | 
			
		||||
  return {
 | 
			
		||||
    muted: state.musicState.muted,
 | 
			
		||||
    playing: state.musicState.playing,
 | 
			
		||||
  };
 | 
			
		||||
    return {
 | 
			
		||||
        muted: state.musicState.muted,
 | 
			
		||||
        playing: state.musicState.playing,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mapDispatchToProps(dispatch: Redux.Dispatch<any>): IndicatorDispatch {
 | 
			
		||||
  return {
 | 
			
		||||
    play: () => {
 | 
			
		||||
      dispatch(togglePlay());
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
function mapDispatchToProps(dispatch: Dispatch): IndicatorDispatch {
 | 
			
		||||
    return {
 | 
			
		||||
        play: () => {
 | 
			
		||||
            dispatch(togglePlay());
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(Indicator);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,218 +1,220 @@
 | 
			
		|||
import React from "react";
 | 
			
		||||
import * as three from "three";
 | 
			
		||||
 | 
			
		||||
import { State } from "../store";
 | 
			
		||||
 | 
			
		||||
type VisualizerProps = {
 | 
			
		||||
  audioContext: AudioContext;
 | 
			
		||||
  audioSource: AudioNode;
 | 
			
		||||
    audioContext: AudioContext;
 | 
			
		||||
    audioSource: AudioNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class CanvasDrawer {
 | 
			
		||||
  private analyser: AnalyserNode;
 | 
			
		||||
  private canvas: HTMLCanvasElement;
 | 
			
		||||
    private analyser: AnalyserNode;
 | 
			
		||||
    private canvas: HTMLCanvasElement;
 | 
			
		||||
 | 
			
		||||
  private analyserData: Float32Array;
 | 
			
		||||
    private analyserData: Float32Array;
 | 
			
		||||
 | 
			
		||||
  private boxes: Array<three.Mesh>;
 | 
			
		||||
  private camera: three.Camera;
 | 
			
		||||
  private renderer: three.Renderer;
 | 
			
		||||
  private scene: three.Scene;
 | 
			
		||||
    private boxes: Array<three.Mesh>;
 | 
			
		||||
    private camera: three.PerspectiveCamera;
 | 
			
		||||
    private renderer: three.WebGLRenderer;
 | 
			
		||||
    private scene: three.Scene;
 | 
			
		||||
 | 
			
		||||
  private angle: number;
 | 
			
		||||
    private angle: number;
 | 
			
		||||
 | 
			
		||||
  private animationFrame: number;
 | 
			
		||||
  private lastTime: number;
 | 
			
		||||
    private animationFrame: number;
 | 
			
		||||
    private lastTime: number;
 | 
			
		||||
 | 
			
		||||
  constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) {
 | 
			
		||||
    this.analyser = analyser;
 | 
			
		||||
    this.canvas = canvas;
 | 
			
		||||
    constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) {
 | 
			
		||||
        this.analyser = analyser;
 | 
			
		||||
        this.canvas = canvas;
 | 
			
		||||
 | 
			
		||||
    // Set up analyser data storage
 | 
			
		||||
    this.analyserData = new Float32Array(analyser.frequencyBinCount);
 | 
			
		||||
        // Set up analyser data storage
 | 
			
		||||
        this.analyserData = new Float32Array(analyser.frequencyBinCount);
 | 
			
		||||
 | 
			
		||||
    // Initialize the scene
 | 
			
		||||
    this.scene = new three.Scene();
 | 
			
		||||
        // Initialize the scene
 | 
			
		||||
        this.scene = new three.Scene();
 | 
			
		||||
 | 
			
		||||
    // Make a bunch of boxes to represent the bars
 | 
			
		||||
    this.boxes = Array(analyser.frequencyBinCount);
 | 
			
		||||
    let width = 2 / analyser.frequencyBinCount;
 | 
			
		||||
    for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
 | 
			
		||||
      let geometry = new three.BoxGeometry(1, 1, 1);
 | 
			
		||||
      let material = new three.MeshLambertMaterial({
 | 
			
		||||
        color: new three.Color(0x99d1ce),
 | 
			
		||||
      });
 | 
			
		||||
      let cube = new three.Mesh(geometry, material);
 | 
			
		||||
        // Make a bunch of boxes to represent the bars
 | 
			
		||||
        this.boxes = Array(analyser.frequencyBinCount);
 | 
			
		||||
        const width = 2 / analyser.frequencyBinCount;
 | 
			
		||||
        for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
 | 
			
		||||
            const geometry = new three.BoxGeometry(1, 1, 1);
 | 
			
		||||
            const material = new three.MeshLambertMaterial({
 | 
			
		||||
                color: new three.Color(0x99d1ce),
 | 
			
		||||
            });
 | 
			
		||||
            const cube = new three.Mesh(geometry, material);
 | 
			
		||||
 | 
			
		||||
      cube.scale.set(width, 1e-6, width);
 | 
			
		||||
      cube.position.set(-1 + freq * width, 0, 0);
 | 
			
		||||
            cube.scale.set(width, 1e-6, width);
 | 
			
		||||
            cube.position.set(-1 + freq * width, 0, 0);
 | 
			
		||||
 | 
			
		||||
      this.scene.add(cube);
 | 
			
		||||
      this.boxes[freq] = cube;
 | 
			
		||||
            this.scene.add(cube);
 | 
			
		||||
            this.boxes[freq] = cube;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add lights for shadowing
 | 
			
		||||
        const ambientLight = new three.AmbientLight(0xffffff, 0.4);
 | 
			
		||||
        this.scene.add(ambientLight);
 | 
			
		||||
 | 
			
		||||
        const directionalLight = new three.DirectionalLight(0xffffff, 1);
 | 
			
		||||
        directionalLight.position.set(-1, 0.3, -1);
 | 
			
		||||
        directionalLight.castShadow = true;
 | 
			
		||||
        this.scene.add(directionalLight);
 | 
			
		||||
 | 
			
		||||
        // Add a camera
 | 
			
		||||
        this.angle = 3;
 | 
			
		||||
        this.camera = new three.PerspectiveCamera(
 | 
			
		||||
            70,
 | 
			
		||||
            canvas.width / canvas.height,
 | 
			
		||||
            0.01,
 | 
			
		||||
            10
 | 
			
		||||
        );
 | 
			
		||||
        this.camera.lookAt(0, 0, 0);
 | 
			
		||||
        this.scene.add(this.camera);
 | 
			
		||||
        this.rotateCamera(1);
 | 
			
		||||
 | 
			
		||||
        // Add a renderer
 | 
			
		||||
        this.renderer = new three.WebGLRenderer({
 | 
			
		||||
            antialias: true,
 | 
			
		||||
            canvas: canvas,
 | 
			
		||||
            powerPreference: "low-power",
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.renderer.setClearColor(new three.Color(0x0f0f0f));
 | 
			
		||||
        this.renderer.setSize(canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
        // Set up canvas resizing
 | 
			
		||||
        window.addEventListener("resize", this.resize.bind(this));
 | 
			
		||||
 | 
			
		||||
        // Run the first, set the first animation frame time and start requesting
 | 
			
		||||
        // animation frames
 | 
			
		||||
        this.resize();
 | 
			
		||||
        this.lastTime = 0;
 | 
			
		||||
        this.animationFrame = requestAnimationFrame(this.render.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add lights for shadowing
 | 
			
		||||
    let ambientLight = new three.AmbientLight(0xffffff, 0.4);
 | 
			
		||||
    this.scene.add(ambientLight);
 | 
			
		||||
    render(time: number) {
 | 
			
		||||
        // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame
 | 
			
		||||
        this.animationFrame = 0;
 | 
			
		||||
        // Update elapsed time
 | 
			
		||||
        const elapsed = time - this.lastTime;
 | 
			
		||||
        this.lastTime = time;
 | 
			
		||||
 | 
			
		||||
    let directionalLight = new three.DirectionalLight(0xffffff, 1);
 | 
			
		||||
    directionalLight.position.set(-1, 0.3, -1);
 | 
			
		||||
    directionalLight.castShadow = true;
 | 
			
		||||
    this.scene.add(directionalLight);
 | 
			
		||||
        const camera = this.camera;
 | 
			
		||||
        const renderer = this.renderer;
 | 
			
		||||
        const scene = this.scene;
 | 
			
		||||
 | 
			
		||||
    // Add a camera
 | 
			
		||||
    this.angle = 3;
 | 
			
		||||
    this.camera = new three.PerspectiveCamera(
 | 
			
		||||
      70,
 | 
			
		||||
      canvas.width / canvas.height,
 | 
			
		||||
      0.01,
 | 
			
		||||
      10
 | 
			
		||||
    );
 | 
			
		||||
    this.camera.lookAt(0, 0, 0);
 | 
			
		||||
    this.scene.add(this.camera);
 | 
			
		||||
    this.rotateCamera(1);
 | 
			
		||||
        this.scaleBoxes();
 | 
			
		||||
        this.rotateCamera(elapsed);
 | 
			
		||||
 | 
			
		||||
    // Add a renderer
 | 
			
		||||
    this.renderer = new three.WebGLRenderer({
 | 
			
		||||
      antialias: true,
 | 
			
		||||
      canvas: canvas,
 | 
			
		||||
      powerPreference: "low-power",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.renderer.setClearColor(new three.Color(0x0f0f0f));
 | 
			
		||||
    this.renderer.setSize(canvas.width, canvas.height);
 | 
			
		||||
 | 
			
		||||
    // Set up canvas resizing
 | 
			
		||||
    window.addEventListener("resize", this.resize.bind(this));
 | 
			
		||||
 | 
			
		||||
    // Run the first, set the first animation frame time and start requesting
 | 
			
		||||
    // animation frames
 | 
			
		||||
    this.resize();
 | 
			
		||||
    this.lastTime = 0;
 | 
			
		||||
    this.animationFrame = requestAnimationFrame(this.render.bind(this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(time: number) {
 | 
			
		||||
    // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame
 | 
			
		||||
    this.animationFrame = 0;
 | 
			
		||||
    // Update elapsed time
 | 
			
		||||
    let elapsed = time - this.lastTime;
 | 
			
		||||
    this.lastTime = time;
 | 
			
		||||
 | 
			
		||||
    let camera = this.camera;
 | 
			
		||||
    let renderer = this.renderer;
 | 
			
		||||
    let scene = this.scene;
 | 
			
		||||
 | 
			
		||||
    this.scaleBoxes();
 | 
			
		||||
    this.rotateCamera(elapsed);
 | 
			
		||||
 | 
			
		||||
    renderer.render(scene, camera);
 | 
			
		||||
    this.animationFrame = requestAnimationFrame(this.render.bind(this));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  scaleBoxes() {
 | 
			
		||||
    let analyser = this.analyser;
 | 
			
		||||
 | 
			
		||||
    analyser.getFloatFrequencyData(this.analyserData);
 | 
			
		||||
 | 
			
		||||
    for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
 | 
			
		||||
      let height = analyser.maxDecibels / this.analyserData[freq];
 | 
			
		||||
 | 
			
		||||
      if (height > 0.3) {
 | 
			
		||||
        height -= 0.3;
 | 
			
		||||
      } else {
 | 
			
		||||
        height = 1e-6;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.boxes[freq].scale.y = height;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rotateCamera(elapsed: number) {
 | 
			
		||||
    if (this.angle >= Math.PI * 2) {
 | 
			
		||||
      this.angle = 0;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.angle += 0.1 * (elapsed / 1000);
 | 
			
		||||
        renderer.render(scene, camera);
 | 
			
		||||
        this.animationFrame = requestAnimationFrame(this.render.bind(this));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let camera = this.camera;
 | 
			
		||||
    let angle = this.angle;
 | 
			
		||||
    scaleBoxes() {
 | 
			
		||||
        const analyser = this.analyser;
 | 
			
		||||
 | 
			
		||||
    camera.position.x = 1.01 * Math.sin(angle);
 | 
			
		||||
    camera.position.z = 1.01 * Math.cos(angle);
 | 
			
		||||
        analyser.getFloatFrequencyData(this.analyserData);
 | 
			
		||||
 | 
			
		||||
    /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */
 | 
			
		||||
    camera.lookAt(0, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
        for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
 | 
			
		||||
            let height = analyser.maxDecibels / this.analyserData[freq];
 | 
			
		||||
 | 
			
		||||
  resize() {
 | 
			
		||||
    let canvas = this.canvas;
 | 
			
		||||
    if (canvas.parentElement === null) {
 | 
			
		||||
      throw Error("Could not access canvas parent for size calculation");
 | 
			
		||||
            if (height > 0.3) {
 | 
			
		||||
                height -= 0.3;
 | 
			
		||||
            } else {
 | 
			
		||||
                height = 1e-6;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.boxes[freq].scale.y = height;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Compute the height of all our siblings
 | 
			
		||||
    let combinedHeight = 0;
 | 
			
		||||
    for (let i = 0; i < canvas.parentElement.children.length; i++) {
 | 
			
		||||
      const child = canvas.parentElement.children[i];
 | 
			
		||||
    rotateCamera(elapsed: number) {
 | 
			
		||||
        if (this.angle >= Math.PI * 2) {
 | 
			
		||||
            this.angle = 0;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.angle += 0.1 * (elapsed / 1000);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      if (child != canvas) {
 | 
			
		||||
        combinedHeight += child.clientHeight;
 | 
			
		||||
      }
 | 
			
		||||
        const camera = this.camera;
 | 
			
		||||
        const angle = this.angle;
 | 
			
		||||
 | 
			
		||||
        camera.position.x = 1.01 * Math.sin(angle);
 | 
			
		||||
        camera.position.z = 1.01 * Math.cos(angle);
 | 
			
		||||
 | 
			
		||||
        /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */
 | 
			
		||||
        camera.lookAt(0, 0, 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The remaining space we want to fill
 | 
			
		||||
    let remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
 | 
			
		||||
    canvas.height = remainingHeight;
 | 
			
		||||
    canvas.width = canvas.parentElement.clientWidth;
 | 
			
		||||
    resize() {
 | 
			
		||||
        const canvas = this.canvas;
 | 
			
		||||
        if (canvas.parentElement === null) {
 | 
			
		||||
            throw Error("Could not access canvas parent for size calculation");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    this.camera.aspect = canvas.width / remainingHeight;
 | 
			
		||||
    this.camera.updateProjectionMatrix();
 | 
			
		||||
    this.renderer.setSize(canvas.width, remainingHeight);
 | 
			
		||||
  }
 | 
			
		||||
        // Compute the height of all our siblings
 | 
			
		||||
        let combinedHeight = 0;
 | 
			
		||||
        for (let i = 0; i < canvas.parentElement.children.length; i++) {
 | 
			
		||||
            const child = canvas.parentElement.children[i];
 | 
			
		||||
 | 
			
		||||
  stop() {
 | 
			
		||||
    if (this.animationFrame != 0) {
 | 
			
		||||
      cancelAnimationFrame(this.animationFrame);
 | 
			
		||||
            if (child != canvas) {
 | 
			
		||||
                combinedHeight += child.clientHeight;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The remaining space we want to fill
 | 
			
		||||
        const remainingHeight = canvas.parentElement.clientHeight - combinedHeight;
 | 
			
		||||
        canvas.height = remainingHeight;
 | 
			
		||||
        canvas.width = canvas.parentElement.clientWidth;
 | 
			
		||||
 | 
			
		||||
        this.camera.aspect = canvas.width / remainingHeight;
 | 
			
		||||
        this.camera.updateProjectionMatrix();
 | 
			
		||||
        this.renderer.setSize(canvas.width, remainingHeight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stop() {
 | 
			
		||||
        if (this.animationFrame != 0) {
 | 
			
		||||
            cancelAnimationFrame(this.animationFrame);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Visualizer extends React.Component<VisualizerProps, {}> {
 | 
			
		||||
  private analyser: AnalyserNode;
 | 
			
		||||
  private canvas: React.RefObject<HTMLCanvasElement>;
 | 
			
		||||
  private drawer: CanvasDrawer;
 | 
			
		||||
class Visualizer extends React.Component<VisualizerProps, State> {
 | 
			
		||||
    private analyser: AnalyserNode;
 | 
			
		||||
    private canvas: React.RefObject<HTMLCanvasElement>;
 | 
			
		||||
    private drawer: CanvasDrawer;
 | 
			
		||||
 | 
			
		||||
  constructor(props: VisualizerProps) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.canvas = React.createRef();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <canvas
 | 
			
		||||
        id="visualizer"
 | 
			
		||||
        ref={this.canvas}
 | 
			
		||||
        style={{ width: "100%", height: "100%" }}
 | 
			
		||||
      ></canvas>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    if (this.canvas.current === null) {
 | 
			
		||||
      throw Error("Failed to create canvas; aborting");
 | 
			
		||||
    constructor(props: VisualizerProps) {
 | 
			
		||||
        super(props);
 | 
			
		||||
        this.canvas = React.createRef();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.analyser = this.props.audioContext.createAnalyser();
 | 
			
		||||
    this.analyser.fftSize = 2048;
 | 
			
		||||
    this.analyser.smoothingTimeConstant = 0.8;
 | 
			
		||||
    this.props.audioSource.connect(this.analyser);
 | 
			
		||||
    render(): React.ReactNode {
 | 
			
		||||
        return (
 | 
			
		||||
            <canvas
 | 
			
		||||
                id="visualizer"
 | 
			
		||||
                ref={this.canvas}
 | 
			
		||||
                style={{ width: "100%", height: "100%" }}
 | 
			
		||||
            ></canvas>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
 | 
			
		||||
  }
 | 
			
		||||
    componentDidMount(): void {
 | 
			
		||||
        if (this.canvas.current === null) {
 | 
			
		||||
            throw Error("Failed to create canvas; aborting");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    this.drawer.stop();
 | 
			
		||||
    this.props.audioSource.disconnect(this.analyser);
 | 
			
		||||
  }
 | 
			
		||||
        this.analyser = this.props.audioContext.createAnalyser();
 | 
			
		||||
        this.analyser.fftSize = 2048;
 | 
			
		||||
        this.analyser.smoothingTimeConstant = 0.8;
 | 
			
		||||
        this.props.audioSource.connect(this.analyser);
 | 
			
		||||
 | 
			
		||||
        this.drawer = new CanvasDrawer(this.analyser, this.canvas.current);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount(): void {
 | 
			
		||||
        this.drawer.stop();
 | 
			
		||||
        this.props.audioSource.disconnect(this.analyser);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Visualizer;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,23 +5,24 @@ import { Provider } from "react-redux";
 | 
			
		|||
import { store } from "./store";
 | 
			
		||||
import MusicPlayer from "./MusicPlayer";
 | 
			
		||||
import { setSource, setTitle } from "./store/music/types";
 | 
			
		||||
// @ts-ignore - mp3 files have no types.
 | 
			
		||||
import mseq from "./Mseq_-_Journey.mp3";
 | 
			
		||||
 | 
			
		||||
const rootElement = document.getElementById("playerUI");
 | 
			
		||||
 | 
			
		||||
ReactDOM.render(
 | 
			
		||||
  <Provider store={store}>
 | 
			
		||||
    <MusicPlayer />
 | 
			
		||||
  </Provider>,
 | 
			
		||||
  rootElement
 | 
			
		||||
    <Provider store={store}>
 | 
			
		||||
        <MusicPlayer />
 | 
			
		||||
    </Provider>,
 | 
			
		||||
    rootElement
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
store.dispatch(setSource(mseq));
 | 
			
		||||
store.dispatch(
 | 
			
		||||
  setTitle({
 | 
			
		||||
    name: "Journey",
 | 
			
		||||
    artist: "Mseq",
 | 
			
		||||
    album: "Unknown album",
 | 
			
		||||
    length: 192052244,
 | 
			
		||||
  })
 | 
			
		||||
    setTitle({
 | 
			
		||||
        name: "Journey",
 | 
			
		||||
        artist: "Mseq",
 | 
			
		||||
        album: "Unknown album",
 | 
			
		||||
        length: 192052244,
 | 
			
		||||
    })
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,14 +4,17 @@ import { MusicState } from "./music/types";
 | 
			
		|||
import { musicStateReducer } from "./music/reducers";
 | 
			
		||||
 | 
			
		||||
export interface State {
 | 
			
		||||
  musicState: MusicState;
 | 
			
		||||
    musicState: MusicState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rootReducer = combineReducers<State>({
 | 
			
		||||
  musicState: musicStateReducer,
 | 
			
		||||
    musicState: musicStateReducer,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const store = createStore(
 | 
			
		||||
  rootReducer,
 | 
			
		||||
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 | 
			
		||||
    rootReducer,
 | 
			
		||||
    // @ts-ignore - These properties are set by the devtools extension
 | 
			
		||||
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export type Dispatch = typeof store.dispatch;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,44 +2,44 @@ import { createReducer } from "redux-act";
 | 
			
		|||
import update from "immutability-helper";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Title,
 | 
			
		||||
  MusicState,
 | 
			
		||||
  setTitle,
 | 
			
		||||
  toggleMute,
 | 
			
		||||
  togglePlay,
 | 
			
		||||
  setSource,
 | 
			
		||||
    Title,
 | 
			
		||||
    MusicState,
 | 
			
		||||
    setTitle,
 | 
			
		||||
    toggleMute,
 | 
			
		||||
    togglePlay,
 | 
			
		||||
    setSource,
 | 
			
		||||
} from "./types";
 | 
			
		||||
 | 
			
		||||
const defaultTitle: Title = {
 | 
			
		||||
  name: "Untitled",
 | 
			
		||||
  artist: "Unknown Artist",
 | 
			
		||||
  album: "Unknown Album",
 | 
			
		||||
  length: 0,
 | 
			
		||||
    name: "Untitled",
 | 
			
		||||
    artist: "Unknown Artist",
 | 
			
		||||
    album: "Unknown Album",
 | 
			
		||||
    length: 0,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const initialState: MusicState = {
 | 
			
		||||
  muted: false,
 | 
			
		||||
  playing: false,
 | 
			
		||||
  title: defaultTitle,
 | 
			
		||||
  playTime: 0,
 | 
			
		||||
    muted: false,
 | 
			
		||||
    playing: false,
 | 
			
		||||
    title: defaultTitle,
 | 
			
		||||
    playTime: 0,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const musicStateReducer = createReducer<MusicState>(
 | 
			
		||||
  {
 | 
			
		||||
    [setTitle]: (state: MusicState, title: Title): MusicState => {
 | 
			
		||||
      return update(state, {
 | 
			
		||||
        title: { $set: title },
 | 
			
		||||
      });
 | 
			
		||||
    {
 | 
			
		||||
        [setTitle]: (state: MusicState, title: Title): MusicState => {
 | 
			
		||||
            return update(state, {
 | 
			
		||||
                title: { $set: title },
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [togglePlay]: (state: MusicState): MusicState => {
 | 
			
		||||
            return update(state, { $toggle: ["playing"] });
 | 
			
		||||
        },
 | 
			
		||||
        [toggleMute]: (state: MusicState): MusicState => {
 | 
			
		||||
            return update(state, { $toggle: ["muted"] });
 | 
			
		||||
        },
 | 
			
		||||
        [setSource]: (state: MusicState, source: string): MusicState => {
 | 
			
		||||
            return update(state, { source: { $set: source } });
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    [togglePlay]: (state: MusicState): MusicState => {
 | 
			
		||||
      return update(state, { $toggle: ["playing"] });
 | 
			
		||||
    },
 | 
			
		||||
    [toggleMute]: (state: MusicState): MusicState => {
 | 
			
		||||
      return update(state, { $toggle: ["muted"] });
 | 
			
		||||
    },
 | 
			
		||||
    [setSource]: (state: MusicState, source: string): MusicState => {
 | 
			
		||||
      return update(state, { source: { $set: source } });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  initialState
 | 
			
		||||
    initialState
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,35 +1,35 @@
 | 
			
		|||
import { Action, createAction } from "redux-act";
 | 
			
		||||
 | 
			
		||||
export interface Title {
 | 
			
		||||
  name: string;
 | 
			
		||||
  artist: string;
 | 
			
		||||
  album: string;
 | 
			
		||||
  /**
 | 
			
		||||
   * The length of the title in nanoseconds.
 | 
			
		||||
   */
 | 
			
		||||
  length: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    artist: string;
 | 
			
		||||
    album: string;
 | 
			
		||||
    /**
 | 
			
		||||
     * The length of the title in nanoseconds.
 | 
			
		||||
     */
 | 
			
		||||
    length: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MusicState {
 | 
			
		||||
  muted: boolean;
 | 
			
		||||
  playing: boolean;
 | 
			
		||||
  title: Title;
 | 
			
		||||
  playTime: number;
 | 
			
		||||
  source?: string;
 | 
			
		||||
    muted: boolean;
 | 
			
		||||
    playing: boolean;
 | 
			
		||||
    title: Title;
 | 
			
		||||
    playTime: number;
 | 
			
		||||
    source?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const setTitle: (title: Title) => Action<null, null> = createAction(
 | 
			
		||||
  "set currently playing title"
 | 
			
		||||
export const setTitle: (_title: Title) => Action<null, null> = createAction(
 | 
			
		||||
    "set currently playing title"
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const setPlayTime: (time: number) => Action<null, null> = createAction(
 | 
			
		||||
  "set the play time"
 | 
			
		||||
export const setPlayTime: (_time: number) => Action<null, null> = createAction(
 | 
			
		||||
    "set the play time"
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const toggleMute: () => Action<null, null> = createAction("toggle mute");
 | 
			
		||||
 | 
			
		||||
export const togglePlay: () => Action<null, null> = createAction("toggle play");
 | 
			
		||||
 | 
			
		||||
export const setSource: (source: string) => Action<null, null> = createAction(
 | 
			
		||||
  "set the title"
 | 
			
		||||
export const setSource: (_source: string) => Action<null, null> = createAction(
 | 
			
		||||
    "set the title"
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue