Initial commit

This commit is contained in:
Tobias Hopp 2023-05-24 00:23:46 +02:00
commit 7f52850b2e
30 changed files with 5785 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
images/cap*
package-lock.json
temp
dist

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

7
.idea/jsLibraryMappings.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{Roboto}" />
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="ASK" />
<option name="description" value="" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/photobox.iml" filepath="$PROJECT_DIR$/.idea/photobox.iml" />
</modules>
</component>
</project>

13
.idea/photobox.iml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Roboto" level="application" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

25
capture.py Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/python3
import cv2
import sys
cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)
cap.set(cv2.CAP_PROP_FPS, 30) # Setze die Framerate auf 30 fps
print("Starting")
doSave = 0
while True:
ret, frame = cap.read()
#cv2.imshow('frame', frame)
# Speichern des Frames als JPEG-Bild
file_name = f'./temp/picture.jpg'
cv2.imwrite(file_name, frame)
print("wrote_frame")
sys.stdout.flush() # Leere den Ausgabepuffer
cap.release()
cv2.destroyAllWindows()

44
html/index.html Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Photobox</title>
<meta name="viewport" content="width=device-width">
<script src="../src/web/socket.io.min.js"></script>
<script src="../dist/photobox.js"></script>
<link rel="stylesheet" href="style.css">
<link rel="preload" as="image" href="../images/broken.png">
<link rel="preload" as="image" href="../images/lock.png">
<link rel="preload" as="image" href="../images/buzzer_blue.png">
<link rel="preload" as="image" href="../images/buzzer_red.png">
<link rel="stylesheet" href="https://fonts.gaminggeneration.de/Roboto.css">
</head>
<body>
<!-- The Modal -->
<div id="myModal" class="modal">
<!-- Modal content -->
<div class="modal-content">
<span id="modalClose" class="close">&times;</span>
<h1 id="modalTitle"></h1>
<p id="modalText"></p>
</div>
</div>
<div id="flash"></div>
<div id="titleBox">
<h1>Fotobox</h1>
</div>
<div id="imageBox"><img id="imageElement"></div>
<p id="countdown"></p>
<div id="buttonBox">
<h2>Foto machen</h2>
<img id="capture" src="../images/buzzer_red.png" class="button">
<br><br>
<h2>Foto löschen</h2>
<img id="delete" src="../images/buzzer_blue.png" class="button">
</div>
<div id="lastImages">
</div>
</body>
</html>

176
html/style.css Normal file
View File

@ -0,0 +1,176 @@
body {
margin: 0;
padding: 0;
background-color: #2c2d42;
font-size: 1.1em;
font-family: Roboto, sans-serif;
}
#flash {
display: none;
position: fixed;
top:0;
left:0;
right:0;
bottom:0;
background-color: #FFFFFF;
z-index: 9999;
}
#titleBox {
text-align: center;
font-size: 2.2em;
margin-top: -2.8%;
color: #FFFFFF;
font-family: Rachana, serif;
}
#imageBox {
display: block;
position: fixed;
top:10%;
left:1%;
right:20%;
bottom: 10%;
text-align: center;
}
#buttonBox {
display: block;
position: fixed;
top: 8%;
right: 1%;
left: 79%;
width: 20%;
height: 85%;
text-align: center;
}
#buttonBox h2 {
margin-left: -5%;
color: white;
margin-bottom: -2%;
}
#imageElement {
width: 90%;
height: 85%;
box-shadow: 8px 8px #1B1B23;
margin-top: -0.5%;
border: 1px solid gray;
}
#lastImages {
position: fixed;
bottom: 0;
left: 0;
right: 2%;
width: 98%;
overflow: hidden;
height: 17%;
display: grid;
text-align: center;
grid-template-columns: repeat(7, calc(100% / 7));
grid-template-rows: 100%;
grid-column-gap: 2%;
direction: rtl;
}
.lastImage {
width: 100%;
height: 100%;
position: relative;
}
.slowFadeInAnimation {
animation: fadeIn 1s linear forwards;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.lastImageImg {
width: 100%;
height: 79%;
display: block;
}
.lastImageLock {
position: absolute;
width: 10%;
top: 65%;
left: 92%;
}
#countdown {
display: inline;
position: fixed;
top:10%;
left:1%;
right:20%;
bottom: 10%;
text-align: center;
font-size: 10em;
z-index: 99;
color: white;
font-weight: 600;
}
.button {
width: 80%;
}
/* Modal */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content */
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 70%;
text-align: center;
font-size: 1.3em;
font-family: "Roboto Light", sans-serif;;
border-radius: 15px;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}

BIN
images/broken.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/buzzer_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/buzzer_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
images/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

4
mount.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
echo "Add this script to autostart"
sudo mount -t tmpfs -o size=2m tmpfs ./temp/
echo "Mounted!"

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "photobox",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"author": "Tobias Hopp <tobi@gaminggeneration.de>",
"dependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.2.1",
"@types/node-webcam": "^0.8.0",
"@types/socket.io": "^3.0.2",
"canvas": "^2.11.2",
"express": "^4.18.2",
"node-webcam": "^0.8.1",
"socket.io": "^4.6.1",
"socket.io-client": "^4.6.1"
},
"scripts": {
"build": "tsc; webpack",
"watch": "tsc -w",
"start": "npm run build && electron ./dist/main.js"
},
"devDependencies": {
"@types/electron": "^1.6.10",
"electron": "^24.3.1",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"webpack": "^5.83.1",
"webpack-cli": "^5.1.1"
}
}

85
src/file_handler.ts Normal file
View File

@ -0,0 +1,85 @@
import * as fs from "fs";
export class FileHandler {
public static saveBase64Photo(data: string): Promise<string> {
return new Promise((resolve, reject) => {
try {
// Entferne das Präfix "data:image/jpeg;base64,"
const base64Data = data.replace(/^data:image\/jpeg;base64,/, '');
// Konvertiere den Base64-String in ein Buffer-Objekt
const imageBuffer = Buffer.from(base64Data, 'base64');
const date = new Date(); // cap_2022-06-07T23_02_23.jpeg
const filename = "cap_" + date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + "T" + date.getHours() + "_" + date.getMinutes() + "_" + date.getSeconds() + ".jpeg";
try {
fs.mkdirSync("./images/");
} catch (e) {
}
// Speichere den Buffer als JPEG-Datei
fs.writeFile("./images/" + filename, imageBuffer, (err) => {
if (err) {
console.error('Error on saving image', err);
reject(err);
} else {
console.log('Image saved as: ', filename);
resolve(filename);
}
});
} catch (e) {
reject(e);
}
});
}
public static getLastImages(): Promise<{ image: string, locked: boolean }[]> {
return new Promise(async (resolve, reject) => {
let files = await this.getSortedFiles("./images/");
console.log(files);
let i = 0;
let data: { image: string, locked: boolean }[] = [];
for (let file of files) {
if (i == 7) break;
let d = await fs.promises.readFile("./images/" + file);
let locked = ((await fs.promises.stat("./images/" + file)).birthtime.getTime() + 30000 < Date.now());
data.push({image: "data:image/jpeg;base64," + d.toString("base64"), locked: locked});
i++;
}
resolve(data);
});
}
public static getTempPicture() {
return "data:image/jpeg;base64," + (fs.readFileSync('./temp/picture.jpg').toString("base64"));
}
public static deleteLastImage() {
return new Promise(async resolve => {
let files = await this.getSortedFiles("./images/");
await fs.promises.unlink("./images/" + files[0]);
});
}
private static getSortedFiles(dir: string): Promise<string[]> {
return new Promise(async (resolve, reject) => {
const files = await fs.promises.readdir(dir);
resolve(files
.map(fileName => ({
name: fileName,
time: fs.statSync(`${dir}/${fileName}`).mtime.getTime(),
}))
.sort((a, b) => a.time - b.time)
.reverse()
.map(file => file.name)
.filter((element) => element.startsWith("cap_") )
);
});
}
}

129
src/gpio.ts Normal file
View File

