Integrate previously external templates into this repository #12
|
@ -1,4 +1,4 @@
|
||||||
((nil . ((indent-tabs-mode . nil)
|
((rust-mode . ((indent-tabs-mode . nil)
|
||||||
(tab-width . 4)
|
(tab-width . 4)
|
||||||
(fill-column . 80)
|
(fill-column . 80)
|
||||||
(projectile-project-run-cmd . "cargo run -- --dev-mode --template-directory ~/Documents/Projects/tlaternet-templates/result"))))
|
(projectile-project-run-cmd . "cd server && cargo run -- --dev-mode --template-directory ~/Documents/Projects/tlaternet-templates/result"))))
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
||||||
/target
|
/server/target
|
||||||
/templates/
|
/result
|
||||||
|
|
314
flake.lock
314
flake.lock
|
@ -1,56 +1,208 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"alejandra": {
|
||||||
"locked": {
|
|
||||||
"lastModified": 1656928814,
|
|
||||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"naersk": {
|
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flakeCompat": "flakeCompat",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
|
"dream2nix",
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1662220400,
|
"lastModified": 1658427149,
|
||||||
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=",
|
"narHash": "sha256-ToD/1z/q5VHsLMrS2h96vjJoLho59eNRtknOUd19ey8=",
|
||||||
"owner": "nmattia",
|
"owner": "kamadorueda",
|
||||||
"repo": "naersk",
|
"repo": "alejandra",
|
||||||
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3",
|
"rev": "f5a22afd2adfb249b4e68e0b33aa1f0fb73fb1be",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nmattia",
|
"owner": "kamadorueda",
|
||||||
"repo": "naersk",
|
"repo": "alejandra",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nix-filter": {
|
"crane": {
|
||||||
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1661201956,
|
"lastModified": 1661875961,
|
||||||
"narHash": "sha256-RizGJH/buaw9A2+fiBf9WnXYw4LZABB5kMAZIEE5/T8=",
|
"narHash": "sha256-f1h/2c6Teeu1ofAHWzrS8TwBPcnN+EEu+z1sRVmMQTk=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "d9f394e4e20e97c2a60c3ad82c2b6ef99be19e24",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devshell": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1653917170,
|
||||||
|
"narHash": "sha256-FyxOnEE/V4PNEcMU62ikY4FfYPo349MOhMM97HS0XEo=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "nix-filter",
|
"repo": "devshell",
|
||||||
"rev": "3b821578685d661a10b563cba30b1861eec05748",
|
"rev": "fc7a3e3adde9bbcab68af6d1e3c6eb738e296a92",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "nix-filter",
|
"repo": "devshell",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dream2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"alejandra": "alejandra",
|
||||||
|
"crane": "crane",
|
||||||
|
"devshell": "devshell",
|
||||||
|
"flake-utils-pre-commit": "flake-utils-pre-commit",
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
|
"mach-nix": "mach-nix",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"poetry2nix": "poetry2nix",
|
||||||
|
"pre-commit-hooks": "pre-commit-hooks"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1663323895,
|
||||||
|
"narHash": "sha256-ZmI9C8HNVz2w3OnB79WR/LIgVEY8tDnR8tEPi3hMiJk=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "dream2nix",
|
||||||
|
"rev": "25be741ec92c77b8308ca6a7ab89593fe37b6542",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "dream2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"dream2nix",
|
||||||
|
"alejandra",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1657607339,
|
||||||
|
"narHash": "sha256-HaqoAwlbVVZH2n4P3jN2FFPMpVuhxDy1poNOR7kzODc=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "b814c83d9e6aa5a28d0cf356ecfdafb2505ad37d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix_2": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1663396212,
|
||||||
|
"narHash": "sha256-dlK10QPTDYNpJ/vl2QPKOTrqEbQwAR/v2f4+xsetTkw=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "263cd7f991c07a9592a6e825bfc37b23b00eb244",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils-pre-commit": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1644229661,
|
||||||
|
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flakeCompat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1650374568,
|
||||||
|
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gomod2nix": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1627572165,
|
||||||
|
"narHash": "sha256-MFpwnkvQpauj799b4QTBJQFEddbD02+Ln5k92QyHOSk=",
|
||||||
|
"owner": "tweag",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"rev": "67f22dd738d092c6ba88e420350ada0ed4992ae8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tweag",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mach-nix": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1634711045,
|
||||||
|
"narHash": "sha256-m5A2Ty88NChLyFhXucECj6+AuiMZPHXNbw+9Kcs7F6Y=",
|
||||||
|
"owner": "DavHau",
|
||||||
|
"repo": "mach-nix",
|
||||||
|
"rev": "4433f74a97b94b596fa6cd9b9c0402104aceef5d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "mach-nix",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1657638268,
|
||||||
|
"narHash": "sha256-blBNtQSslAFkg0Gym9fWNJk+bPxGSZib4SOcPrmTPi4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "d80993b5f885515254746ba6d1917276ee386149",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1663244735,
|
"lastModified": 1663244735,
|
||||||
"narHash": "sha256-+EukKkeAx6ithOLM1u5x4D12ZFuoi6vpPYjhNDmLz1o=",
|
"narHash": "sha256-+EukKkeAx6ithOLM1u5x4D12ZFuoi6vpPYjhNDmLz1o=",
|
||||||
|
@ -66,85 +218,87 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"poetry2nix": {
|
||||||
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1660318005,
|
"lastModified": 1632969109,
|
||||||
"narHash": "sha256-g9WCa9lVUmOV6dYRbEPjv/TLOR5hamjeCcKExVGS3OQ=",
|
"narHash": "sha256-jPDclkkiAy5m2gGLBlKgH+lQtbF7tL4XxBrbSzw+Ioc=",
|
||||||
"owner": "nixos",
|
"owner": "nix-community",
|
||||||
"repo": "nixpkgs",
|
"repo": "poetry2nix",
|
||||||
"rev": "5c211b47aeadcc178c5320afd4e74c7eed5c389f",
|
"rev": "aee8f04296c39d88155e05d25cfc59dfdd41cc77",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nix-community",
|
||||||
"ref": "nixos-22.05",
|
"ref": "1.21.0",
|
||||||
"repo": "nixpkgs",
|
"repo": "poetry2nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npmlock2nix": {
|
"pre-commit-hooks": {
|
||||||
"flake": false,
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"dream2nix",
|
||||||
|
"flake-utils-pre-commit"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"dream2nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1654775747,
|
"lastModified": 1646153636,
|
||||||
"narHash": "sha256-9pXHDpIjmsK5390wmpGHu9aA4QOPpegPBvThHeBlef4=",
|
"narHash": "sha256-AlWHMzK+xJ1mG267FdT8dCq/HvLCA6jwmx2ZUy5O8tY=",
|
||||||
"owner": "nix-community",
|
"owner": "cachix",
|
||||||
"repo": "npmlock2nix",
|
"repo": "pre-commit-hooks.nix",
|
||||||
"rev": "5c4f247688fc91d665df65f71c81e0726621aaa8",
|
"rev": "b6bc0b21e1617e2b07d8205e7fae7224036dfa4b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "cachix",
|
||||||
"repo": "npmlock2nix",
|
"repo": "pre-commit-hooks.nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"naersk": "naersk",
|
"dream2nix": "dream2nix",
|
||||||
"nixpkgs": "nixpkgs",
|
"fenix": "fenix_2",
|
||||||
"rust-overlay": "rust-overlay",
|
"nixpkgs": "nixpkgs_2"
|
||||||
"tlaternet-templates": "tlaternet-templates"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-overlay": {
|
"rust-analyzer-src": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1663297375,
|
"lastModified": 1657557289,
|
||||||
"narHash": "sha256-7pjd2x9fSXXynIzp9XiXjbYys7sR6MKCot/jfGL7dgE=",
|
"narHash": "sha256-PRW+nUwuqNTRAEa83SfX+7g+g8nQ+2MMbasQ9nt6+UM=",
|
||||||
"owner": "oxalica",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-analyzer",
|
||||||
"rev": "0678b6187a153eb0baa9688335b002fe14ba6712",
|
"rev": "caf23f29144b371035b864a1017dbc32573ad56d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "oxalica",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-overlay",
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tlaternet-templates": {
|
"rust-analyzer-src_2": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"nix-filter": "nix-filter",
|
|
||||||
"nixpkgs": "nixpkgs_2",
|
|
||||||
"npmlock2nix": "npmlock2nix"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1663345814,
|
"lastModified": 1662896065,
|
||||||
"narHash": "sha256-wIl8P+Hv8zHwBATlEoppPNJMpcR2EiQ4dbkgGXszmf8=",
|
"narHash": "sha256-1LkSsXzI1JTAmP/GMTz4fTJd8y/tw8R79l96q+h7mu8=",
|
||||||
"ref": "master",
|
"owner": "rust-lang",
|
||||||
"rev": "789431c13cf1e906cbaf48e9b1078056c8ec3cc8",
|
"repo": "rust-analyzer",
|
||||||
"revCount": 111,
|
"rev": "2e9f1204ca01c3e20898d4a67c8b84899d394a88",
|
||||||
"type": "git",
|
"type": "github"
|
||||||
"url": "https://gitea.tlater.net/tlaternet/tlaternet-templates.git"
|
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "git",
|
"owner": "rust-lang",
|
||||||
"url": "https://gitea.tlater.net/tlaternet/tlaternet-templates.git"
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
95
flake.nix
95
flake.nix
|
@ -3,78 +3,63 @@
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05";
|
||||||
rust-overlay = {
|
dream2nix.url = "github:nix-community/dream2nix";
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
naersk = {
|
|
||||||
url = "github:nmattia/naersk";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
tlaternet-templates = {
|
fenix = {
|
||||||
url = "git+https://gitea.tlater.net/tlaternet/tlaternet-templates.git";
|
url = "github:nix-community/fenix";
|
||||||
# No need to override anything here; we can save some downloads
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
# if we rely on the webserver to do that.
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
rust-overlay,
|
dream2nix,
|
||||||
naersk,
|
fenix,
|
||||||
tlaternet-templates,
|
|
||||||
}: let
|
}: let
|
||||||
# At the moment, we only deploy to x86_64-linux. Update when we
|
# At the moment, we only deploy to x86_64-linux. Update when we
|
||||||
# care about another platform.
|
# care about another platform.
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
overlays = [
|
flakeOutputs = import ./nix/packages.nix {inherit nixpkgs dream2nix fenix system;};
|
||||||
rust-overlay.overlays.default
|
|
||||||
];
|
|
||||||
pkgs = import nixpkgs {inherit system overlays;};
|
|
||||||
|
|
||||||
# Rust build config
|
|
||||||
rust-toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
|
||||||
naersk-lib = naersk.lib.${system}.override {
|
|
||||||
cargo = rust-toolchain;
|
|
||||||
rustc = rust-toolchain;
|
|
||||||
};
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
pkg-config
|
|
||||||
openssl
|
|
||||||
];
|
|
||||||
in {
|
in {
|
||||||
packages.${system} = rec {
|
packages.${system} = {
|
||||||
tlaternet-webserver = naersk-lib.buildPackage {
|
server = flakeOutputs.server.packages.${system}.default;
|
||||||
inherit buildInputs;
|
templates = flakeOutputs.templates.packages.${system}.default;
|
||||||
pname = "tlaternet-webserver";
|
|
||||||
root = pkgs.lib.cleanSource self;
|
|
||||||
};
|
|
||||||
default = tlaternet-webserver;
|
|
||||||
};
|
|
||||||
|
|
||||||
apps.${system} = {
|
|
||||||
run-with-templates = let
|
|
||||||
script = pkgs.writeShellScriptBin "run-with-templates" ''
|
|
||||||
RUST_LOG=info ${self.packages.${system}.tlaternet-webserver}/bin/tlaternet-webserver \
|
|
||||||
--template-directory ${tlaternet-templates.packages.${system}.default}
|
|
||||||
'';
|
|
||||||
in {
|
|
||||||
type = "app";
|
|
||||||
program = "${script}/bin/run-with-templates";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.${system} = {
|
devShells.${system} = {
|
||||||
default = pkgs.mkShell {
|
templates = flakeOutputs.templates.devShells.${system}.default.overrideAttrs (old: {
|
||||||
packages = builtins.concatLists [
|
buildInputs = with nixpkgs.legacyPackages.${system};
|
||||||
buildInputs
|
|
||||||
[
|
[
|
||||||
(rust-toolchain.override {
|
yj
|
||||||
extensions = ["rust-src" "rust-analysis" "rust-analyzer-preview"];
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
|
++ old.buildInputs;
|
||||||
|
|
||||||
|
shellHook =
|
||||||
|
''
|
||||||
|
# Update package.json
|
||||||
|
if [ -e ./package.json ]; then
|
||||||
|
unlink ./package.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat ./package.yaml | yj > ./package.json
|
||||||
|
''
|
||||||
|
+ old.shellHook;
|
||||||
|
});
|
||||||
|
|
||||||
|
server = nixpkgs.legacyPackages.${system}.mkShell {
|
||||||
|
packages = [
|
||||||
|
(flakeOutputs.server.rust-toolchain.withComponents [
|
||||||
|
"rustc"
|
||||||
|
"cargo"
|
||||||
|
"rustfmt"
|
||||||
|
"rust-std"
|
||||||
|
"rust-docs"
|
||||||
|
"clippy"
|
||||||
|
"rust-src"
|
||||||
|
"rust-analysis"
|
||||||
|
])
|
||||||
|
fenix.packages.${system}.rust-analyzer
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
94
nix/packages.nix
Normal file
94
nix/packages.nix
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
dream2nix,
|
||||||
|
fenix,
|
||||||
|
system,
|
||||||
|
}: {
|
||||||
|
server = let
|
||||||
|
rust-toolchain = fenix.packages.${system}.stable;
|
||||||
|
in
|
||||||
|
dream2nix.lib.makeFlakeOutputs {
|
||||||
|
systems = [system];
|
||||||
|
config.projectRoot = ../server;
|
||||||
|
source = ../server;
|
||||||
|
|
||||||
|
packageOverrides = {
|
||||||
|
"^.*".set-toolchain.overrideRustToolchain = old: {
|
||||||
|
cargo = rust-toolchain.minimalToolchain;
|
||||||
|
rustc = rust-toolchain.minimalToolchain;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
inherit rust-toolchain;
|
||||||
|
};
|
||||||
|
|
||||||
|
templates = let
|
||||||
|
inherit (nixpkgs.legacyPackages.${system}) runCommandLocal yj;
|
||||||
|
in
|
||||||
|
dream2nix.lib.makeFlakeOutputs {
|
||||||
|
systems = [system];
|
||||||
|
config.projectRoot = ../templates;
|
||||||
|
|
||||||
|
# Generate `package.json` from `package.yaml`, since the nodejs
|
||||||
|
# ecosystem doesn't support yaml.
|
||||||
|
source = runCommandLocal "templates" {nativeBuildInputs = [yj];} ''
|
||||||
|
cp -r ${../templates} $out/
|
||||||
|
chmod u+w $out
|
||||||
|
yj < ${../templates/package.yaml} > $out/package.json
|
||||||
|
'';
|
||||||
|
|
||||||
|
packageOverrides = {
|
||||||
|
tlaternet = {
|
||||||
|
add-build-script = {
|
||||||
|
# Dream2nix' built-in install script assumes this is just
|
||||||
|
# a usual npm package and will install it in
|
||||||
|
# `node_modules`.
|
||||||
|
#
|
||||||
|
# Parcel will detect this, and completely break all
|
||||||
|
# configuration. Furthermore, we don't actually want to
|
||||||
|
# install this as if it was an npm library.
|
||||||
|
#
|
||||||
|
# The easiest way to fix this is just to rename the
|
||||||
|
# top-level directory.
|
||||||
|
preBuild = ''
|
||||||
|
# Rename top-level directory so parcel doesn't think we're in a
|
||||||
|
# node_module
|
||||||
|
mv ../../node_modules ../../top-level
|
||||||
|
|
||||||
|
# Rewrite $PATH and co. to use the new path.
|
||||||
|
export PATH=''${PATH//lib\/node_modules\/.bin/lib\/top-level\/.bin}
|
||||||
|
export NODE_PATH=''${NODE_PATH//lib\/node_modules/lib\/top-level}
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
# For some reason, dream2nix builds in the out directory. Don't ask
|
||||||
|
# me, I don't know either.
|
||||||
|
|
||||||
|
# First, go to a sane directory and back up our actual build output
|
||||||
|
mv dist /build/dist
|
||||||
|
cd /build
|
||||||
|
|
||||||
|
# Then, delete everything currently in $out
|
||||||
|
chmod -R u+rwx $out
|
||||||
|
rm -r $out
|
||||||
|
|
||||||
|
# Finally, actually install our output
|
||||||
|
mv dist $out
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sharp = {
|
||||||
|
add-libvips = {
|
||||||
|
buildInputs = old:
|
||||||
|
old
|
||||||
|
++ (with nixpkgs.legacyPackages.${system}; [
|
||||||
|
vips
|
||||||
|
pkg-config
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
[toolchain]
|
|
||||||
channel = "nightly"
|
|
0
Cargo.lock → server/Cargo.lock
generated
0
Cargo.lock → server/Cargo.lock
generated
59
templates/.eslintrc.yaml
Normal file
59
templates/.eslintrc.yaml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
root: true
|
||||||
|
|
||||||
|
parser: "@typescript-eslint/parser"
|
||||||
|
parserOptions:
|
||||||
|
project:
|
||||||
|
- ./tsconfig.json
|
||||||
|
plugins:
|
||||||
|
- "@typescript-eslint"
|
||||||
|
|
||||||
|
extends:
|
||||||
|
- eslint:recommended
|
||||||
|
- plugin:@typescript-eslint/recommended
|
||||||
|
- plugin:@typescript-eslint/recommended-requiring-type-checking
|
||||||
|
|
||||||
|
env:
|
||||||
|
es6: true
|
||||||
|
browser: true
|
||||||
|
|
||||||
|
|
||||||
|
# {
|
||||||
|
# "parser": "@typescript-eslint/parser",
|
||||||
|
# "plugins": [
|
||||||
|
# "@typescript-eslint"
|
||||||
|
# ],
|
||||||
|
# "env": {
|
||||||
|
# "es6": true,
|
||||||
|
# "browser": true,
|
||||||
|
# "jquery": true
|
||||||
|
# },
|
||||||
|
# "extends": [
|
||||||
|
# "eslint:recommended",
|
||||||
|
# "plugin:@typescript-eslint/recommended"
|
||||||
|
# ],
|
||||||
|
# "rules": {
|
||||||
|
# "indent": [
|
||||||
|
# "error",
|
||||||
|
# 4
|
||||||
|
# ],
|
||||||
|
# "linebreak-style": [
|
||||||
|
# "error",
|
||||||
|
# "unix"
|
||||||
|
# ],
|
||||||
|
# "quotes": [
|
||||||
|
# "error",
|
||||||
|
# "double"
|
||||||
|
# ],
|
||||||
|
# "semi": [
|
||||||
|
# "warn",
|
||||||
|
# "always"
|
||||||
|
# ],
|
||||||
|
# "no-console": [
|
||||||
|
# "off"
|
||||||
|
# ],
|
||||||
|
# "no-unused-vars": [
|
||||||
|
# "warn",
|
||||||
|
# { "argsIgnorePattern": "^_" }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# }
|
5
templates/.gitignore
vendored
Normal file
5
templates/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.parcel-cache/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
result
|
6
templates/.parcelrc
Normal file
6
templates/.parcelrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"extends": ["@parcel/config-default"],
|
||||||
|
"transformers": {
|
||||||
|
"*.mp3": [ "@parcel/transformer-raw" ]
|
||||||
|
}
|
||||||
|
}
|
36
templates/.posthtmlrc
Normal file
36
templates/.posthtmlrc
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"posthtml-markdownit": {
|
||||||
|
"root": "src",
|
||||||
|
},
|
||||||
|
"posthtml-extend": {
|
||||||
|
"root": "src",
|
||||||
|
},
|
||||||
|
"posthtml-include": {
|
||||||
|
"root": "src",
|
||||||
|
},
|
||||||
|
"posthtml-favicons": {
|
||||||
|
"root": "src",
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"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": "#99d1ce",
|
||||||
|
"appleStatusBarStyle": "black-translucent",
|
||||||
|
"display": "browser",
|
||||||
|
"orientation": "any",
|
||||||
|
"start_url": "https://tlater.net",
|
||||||
|
"version": "1.0",
|
||||||
|
"icons": {
|
||||||
|
"favicons": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
11
templates/.prettierrc
Normal file
11
templates/.prettierrc
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# -*- yaml -*-
|
||||||
|
|
||||||
|
tabWidth: 4
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
- files: "**/*.html"
|
||||||
|
options:
|
||||||
|
tabWidth: 2
|
||||||
|
- files: "**/*.scss"
|
||||||
|
options:
|
||||||
|
tabWidth: 2
|
9539
templates/package-lock.json
generated
Normal file
9539
templates/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
85
templates/package.yaml
Normal file
85
templates/package.yaml
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
name: tlaternet
|
||||||
|
version: 1.0.0
|
||||||
|
description: tlaternet web interface
|
||||||
|
author: Tristan Maat <tm@tlater.net>
|
||||||
|
license: MIT
|
||||||
|
private: true
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
# Libraries
|
||||||
|
gl-matrix: ^3.4.3 # To help with 3D math in WebGL code
|
||||||
|
classnames: ^2.3.1 # To manage CSS class names in react code
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
hack-font: ^3.3.0
|
||||||
|
'@fontsource/arimo': ^4.5.8
|
||||||
|
'@fontsource/nunito': ^4.5.9
|
||||||
|
'@fortawesome/fontawesome-free': ^6.1.1
|
||||||
|
|
||||||
|
# Frameworks for static content
|
||||||
|
bulma: ^0.9.4
|
||||||
|
|
||||||
|
# React-redux stuff
|
||||||
|
react: ^18.2.0
|
||||||
|
react-dom: ^18.2.0
|
||||||
|
react-use-error-boundary: ^3.0.0 # TODO(tlater): Remove when react implement their own
|
||||||
|
redux: ^4.2.0
|
||||||
|
'@reduxjs/toolkit': ^1.8.3
|
||||||
|
react-redux: ^8.0.2
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
# Parcel & plugins
|
||||||
|
parcel: ^2.7.0
|
||||||
|
'@parcel/transformer-sass': ^2.7.0
|
||||||
|
'@parcel/transformer-glsl': ^2.7.0
|
||||||
|
|
||||||
|
# Build tools
|
||||||
|
typescript: ^4.7.4
|
||||||
|
|
||||||
|
sass: ^1.53.0
|
||||||
|
|
||||||
|
posthtml-extend: ^0.6.3
|
||||||
|
posthtml-favicons: ^1.4.0
|
||||||
|
posthtml-include: ^1.7.4
|
||||||
|
posthtml-markdownit: ^1.3.0
|
||||||
|
|
||||||
|
'@babel/preset-env': ^7.18.6
|
||||||
|
|
||||||
|
# Type shims
|
||||||
|
'@types/react-dom': ^18.0.6
|
||||||
|
'@types/react-redux': ^7.1.24
|
||||||
|
|
||||||
|
# Dev tools
|
||||||
|
npm-check-updates: ^16.0.5
|
||||||
|
|
||||||
|
prettier: ^2.7.1
|
||||||
|
|
||||||
|
typescript-language-server: ^0.11.2
|
||||||
|
typescript-eslint-language-service: ^5.0.0
|
||||||
|
|
||||||
|
eslint: ^8.21.0
|
||||||
|
'@typescript-eslint/parser': ^5.32.0
|
||||||
|
'@typescript-eslint/eslint-plugin': ^5.32.0
|
||||||
|
|
||||||
|
vscode-langservers-extracted: ^4.2.1
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
# Dev workflow
|
||||||
|
build: parcel build --no-autoinstall
|
||||||
|
serve: parcel serve --no-autoinstall
|
||||||
|
watch: parcel watch --no-autoinstall
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
build-dist: parcel build --no-cache --no-autoinstall
|
||||||
|
|
||||||
|
# Checks
|
||||||
|
check: tsc --noEmit
|
||||||
|
style: prettier --check src
|
||||||
|
lint: eslint --max-warnings=0 --format unix src
|
||||||
|
|
||||||
|
# Parcel config
|
||||||
|
source:
|
||||||
|
- src/index.html
|
||||||
|
- src/error.html
|
||||||
|
|
||||||
|
browserslist: '> 1%, not dead'
|
21
templates/src/error.html
Normal file
21
templates/src/error.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<extends src="./lib/html/base.html">
|
||||||
|
<block name="content">
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title has-text-weight-normal">{{ error.status_code }}</h1>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column content">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<markdown>
|
||||||
|
{{ error.message }}
|
||||||
|
|
||||||
|
If you think this is a mistake, feel free to [contact
|
||||||
|
me](~/src/mail.html)!
|
||||||
|
</markdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</block>
|
||||||
|
</extends>
|
78
templates/src/icon.svg
Normal file
78
templates/src/icon.svg
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
version="1.0"
|
||||||
|
width="260.000000pt"
|
||||||
|
height="260.000000pt"
|
||||||
|
viewBox="0 0 260.000000 260.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
id="svg70"
|
||||||
|
sodipodi:docname="icon.svg"
|
||||||
|
inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
|
||||||
|
<metadata
|
||||||
|
id="metadata76">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs74" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1916"
|
||||||
|
inkscape:window-height="1059"
|
||||||
|
id="namedview72"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="2.2269231"
|
||||||
|
inkscape:cx="173.33333"
|
||||||
|
inkscape:cy="173.33333"
|
||||||
|
inkscape:window-x="-2"
|
||||||
|
inkscape:window-y="13"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
inkscape:label="Background">
|
||||||
|
<rect
|
||||||
|
style="fill:#99d1ce;stroke-width:0.75;fill-opacity:1"
|
||||||
|
id="rect843"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
x="2.0117849e-08"
|
||||||
|
y="2.0117849e-08" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:#0f0f0f;fill-opacity:1;stroke:#991dce;stroke-width:0.1"
|
||||||
|
id="path62"
|
||||||
|
d="M 2.0117848e-8,130 V 260 H 130 260 V 130 2.0117848e-8 H 130 2.0117848e-8 Z M 132.5,19.099999 c 1.4,1.300001 2,3.6 2.3,8.300001 l 0.4,6.4 8.2,0.699999 C 160.7,35.9 177.9,40.300001 182.89999,44.600002 188.9,49.6 189.1,58.399998 183.3,62.999999 180,65.6 176.6,65.5 161.7,62.499998 155,61.199999 145.99999,59.8 141.79999,59.399999 l -7.8,-0.700001 V 85.7 v 27 L 147,116.9 c 34.8,11.2 49.4,24.6 50.69999,46.39999 C 198.7,178 194.8,188.9 184.79999,199.5 174.6,210.4 162.6,216.5 144.89999,219.6 l -9.69999,1.7 -0.4,7.5 c -0.3,6.29999 -0.7,7.69999 -2.7,9.29999 -3.5,2.8 -8.5,2.5 -11,-0.69999 -1.7,-2.10001 -2.1,-4.1 -2.1,-9.4 v -6.7 l -7.1,-0.70001 c -8.8,-0.79999 -23.900001,-4 -34.400001,-7.3 -10.300001,-3.3 -15.5,-7.99999 -15.5,-14 0,-5.59999 3.5,-10.49999 8.2,-11.7 2.999999,-0.8 5.4,-0.39999 12.500002,1.7 C 93.099998,192.5 103,194.59999 112.3,195.5 l 6.7,0.7 v -29.1 c 0,-22.50001 -0.3,-29.20001 -1.20001,-29.5 C 93.099998,130.3 81.199997,124.9 72.900001,117.3 58.5,103.9 54.800001,82.499998 63.699999,64.300001 70.999998,49.399999 91.399998,37.199999 113.4,34.499999 L 119,33.8 v -5.7 c 0,-10.400001 7.1,-15.1 13.5,-9.000001 z" />
|
||||||
|
<path
|
||||||
|
style="fill:#0f0f0f;stroke:#991dce;stroke-width:0.1;fill-opacity:1"
|
||||||
|
id="path64"
|
||||||
|
d="m 113.2,60.6 c -18.4,4.7 -25.8,18.1 -17.7,32 1.3,2.2 4,5.3 6,6.8 3.7,2.9 16.4,9 17.1,8.3 0.3,-0.2 0.3,-11.2 0.2,-24.4 l -0.3,-24 z" />
|
||||||
|
<path
|
||||||
|
style="fill:#0f0f0f;stroke:#991dce;stroke-width:0.1;fill-opacity:1"
|
||||||
|
id="path66"
|
||||||
|
d="m 134,169.1 v 26.2 l 4.4,-0.5 c 9.6,-1.1 21.1,-8.9 24.2,-16.4 1.9,-4.6 1.7,-12.1 -0.5,-16.9 -2.6,-5.7 -10,-11.8 -18.6,-15.4 -4,-1.7 -7.8,-3.1 -8.4,-3.1 -0.8,0 -1.1,8.2 -1.1,26.1 z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
80
templates/src/index.html
Normal file
80
templates/src/index.html
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<extends src="./lib/html/base.html">
|
||||||
|
<block name="content">
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title has-text-weight-normal is-family-monospace">
|
||||||
|
$ <span id="typed-welcome"></span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column content">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<markdown>
|
||||||
|
|
||||||
|
### About Me
|
||||||
|
|
||||||
|
Looks like you found my website. I suppose introductions are
|
||||||
|
in order.
|
||||||
|
|
||||||
|
My name's Tristan, I'm an avid Dutch-South African software
|
||||||
|
consultant working in the UK. You probably either met me at an
|
||||||
|
open source conference, a hackathon, a badminton session or at
|
||||||
|
a roleplaying table.
|
||||||
|
|
||||||
|
If not, well, this is also a great place to "meet" me. Have a
|
||||||
|
nosey!
|
||||||
|
|
||||||
|
### This Website
|
||||||
|
|
||||||
|
There is not a whole lot here at the moment.
|
||||||
|
|
||||||
|
You may find the following interesting though:
|
||||||
|
|
||||||
|
- A [little web app](~/src/music_sample.html) showing
|
||||||
|
off what WebGL can do in combination with the JavaScript
|
||||||
|
Audio interface.
|
||||||
|
</markdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column content">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<markdown>
|
||||||
|
### My Work
|
||||||
|
|
||||||
|
I'm a software consultant working for
|
||||||
|
[Codethink](https://www.codethink.co.uk) in Manchester,
|
||||||
|
UK. Our specializaiton is open source software, so this has
|
||||||
|
allowed me to directly contribute to a number of open source
|
||||||
|
projects, notably
|
||||||
|
[BuildStream](https://www.gitlab.com/buildstream/buildstream),
|
||||||
|
an integration tool for large software stacks.
|
||||||
|
|
||||||
|
I've given a couple of talks on it, as well:
|
||||||
|
|
||||||
|
- Build meetup 2017
|
||||||
|
- Build meetup 2018
|
||||||
|
- Build meetup 2019
|
||||||
|
|
||||||
|
Outside of work for Codethink, I'm generally interested in
|
||||||
|
things such as NixOS and other tools that assist maintaining
|
||||||
|
Linux systems - mostly born out of my pursuit of the perfect
|
||||||
|
Linux desktop (feel free to have a browse through my
|
||||||
|
[dotfiles](https://github.com/tlater/dotfiles)).
|
||||||
|
|
||||||
|
I also just enjoy Programming, my core languages currently are
|
||||||
|
Rust, Python, Lisp and JavaScript (including a number of
|
||||||
|
frameworks and tools for these), although I have hopes to
|
||||||
|
eventually reduce these to just Rust ;)
|
||||||
|
|
||||||
|
If you're interested in seeing these things for yourself,
|
||||||
|
visit my [Gitlab](https://gitlab.com/tlater) and
|
||||||
|
[GitHub](https://github.com/tlater) pages.
|
||||||
|
</markdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</block>
|
||||||
|
</extends>
|
BIN
templates/src/lib/favicon.png
Normal file
BIN
templates/src/lib/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
29
templates/src/lib/html/base.html
Normal file
29
templates/src/lib/html/base.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="description" content="tlater.net web server" />
|
||||||
|
<meta name="author" contnet="Tristan Daniël Maat" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="./icon.svg" type="image/x-icon" />
|
||||||
|
<link rel="stylesheet" href="~/src/lib/scss/main.scss" />
|
||||||
|
|
||||||
|
<block name="stylesheets"></block>
|
||||||
|
|
||||||
|
<title>tlater.net</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<block name="navigation">
|
||||||
|
<include src="lib/html/navigation.html"></include>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<include src="lib/html/message-flash.html"></include>
|
||||||
|
|
||||||
|
<block name="content"></block>
|
||||||
|
|
||||||
|
<block name="footer">
|
||||||
|
<script type="module" src="lib/js/index.ts"></script>
|
||||||
|
</block>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
templates/src/lib/html/message-flash.html
Normal file
8
templates/src/lib/html/message-flash.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<span>
|
||||||
|
{{#if flash}}
|
||||||
|
<div class="notification is-{{flash.type}}">
|
||||||
|
<button class="delete" aria-label="Close"></button>
|
||||||
|
<span role="alert"> {{ flash.message }} </span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
23
templates/src/lib/html/navigation.html
Normal file
23
templates/src/lib/html/navigation.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item has-text-primary is-uppercase" href="/">tlater</a>
|
||||||
|
<a
|
||||||
|
class="navbar-burger"
|
||||||
|
role="button"
|
||||||
|
aria-label="menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-target="main-navigation"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="main-navigation" class="navbar-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
<a class="navbar-item" href="~/src/mail.html"> E-Mail </a>
|
||||||
|
<a class="navbar-item" href="https://www.gitlab.com/tlater"> GitLab </a>
|
||||||
|
<a class="navbar-item" href="https://www.github.com/TLATER"> GitHub </a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
67
templates/src/lib/js/index.ts
Normal file
67
templates/src/lib/js/index.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
function registerFlashCloseButtons() {
|
||||||
|
const flashButtons = document.querySelectorAll(".notification .delete");
|
||||||
|
|
||||||
|
for (const flashButton of flashButtons) {
|
||||||
|
if (flashButton.parentNode === null) {
|
||||||
|
throw new Error("invalid flash button");
|
||||||
|
}
|
||||||
|
|
||||||
|
const flash = flashButton.parentNode;
|
||||||
|
|
||||||
|
flash.addEventListener("click", () => {
|
||||||
|
if (flash.parentNode === null) {
|
||||||
|
throw new Error("invalid flash message");
|
||||||
|
}
|
||||||
|
|
||||||
|
flash.parentNode.removeChild(flash);
|
||||||
|
});
|
||||||
|
|
||||||
|
// In development, there won't be a web server hooked up to
|
||||||
|
// this to render the flash message, so we remove it entirely
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
if (
|
||||||
|
flash.parentNode === null ||
|
||||||
|
flash.parentNode.parentNode === null
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("Disabling flash message");
|
||||||
|
|
||||||
|
// Get the containing <span> element
|
||||||
|
const block = flash.parentNode;
|
||||||
|
flash.parentNode.parentNode.removeChild(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerNavCollapseButtons() {
|
||||||
|
const navbarButtons = document.getElementsByClassName("navbar-burger");
|
||||||
|
|
||||||
|
for (const navbarButton of navbarButtons) {
|
||||||
|
navbarButton.addEventListener("click", () => {
|
||||||
|
if (
|
||||||
|
!(navbarButton instanceof HTMLElement) ||
|
||||||
|
!navbarButton.dataset.target
|
||||||
|
) {
|
||||||
|
throw new Error("invalid navbar button");
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = document.getElementById(navbarButton.dataset.target);
|
||||||
|
|
||||||
|
if (target === null) {
|
||||||
|
throw new Error("could not find navbar button target");
|
||||||
|
}
|
||||||
|
|
||||||
|
navbarButton.classList.toggle("is-active");
|
||||||
|
target.classList.toggle("is-active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
registerFlashCloseButtons();
|
||||||
|
registerNavCollapseButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
45
templates/src/lib/scss/_custom-bulma.scss
Normal file
45
templates/src/lib/scss/_custom-bulma.scss
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
@use "sass:color";
|
||||||
|
@use "./_fonts";
|
||||||
|
|
||||||
|
@import "~/node_modules/bulma/sass/utilities/initial-variables.sass";
|
||||||
|
@import "~/node_modules/bulma/sass/utilities/functions.sass";
|
||||||
|
|
||||||
|
$black: #0f0f0f;
|
||||||
|
$grey-darker: #11151c;
|
||||||
|
$grey-light: #dddddd;
|
||||||
|
$white: #ffffff;
|
||||||
|
|
||||||
|
$red: #dc322f;
|
||||||
|
$orange: #d26937;
|
||||||
|
$yellow: #b58900;
|
||||||
|
$blue: #195466;
|
||||||
|
$cyan: #599cab;
|
||||||
|
$green: #2aa889;
|
||||||
|
|
||||||
|
$primary: #99d1ce;
|
||||||
|
$link: $green;
|
||||||
|
$link-hover: color.scale($green, $lightness: +10%);
|
||||||
|
$link-active: color.scale($green, $lightness: +10%);
|
||||||
|
$link-focus: color.scale($green, $lightness: +10%);
|
||||||
|
$input-color: $grey-light;
|
||||||
|
$input-placeholder-color: $grey-light; // Some opacity is applied to this
|
||||||
|
|
||||||
|
$weight-normal: 400;
|
||||||
|
|
||||||
|
$scheme-main: $black;
|
||||||
|
|
||||||
|
$family-sans-serif: Nunito, $family-sans-serif;
|
||||||
|
$family-monospace: Hack, $family-monospace;
|
||||||
|
|
||||||
|
$text: $grey-light;
|
||||||
|
$text-strong: $primary;
|
||||||
|
$label-color: $text;
|
||||||
|
|
||||||
|
$content-heading-color: $text;
|
||||||
|
$hr-background-color: $grey-light;
|
||||||
|
$hr-height: 1px;
|
||||||
|
|
||||||
|
$pre-background: $grey-darker;
|
||||||
|
|
||||||
|
@import "~/node_modules/bulma";
|
||||||
|
@import "./_navbar";
|
46
templates/src/lib/scss/_fonts.scss
Normal file
46
templates/src/lib/scss/_fonts.scss
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
@use "~/node_modules/@fontsource/nunito/scss/mixins" as Nunito;
|
||||||
|
@use "~/node_modules/@fontsource/arimo/scss/mixins" as Arimo;
|
||||||
|
|
||||||
|
$weights: 300, 400, 500, 600, 700;
|
||||||
|
|
||||||
|
@each $weight in $weights {
|
||||||
|
@include Nunito.fontFace(
|
||||||
|
$weight: $weight,
|
||||||
|
$display: auto,
|
||||||
|
$style: normal,
|
||||||
|
$fontDir: "npm:@fontsource/nunito/files"
|
||||||
|
);
|
||||||
|
|
||||||
|
@include Nunito.fontFace(
|
||||||
|
$weight: $weight,
|
||||||
|
$display: auto,
|
||||||
|
$style: italic,
|
||||||
|
$fontDir: "npm:@fontsource/nunito/files"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include Arimo.fontFace(
|
||||||
|
$weight: 400,
|
||||||
|
$display: auto,
|
||||||
|
$style: normal,
|
||||||
|
$fontDir: "npm:@fontsource/arimo/files"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hack *does* come with its own CSS, but it's broken and hasn't seen
|
||||||
|
// a release since https://github.com/source-foundry/Hack/issues/467
|
||||||
|
// was resolved.
|
||||||
|
|
||||||
|
$variants: regular 400 normal, bold 700 normal, italic 400 italic,
|
||||||
|
bolditalic 700 italic;
|
||||||
|
|
||||||
|
@each $name, $weight, $style in $variants {
|
||||||
|
@font-face {
|
||||||
|
font-family: "Hack";
|
||||||
|
src: url("npm:hack-font/build/web/fonts/hack-#{$name}-subset.woff2?sha=3114f1256")
|
||||||
|
format("woff2"),
|
||||||
|
url("npm:hack-font/build/web/fonts/hack-#{$name}-subset.woff?sha=3114f1256")
|
||||||
|
format("woff");
|
||||||
|
font-weight: $weight;
|
||||||
|
font-style: $style;
|
||||||
|
}
|
||||||
|
}
|
21
templates/src/lib/scss/_navbar.scss
Normal file
21
templates/src/lib/scss/_navbar.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.navbar.is-dark {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
|
||||||
|
& .navbar-brand > .navbar-item {
|
||||||
|
font-family: Arimo;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark !important;
|
||||||
|
color: $white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .navbar-start > .navbar-item {
|
||||||
|
color: rgba($white, 0.75);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark !important;
|
||||||
|
color: $white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
134
templates/src/lib/scss/_typed.scss
Normal file
134
templates/src/lib/scss/_typed.scss
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
@use "sass:math";
|
||||||
|
@use "sass:list";
|
||||||
|
|
||||||
|
/// Animate a blinking cursor.
|
||||||
|
@mixin cursor($duration) {
|
||||||
|
$name: cursor-09d03260130069771b6ddc1cb415f39fdd27ddfab7b01ba91273398c2d245ae4;
|
||||||
|
// The number of times we need to blink is = the number of full
|
||||||
|
// seconds (500ms * 2) that fit in the total duration, rounded up,
|
||||||
|
// and doubled.
|
||||||
|
$iterations: math.ceil(math.div($duration, 1s)) * 2;
|
||||||
|
|
||||||
|
animation: $name ease-in-out 500ms $iterations alternate;
|
||||||
|
content: " ";
|
||||||
|
|
||||||
|
@keyframes #{$name} {
|
||||||
|
from {
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
content: "█";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animate a piece of text as if it was being typed by a human.
|
||||||
|
@mixin typed($text, $duration) {
|
||||||
|
// We don't want a linearly typed set of text, which makes this
|
||||||
|
// singificantly more complex.
|
||||||
|
//
|
||||||
|
// CSS animations normally do not permit per-frame changes in
|
||||||
|
// duration (since the total animation time is fixed). This means we
|
||||||
|
// need to create multiple animations, and delay them so that they
|
||||||
|
// happen in the time sequence we want.
|
||||||
|
//
|
||||||
|
// We generate the raw values with _generate-animations, and then
|
||||||
|
// split up the result into the animation API.
|
||||||
|
$frames: str-length($text);
|
||||||
|
$animations: _generate-animations($frames, 1.2s);
|
||||||
|
|
||||||
|
animation-name: _unzip($animations, 1);
|
||||||
|
animation-delay: _unzip($animations, 3);
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
// We need to type each character in separate animations, see above
|
||||||
|
// comment.
|
||||||
|
@each $name, $character in $animations {
|
||||||
|
@keyframes #{$name} {
|
||||||
|
from {
|
||||||
|
content: str-slice($text, 0, $character);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
content: str-slice($text, 0, $character + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unzip a nested set of lists, taking the nth value of each sublist.
|
||||||
|
@function _unzip($lists, $i) {
|
||||||
|
$out: ();
|
||||||
|
$sep: comma;
|
||||||
|
@each $sublist in $lists {
|
||||||
|
$out: list.append($out, list.nth($sublist, $i), $sep);
|
||||||
|
}
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the sum of all numbers in a list.
|
||||||
|
@function _sum($list) {
|
||||||
|
$out: 0;
|
||||||
|
@each $val in $list {
|
||||||
|
$out: $out + $val;
|
||||||
|
}
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a list from a shorter list by repeating it up until size
|
||||||
|
/// $length.
|
||||||
|
@function _round-robin($base, $length) {
|
||||||
|
$out: ();
|
||||||
|
$sep: list.separator($out);
|
||||||
|
@for $i from 0 through $length {
|
||||||
|
$out: list.append($out, list.nth($base, $i % list.length($base) + 1));
|
||||||
|
}
|
||||||
|
@return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the actual animation values.
|
||||||
|
///
|
||||||
|
/// This generates a nested list as:
|
||||||
|
///
|
||||||
|
/// (keyframe-name, index, start time)
|
||||||
|
///
|
||||||
|
/// The duration of each frame is taken from the internal $delays in a
|
||||||
|
/// round robin fashion, to give some amount of human-like variance to
|
||||||
|
/// the duration of each frame.
|
||||||
|
///
|
||||||
|
/// Start time is set to the time at which the frame should start to
|
||||||
|
/// achieve the desired frame-by-frame duration.
|
||||||
|
@function _generate-animations($number, $total_duration) {
|
||||||
|
$id: d66fa0449c0b4d4ca287f8c96428af928b2987b4d88b72b7d60152d9a55d9f29;
|
||||||
|
$out: ();
|
||||||
|
$sep: list.separator($out);
|
||||||
|
|
||||||
|
// A set of "human-like" delays for each typed character. In
|
||||||
|
// practice, my typing seems to be about 20-70ms, but it looks a bit
|
||||||
|
// nicer to increase all typing by 20ms to make the effect more
|
||||||
|
// noticeable.
|
||||||
|
//
|
||||||
|
// Numbers generated once with a random number generator, rather
|
||||||
|
// than using `math.random()`, since they end up in CSS verbatim,
|
||||||
|
// and the build would be non-reproducible if we didn't do it this
|
||||||
|
// way. Using `math.random() wouldn't change this dynamically each
|
||||||
|
// time the page loads anyway, so we don't really lose anything by
|
||||||
|
// pre-generating these numbers.
|
||||||
|
$delays: 69ms, 83ms, 49ms, 48ms, 52ms, 59ms, 40ms, 71ms, 80ms, 67ms;
|
||||||
|
|
||||||
|
@for $animation from 0 through $number {
|
||||||
|
$out: list.append(
|
||||||
|
$out,
|
||||||
|
(
|
||||||
|
type-#{$id}-#{$animation},
|
||||||
|
$animation,
|
||||||
|
_sum(_round_robin($delays, $animation))
|
||||||
|
),
|
||||||
|
$sep
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@return $out;
|
||||||
|
}
|
18
templates/src/lib/scss/main.scss
Normal file
18
templates/src/lib/scss/main.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@import "./_custom-bulma";
|
||||||
|
@import "./_typed";
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#typed-welcome {
|
||||||
|
&::before {
|
||||||
|
@include typed("Welcome to tlater.net!", 1.2s);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@include cursor(6s);
|
||||||
|
}
|
||||||
|
}
|
72
templates/src/mail.html
Normal file
72
templates/src/mail.html
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<extends src="./lib/html/base.html">
|
||||||
|
<block name="content">
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title has-text-weight-normal">Contact Me</h1>
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<form id="sendmail" role="form" action="mail.html" method="post">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="mail">Email address</label>
|
||||||
|
<input
|
||||||
|
id="mail"
|
||||||
|
class="input"
|
||||||
|
type="email"
|
||||||
|
placeholder="Your address"
|
||||||
|
name="mail"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="subject">Subject</label>
|
||||||
|
<input
|
||||||
|
id="subject"
|
||||||
|
class="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="E.g. There's a typo on your home page!"
|
||||||
|
name="subject"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="message">Message</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
class="textarea"
|
||||||
|
type="text"
|
||||||
|
rows="6"
|
||||||
|
name="message"
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column content">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<markdown>
|
||||||
|
Any messages you enter here are directly forwarded to me. I aim to
|
||||||
|
respond within a day.
|
||||||
|
|
||||||
|
Don't be upset about the form, I want to avoid the spam
|
||||||
|
publishing your email address brings with it... And minimize
|
||||||
|
the amount of mail that doesn't reach me, this form is an
|
||||||
|
exception in all my spam filters, you see ;)
|
||||||
|
</markdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</block>
|
||||||
|
</extends>
|
BIN
templates/src/music/assets/Mseq_-_Journey.mp3
Normal file
BIN
templates/src/music/assets/Mseq_-_Journey.mp3
Normal file
Binary file not shown.
2
templates/src/music/assets/Mseq_-_Journey.mp3.d.ts
vendored
Normal file
2
templates/src/music/assets/Mseq_-_Journey.mp3.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
declare const mseq: string;
|
||||||
|
export default mseq;
|
44
templates/src/music/features/controls/Controls.tsx
Normal file
44
templates/src/music/features/controls/Controls.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Indicator from "../indicator/Indicator";
|
||||||
|
import { useAppSelector } from "../../hooks";
|
||||||
|
|
||||||
|
function Controls() {
|
||||||
|
const title = useAppSelector((state) => state.musicPlayer.title);
|
||||||
|
|
||||||
|
let titleLine = <div className="level-item">{title.name}</div>;
|
||||||
|
|
||||||
|
if (title.name === "Journey" && title.artist === "Mseq") {
|
||||||
|
titleLine = (
|
||||||
|
<div className="level-item is-size-7-mobile is-flex-shrink-1">
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
license. Ft: Admiral Bob,Texas Radio Fish
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notification is-primary">
|
||||||
|
<div className="level is-mobile">
|
||||||
|
<div className="level-left is-flex-shrink-1">
|
||||||
|
<Indicator />
|
||||||
|
{titleLine}
|
||||||
|
</div>
|
||||||
|
<div className="level-right is-hidden-mobile">
|
||||||
|
<div className="level-item">{title.artist}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Controls;
|
44
templates/src/music/features/indicator/Indicator.tsx
Normal file
44
templates/src/music/features/indicator/Indicator.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { useAppSelector, useAppDispatch } from "../../hooks";
|
||||||
|
import { togglePlay, PlayState } from "../musicplayer/musicPlayerSlice";
|
||||||
|
|
||||||
|
function Indicator() {
|
||||||
|
const playing = useAppSelector((state) => state.musicPlayer.playing);
|
||||||
|
const muted = useAppSelector((state) => state.musicPlayer.muted);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const buttonClass = classNames({
|
||||||
|
button: true,
|
||||||
|
"is-primary": true,
|
||||||
|
"level-item": true,
|
||||||
|
"is-loading": playing === PlayState.Loading,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconClass = classNames({
|
||||||
|
fas: true,
|
||||||
|
"fa-2x": true,
|
||||||
|
"fa-muted": muted,
|
||||||
|
"fa-play": playing === PlayState.Paused,
|
||||||
|
"fa-pause": playing === PlayState.Playing,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(togglePlay(null)).catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={buttonClass}
|
||||||
|
>
|
||||||
|
<span className="icon is-medium">
|
||||||
|
<i className={iconClass}></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Indicator;
|
17
templates/src/music/features/musicplayer/MusicPlayer.tsx
Normal file
17
templates/src/music/features/musicplayer/MusicPlayer.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Controls from "../controls/Controls";
|
||||||
|
import Visualizer from "../visualizer/Visualizer";
|
||||||
|
|
||||||
|
function MusicPlayer() {
|
||||||
|
return (
|
||||||
|
<div className="is-flex-grow-1 is-flex is-flex-direction-column">
|
||||||
|
<Visualizer />
|
||||||
|
<div className="is-flex-grow-0">
|
||||||
|
<Controls />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MusicPlayer;
|
157
templates/src/music/features/musicplayer/musicPlayerSlice.ts
Normal file
157
templates/src/music/features/musicplayer/musicPlayerSlice.ts
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import { RootState, AppDispatch } from "../../store";
|
||||||
|
|
||||||
|
//************************
|
||||||
|
// Interface definitions *
|
||||||
|
//************************
|
||||||
|
|
||||||
|
interface MusicPlayerState {
|
||||||
|
muted: boolean;
|
||||||
|
playing: PlayState;
|
||||||
|
title: MusicPlayerTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MusicPlayerTitle {
|
||||||
|
source: string;
|
||||||
|
artist: string;
|
||||||
|
name: string;
|
||||||
|
album: string;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlayState {
|
||||||
|
Playing = "Playing",
|
||||||
|
Paused = "Paused",
|
||||||
|
Loading = "Loading",
|
||||||
|
}
|
||||||
|
|
||||||
|
//*********************
|
||||||
|
// Music player logic *
|
||||||
|
//*********************
|
||||||
|
|
||||||
|
class MusicPlayer {
|
||||||
|
private context?: AudioContext;
|
||||||
|
private source: HTMLAudioElement;
|
||||||
|
private sourceNode?: MediaElementAudioSourceNode;
|
||||||
|
private volume?: GainNode;
|
||||||
|
private analyser?: AnalyserNode;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.source = new Audio();
|
||||||
|
}
|
||||||
|
|
||||||
|
get audioAnalyser() {
|
||||||
|
return this.analyser;
|
||||||
|
}
|
||||||
|
|
||||||
|
set src(source: string) {
|
||||||
|
this.source.src = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePlay = async (
|
||||||
|
_: null,
|
||||||
|
{ getState }: { getState: () => RootState }
|
||||||
|
): Promise<PlayState> => {
|
||||||
|
if (this.context === undefined) {
|
||||||
|
this.context = new AudioContext();
|
||||||
|
this.sourceNode = this.context.createMediaElementSource(
|
||||||
|
this.source
|
||||||
|
);
|
||||||
|
this.volume = this.context.createGain();
|
||||||
|
this.analyser = this.context.createAnalyser();
|
||||||
|
|
||||||
|
this.analyser.fftSize = 2048;
|
||||||
|
this.analyser.smoothingTimeConstant = 0.8;
|
||||||
|
|
||||||
|
this.sourceNode.connect(this.analyser);
|
||||||
|
this.sourceNode.connect(this.volume);
|
||||||
|
this.volume.connect(this.context.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
const playing = getState().musicPlayer.playing;
|
||||||
|
|
||||||
|
switch (playing) {
|
||||||
|
case PlayState.Playing:
|
||||||
|
this.source.pause();
|
||||||
|
return PlayState.Paused;
|
||||||
|
case PlayState.Paused:
|
||||||
|
case PlayState.Loading:
|
||||||
|
// Chrome's extra cookie, it refuses to play if we
|
||||||
|
// don't resume after the first user interaction.
|
||||||
|
await this.context.resume();
|
||||||
|
return this.source.play().then(() => PlayState.Playing);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = new MusicPlayer();
|
||||||
|
|
||||||
|
//*************************
|
||||||
|
// Redux state management *
|
||||||
|
//*************************
|
||||||
|
|
||||||
|
const initialState: MusicPlayerState = {
|
||||||
|
muted: false,
|
||||||
|
playing: PlayState.Paused,
|
||||||
|
title: {
|
||||||
|
source: "",
|
||||||
|
artist: "",
|
||||||
|
name: "",
|
||||||
|
album: "",
|
||||||
|
length: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const musicPlayerSlice = createSlice({
|
||||||
|
name: "musicPlayer",
|
||||||
|
initialState,
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
setSource: (state, action: PayloadAction<MusicPlayerTitle>) => {
|
||||||
|
state.title = action.payload;
|
||||||
|
player.src = state.title.source;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(togglePlay.pending, (state) => {
|
||||||
|
// If we are currently paused or loading, then this is
|
||||||
|
// actually an async call, otherwise we just
|
||||||
|
// synchronously pause the music.
|
||||||
|
if (state.playing !== PlayState.Playing) {
|
||||||
|
state.playing = PlayState.Loading;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(togglePlay.fulfilled, (state, { payload }) => {
|
||||||
|
state.playing = payload;
|
||||||
|
})
|
||||||
|
.addCase(togglePlay.rejected, (state, { error }) => {
|
||||||
|
if (error.message !== undefined) {
|
||||||
|
console.error(`Could not play music: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.playing = PlayState.Paused;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const togglePlay = createAsyncThunk<
|
||||||
|
PlayState,
|
||||||
|
null,
|
||||||
|
{ dispatch: AppDispatch; state: RootState }
|
||||||
|
>("musicPlayer/togglePlay", player.togglePlay, {
|
||||||
|
condition: (_, { getState }) => {
|
||||||
|
const playing = getState().musicPlayer.playing;
|
||||||
|
|
||||||
|
if (playing == PlayState.Loading) {
|
||||||
|
// Block updates when we're loading
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setSource } = musicPlayerSlice.actions;
|
||||||
|
export { PlayState, player as musicPlayer };
|
||||||
|
export type { MusicPlayerState };
|
||||||
|
export default musicPlayerSlice.reducer;
|
342
templates/src/music/features/visualizer/Renderer.ts
Normal file
342
templates/src/music/features/visualizer/Renderer.ts
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
import { Shader } from "./Shader";
|
||||||
|
import { mat4 } from "gl-matrix";
|
||||||
|
|
||||||
|
import { Cube } from "./cube";
|
||||||
|
import vertexSource from "./shaders/vertices.glsl";
|
||||||
|
import fragmentSource from "./shaders/fragments.glsl";
|
||||||
|
|
||||||
|
const ROTATION_SPEED = 0.0;
|
||||||
|
const BACKGROUND_COLOR = [0.0588235294118, 0.0588235294118, 0.0588235294118];
|
||||||
|
|
||||||
|
class RendererError extends Error {}
|
||||||
|
|
||||||
|
class Renderer {
|
||||||
|
private canvas: HTMLCanvasElement;
|
||||||
|
private overlay: HTMLSpanElement;
|
||||||
|
|
||||||
|
private analyser: AnalyserNode;
|
||||||
|
private analyserData: Uint8Array;
|
||||||
|
|
||||||
|
private lastFrameTime: number;
|
||||||
|
private dTime: number;
|
||||||
|
private nextAnimationFrame?: number;
|
||||||
|
|
||||||
|
private rotation: number;
|
||||||
|
|
||||||
|
private buffers: {
|
||||||
|
indices?: WebGLBuffer;
|
||||||
|
positions?: WebGLBuffer;
|
||||||
|
normals?: WebGLBuffer;
|
||||||
|
fft?: WebGLBuffer;
|
||||||
|
velocitiesRead?: WebGLBuffer;
|
||||||
|
velocitiesWrite?: WebGLBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
analyser: AnalyserNode,
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
overlay: HTMLSpanElement
|
||||||
|
) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.overlay = overlay;
|
||||||
|
this.analyser = analyser;
|
||||||
|
this.analyserData = new Uint8Array(analyser.frequencyBinCount);
|
||||||
|
|
||||||
|
this.lastFrameTime = 0;
|
||||||
|
this.dTime = 0;
|
||||||
|
this.rotation = 0;
|
||||||
|
this.buffers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeAndDraw(
|
||||||
|
gl: WebGL2RenderingContext,
|
||||||
|
shader: Shader,
|
||||||
|
observerData: ResizeObserverEntry | null
|
||||||
|
) {
|
||||||
|
if (this.canvas.parentElement === null) {
|
||||||
|
throw new Error("renderer has been removed from dom");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.nextAnimationFrame) {
|
||||||
|
cancelAnimationFrame(this.nextAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: For this to work, it's *incredibly important* for the
|
||||||
|
// canvas to be overflowable by its parent, and its parent to
|
||||||
|
// have `overflow: hidden` set. If using a flexbox, this means
|
||||||
|
// that the canvas has to be `position: absolute`.
|
||||||
|
let width: number;
|
||||||
|
let height: number;
|
||||||
|
|
||||||
|
if (observerData !== null && observerData.devicePixelContentBoxSize) {
|
||||||
|
width = observerData.devicePixelContentBoxSize[0].inlineSize;
|
||||||
|
height = observerData.devicePixelContentBoxSize[0].blockSize;
|
||||||
|
} else {
|
||||||
|
// Fallback; the above API is even newer than
|
||||||
|
// ResizeObserver, and by setting the observerData to null
|
||||||
|
// we can manually resize at least once without going
|
||||||
|
// through the API.
|
||||||
|
if (this.canvas.parentElement === null) {
|
||||||
|
throw new Error("canvas parent disappeared");
|
||||||
|
}
|
||||||
|
// Note: This *requires* `box-sizing: border-box`
|
||||||
|
({ width, height } =
|
||||||
|
this.canvas.parentElement.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas.width = width;
|
||||||
|
this.canvas.height = height;
|
||||||
|
|
||||||
|
gl.viewport(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
|
||||||
|
this.updateProjection(gl, shader);
|
||||||
|
|
||||||
|
// ResizeObserver will call when we should draw, so do our own
|
||||||
|
// time calculation and draw the scene.
|
||||||
|
this.updateTime(performance.now());
|
||||||
|
this.drawScene(gl, shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTime(time: number) {
|
||||||
|
this.dTime = time - this.lastFrameTime;
|
||||||
|
this.lastFrameTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeScene() {
|
||||||
|
if (this.canvas.parentElement === null) {
|
||||||
|
throw new Error("canvas was not added to page");
|
||||||
|
}
|
||||||
|
|
||||||
|
const gl = this.canvas.getContext("webgl2");
|
||||||
|
if (gl === null) {
|
||||||
|
throw new RendererError("WebGL (2) is unsupported on this browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
const shader = Shader.builder(gl)
|
||||||
|
.addShader(vertexSource, gl.VERTEX_SHADER)
|
||||||
|
.addShader(fragmentSource, gl.FRAGMENT_SHADER)
|
||||||
|
.addAttribute("aVertexPosition")
|
||||||
|
.addAttribute("aVertexNormal")
|
||||||
|
.addAttribute("aHeight")
|
||||||
|
.addUniforms("uProjectionMatrix")
|
||||||
|
.addUniforms("uModelViewMatrix")
|
||||||
|
.addUniforms("uNormalMatrix")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.initGL(gl, shader);
|
||||||
|
this.updateProjection(gl, shader);
|
||||||
|
this.initBuffers(gl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const observer = new ResizeObserver((elements) => {
|
||||||
|
// We only observe one element
|
||||||
|
const element = elements[0];
|
||||||
|
this.resizeAndDraw(gl, shader, element);
|
||||||
|
});
|
||||||
|
observer.observe(this.canvas.parentElement);
|
||||||
|
} catch (error) {
|
||||||
|
// If the browser does not support ResizeObserver, we
|
||||||
|
// simply don't resize. Resizing is hard enough, just use
|
||||||
|
// a modern browser.
|
||||||
|
if (error instanceof ReferenceError) {
|
||||||
|
console.warn(
|
||||||
|
"Browser does not support `ResizeObserver`. Canvas resizing will be disabled."
|
||||||
|
);
|
||||||
|
} else throw error;
|
||||||
|
}
|
||||||
|
this.resizeAndDraw(gl, shader, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProjection(gl: WebGLRenderingContext, shader: Shader) {
|
||||||
|
const projectionMatrix = mat4.create();
|
||||||
|
mat4.perspective(
|
||||||
|
projectionMatrix,
|
||||||
|
(45 * Math.PI) / 180,
|
||||||
|
gl.canvas.clientWidth / gl.canvas.clientHeight,
|
||||||
|
0.1,
|
||||||
|
100.0
|
||||||
|
);
|
||||||
|
gl.uniformMatrix4fv(
|
||||||
|
shader.getUniform("uProjectionMatrix"),
|
||||||
|
false,
|
||||||
|
projectionMatrix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
initBuffers(gl: WebGLRenderingContext) {
|
||||||
|
// Scale down the unit cube before we use it
|
||||||
|
Cube.vertices = Cube.vertices.map(
|
||||||
|
(num: number) => num / this.analyser.frequencyBinCount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Position buffer
|
||||||
|
const positionBuffer = gl.createBuffer();
|
||||||
|
|
||||||
|
if (positionBuffer === null) {
|
||||||
|
throw new Error("could not initialize position buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, Cube.vertices, gl.STATIC_DRAW);
|
||||||
|
this.buffers.positions = positionBuffer;
|
||||||
|
|
||||||
|
// Index buffer
|
||||||
|
const indexBuffer = gl.createBuffer();
|
||||||
|
|
||||||
|
if (indexBuffer === null) {
|
||||||
|
throw new Error("could not initialize index buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||||
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Cube.indices, gl.STATIC_DRAW);
|
||||||
|
this.buffers.indices = indexBuffer;
|
||||||
|
|
||||||
|
// Surface normal buffer
|
||||||
|
const normalBuffer = gl.createBuffer();
|
||||||
|
|
||||||
|
if (normalBuffer === null) {
|
||||||
|
throw new Error("could not initialize normal buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, Cube.normals, gl.STATIC_DRAW);
|
||||||
|
this.buffers.normals = normalBuffer;
|
||||||
|
|
||||||
|
// fft data buffer
|
||||||
|
const fftBuffer = gl.createBuffer();
|
||||||
|
|
||||||
|
if (fftBuffer === null) {
|
||||||
|
throw new Error("could not initialize fft buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to initialize this buffer here since we will be
|
||||||
|
// updating it as soon as we start rendering anyway.
|
||||||
|
this.buffers.fft = fftBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
initGL(gl: WebGLRenderingContext, shader: Shader) {
|
||||||
|
gl.useProgram(shader.program);
|
||||||
|
gl.clearColor(
|
||||||
|
BACKGROUND_COLOR[0],
|
||||||
|
BACKGROUND_COLOR[1],
|
||||||
|
BACKGROUND_COLOR[2],
|
||||||
|
1.0
|
||||||
|
);
|
||||||
|
gl.clearDepth(1.0);
|
||||||
|
gl.enable(gl.DEPTH_TEST);
|
||||||
|
gl.depthFunc(gl.LESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMatrices(gl: WebGLRenderingContext, shader: Shader) {
|
||||||
|
this.rotation += (this.dTime / 1000.0) * ROTATION_SPEED;
|
||||||
|
|
||||||
|
const modelViewMatrix = mat4.create();
|
||||||
|
mat4.translate(modelViewMatrix, modelViewMatrix, [
|
||||||
|
0.0,
|
||||||
|
0.025,
|
||||||
|
-((this.analyser.frequencyBinCount / gl.canvas.clientWidth) * 3),
|
||||||
|
]);
|
||||||
|
mat4.rotateX(modelViewMatrix, modelViewMatrix, Math.PI / 16);
|
||||||
|
mat4.rotateY(modelViewMatrix, modelViewMatrix, this.rotation);
|
||||||
|
mat4.translate(modelViewMatrix, modelViewMatrix, [-1.0, 0.0, 0.0]);
|
||||||
|
gl.uniformMatrix4fv(
|
||||||
|
shader.getUniform("uModelViewMatrix"),
|
||||||
|
false,
|
||||||
|
modelViewMatrix
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalMatrix = mat4.create();
|
||||||
|
mat4.invert(normalMatrix, modelViewMatrix);
|
||||||
|
mat4.transpose(normalMatrix, normalMatrix);
|
||||||
|
gl.uniformMatrix4fv(
|
||||||
|
shader.getUniform("uNormalMatrix"),
|
||||||
|
false,
|
||||||
|
normalMatrix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBuffers(gl: WebGL2RenderingContext, shader: Shader) {
|
||||||
|
if (
|
||||||
|
this.buffers.indices === undefined ||
|
||||||
|
this.buffers.positions === undefined ||
|
||||||
|
this.buffers.normals === undefined ||
|
||||||
|
this.buffers.fft === undefined
|
||||||
|
) {
|
||||||
|
throw new Error("failed to create buffers before rendering");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cube buffers
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.positions);
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
shader.getAttribute("aVertexPosition"),
|
||||||
|
3,
|
||||||
|
gl.FLOAT,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
gl.enableVertexAttribArray(shader.getAttribute("aVertexPosition"));
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffers.indices);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.normals);
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
shader.getAttribute("aVertexNormal"),
|
||||||
|
3,
|
||||||
|
gl.FLOAT,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
gl.enableVertexAttribArray(shader.getAttribute("aVertexNormal"));
|
||||||
|
|
||||||
|
// Update fft
|
||||||
|
this.analyser.getByteFrequencyData(this.analyserData);
|
||||||
|
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.fft);
|
||||||
|
gl.bufferData(gl.ARRAY_BUFFER, this.analyserData, gl.STREAM_DRAW);
|
||||||
|
gl.vertexAttribPointer(
|
||||||
|
shader.getAttribute("aHeight"),
|
||||||
|
1,
|
||||||
|
gl.UNSIGNED_BYTE,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
gl.vertexAttribDivisor(shader.getAttribute("aHeight"), 1);
|
||||||
|
gl.enableVertexAttribArray(shader.getAttribute("aHeight"));
|
||||||
|
}
|
||||||
|
|
||||||
|
drawScene(gl: WebGL2RenderingContext, shader: Shader) {
|
||||||
|
this.updateMatrices(gl, shader);
|
||||||
|
this.updateBuffers(gl, shader);
|
||||||
|
|
||||||
|
let cpuTime = 0;
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
cpuTime = Math.round(performance.now() - this.lastFrameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||||
|
gl.drawElementsInstanced(
|
||||||
|
gl.TRIANGLES,
|
||||||
|
36,
|
||||||
|
gl.UNSIGNED_SHORT,
|
||||||
|
0,
|
||||||
|
this.analyser.frequencyBinCount
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
const gpuTime = Math.round(performance.now() - this.lastFrameTime);
|
||||||
|
const dTime = Math.round(this.dTime);
|
||||||
|
|
||||||
|
this.overlay.innerText = `${dTime}ms (${cpuTime}ms / ${gpuTime}ms)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextAnimationFrame = requestAnimationFrame((time) => {
|
||||||
|
this.updateTime(time);
|
||||||
|
|
||||||
|
this.drawScene(gl, shader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Renderer, RendererError };
|
183
templates/src/music/features/visualizer/Shader.ts
Normal file
183
templates/src/music/features/visualizer/Shader.ts
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
type ShaderType =
|
||||||
|
| WebGLRenderingContext["VERTEX_SHADER"]
|
||||||
|
| WebGLRenderingContext["FRAGMENT_SHADER"];
|
||||||
|
|
||||||
|
interface ShaderSource {
|
||||||
|
source: string;
|
||||||
|
kind: ShaderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShaderAttributes = Map<string, number>;
|
||||||
|
type ShaderUniforms = Map<string, WebGLUniformLocation>;
|
||||||
|
|
||||||
|
class ShaderError extends Error {}
|
||||||
|
|
||||||
|
class Shader {
|
||||||
|
private program_: WebGLProgram;
|
||||||
|
private attributes_: ShaderAttributes;
|
||||||
|
private uniforms_: ShaderUniforms;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
program: WebGLProgram,
|
||||||
|
attributes: ShaderAttributes,
|
||||||
|
uniforms: ShaderUniforms
|
||||||
|
) {
|
||||||
|
this.program_ = program;
|
||||||
|
this.attributes_ = attributes;
|
||||||
|
this.uniforms_ = uniforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
static builder(gl: WebGLRenderingContext): ShaderBuilder {
|
||||||
|
return new ShaderBuilder(gl);
|
||||||
|
}
|
||||||
|
|
||||||
|
get program(): WebGLProgram {
|
||||||
|
return this.program_;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAttribute(name: string): number {
|
||||||
|
const attribute = this.attributes_.get(name);
|
||||||
|
|
||||||
|
if (attribute === undefined) {
|
||||||
|
throw new ShaderError(`undefined shader attribute: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUniform(name: string): WebGLUniformLocation {
|
||||||
|
const uniform = this.uniforms_.get(name);
|
||||||
|
|
||||||
|
if (uniform === undefined) {
|
||||||
|
throw new ShaderError(`undefined shader uniform: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniform;
|
||||||
|
}
|
||||||
|
|
||||||
|
get uniforms(): ShaderUniforms {
|
||||||
|
return this.uniforms_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShaderBuilder {
|
||||||
|
private gl: WebGLRenderingContext;
|
||||||
|
private sources: Array<ShaderSource>;
|
||||||
|
private attributes: Array<string>;
|
||||||
|
private uniforms: Array<string>;
|
||||||
|
|
||||||
|
public constructor(gl: WebGLRenderingContext) {
|
||||||
|
this.gl = gl;
|
||||||
|
this.sources = new Array<ShaderSource>();
|
||||||
|
this.attributes = new Array<string>();
|
||||||
|
this.uniforms = new Array<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public addShader(source: string, kind: ShaderType): ShaderBuilder {
|
||||||
|
this.sources.push({ source, kind });
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addAttribute(name: string): ShaderBuilder {
|
||||||
|
this.attributes.push(name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addUniforms(name: string): ShaderBuilder {
|
||||||
|
this.uniforms.push(name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): Shader {
|
||||||
|
// Load, compile and link shader sources
|
||||||
|
const shaders = this.sources.map(({ source, kind }) => {
|
||||||
|
return this.loadShader(source, kind);
|
||||||
|
});
|
||||||
|
|
||||||
|
const shaderProgram = this.gl.createProgram();
|
||||||
|
if (shaderProgram === null) {
|
||||||
|
throw new ShaderError("failed to create shader program");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const shader of shaders) {
|
||||||
|
this.gl.attachShader(shaderProgram, shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.linkProgram(shaderProgram);
|
||||||
|
if (!this.gl.getProgramParameter(shaderProgram, this.gl.LINK_STATUS)) {
|
||||||
|
let message = "failed to link shader program";
|
||||||
|
const log = this.gl.getProgramInfoLog(shaderProgram);
|
||||||
|
if (log !== null) {
|
||||||
|
message = `failed to link shader program: ${log}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ShaderError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find attribute and uniform locations
|
||||||
|
const attributes = this.attributes.reduce((acc, attribute) => {
|
||||||
|
const attributeLocation = this.gl.getAttribLocation(
|
||||||
|
shaderProgram,
|
||||||
|
attribute
|
||||||
|
);
|
||||||
|
|
||||||
|
if (attributeLocation === -1) {
|
||||||
|
throw new ShaderError(
|
||||||
|
`shader attribute '${attribute}' could not be found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map<string, number>([
|
||||||
|
...acc,
|
||||||
|
[attribute, attributeLocation],
|
||||||
|
]);
|
||||||
|
}, new Map<string, number>());
|
||||||
|
|
||||||
|
const uniforms = this.uniforms.reduce((acc, uniform) => {
|
||||||
|
const uniformLocation = this.gl.getUniformLocation(
|
||||||
|
shaderProgram,
|
||||||
|
uniform
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uniformLocation === null) {
|
||||||
|
throw new ShaderError(
|
||||||
|
`shader uniform '${uniform}' could not be found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map<string, WebGLUniformLocation>([
|
||||||
|
...acc,
|
||||||
|
[uniform, uniformLocation],
|
||||||
|
]);
|
||||||
|
}, new Map<string, WebGLUniformLocation>());
|
||||||
|
|
||||||
|
// Build actual shader object
|
||||||
|
return new Shader(shaderProgram, attributes, uniforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadShader(source: string, kind: ShaderType): WebGLShader {
|
||||||
|
const shader = this.gl.createShader(kind);
|
||||||
|
if (shader === null) {
|
||||||
|
throw new ShaderError(`failed to initialize shader "${source}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.shaderSource(shader, source);
|
||||||
|
this.gl.compileShader(shader);
|
||||||
|
|
||||||
|
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
||||||
|
let message = `failed to compile shader "${source}"`;
|
||||||
|
const log = this.gl.getShaderInfoLog(shader);
|
||||||
|
if (log !== null) {
|
||||||
|
message = `failed to compile shader "${source}": ${log}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gl.deleteShader(shader);
|
||||||
|
|
||||||
|
throw new ShaderError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Shader, ShaderError };
|
139
templates/src/music/features/visualizer/Visualizer.tsx
Normal file
139
templates/src/music/features/visualizer/Visualizer.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { Renderer, RendererError } from "./Renderer";
|
||||||
|
import { ShaderError } from "./Shader";
|
||||||
|
|
||||||
|
import { useAppSelector } from "../../hooks";
|
||||||
|
import { PlayState, musicPlayer } from "../musicplayer/musicPlayerSlice";
|
||||||
|
|
||||||
|
function Visualizer() {
|
||||||
|
const playing = useAppSelector((state) => state.musicPlayer.playing);
|
||||||
|
const rendererState = useState<Renderer | null>(null);
|
||||||
|
let renderer = rendererState[0];
|
||||||
|
const setRenderer = rendererState[1];
|
||||||
|
const [renderError, setRenderError] = useState<JSX.Element | null>(null);
|
||||||
|
|
||||||
|
const visualizer = useCallback(
|
||||||
|
(visualizer: HTMLDivElement | null) => {
|
||||||
|
// TODO(tlater): Clean up state management. This is all
|
||||||
|
// but trivial; there's seemingly no good place to keep
|
||||||
|
// these big api objects (WebGLRenderingcontext or
|
||||||
|
// AudioContext).
|
||||||
|
//
|
||||||
|
// It's tricky, too, because obviously react expects to be
|
||||||
|
// in control of the DOM, and be allowed to delete our
|
||||||
|
// canvas and create a new one.
|
||||||
|
//
|
||||||
|
// For the moment, this works, but it's a definite hack.
|
||||||
|
if (renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Until we start playing music, there is nothing to render.
|
||||||
|
if (playing !== PlayState.Playing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (musicPlayer.audioAnalyser === undefined) {
|
||||||
|
throw new Error("MusicPlayer analyser was not set up on time");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're rendering an error message, we won't be
|
||||||
|
// setting up the visualizer.
|
||||||
|
//
|
||||||
|
// Also, nonintuitively, renderError will be null here on
|
||||||
|
// subsequent iterations, so we can't rely on it to
|
||||||
|
// identify errors.
|
||||||
|
if (visualizer === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = visualizer.children[0];
|
||||||
|
const overlay = visualizer.children[1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(canvas instanceof HTMLCanvasElement) ||
|
||||||
|
!(overlay instanceof HTMLSpanElement)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"react did not create our visualizer div correctly"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer === null) {
|
||||||
|
renderer = new Renderer(
|
||||||
|
musicPlayer.audioAnalyser,
|
||||||
|
canvas,
|
||||||
|
overlay
|
||||||
|
);
|
||||||
|
setRenderer(renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderer.initializeScene();
|
||||||
|
} catch (error) {
|
||||||
|
// Log so we don't lose the stack trace
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error instanceof ShaderError) {
|
||||||
|
setRenderError(
|
||||||
|
<span>
|
||||||
|
Failed to compile shader; This is a bug, feel free
|
||||||
|
to contact me with this error message:
|
||||||
|
<pre>
|
||||||
|
<code className="has-text-danger">
|
||||||
|
{error.message}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (error instanceof RendererError) {
|
||||||
|
setRenderError(
|
||||||
|
<span>
|
||||||
|
This browser does not support WebGL 2, sadly. This
|
||||||
|
demo uses WebGL and specifically instanced drawing,
|
||||||
|
so unfortunately this means it can't run on your
|
||||||
|
browser/device.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
setRenderError(
|
||||||
|
<span>
|
||||||
|
Something went very wrong; apologies, either your
|
||||||
|
browser is not behaving or there's a serious bug.
|
||||||
|
You can contact me with this error message:
|
||||||
|
<pre>
|
||||||
|
<code className="has-text-danger">
|
||||||
|
{error.message}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setRenderError(
|
||||||
|
<span>
|
||||||
|
Something went very wrong; apologies, either your
|
||||||
|
browser is not behaving or there's a serious bug.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[playing, renderer, musicPlayer.audioAnalyser]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (renderError === null) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={visualizer}
|
||||||
|
className="is-flex-grow-1 is-clipped is-relative"
|
||||||
|
>
|
||||||
|
<canvas className="is-block is-absolute is-border-box"></canvas>
|
||||||
|
<span className="is-bottom-left"></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Visualizer;
|
84
templates/src/music/features/visualizer/cube.ts
Normal file
84
templates/src/music/features/visualizer/cube.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/** * A hand-written 3d model of a cube.
|
||||||
|
*
|
||||||
|
* If this ever needs to be more than this, consider moving it to a
|
||||||
|
* proper .obj model.
|
||||||
|
*/
|
||||||
|
const Cube = {
|
||||||
|
// prettier-ignore
|
||||||
|
vertices: new Float32Array([
|
||||||
|
-1.0, -1.0, 1.0,
|
||||||
|
1.0, -1.0, 1.0,
|
||||||
|
1.0, 1.0, 1.0,
|
||||||
|
-1.0, 1.0, 1.0,
|
||||||
|
|
||||||
|
-1.0, -1.0, -1.0,
|
||||||
|
-1.0, 1.0, -1.0,
|
||||||
|
1.0, 1.0, -1.0,
|
||||||
|
1.0, -1.0, -1.0,
|
||||||
|
|
||||||
|
-1.0, 1.0, -1.0,
|
||||||
|
-1.0, 1.0, 1.0,
|
||||||
|
1.0, 1.0, 1.0,
|
||||||
|
1.0, 1.0, -1.0,
|
||||||
|
|
||||||
|
-1.0, -1.0, -1.0,
|
||||||
|
1.0, -1.0, -1.0,
|
||||||
|
1.0, -1.0, 1.0,
|
||||||
|
-1.0, -1.0, 1.0,
|
||||||
|
|
||||||
|
1.0, -1.0, -1.0,
|
||||||
|
1.0, 1.0, -1.0,
|
||||||
|
1.0, 1.0, 1.0,
|
||||||
|
1.0, -1.0, 1.0,
|
||||||
|
|
||||||
|
-1.0, -1.0, -1.0,
|
||||||
|
-1.0, -1.0, 1.0,
|
||||||
|
-1.0, 1.0, 1.0,
|
||||||
|
-1.0, 1.0, -1.0,
|
||||||
|
]),
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
indices: new Uint16Array([
|
||||||
|
0, 1, 2, 0, 2, 3,
|
||||||
|
4, 5, 6, 4, 6, 7,
|
||||||
|
8, 9, 10, 8, 10, 11,
|
||||||
|
12, 13, 14, 12, 14, 15,
|
||||||
|
16, 17, 18, 16, 18, 19,
|
||||||
|
20, 21, 22, 20, 22, 23,
|
||||||
|
]),
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
normals: new Float32Array([
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
0.0, 0.0, 1.0,
|
||||||
|
|
||||||
|
0.0, 0.0, -1.0,
|
||||||
|
0.0, 0.0, -1.0,
|
||||||
|
0.0, 0.0, -1.0,
|
||||||
|
0.0, 0.0, -1.0,
|
||||||
|
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
|
||||||
|
0.0, -1.0, 0.0,
|
||||||
|
0.0, -1.0, 0.0,
|
||||||
|
0.0, -1.0, 0.0,
|
||||||
|
0.0, -1.0, 0.0,
|
||||||
|
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
|
||||||
|
-1.0, 0.0, 0.0,
|
||||||
|
-1.0, 0.0, 0.0,
|
||||||
|
-1.0, 0.0, 0.0,
|
||||||
|
-1.0, 0.0, 0.0
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Cube };
|
|
@ -0,0 +1,12 @@
|
||||||
|
#version 300 es
|
||||||
|
// FRAGMENT SHADER
|
||||||
|
//
|
||||||
|
// Basic fragment shader, just passes along colors, we don't do much
|
||||||
|
// with textures or anything else complex in this project.
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
flat in vec4 vColor;
|
||||||
|
out vec4 color;
|
||||||
|
|
||||||
|
void main() { color = vColor; }
|
2
templates/src/music/features/visualizer/shaders/fragments.glsl.d.ts
vendored
Normal file
2
templates/src/music/features/visualizer/shaders/fragments.glsl.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
declare const fragments: string;
|
||||||
|
export default fragments;
|
|
@ -0,0 +1,50 @@
|
||||||
|
#version 300 es
|
||||||
|
// VERTEX SHADER
|
||||||
|
//
|
||||||
|
// Takes vertices of a unit cube, scales them up along Y according to
|
||||||
|
// aHeight, and colors them with basic diffuse shading.
|
||||||
|
|
||||||
|
#define CLEAR_COLOR vec4(0.0588235294118, 0.0588235294118, 0.0588235294118, 1.0)
|
||||||
|
#define BASE_COLOR vec3(0.6, 0.819607843137, 0.807843137255)
|
||||||
|
#define AMBIENT_LIGHT vec3(0.3, 0.3, 0.3)
|
||||||
|
#define LIGHT_DIRECTION normalize(vec3(0.85, 0.8, 0.75))
|
||||||
|
#define LIGHT_COLOR vec3(1.0, 1.0, 1.0)
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
layout(location = 0) in vec4 aVertexPosition;
|
||||||
|
layout(location = 1) in vec3 aVertexNormal;
|
||||||
|
layout(location = 2) in float aHeight;
|
||||||
|
flat out vec4 vColor;
|
||||||
|
|
||||||
|
uniform mat4 uModelViewMatrix;
|
||||||
|
uniform mat4 uProjectionMatrix;
|
||||||
|
uniform mat4 uNormalMatrix;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// The X position of each vertex depends on its cube's instance;
|
||||||
|
// they should align to the X axis.
|
||||||
|
float instanceX =
|
||||||
|
float(gl_InstanceID * 2) * abs(aVertexPosition.x) + aVertexPosition.x;
|
||||||
|
// To scale the boxes by their frequencies, scale vertex Y by the
|
||||||
|
// frequency.
|
||||||
|
float vertexY = aVertexPosition.y * aHeight;
|
||||||
|
|
||||||
|
gl_Position = uProjectionMatrix * uModelViewMatrix *
|
||||||
|
vec4(instanceX, vertexY, aVertexPosition.zw);
|
||||||
|
|
||||||
|
if (aHeight == 0.0) {
|
||||||
|
// Don't render cubes that don't currently have a height
|
||||||
|
// (frequency = 0)
|
||||||
|
vColor = CLEAR_COLOR;
|
||||||
|
} else {
|
||||||
|
// Properly shade and color any other cubes
|
||||||
|
vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0);
|
||||||
|
float directionalLight =
|
||||||
|
max(dot(transformedNormal.xyz, LIGHT_DIRECTION), 0.0);
|
||||||
|
vec3 appliedColor =
|
||||||
|
BASE_COLOR * (directionalLight * LIGHT_COLOR + AMBIENT_LIGHT);
|
||||||
|
|
||||||
|
vColor = vec4(appliedColor.rgb, 1.0);
|
||||||
|
}
|
||||||
|
}
|
2
templates/src/music/features/visualizer/shaders/vertices.glsl.d.ts
vendored
Normal file
2
templates/src/music/features/visualizer/shaders/vertices.glsl.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
declare const vertices: string;
|
||||||
|
export default vertices;
|
5
templates/src/music/hooks.ts
Normal file
5
templates/src/music/hooks.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
|
import type { RootState, AppDispatch } from "./store";
|
||||||
|
|
||||||
|
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
31
templates/src/music/index.tsx
Normal file
31
templates/src/music/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
|
||||||
|
import store from "./store";
|
||||||
|
import MusicPlayer from "./features/musicplayer/MusicPlayer";
|
||||||
|
import { setSource } from "./features/musicplayer/musicPlayerSlice";
|
||||||
|
import mseq from "./assets/Mseq_-_Journey.mp3";
|
||||||
|
|
||||||
|
const rootElement = document.getElementById("playerUI");
|
||||||
|
|
||||||
|
if (rootElement === null) {
|
||||||
|
throw Error("DOM seems to have failed to load. Something went very wrong.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(rootElement);
|
||||||
|
root.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<MusicPlayer />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
setSource({
|
||||||
|
source: mseq,
|
||||||
|
artist: "Mseq",
|
||||||
|
name: "Journey",
|
||||||
|
album: "Unknown album",
|
||||||
|
length: 192052244,
|
||||||
|
})
|
||||||
|
);
|
18
templates/src/music/music.scss
Normal file
18
templates/src/music/music.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
$fa-font-path: "npm:@fortawesome/fontawesome-free/webfonts";
|
||||||
|
|
||||||
|
@import "~/node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
|
@import "~/node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||||
|
|
||||||
|
.is-border-box {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-absolute {
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-bottom-left {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
15
templates/src/music/player.ts
Normal file
15
templates/src/music/player.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
class Player {
|
||||||
|
constructor() {
|
||||||
|
console.info("Test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let player: Player | null = null;
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
if (player === null) {
|
||||||
|
player = new Player();
|
||||||
|
}
|
||||||
|
|
||||||
|
return player;
|
||||||
|
};
|
13
templates/src/music/store.ts
Normal file
13
templates/src/music/store.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import musicPlayerReducer from "./features/musicplayer/musicPlayerSlice";
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
musicPlayer: musicPlayerReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export default store;
|
13
templates/src/music_sample.html
Normal file
13
templates/src/music_sample.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<extends src="./lib/html/base.html">
|
||||||
|
<block name="stylesheets">
|
||||||
|
<link rel="stylesheet" href="music/music.scss" />
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block name="content">
|
||||||
|
<div id="playerUI" class="is-flex-grow-1 is-flex"></div>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block name="footer" type="append">
|
||||||
|
<script type="module" src="music/index.tsx"></script>
|
||||||
|
</block>
|
||||||
|
</extends>
|
16
templates/tsconfig.json
Normal file
16
templates/tsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "es2015",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "typescript-eslint-language-service"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue