feat: Migrate project to TypeScript by adding configuration, dependencies, and converting preload script.

This commit is contained in:
2025-12-12 15:16:01 +01:00
parent 87e5f5a72c
commit 8b83428803
8 changed files with 155 additions and 78 deletions
+28 -15
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -7,9 +8,12 @@
<!-- Premium Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700&family=JetBrains+Mono:wght@400&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="wrapper">
<!-- Main Scroll Area -->
@@ -32,7 +36,9 @@
<button id="btn-open" class="icon-btn" title="Open File">
<!-- Folder Icon -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="icon">
<path d="M4 6C4 4.89543 4.89543 4 6 4H10L12 6H18C19.1046 6 20 6.89543 20 8V18C20 19.1046 19.1046 20 18 20H6C4.89543 20 4 19.1046 4 18V6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M4 6C4 4.89543 4.89543 4 6 4H10L12 6H18C19.1046 6 20 6.89543 20 8V18C20 19.1046 19.1046 20 18 20H6C4.89543 20 4 19.1046 4 18V6Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
@@ -41,14 +47,18 @@
<div class="control-group">
<button id="btn-play" class="action-btn" title="Start/Stop Scrolling">
<!-- Play Icon -->
<!-- Play Icon -->
<svg id="icon-play" width="24" height="24" viewBox="0 0 24 24" fill="none" class="icon">
<path d="M5 3L19 12L5 21V3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="currentColor"/>
<path d="M5 3L19 12L5 21V3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" fill="currentColor" />
</svg>
<!-- Pause Icon (Initially hidden) -->
<svg id="icon-pause" width="24" height="24" viewBox="0 0 24 24" fill="none" class="icon" style="display: none;">
<path d="M10 4H6V20H10V4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="currentColor"/>
<path d="M18 4H14V20H18V4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="currentColor"/>
<svg id="icon-pause" width="24" height="24" viewBox="0 0 24 24" fill="none" class="icon"
style="display: none;">
<path d="M10 4H6V20H10V4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" fill="currentColor" />
<path d="M18 4H14V20H18V4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" fill="currentColor" />
</svg>
</button>
</div>
@@ -67,20 +77,23 @@
<input type="range" id="input-size" min="20" max="150" value="60">
</div>
<div class="divider"></div>
<div class="divider"></div>
<div class="control-group">
<button id="btn-reset" class="icon-btn" title="Reset Position">
<div class="control-group">
<button id="btn-reset" class="icon-btn" title="Reset Position">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="icon">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3v5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M3 3v5h5" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
</div>
</button>
</div>
</div>
</div>
<script src="./node_modules/marked/marked.min.js"></script>
<script src="renderer.js"></script>
<script src="dist/renderer.js"></script>
</body>
</html>
+49 -8
View File
@@ -12,7 +12,10 @@
"marked": "^15.0.12"
},
"devDependencies": {
"electron": "^39.2.6"
"@types/marked": "^5.0.2",
"@types/node": "^25.0.1",
"electron": "^39.2.6",
"typescript": "^5.9.3"
}
},
"node_modules/@electron/get": {
@@ -93,14 +96,21 @@
"@types/node": "*"
}
},
"node_modules/@types/marked": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.19.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz",
"integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~7.16.0"
}
},
"node_modules/@types/responselike": {
@@ -307,6 +317,23 @@
"node": ">= 12.20.55"
}
},
"node_modules/electron/node_modules/@types/node": {
"version": "22.19.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/electron/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -848,10 +875,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
+7 -3
View File
@@ -1,16 +1,20 @@
{
"name": "teleprompter",
"version": "1.0.0",
"main": "main.js",
"main": "dist/main.js",
"scripts": {
"start": "electron ."
"build": "tsc",
"start": "npm run build && electron ."
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"electron": "^39.2.6"
"@types/marked": "^5.0.2",
"@types/node": "^25.0.1",
"electron": "^39.2.6",
"typescript": "^5.9.3"
},
"dependencies": {
"marked": "^15.0.12"
-6
View File
@@ -1,6 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath)
});
+6 -5
View File
@@ -1,6 +1,6 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
function createWindow() {
const win = new BrowserWindow({
@@ -17,7 +17,8 @@ function createWindow() {
titleBarStyle: 'hiddenInset' // Looks cleaner on macOS, normal on others
});
win.loadFile('index.html');
win.loadFile(path.join(__dirname, '../index.html'));
// Note: index.html is in the root, dist/main.js is in dist/. So we need to go up one level.
}
app.whenReady().then(() => {
@@ -52,7 +53,7 @@ ipcMain.handle('open-file-dialog', async () => {
}
});
ipcMain.handle('read-file', async (event, filePath) => {
ipcMain.handle('read-file', async (event, filePath: string) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return content;
+6
View File
@@ -0,0 +1,6 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath)
});
+39 -36
View File
@@ -1,18 +1,31 @@
const btnHeroOpen = document.getElementById('btn-hero-open');
const btnOpen = document.getElementById('btn-open');
const btnPlay = document.getElementById('btn-play');
const btnReset = document.getElementById('btn-reset');
const inputSpeed = document.getElementById('input-speed');
const inputSize = document.getElementById('input-size');
const markdownOutput = document.getElementById('markdown-output');
const placeholder = document.getElementById('placeholder');
const scrollContainer = document.getElementById('scroll-container');
const iconPlay = document.getElementById('icon-play');
const iconPause = document.getElementById('icon-pause');
interface ElectronAPI {
openFileDialog: () => Promise<string | null>;
readFile: (path: string) => Promise<string>;
}
interface Window {
electronAPI: ElectronAPI;
}
declare const marked: {
parse: (content: string) => string;
};
const btnHeroOpen = document.getElementById('btn-hero-open') as HTMLButtonElement;
const btnOpen = document.getElementById('btn-open') as HTMLButtonElement;
const btnPlay = document.getElementById('btn-play') as HTMLButtonElement;
const btnReset = document.getElementById('btn-reset') as HTMLButtonElement;
const inputSpeed = document.getElementById('input-speed') as HTMLInputElement;
const inputSize = document.getElementById('input-size') as HTMLInputElement;
const markdownOutput = document.getElementById('markdown-output') as HTMLDivElement;
const placeholder = document.getElementById('placeholder') as HTMLDivElement;
const scrollContainer = document.getElementById('scroll-container') as HTMLDivElement;
const iconPlay = document.getElementById('icon-play') as HTMLElement;
const iconPause = document.getElementById('icon-pause') as HTMLElement;
let isScrolling = false;
let scrollAccumulator = 0;
let animationId;
let animationId: number;
let scrollSpeed = parseInt(inputSpeed.value, 10);
// Initialize
@@ -28,7 +41,7 @@ function setupEventListeners() {
btnPlay.addEventListener('click', toggleScrolling);
// Spacebar to toggle
document.addEventListener('keydown', (e) => {
document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.code === 'Space') {
e.preventDefault(); // Prevent page down scroll
toggleScrolling();
@@ -40,27 +53,31 @@ function setupEventListeners() {
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
});
inputSpeed.addEventListener('input', (e) => {
scrollSpeed = parseInt(e.target.value, 10);
inputSpeed.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement;
scrollSpeed = parseInt(target.value, 10);
});
inputSize.addEventListener('input', (e) => {
updateFontSize(e.target.value);
inputSize.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement;
updateFontSize(target.value);
});
}
async function openFile() {
const filePath = await window.electronAPI.openFileDialog();
// Explicitly cast window if interface merging fails in some environments
const api = (window as unknown as Window).electronAPI;
const filePath = await api.openFileDialog();
if (filePath) {
const content = await window.electronAPI.readFile(filePath);
const content = await api.readFile(filePath);
renderMarkdown(content);
}
}
function renderMarkdown(content) {
function renderMarkdown(content: string) {
if (!content) return;
// Using marked from global scope (loaded via script tag)
// Using marked from global scope
const html = marked.parse(content);
markdownOutput.innerHTML = html;
@@ -72,7 +89,7 @@ function renderMarkdown(content) {
scrollContainer.scrollTop = 0;
}
function updateFontSize(size) {
function updateFontSize(size: string) {
markdownOutput.style.fontSize = `${size}px`;
}
@@ -106,31 +123,17 @@ function stopScrolling() {
function step() {
if (!isScrolling) return;
// Calculate scroll amount based on speed
// Map 1-100 to sensible range.
// Let's say max speed (100) is ~300 pixels per second? ~5px/frame at 60fps
// Min speed (1) is ~10 pixels per second? ~0.16px/frame
// Formula: (speed / 100) * maxPixelsPerFrame
// Adding a base speed so 1 isn't 0.
const maxSpeedValues = 5.0;
const minSpeedValues = 0.2;
// Exponential curve for better control at lower speeds
// Normalized speed 0-1
const normalizedSpeed = (scrollSpeed - 1) / 99;
// Linear interpolation? Or quadratic? Quadratic feels "acceleration"
// Let's stick to linear for predictability first, user says "simple"
const pixelsPerFrame = minSpeedValues + (normalizedSpeed * (maxSpeedValues - minSpeedValues));
scrollAccumulator += pixelsPerFrame;
const pixelsToScroll = Math.floor(scrollAccumulator);
if (pixelsToScroll >= 1) {
// Stop if at bottom
// Use a small buffer (1px) for float inconsistencies
if (Math.ceil(scrollContainer.scrollTop + scrollContainer.clientHeight) >= scrollContainer.scrollHeight) {
stopScrolling();
return;
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
]
}