@ -0,0 +1,129 @@
import * as fs from "fs";
export class GPIO {
private static openPorts: GPIO[] = [];
private readonly gpioPath: string;
private readonly gpioPin: number;
public constructor(pin: number, direction: 'in' | 'out') {
const gpioPath = `/sys/class/gpio/gpio${pin}`;
this.gpioPath = gpioPath;
this.gpioPin = pin;
try {
fs.accessSync(gpioPath);
} catch( e )
{
throw new GPIOError("GPIO not accessible");
}
// Überprüfe, ob der GPIO-Pin bereits exportiert ist
if (!fs.existsSync(gpioPath)) {
this.export();
}
// Setze den Richtungsmodus auf Ausgang
fs.writeFileSync(`${gpioPath}/direction`, direction);
GPIO.openPorts.push(this);
}
public static getPin(pin: number, direction: 'in' | 'out'): GPIO {
let port = new GPIO(pin, direction);
this.openPorts.push(port);
return port;
}
public static watchForButton(pin: number, onPress: (pin: number) => void) {
const button = new GPIO(pin, 'in');
this.openPorts.push(button);
button.watch((err) => {
if (err) {
console.error(err);
return;
}
onPress(pin);
}, 100, 500);
}
public static unexportAll() {
for (let p of this.openPorts) {
try {
p.unexport();
} catch (e) {
}
}
}
public getValue(): number {
// Überprüfe, ob der GPIO-Pin bereits exportiert ist
if (!fs.existsSync(this.gpioPath)) {
throw new GPIOError('Pin not exported');
}
// Lese den Wert des GPIO-Pins
const value = fs.readFileSync(`${this.gpioPath}/value`, 'utf8');
return Number.parseInt(value.trim());
}
public setValue(value: number) {
// Setze den Wert auf X
fs.writeFileSync(`${(this.gpioPath)}/value`, String(value));
}
public setHigh() {
this.setValue(1);
}
public setLow() {
this.setValue(0);
}
public changeDirection(direction: 'in' | 'out') {
// Setze den Richtungsmodus auf Ausgang
fs.writeFileSync(`${this.gpioPath}/direction`, direction);
}
public watch(callback: (value: number) => void, interval: number = 100, bounceWait = 500) {
let lastState = this.getValue();
let lastBounce = Date.now();
setInterval(() => {
if (lastBounce + bounceWait > Date.now())
return;
let val = this.getValue();
if (lastState != val) {
callback(val);
lastState = val;
}
}, interval);
}
private export() {
// Exportiere den GPIO-Pin
fs.writeFileSync('/sys/class/gpio/export', this.gpioPin.toString());
}
private unexport() {
// Exportiere den GPIO-Pin
fs.writeFileSync('/sys/class/gpio/unexport', this.gpioPin.toString());
}
}
export class GPIOError extends Error {
constructor(msg: string) {
super(msg);
// Set the prototype explicitly.
Object.setPrototypeOf(this, GPIOError.prototype);
}
}

110
src/main.ts Normal file
View File

@ -0,0 +1,110 @@
import {app, BrowserWindow} from "electron";
import * as path from "path";
import {Server} from "./server";
import * as process from "process";
import {GPIO} from "./gpio";
import {PythonWebcam} from "./python_webcam";
import {FileHandler} from "./file_handler";
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
height: 1080,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
width: 1920,
fullscreenable: true,
fullscreen: true,
closable: true,
autoHideMenuBar: true,
});
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, "../html/index.html"));
// Open the DevTools.
//mainWindow.webContents.openDevTools();
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow();
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
app.quit();
});
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
Server.initServer().then();
PythonWebcam.startPythonService();
/*setInterval(() => {
capture()
}, 1000*10);*/
try {
// Capture button
GPIO.watchForButton(10, (pin) => {
Main.capture()
});
// Delete button
GPIO.watchForButton(11, (pin) => {
console.log("Delete fired!");
});
} catch (e) {
console.error("Could not bind gpio ports");
}
process.on("SIGINT", _ => {
GPIO.unexportAll();
});
export class Main {
private static port = GPIO.getPin(20, 'out')
static async delete_last() {
console.log("Delete fired!");
let last_shots = await FileHandler.getLastImages();
let last = last_shots[0];
if(last.locked) {
Server.io.emit("deleting", false);
}
else
{
Server.io.emit("deleting", true);
await FileHandler.deleteLastImage();
await Server.sendRecentPhotos();
}
}
static capture() {
console.log("Capture fired!");
Server.io.emit("capturing", 3);
setTimeout( async () => {
this.port.setHigh();
}, 2300 );
setTimeout(async () => {
let base64_string = await PythonWebcam.captureImage();
Server.io.emit("captured", base64_string);
this.port.setLow();
}, 3000);
}
}

0
src/preload.ts Normal file
View File

