diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 7abab24..18ed763 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,31 +2,30 @@ name: Go on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: - build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.22 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.24.0 - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: go test -v ./... + - name: Test + run: go test -v ./... - - name: GoGitOps Step - id: gogitops - uses: beaujr/gogitops-action@v0.2 - with: - github-actions-user: owulveryck - github-actions-token: ${{secrets.GITHUB_TOKEN}} + - name: GoGitOps Step + id: gogitops + uses: beaujr/gogitops-action@v0.2 + with: + github-actions-user: owulveryck + github-actions-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 2c6a763..8db8094 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.23 + go-version: 1.24.0 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: diff --git a/README.md b/README.md index 49aed60..eb48b7e 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ When running on the Remarkable Paper Pro, `RLE_COMPRESSION` environment variable - **Laser Pointer**: Features a laser pointer that activates on hovering. - **Gesture Support**: Full integration with Reveal.js, allowing slide switching directly from the reMarkable. - **Overlay Feature**: Allows overlaying over existing websites that support iframe embedding. -- **Built-in Ngrok**: Enables streaming over different networks easily. - **Live Parameter Tweaking**: Side menu for live adjustments, including screen rotation. +- **Dark Mode**: Toggle between light and dark themes for comfortable viewing in any environment. +- **Version API**: Check the current version via the `/version` endpoint. ## Quick Start @@ -173,23 +174,12 @@ Add query parameters to the URL (`?parameter=value&otherparameter=value`): - `rate`: (integer, 100-...) Set the frame rate. - `flip`: (true/false) Enable or disable flipping 180 degree. -## Tunneling -Tunneling with built-in Ngrok allows for streaming across different networks. -This feature is particularly useful for remote presentations or collaborative sessions. -To set up tunneling, simply enable Ngrok in the tool's settings and follow the instructions provided in the user interface. - -If your reMarkable is on a different network than the displaying device, you can use the `ngrok` builtin feature for automatic tunneling. -To utilize this tunneling, you need to sign up for an ngrok account and [obtain a token from the dashboard](https://dashboard.ngrok.com/get-started/your-authtoken). -Once you have the token, launch reMarkable using the following command: - -`NGROK_AUTHTOKEN=YOURTOKEN RK_SERVER_BIND_ADDR=ngrok ./goMarkableStream` - -The app will start, displaying a message similar to: - -`2023/09/29 16:49:20 listening on 72e5-22-159-32-48.ngrok-free.app` - -Then, connect to `https://72e5-22-159-32-48.ngrok-free.app` to view the result. - +### API Endpoints +- `/`: Main web interface +- `/stream`: The image data stream +- `/events`: WebSocket endpoint for pen input events +- `/gestures`: Endpoint for touch events +- `/version`: Returns the current version of goMarkableStream ## Presentation Mode `goMarkableStream` introduces an innovative experimental feature that allows users to set a presentation or video in the background, enabling live annotations using a reMarkable tablet. @@ -218,9 +208,9 @@ This includes a variety of presentation and video platforms. Switch slides or navigate through your presentation directly from your reMarkable tablet. This seamless integration enhances the experience of both presenting and viewing, making it ideal for educational and professional environments. -Howto: add the `?present=https://your-reveal-js-presentation` +How to: add the `?present=https://your-reveal-js-presentation` -_note_: due to browser restrictions, the URL mus +_note_: due to browser restrictions, the URL must be HTTPS. ### Limitations and Performance @@ -229,12 +219,12 @@ _note_: due to browser restrictions, the URL mus Users must use the side menu for navigation and control. - This feature operates seamlessly, with no additional load on the reMarkable tablet, as all rendering is done in the client's browser. -### Feedback and Contributions +### UI Features -- As this is an experimental feature, your feedback is crucial for its development. -Please share your experiences, suggestions, and any issues encountered using the GitHub issues section of this repository. - ---- +- **Dark Mode**: Toggle between light and dark themes using the sun/moon icon in the sidebar +- **Modern Interface**: Improved UI with better typography and layout +- **Tooltips**: Helpful tooltips on hover for all buttons +- **Feedback Messages**: Visual feedback for user actions ## Technical Details @@ -246,16 +236,7 @@ This tool suits my need and is an ongoing development. You can find various info This is a standalone application that runs directly on a Remarkable tablet. It does not have any dependencies on third-party libraries, making it a completely self-sufficient solution. -This application exposes an HTTP server with two endpoints: -### Endpoints - -- `/`: This endpoint serves an embedded HTML and JavaScript file containing the necessary logic to display an image from the Remarkable tablet on a client's web browser. - -- `/stream`: This endpoint streams the image data from the Remarkable tablet to the client continuously. -- `/events`: This endpoint streams the pen input events via websockets -- `gestures`: This endpoints streams the touch events in binary - -**Caution**: the API may change over time +This application exposes an HTTP server with several endpoints. ### Implementation @@ -296,5 +277,4 @@ Feel free to modify, distribute, and use the tool in accordance with the terms o ## Tipping If you plan to buy a reMarkable 2, you can use my [referal program link](https://remarkable.com/referral/PY5B-PH8U). -It will provide a discount for you and also for me. - +It will provide a discount for you and also for me. \ No newline at end of file diff --git a/client/glCanvas.js b/client/glCanvas.js index 5c028a6..6f46481 100644 --- a/client/glCanvas.js +++ b/client/glCanvas.js @@ -1,9 +1,12 @@ // WebGL initialization -//const gl = visibleCanvas.getContext('webgl'); -//const gl = canvas.getContext('webgl', { antialias: true, preserveDrawingBuffer: true }); -let laserX = 0; // Initialize with default values -let laserY = 0; -const gl = canvas.getContext('webgl', { antialias: true }); +// Use -10,-10 as the default laser coordinate (off-screen) to hide the pointer initially +let laserX = -10; +let laserY = -10; +const gl = canvas.getContext('webgl', { + antialias: true, + preserveDrawingBuffer: true, // Important for proper rendering + alpha: true // Enable transparency +}); if (!gl) { @@ -19,30 +22,64 @@ uniform float uScaleFactor; varying highp vec2 vTextureCoord; void main(void) { - gl_Position = uRotationMatrix * vec4(aVertexPosition.xy * uScaleFactor, aVertexPosition.zw); - vTextureCoord = aTextureCoord; + // Apply scaling and rotation transformations + gl_Position = uRotationMatrix * vec4(aVertexPosition.xy * uScaleFactor, aVertexPosition.zw); + + // Pass texture coordinates to fragment shader + vTextureCoord = aTextureCoord; } `; // Fragment shader program const fsSource = ` -precision mediump float; +precision highp float; varying highp vec2 vTextureCoord; uniform sampler2D uSampler; uniform float uLaserX; uniform float uLaserY; +uniform bool uDarkMode; +uniform float uContrastLevel; + +// Constants for laser pointer visualization +const float LASER_RADIUS = 6.0; +const float LASER_EDGE_SOFTNESS = 2.0; +const vec3 LASER_COLOR = vec3(1.0, 0.0, 0.0); + +// Constants for image processing +const float BRIGHTNESS = 0.05; // Slight brightness boost +const float SHARPNESS = 0.5; // Sharpness level + +// Get texture color without any sharpening - better for handwriting +vec4 getBaseTexture(sampler2D sampler, vec2 texCoord) { + return texture2D(sampler, texCoord); +} void main(void) { + // Get base texture color directly - no sharpening for clearer handwriting + vec4 texColor = getBaseTexture(uSampler, vTextureCoord); + + // Apply contrast adjustments based on the slider value + vec3 adjusted = (texColor.rgb - 0.5) * uContrastLevel + 0.5; + texColor.rgb = clamp(adjusted, 0.0, 1.0); + + // Calculate laser pointer effect float dx = gl_FragCoord.x - uLaserX; float dy = gl_FragCoord.y - uLaserY; float distance = sqrt(dx * dx + dy * dy); - float radius = 5.0; // Radius of the dot, adjust as needed - - if(distance < radius) { - gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // Red color for the laser dot + + if (uDarkMode) { + // Invert colors in dark mode, but preserve alpha + texColor.rgb = 1.0 - texColor.rgb; + } + + // Simple laser pointer - more reliable rendering + if (distance < 8.0 && uLaserX > 0.0 && uLaserY > 0.0) { + // Create solid circle with slight fade at edge + float fade = 1.0 - smoothstep(6.0, 8.0, distance); + gl_FragColor = vec4(1.0, 0.0, 0.0, fade); // Red with fade at edge } else { - gl_FragColor = texture2D(uSampler, vTextureCoord); + gl_FragColor = texColor; } } `; @@ -107,6 +144,8 @@ const programInfo = { uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'), uLaserX: gl.getUniformLocation(shaderProgram, 'uLaserX'), uLaserY: gl.getUniformLocation(shaderProgram, 'uLaserY'), + uDarkMode: gl.getUniformLocation(shaderProgram, 'uDarkMode'), + uContrastLevel: gl.getUniformLocation(shaderProgram, 'uContrastLevel'), }, }; @@ -172,18 +211,34 @@ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); let imageData = new ImageData(screenWidth, screenHeight); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData); +// Variables to track display state +let isDarkMode = false; +let contrastValue = 1.15; // Default contrast value + // Draw the scene function drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture) { + // Handle canvas resize for proper rendering if (resizeGLCanvas(gl.canvas)) { gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); } - gl.clearColor(0.5, 0.5, 0.5, 0.25); // Gray with 75% transparency - gl.clearDepth(1.0); // Clear everything - gl.enable(gl.DEPTH_TEST); // Enable depth testing - gl.depthFunc(gl.LEQUAL); // Near things obscure far things + + // Adjust background color based on dark mode + const bgColor = isDarkMode + ? [0.12, 0.12, 0.13, 0.25] // Darker, more neutral dark mode bg + : [0.98, 0.98, 0.98, 0.25]; // Nearly white light mode bg + gl.clearColor(bgColor[0], bgColor[1], bgColor[2], bgColor[3]); + + // Enable alpha blending for transparency + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Setup depth buffer + gl.clearDepth(1.0); + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); // Clear the canvas before we start drawing on it. - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // Tell WebGL to use our program when drawing gl.useProgram(programInfo.program); @@ -206,9 +261,13 @@ function drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture) // Tell the shader we bound the texture to texture unit 0 gl.uniform1i(programInfo.uniformLocations.uSampler, 0); - // Set the laser coordinates + // Set the laser coordinates gl.uniform1f(programInfo.uniformLocations.uLaserX, laserX); gl.uniform1f(programInfo.uniformLocations.uLaserY, laserY); + + // Set display flags + gl.uniform1i(programInfo.uniformLocations.uDarkMode, isDarkMode ? 1 : 0); + gl.uniform1f(programInfo.uniformLocations.uContrastLevel, contrastValue); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } @@ -263,9 +322,72 @@ function resizeGLCanvas(canvas) { return false; // indicates no change in size } +// Direct laser pointer position - no animation for more reliability function updateLaserPosition(x, y) { - laserX = x / screenWidth * gl.canvas.width; - laserY = gl.canvas.height - (y / screenHeight * gl.canvas.height); - + // If x and y are valid positive values + if (x > 0 && y > 0) { + // Position is now directly proportional to canvas size + laserX = x * (gl.canvas.width / screenWidth); + laserY = gl.canvas.height - (y * (gl.canvas.height / screenHeight)); + } else { + // Hide the pointer by moving it off-screen + laserX = -10; + laserY = -10; + } + + // Redraw immediately drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); } + +// Function to update dark mode state with transition effect +let darkModeTransition = 0; // 0 = light mode, 1 = dark mode +let transitionActive = false; + +function setDarkMode(darkModeEnabled) { + isDarkMode = darkModeEnabled; + + // If not already transitioning, start a smooth transition + if (!transitionActive) { + transitionActive = true; + const startTime = performance.now(); + const duration = 300; // transition duration in ms + + function animateDarkModeTransition(timestamp) { + const elapsed = timestamp - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Update transition value (0 to 1 for light to dark) + darkModeTransition = darkModeEnabled ? progress : 1 - progress; + + // Render with current transition value + drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); + + // Continue animation if not complete + if (progress < 1) { + requestAnimationFrame(animateDarkModeTransition); + } else { + transitionActive = false; + } + } + + requestAnimationFrame(animateDarkModeTransition); + } else { + // Just update the scene if already transitioning + drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); + } +} + +// Function to set contrast level +function setContrast(contrastLevel) { + // Store the contrast value (between 1.0 and 3.0) + contrastValue = parseFloat(contrastLevel); + + // If the value is valid, update rendering + if (!isNaN(contrastValue) && contrastValue >= 1.0 && contrastValue <= 3.0) { + // Update the scene with new contrast + drawScene(gl, programInfo, positionBuffer, textureCoordBuffer, texture); + + // Save user preference to localStorage + localStorage.setItem('contrastLevel', contrastValue); + } +} diff --git a/client/index.html b/client/index.html index 7f889be..26e5059 100644 --- a/client/index.html +++ b/client/index.html @@ -3,7 +3,7 @@ goMarkableStream - + + + + + + + - - - - - - - - + \ No newline at end of file diff --git a/client/main.js b/client/main.js index ed5f31a..4141de3 100644 --- a/client/main.js +++ b/client/main.js @@ -54,7 +54,7 @@ function getBoolQueryParam(param, defaultValue = false) { return value === 'true'; } -window.onload = function() { +window.onload = async function() { // Function to get the value of a query parameter by name // Get the 'present' parameter from the URL const presentURL = getQueryParam('present'); @@ -63,6 +63,13 @@ window.onload = function() { if (presentURL) { document.getElementById('content').src = presentURL; } + + // Update version in the sidebar footer + const version = await fetchVersion(); + const versionElement = document.querySelector('.sidebar-footer small'); + if (versionElement) { + versionElement.textContent = `goMarkableStream ${version}`; + } }; // Add an event listener for the 'beforeunload' event, which is triggered when the page is refreshed or closed diff --git a/client/style.css b/client/style.css index 3d26d8e..f64add7 100644 --- a/client/style.css +++ b/client/style.css @@ -1,182 +1,672 @@ /* CSS styles for the layout */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +:root { + /* Light Mode Colors */ + --background-color: #f8f9fa; + --container-bg: #ffffff; + --sidebar-bg: #f0f4f8; + --text-color: #333333; + --text-secondary: #6c757d; + --accent-color: #007AFF; + --accent-hover: #0056b3; + --border-color: rgba(0, 0, 0, 0.1); + --shadow-color: rgba(0, 0, 0, 0.08); + --toggle-bg: #e9ecef; + --toggle-circle: #ffffff; + --button-success: #34C759; + --button-success-hover: #28a745; + --tooltip-bg: rgba(0, 0, 0, 0.7); + --tooltip-color: #ffffff; + --menu-hover: rgba(0, 122, 255, 0.1); + --sidebar-width: 200px; + --sidebar-collapsed-width: 30px; +} + +/* Dark Mode Colors */ +.dark-mode { + --background-color: #121212; + --container-bg: #1e1e1e; + --sidebar-bg: #1a1a1a; + --text-color: #f0f0f0; + --text-secondary: #adb5bd; + --accent-color: #4dabf7; + --accent-hover: #339af0; + --border-color: rgba(255, 255, 255, 0.1); + --shadow-color: rgba(0, 0, 0, 0.2); + --toggle-bg: #495057; + --toggle-circle: #343a40; + --button-success: #2ebd5f; + --button-success-hover: #249d4e; + --tooltip-bg: rgba(255, 255, 255, 0.8); + --tooltip-color: #1a1a1a; + --menu-hover: rgba(77, 171, 247, 0.15); +} + body, html { - margin: 0; - padding: 0; - height: 100%; - background-color: white; + margin: 0; + padding: 0; + height: 100%; + background-color: var(--background-color); + font-family: 'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: var(--text-color); + transition: background-color 0.3s ease, color 0.3s ease; + font-size: 16px; + line-height: 1.5; } #container { - width: 100%; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; + width: 100%; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + background-color: var(--container-bg); + box-shadow: 0 0 20px var(--shadow-color); + transition: background-color 0.3s ease, box-shadow 0.3s ease; + position: relative; } #canvas { - position: absolute; - /* - * max-width: 100vh; - max-height: 100vw; - */ - width: 100vw; /* 100% of the viewport width */ - height: 100vh; /* 100% of the viewport height */ - display: block; /* Remove extra space around the canvas */ - - z-index: 2; + position: absolute; + width: 100vw; /* 100% of the viewport width */ + height: 100vh; /* 100% of the viewport height */ + display: block; /* Remove extra space around the canvas */ + z-index: 2; + transition: filter 0.3s ease; } #content { - position: absolute; - z-index: 1; + position: absolute; + z-index: 1; } + canvas.hidden { - display: none; + display: none; } -.sidebar { - width: 150px; - height: 100vh; - background-color: #f5f5f5; /* Light gray */ - border-right: 1px solid #e0e0e0; /* Slight border to separate from the main content */ - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); - position: fixed; - top: 0; - left: -140px; - transition: left 0.3s; - z-index: 5; +.sidebar { + width: var(--sidebar-width); + height: 100vh; + background-color: var(--sidebar-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-right: 1px solid var(--border-color); + box-shadow: 2px 0 15px var(--shadow-color); + position: fixed; + top: 0; + left: calc(-1 * (var(--sidebar-width) - var(--sidebar-collapsed-width))); + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + z-index: 5; + padding-top: 0; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; } .sidebar.active { - left: 0; + left: 0; +} + +.sidebar:after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: var(--sidebar-collapsed-width); + height: 100%; + background: linear-gradient(90deg, transparent, var(--sidebar-bg)); + z-index: -1; + opacity: 0.8; } .menu { - list-style: none; - padding: 0; + list-style: none; + padding: 0; + margin: 10px 0; + flex: 1; } .menu li { - padding: 10px; + padding: 0; + margin: 8px 15px; + border-radius: 12px; + transition: all 0.2s ease; + position: relative; +} + +.menu li:hover { + background-color: var(--menu-hover); + transform: translateY(-1px); +} + +.menu li button { + width: 100%; + text-align: left; + padding: 12px 16px; + border-radius: inherit; +} + +.menu li .button-tooltip { + position: absolute; + left: 100%; + top: 50%; + transform: translateY(-50%); + background-color: var(--tooltip-bg); + color: var(--tooltip-color); + padding: 6px 12px; + border-radius: 6px; + font-size: 14px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; + z-index: 10; + margin-left: 10px; + font-weight: 500; + pointer-events: none; +} + +.menu li .button-tooltip:after { + content: ''; + position: absolute; + left: -5px; + top: 50%; + transform: translateY(-50%); + border-width: 5px 5px 5px 0; + border-style: solid; + border-color: transparent var(--tooltip-bg) transparent transparent; +} + +.menu li:hover .button-tooltip { + opacity: 1; + visibility: visible; } .menu li a { - text-decoration: none; - color: #000; + text-decoration: none; + color: var(--text-color); + font-weight: 500; + transition: color 0.2s ease; + display: block; + padding: 10px 15px; } .menu li a:hover { - color: #ff0000; + color: var(--accent-color); } .my-button { - background-color: #4CAF50; - border: none; - color: white; - padding: 10px 20px; - text-align: center; - text-decoration: none; - display: inline-block; - font-size: 16px; - cursor: pointer; - border-radius: 4px; + background-color: var(--accent-color); + border: none; + color: white; + padding: 12px 22px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 15px; + cursor: pointer; + border-radius: 10px; + transition: all 0.2s ease; + box-shadow: 0 2px 5px var(--shadow-color); + font-weight: 500; +} + +.my-button:hover { + background-color: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 8px var(--shadow-color); +} + +.my-button:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.25); } + .my-button.toggled { - /* Styles for the toggled state */ - background-color: #2E7D32; /* Darker green for example */ - color: #f0f0f0; /* Slightly off-white for contrast */ + background-color: var(--button-success); + color: #ffffff; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); } .icon { - display: inline-block; - width: 16px; - height: 16px; - margin-right: 5px; - background-color: transparent; - position: relative; - border-radius: 50%; - border: 2px solid white; /* White border for record button */ - background-color: red; /* Red circle for record button */ + display: inline-block; + width: 18px; + height: 18px; + margin-right: 8px; + background-color: transparent; + position: relative; + border-radius: 50%; + border: 2px solid white; + background-color: red; + box-shadow: 0 0 5px rgba(255, 0, 0, 0.5); } @keyframes fadeInOut { - 0%, 100% { - opacity: 0.5; - } - 50% { - opacity: 1; - } + 0%, 100% { + opacity: 0.5; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.1); + } } .recording { - animation: fadeInOut 1s linear infinite; + animation: fadeInOut 1.2s ease-in-out infinite; } -/* Reset button styles to avoid browser defaults */ +/* Apple-style button */ .apple-button { - display: inline-block; - width: 120px; /* Fixed width */ - line-height: 30px; /* Centers the text vertically */ - padding: 0; /* Since we've set fixed width and height, padding is set to 0 */ - border-radius: 8px; - border: none; - font-family: 'San Francisco', 'Helvetica Neue', sans-serif; - font-size: 8px; - cursor: pointer; - transition: background-color 0.3s; - outline: none; -} - -/* Main button styles */ -.apple-button { - background-color: #007AFF; /* Default blue Apple uses */ - color: #fff; - border-radius: 8px; - padding: 10px 20px; - font-size: 16px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); /* subtle shadow for depth */ + display: inline-flex; + align-items: center; + width: 100%; + padding: 0; + border-radius: 10px; + border: none; + font-family: 'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); + outline: none; + background-color: transparent; + color: var(--text-color); + box-shadow: none; } .apple-button:hover { - background-color: #005EDD; /* A bit darker on hover */ + background-color: var(--menu-hover); + transform: translateY(-1px); } .apple-button:active { - background-color: #004ABF; /* Even darker when active (pressed) */ - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); /* reduce shadow for pressed state */ + transform: translateY(1px); +} + +.button-icon { + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 12px; + color: var(--accent-color); + transition: transform 0.2s ease; } -.apple-button:toggled { - background-color: #B3C7D6; /* Greyed out when disabled */ - color: #FFFFFF; - cursor: not-allowed; - box-shadow: none; +.apple-button:hover .button-icon { + transform: scale(1.1); } +.dark-mode .apple-button { + color: var(--text-color); +} + +.apple-button.toggled { + background-color: var(--menu-hover); + font-weight: 600; +} + +.apple-button.toggled .button-icon { + color: var(--button-success); +} /* Base styles for the toggle button */ .toggle-button { - display: inline-block; - padding: 10px 20px; - border-radius: 8px; - border: none; - font-family: 'San Francisco', 'Helvetica Neue', sans-serif; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s; - outline: none; + display: inline-block; + padding: 12px 24px; + border-radius: 10px; + border: none; + font-family: 'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; + font-size: 15px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + outline: none; + text-align: center; } /* Style for the "off" state (default) */ .toggle-button { - background-color: #D1D1D1; /* Gray background */ - color: #6B6B6B; /* Dark gray text */ + background-color: var(--toggle-bg); + color: var(--text-secondary); + box-shadow: 0 1px 5px var(--shadow-color); } /* Style for the "on" state */ .toggle-button.active { - background-color: #007AFF; /* Blue background (like our Apple button) */ - color: #FFFFFF; /* White text */ + background-color: var(--accent-color); + color: #FFFFFF; + box-shadow: 0 2px 8px var(--shadow-color); +} + +/* Notification and message styling */ +#message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: 'Inter', 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; + background-color: rgba(255, 255, 255, 0.92); + padding: 16px 24px; + border-radius: 16px; + box-shadow: 0 10px 25px var(--shadow-color); + color: #FF3B30; /* Apple's red */ + font-weight: 500; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 59, 48, 0.2); + z-index: 10; + max-width: 80%; + text-align: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +#message.visible { + opacity: 1; + visibility: visible; +} + +.dark-mode #message { + background-color: rgba(45, 45, 45, 0.92); + border: 1px solid rgba(255, 59, 48, 0.2); +} + +/* Sidebar header styling */ +.sidebar-header { + display: flex; + align-items: center; + padding: 24px 16px; + border-bottom: 1px solid var(--border-color); + margin-bottom: 15px; + background: linear-gradient(to right, rgba(0, 122, 255, 0.15), rgba(0, 122, 255, 0.05)); +} + +.sidebar-logo { + width: 38px; + height: 38px; + margin-right: 12px; + border-radius: 10px; + box-shadow: 0 4px 10px var(--shadow-color); + transition: transform 0.3s ease; +} + +.sidebar-header:hover .sidebar-logo { + transform: rotate(10deg); +} + +.sidebar-header h3 { + margin: 0; + color: var(--text-color); + font-size: 20px; + font-weight: 600; + letter-spacing: -0.2px; +} + +/* Sidebar footer styling */ +.sidebar-footer { + position: relative; + width: 100%; + text-align: center; + color: var(--text-secondary); + font-size: 12px; + padding: 12px 0; + border-top: 1px solid var(--border-color); + background-color: var(--sidebar-bg); + margin-top: auto; +} + +/* Pulse animation for notifications */ +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(255, 59, 48, 0.4); + } + 70% { + box-shadow: 0 0 0 10px rgba(255, 59, 48, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(255, 59, 48, 0); + } +} + +/* Theme toggle switch */ +/* Slider container */ +.slider-container { + display: flex; + flex-direction: column; + padding: 12px 16px; + position: relative; + width: 100%; +} + +.slider-label { + display: flex; + align-items: center; + margin-bottom: 8px; + cursor: pointer; +} + +.slider-label span { + margin-left: 8px; +} + +.contrast-slider { + width: 100%; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: var(--toggle-bg); + outline: none; + border-radius: 2px; + cursor: pointer; + margin-top: 5px; +} + +.contrast-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + background: var(--accent-color); + border-radius: 50%; + cursor: pointer; + transition: background 0.2s; +} + +.contrast-slider::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--accent-color); + border-radius: 50%; + cursor: pointer; + border: none; + transition: background 0.2s; +} + +.contrast-slider:hover::-webkit-slider-thumb { + background: var(--accent-hover); +} + +.contrast-slider:hover::-moz-range-thumb { + background: var(--accent-hover); +} + +.dark-mode .contrast-slider { + background: #444; +} + +.dark-mode .contrast-slider::-webkit-slider-thumb { + background: var(--accent-color); +} + +.dark-mode .contrast-slider::-moz-range-thumb { + background: var(--accent-color); +} + +.theme-switch-wrapper { + display: flex; + align-items: center; + padding: 16px 20px; + margin-top: 10px; + border-top: 1px solid var(--border-color); + background: linear-gradient(to right, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0)); + transition: background-color 0.3s ease; +} + +.dark-mode .theme-switch-wrapper { + background: linear-gradient(to right, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)); +} + +.theme-switch { + display: inline-block; + height: 26px; + position: relative; + width: 50px; + margin-right: 12px; +} + +.theme-switch input { + display: none; +} + +.slider { + background-color: var(--toggle-bg); + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: .4s; + border-radius: 34px; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.slider:before { + background-color: var(--toggle-circle); + bottom: 3px; + content: ""; + height: 20px; + left: 3px; + position: absolute; + transition: .4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + width: 20px; + border-radius: 50%; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.slider:after { + content: "☀️"; + position: absolute; + left: 7px; + top: 5px; + font-size: 12px; + opacity: 1; + transition: opacity 0.3s ease; +} + +input:checked + .slider:after { + content: "🌙"; + left: 30px; +} + +input:checked + .slider { + background-color: var(--accent-color); +} + +input:checked + .slider:before { + transform: translateX(24px); +} + +input:focus + .slider { + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.25); +} + +.theme-switch-label { + color: var(--text-color); + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; +} + +.theme-switch-wrapper:hover .theme-switch-label { + color: var(--accent-color); +} + +/* Tooltip styles */ +[data-tooltip] { + position: relative; + cursor: help; +} + +[data-tooltip]:before { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: 8px 12px; + background-color: var(--tooltip-bg); + color: var(--tooltip-color); + border-radius: 6px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; + z-index: 1000; +} + +[data-tooltip]:hover:before { + opacity: 1; + visibility: visible; +} + +/* Loading animation */ +.loading-dots { + display: inline-block; + position: relative; + width: 40px; + height: 16px; +} + +.loading-dots:after { + content: '...'; + position: absolute; + left: 0; + animation: loading 1.5s infinite; + font-size: 20px; + line-height: 10px; + letter-spacing: 2px; +} + +@keyframes loading { + 0% { content: '.'; } + 33% { content: '..'; } + 66% { content: '...'; } +} + +/* Toast notification styles */ +.toast { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 20px; + background-color: var(--tooltip-bg); + color: var(--tooltip-color); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(100px); + opacity: 0; + transition: transform 0.3s ease, opacity 0.3s ease; + z-index: 1000; +} + +.toast.visible { + transform: translateY(0); + opacity: 1; } diff --git a/client/uiInteractions.js b/client/uiInteractions.js index 4bc81fa..fccbf1a 100644 --- a/client/uiInteractions.js +++ b/client/uiInteractions.js @@ -1,48 +1,128 @@ -let recordingWithSound = false; +// UI interactions module -document.getElementById('rotate').addEventListener('click', function () { - portrait = !portrait; - this.classList.toggle('toggled'); - //visibleCanvas.style.transform = "portrait(270deg)"; - //visibleCanvas.style.transformOrigin = "center center"; - eventWorker.postMessage({ type: 'portrait', portrait: portrait }); +// Function to toggle dark mode +function toggleDarkMode() { + document.body.classList.toggle('dark-mode'); + + // Save user preference to localStorage + const isDarkMode = document.body.classList.contains('dark-mode'); + localStorage.setItem('darkMode', isDarkMode ? 'enabled' : 'disabled'); + + // Update the canvas to invert colors in dark mode + if (typeof setDarkMode === 'function') { + setDarkMode(isDarkMode); + } +} - resizeVisibleCanvas(); -}); -document.getElementById('pointerButton').addEventListener('click', function () { - if (isWebSocketConnected(ws)) { - stopWebSocket(); - } else { - connectWebSocket(); - } +// Check user preference on page load +document.addEventListener('DOMContentLoaded', function() { + // Check for saved theme preference + const savedTheme = localStorage.getItem('darkMode'); + const checkbox = document.getElementById('checkbox'); + + // If user previously enabled dark mode + if (savedTheme === 'enabled') { + document.body.classList.add('dark-mode'); + checkbox.checked = true; + + // Apply dark mode to canvas + if (typeof setDarkMode === 'function') { + setDarkMode(true); + } + } + + // Ensure colors button starts toggled since colors are on by default + const colorsButton = document.getElementById('colors'); + if (!colorsButton.classList.contains('toggled')) { + colorsButton.classList.add('toggled'); + } }); +// Event listeners for dark mode toggle +document.getElementById('checkbox').addEventListener('change', toggleDarkMode); + +// Rotate button functionality +document.getElementById('rotate').addEventListener('click', function () { + portrait = !portrait; + this.classList.toggle('toggled'); + eventWorker.postMessage({ type: 'portrait', portrait: portrait }); + resizeVisibleCanvas(); + + // Show confirmation message + showMessage(`Display ${portrait ? 'portrait' : 'landscape'} mode activated`, 2000); +}); +// Colors button functionality document.getElementById('colors').addEventListener('click', function () { - withColor = !withColor; - this.classList.toggle('toggled'); - streamWorker.postMessage({ type: 'withColorChanged', withColor: withColor }); + withColor = !withColor; + this.classList.toggle('toggled'); + streamWorker.postMessage({ type: 'withColorChanged', withColor: withColor }); + + // Show confirmation message + showMessage(`${withColor ? 'Color' : 'Grayscale'} mode enabled`, 2000); }); +// Sidebar hover effect const sidebar = document.querySelector('.sidebar'); sidebar.addEventListener('mouseover', function () { - sidebar.classList.add('active'); + sidebar.classList.add('active'); }); sidebar.addEventListener('mouseout', function () { - sidebar.classList.remove('active'); + sidebar.classList.remove('active'); }); // Resize the canvas whenever the window is resized window.addEventListener("resize", resizeVisibleCanvas); resizeVisibleCanvas(); +// Mask drawing button functionality document.getElementById('switchOrderButton').addEventListener('click', function () { - // Swap z-index values - if (iFrame.style.zIndex == 1) { - iFrame.style.zIndex = 4; - return; - } - iFrame.style.zIndex = 1; + // Swap z-index values + const isLayerSwitched = iFrame.style.zIndex != 1; + + if (isLayerSwitched) { + iFrame.style.zIndex = 1; + this.classList.remove('toggled'); + showMessage('Drawing layer on top', 2000); + } else { + iFrame.style.zIndex = 4; + this.classList.add('toggled'); + showMessage('Content layer on top', 2000); + } +}); + +// Contrast slider functionality +document.getElementById('contrastSlider').addEventListener('input', function() { + // Get the slider value (between 1.0 and 3.0) + const contrastLevel = this.value; + + // Update renderer if function exists + if (typeof setContrast === 'function') { + setContrast(contrastLevel); + } + + // Show feedback when user stops moving the slider + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + showMessage(`Contrast: ${parseFloat(contrastLevel).toFixed(1)}`, 1000); + }, 500); +}); + +// Load saved contrast value on initialization +document.addEventListener('DOMContentLoaded', function() { + // Check for saved contrast preference + const savedContrast = localStorage.getItem('contrastLevel'); + const contrastSlider = document.getElementById('contrastSlider'); + + if (savedContrast) { + // Set the slider to the saved value + contrastSlider.value = savedContrast; + + // Update the contrast setting + if (typeof setContrast === 'function') { + setContrast(savedContrast); + } + } }); diff --git a/client/utilities.js b/client/utilities.js index ad754d3..456b928 100644 --- a/client/utilities.js +++ b/client/utilities.js @@ -8,4 +8,38 @@ function downloadScreenshot(dataUrl) { link.click(); } +// Function to show a message with auto-hide after specified duration +function showMessage(message, duration = 3000) { + const messageDiv = document.getElementById('message'); + messageDiv.textContent = message; + messageDiv.classList.add('visible'); + + // Auto-hide after specified duration + setTimeout(() => { + messageDiv.classList.remove('visible'); + }, duration); +} + +// Wait/loading message display +function waiting(message) { + const messageDiv = document.getElementById('message'); + messageDiv.innerHTML = `${message} `; + messageDiv.classList.add('visible'); +} + +// Function to fetch app version from the server +async function fetchVersion() { + try { + const response = await fetch('/version'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const version = await response.text(); + return version; + } catch (error) { + console.error('Error fetching version:', error); + return 'unknown'; + } +} + diff --git a/client/worker_event_processing.js b/client/worker_event_processing.js index a50bf19..ae3540a 100644 --- a/client/worker_event_processing.js +++ b/client/worker_event_processing.js @@ -73,24 +73,21 @@ async function initiateEventsListener() { eventSource.onerror = () => { postMessage({ type: 'error', - message: "websocket error", + message: "EventSource error", }); - console.error('EventStrean error occurred. Attempting to reconnect...'); - //setTimeout(connectWebSocket, 3000); // Reconnect after 3 seconds + console.error('EventSource error occurred.'); }; eventSource.onclose = () => { postMessage({ type: 'error', - message: 'closed connection' + message: 'Connection closed' }); - console.log('EventStream connection closed. Attempting to reconnect...'); - //setTimeout(connectWebSocket, 3000); // Reconnect after 3 seconds + console.log('EventSource connection closed.'); }; } // Function to scale the incoming value to the canvas size function scaleValue(value, maxValue, canvasSize) { return (value / maxValue) * canvasSize; -} - +} \ No newline at end of file diff --git a/go.mod b/go.mod index e8199aa..c2893fb 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,27 @@ module github.com/owulveryck/goMarkableStream -go 1.22 +go 1.24.0 require ( github.com/kelseyhightower/envconfig v1.4.0 - github.com/klauspost/compress v1.17.11 - golang.ngrok.com/ngrok v1.4.1 + github.com/klauspost/compress v1.18.0 + golang.ngrok.com/ngrok v1.13.0 ) require ( github.com/go-stack/stack v1.8.1 // indirect - github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible // indirect + github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible // indirect github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect github.com/jpillora/backoff v1.0.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.ngrok.com/muxado/v2 v2.0.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/term v0.8.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.ngrok.com/muxado/v2 v2.0.1 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2491ffc..e12b9c5 100644 --- a/go.sum +++ b/go.sum @@ -2,48 +2,52 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible h1:zaX5fYT98jX5j4UhO/WbfY8T1HkgVrydiDMC9PWqGCo= -github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= +github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible h1:VryeOTiaZfAzwx8xBcID1KlJCeoWSIpsNbSk+/D2LNk= +github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/inconshreveable/log15/v3 v3.0.0-testing.5 h1:h4e0f3kjgg+RJBlKOabrohjHe47D3bbAB9BgMrc3DYA= github.com/inconshreveable/log15/v3 v3.0.0-testing.5/go.mod h1:3GQg1SVrLoWGfRv/kAZMsdyU5cp8eFc1P3cw+Wwku94= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.ngrok.com/muxado/v2 v2.0.0 h1:bu9eIDhRdYNtIXNnqat/HyMeHYOAbUH55ebD7gTvW6c= -golang.ngrok.com/muxado/v2 v2.0.0/go.mod h1:wzxJYX4xiAtmwumzL+QsukVwFRXmPNv86vB8RPpOxyM= -golang.ngrok.com/ngrok v1.4.1 h1:z53H/hAqSJf+K5wL3v4m01Dp4rU0wcf323iMPBQ27QA= -golang.ngrok.com/ngrok v1.4.1/go.mod h1:8a8GVoqR305t0O51ld211Xq2UeKgm32o8px24ddvXZI= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.ngrok.com/muxado/v2 v2.0.1 h1:jM9i6Pom6GGmnPrHKNR6OJRrUoHFkSZlJ3/S0zqdVpY= +golang.ngrok.com/muxado/v2 v2.0.1/go.mod h1:wzxJYX4xiAtmwumzL+QsukVwFRXmPNv86vB8RPpOxyM= +golang.ngrok.com/ngrok v1.13.0 h1:6SeOS+DAeIaHlkDmNH5waFHv0xjlavOV3wml0Z59/8k= +golang.ngrok.com/ngrok v1.13.0/go.mod h1:BKOMdoZXfD4w6o3EtE7Cu9TVbaUWBqptrZRWnVcAuI4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http.go b/http.go index 104d5cc..ab89bcb 100644 --- a/http.go +++ b/http.go @@ -2,10 +2,12 @@ package main import ( "crypto/tls" + "fmt" "html/template" "log" "net" "net/http" + "runtime/debug" "github.com/owulveryck/goMarkableStream/internal/eventhttphandler" "github.com/owulveryck/goMarkableStream/internal/pubsub" @@ -41,6 +43,17 @@ func setMuxer(eventPublisher *pubsub.PubSub) *http.ServeMux { gestureHandler := eventhttphandler.NewGestureHandler(eventPublisher) mux.Handle("/gestures", gestureHandler) + // Version endpoint + mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { + bi, ok := debug.ReadBuildInfo() + if !ok { + http.Error(w, "Unable to read build info", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/plain") + fmt.Fprintf(w, "%s", bi.Main.Version) + }) + if c.DevMode { rawHandler := stream.NewRawHandler(file, pointerAddr) mux.Handle("/raw", rawHandler) @@ -95,10 +108,9 @@ func newIndexHandler(fs http.FileSystem) http.HandlerFunc { log.Printf("Error rendering template: %v", err) } return - } else { - - staticFileServer.ServeHTTP(w, r) } + + staticFileServer.ServeHTTP(w, r) } } diff --git a/internal/remarkable/const.go b/internal/remarkable/const.go index 729803b..8941cc1 100644 --- a/internal/remarkable/const.go +++ b/internal/remarkable/const.go @@ -3,6 +3,7 @@ package remarkable const ( + // Model defines the current device model being used Model = Remarkable2 // ScreenWidth of the remarkable 2 @@ -10,12 +11,16 @@ const ( // ScreenHeight of the remarkable 2 ScreenHeight = 1404 + // ScreenSizeBytes is the total memory size of the screen buffer in bytes ScreenSizeBytes = ScreenWidth * ScreenHeight * 2 - // These values are from Max values of /dev/input/event1 (ABS_X and ABS_Y) + // MaxXValue represents the maximum X coordinate value from /dev/input/event1 (ABS_X) MaxXValue = 15725 + // MaxYValue represents the maximum Y coordinate value from /dev/input/event1 (ABS_Y) MaxYValue = 20966 - PenInputDevice = "/dev/input/event1" + // PenInputDevice ... + PenInputDevice = "/dev/input/event1" + // TouchInputDevice ... TouchInputDevice = "/dev/input/event2" ) diff --git a/internal/remarkable/device.go b/internal/remarkable/device.go index 3519d8e..37c6b38 100644 --- a/internal/remarkable/device.go +++ b/internal/remarkable/device.go @@ -1,10 +1,14 @@ package remarkable +// DeviceModel represents the type of reMarkable device being used type DeviceModel int const ( + // UnknownDevice represents an unidentified reMarkable device UnknownDevice DeviceModel = iota + // Remarkable2 represents the reMarkable 2 device Remarkable2 + // RemarkablePaperPro represents the reMarkable Paper Pro device RemarkablePaperPro ) diff --git a/internal/remarkable/fb_rm.go b/internal/remarkable/fb_rm.go index 439fdb2..2aeb513 100644 --- a/internal/remarkable/fb_rm.go +++ b/internal/remarkable/fb_rm.go @@ -7,6 +7,7 @@ import ( "os" ) +// GetFileAndPointer returns the memory file handle and pointer address for the reMarkable framebuffer func GetFileAndPointer() (io.ReaderAt, int64, error) { pid := findXochitlPID() file, err := os.OpenFile("/proc/"+pid+"/mem", os.O_RDONLY, os.ModeDevice) diff --git a/main.go b/main.go index 3acc8f1..96c632e 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,11 @@ import ( "embed" "errors" "flag" + "fmt" "io" "log" "net/http" + "runtime/debug" "github.com/kelseyhightower/envconfig" @@ -55,6 +57,12 @@ func validateConfiguration(c *configuration) error { } func main() { + bi, ok := debug.ReadBuildInfo() + if !ok { + fmt.Println("not ok") + return + } + fmt.Printf("Version: %s\n", bi.Main.Version) var err error ifaces()