73
src/python_webcam.ts Normal file
View File

@ -0,0 +1,73 @@
import * as child_process from "child_process";
import {FileHandler} from "./file_handler";
import {Server} from "./server";
const {spawn} = require('node:child_process');
export class PythonWebcam {
private static filename = __dirname + "/../capture.py";
private static waitFor = "wrote_frame";
private static calls: Function[] = [];
private static noCamera = true;
private static process: child_process.ChildProcess;
public static startPythonService() {
this.process = spawn("python", [this.filename], {stdio: 'pipe'});
this.process.stdout.setEncoding('utf-8');
console.log("spawned")
this.process.stdout.on('data', (data) => {
this.noCamera = false;
const line = data.trim().split('\n')[0].replace(/\r?\n|\r/gm, "").replace("\n", '');
if (line.startsWith(this.waitFor.trim())) {
for (let c of this.calls) {
c();
}
this.calls = [];
}
});
this.process.on("exit", (code) => {
console.log("Subprocess exited! " + code)
if (code != 0)
{
this.noCamera = true;
throw new Error("Could not open camera device/script - Code: " + code.toString());
}
});
}
public static getImage(): Promise<String | Buffer> {
return new Promise((resolve, reject) => {
if( this.calls.length > 1 )
{
return reject("Already capturing!");
}
this.waitForScriptFrame(() => {
resolve(FileHandler.getTempPicture());
});
});
}
public static captureImage(): Promise<string> {
return new Promise(async (resolve, reject) => {
let img = (await this.getImage()).toString();
await FileHandler.saveBase64Photo(img);
resolve(img);
await Server.sendRecentPhotos();
});
}
private static waitForScriptFrame(callback: () => void) {
if(this.noCamera)
{
return;
}
this.calls.push(callback);
}
}

0
src/renderer.ts Normal file
View File

64
src/server.ts Normal file
View File

@ -0,0 +1,64 @@
import {Socket} from "socket.io";
import {FileHandler} from "./file_handler";
import {PythonWebcam} from "./python_webcam";
import {Main} from "./main";
export class Server {
private static app = require("express")();
private static server = require('http').createServer(this.app);
public static io = require('socket.io')(this.server);
private static interval : NodeJS.Timer;
public static initServer(): Promise<void> {
return new Promise((resolve, reject) => {
// Socket.IO-Verbindung herstellen
this.io.on('connection', async (socket: Socket) => {
console.log('Client connected');
await this.sendRecentPhotos();
socket.on("do_shot", () => {
console.log("Capture fired!");
Server.io.emit("capturing", "");
setTimeout(async () => {
let base64_string = await PythonWebcam.captureImage();
Server.io.emit("captured", base64_string);
}, 3000);
});
socket.on("do_delete", () => {
Main.delete_last();
})
});
//Webcam.startCameraStream();
this.server.listen(3000);
this.startInterval();
});
}
public static async sendRecentPhotos()
{
let lastImages = await FileHandler.getLastImages();
console.log("Sending last shots... " );
this.io.emit('last_shots', lastImages);
}
public static startInterval(){
setTimeout(async () => {
try {
this.io.emit("stream", (await PythonWebcam.getImage()));
} catch( e )
{
}
this.startInterval();
}, 1000/30);
}
}

126
src/web/main.ts Normal file
View File

@ -0,0 +1,126 @@
import {io} from "socket.io-client";
let isShowingCaptured = false;
function openModal(title: string, text: string, closeTimeout = 4000 )
{
let modal = document.getElementById("myModal");
let modalTitle = document.getElementById("modalTitle");
let modalText = document.getElementById("modalText");
modalTitle.innerText = title;
modalText.innerHTML = text;
modal.style.display = "block";
let t = setTimeout( () => {
modal.style.display = "none";
}, closeTimeout );
let modalClose = document.getElementById("modalClose");
modalClose.onclick = () => {
clearTimeout(t);
modal.style.display = "none";
}
}
document.addEventListener("DOMContentLoaded", () => {
const imageElement = document.getElementById('imageElement') as HTMLImageElement;
const lastImages = document.getElementById('lastImages') as HTMLImageElement;
const countdown = document.getElementById("countdown") as HTMLParagraphElement;
const flash = document.getElementById("flash");
const capture_btn = document.getElementById("capture");
const del_btn = document.getElementById("delete");
let socket = io("http://localhost:3000");
socket.on("stream", (data) => {
//console.log(data);
if (isShowingCaptured) return;
imageElement.src = data;
});
socket.on("last_shots", (data: { image: string, locked: boolean }[]) => {
lastImages.innerHTML = "";
for (let last of data) {
let img = document.createElement("img");
img.src = last.image;
img.classList.add("lastImageImg");
img.alt = "Bild wird geladen...";
let lock = document.createElement("img");
lock.src = last.locked ? "../images/lock.png" : "../images/broken.png";
lock.classList.add("lastImageLock");
lock.alt = "[Lock]";
let div = document.createElement("div");
div.classList.add("lastImage");
div.append(img);
div.onclick = () => {
openModal("Foto Ansicht", `<img src="${last.image}" style="width: 90%">`, 1000 * 15 );
}
div.append(lock);
if (data[0] == last) {
div.classList.add("slowFadeInAnimation");
setTimeout(() => lastImages.prepend(div), 500);
} else
lastImages.append(div);
}
});
socket.on('capturing', (data) => {
countdown.innerText = "3";
setTimeout(() => {
countdown.innerText = "2";
setTimeout(() => {
countdown.innerText = "1";
setTimeout(() => {
countdown.innerText = "CHEESE!";
setTimeout(() => {
flash.style.display = "block";
}, 150);
}, 800);
}, 950);
}, 1000)
});
socket.on('captured', (data) => {
isShowingCaptured = true;
flash.style.display = "none";
imageElement.src = data;
setTimeout(() => {
countdown.innerText = "";
}, 250);
setTimeout(() => isShowingCaptured = false, 3000);
});
socket.on('deleting', (data: boolean) => {
if(!data)
{
openModal("Löschen nicht möglich!", "Foto kann nicht entfernt werden, da es bereits zu alt ist.<br>Wende dich bei Fragen an Tobi");
}
else
{
openModal("Löschen erfolgreich!", "Foto wurde unwiderruflich entfernt.");
}
});
capture_btn.onclick = () => {
socket.emit("do_shot");
}
del_btn.onclick = () => {
socket.emit("do_delete");
}
});

7
src/web/socket.io.min.js vendored Normal file

File diff suppressed because one or more lines are too long

56
src/webcam.ts Normal file
View File

@ -0,0 +1,56 @@
import {FSWebcam, ImageSnapWebcam, WebcamOptions, WindowsWebcam} from "node-webcam";
import * as fs from "fs";
import {FileHandler} from "./file_handler";
import {Server} from "./server";
const NodeWebcam = require("node-webcam");
export class Webcam {
// Konfiguriere Kamera
private static videoDevice = '/dev/video0';
private static camera: ImageSnapWebcam | FSWebcam | WindowsWebcam = null;
private static width = 1280;
private static height = 720;
public static startCameraStream() {
const webcamOptions = {
width: this.width,
height: this.height,
quality: 100,
output: "jpeg",
callbackReturn: "base64",
verbose: false,
device: this.videoDevice
} as WebcamOptions;
this.camera = NodeWebcam.create(webcamOptions);
}
public static getImage(): Promise<String | Buffer> {
return new Promise(resolve => {
if( !fs.existsSync("./temp/") )
fs.mkdirSync("./temp/");
this.camera.capture("./temp/picture.jpg", function (err, data) {
if (!err) {
resolve(data);
} else {
console.error("Error on webcam capture: ", err);
}
});
});
}
public static captureImage(): Promise<string>
{
return new Promise(async (resolve,reject) => {
let img = (await this.getImage() ).toString();
await FileHandler.saveBase64Photo(img);
resolve(img);
await Server.sendRecentPhotos();
});
}
}

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*"
]
},
"removeComments": true
},
"include": [
"src/**/*"
]
}

22
webpack.config.js Normal file
View File

@ -0,0 +1,22 @@
// webpack.config.js
module.exports = [
{
mode: 'development',
entry: './src/web/main.ts',
devtool: "inline-source-map",
module: {
rules: [{
test: /\.ts$/,
include: /src/,
use: [{ loader: 'ts-loader' }]
}]
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
path: __dirname + '/dist',
filename: 'photobox.js'
}
}
];

2468
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

2296
yarn.lock Normal file

File diff suppressed because it is too large Load Diff