From e8cd2a8f14c38d437da1a35edf649d19948664e3 Mon Sep 17 00:00:00 2001 From: Tien Ngo Date: Mon, 29 Dec 2025 16:27:20 +0700 Subject: [PATCH] feat: Implement timelapse calculator application with core logic, UI components, and Gitea deployment workflow. --- .gitea/workflows/deploy.yml | 14 +++- src/App.jsx | 156 ++++++++++++++++++++++-------------- src/components/Results.jsx | 29 ++----- 3 files changed, 117 insertions(+), 82 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 69e75c2..967359a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -31,4 +31,16 @@ jobs: -p 3005:80 \ timelapse-calc:latest - docker ps --format "{{.Names}} {{.Image}} {{.Status}}" \ No newline at end of file + docker ps --format "{{.Names}} {{.Image}} {{.Status}}" + + - name: Send Deployment Notification + uses: actions/deploy-notify@v1 + if: always() + with: + status: ${{ job.status }} + title: "Deploy: ${{ github.repository }}" + message: | + Ref: ${{ github.ref_name }} + Commit: ${{ github.sha }} + Status: ${{ job.status }} + Actor: ${{ github.actor }} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 06e261e..ba52209 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { Card, ConfigProvider } from 'antd'; +import { useState } from 'react'; +import { Card, ConfigProvider, Segmented } from 'antd'; import Header from './components/Header'; import CalculatorMode from './components/CalculatorMode'; import TimeInput from './components/TimeInput'; @@ -13,43 +13,56 @@ function App() { // State for inputs const [clipLength, setClipLength] = useState({ hours: 0, minutes: 0, seconds: 20 }); - const [eventDuration, setEventDuration] = useState({ hours: 1, minutes: 0, seconds: 0 }); + const [eventDuration, setEventDuration] = useState({ hours: 0, minutes: 40, seconds: 0 }); + const [intervalInput, setIntervalInput] = useState(2); // Seconds const [fps, setFps] = useState(30); - const [imageSize, setImageSize] = useState(12); - - // State for results - const [shootingInterval, setShootingInterval] = useState(0); - const [numberOfPhotos, setNumberOfPhotos] = useState(0); - const [totalMemory, setTotalMemory] = useState(0); // Helper function to convert time object to seconds const timeToSeconds = (time) => { return time.hours * 3600 + time.minutes * 60 + time.seconds; }; - // Calculate results whenever inputs change - useEffect(() => { - const clipSeconds = timeToSeconds(clipLength); - const eventSeconds = timeToSeconds(eventDuration); + // Helper to convert seconds to time object (for derived values if needed, but we output seconds) + // ... - if (clipSeconds > 0 && eventSeconds > 0 && fps > 0) { - // Calculate shooting interval (in seconds) - const interval = eventSeconds / (clipSeconds * fps); - setShootingInterval(interval); + // Calculate results + const calculateResults = () => { + const clipSec = timeToSeconds(clipLength); + const eventSec = timeToSeconds(eventDuration); - // Calculate number of photos - const photos = Math.ceil(eventSeconds / interval); - setNumberOfPhotos(photos); + let photos = 0; + let result = 0; - // Calculate total memory (in MB) - const memory = photos * imageSize; - setTotalMemory(memory); - } else { - setShootingInterval(0); - setNumberOfPhotos(0); - setTotalMemory(0); + if (mode === 'shooting-interval') { + if (clipSec > 0 && fps > 0) { + result = eventSec / (clipSec * fps); + if (result > 0) photos = Math.ceil(eventSec / result); + } + } else if (mode === 'clip-length') { + if (intervalInput > 0 && fps > 0) { + result = eventSec / (intervalInput * fps); + photos = Math.ceil(eventSec / intervalInput); + } + } else if (mode === 'event-duration') { + if (clipSec > 0 && intervalInput > 0 && fps > 0) { + result = clipSec * fps * intervalInput; + photos = Math.ceil(result / intervalInput); + } } - }, [clipLength, eventDuration, fps, imageSize]); + + return { resultSeconds: result, numberOfPhotos: photos }; + }; + + const { resultSeconds, numberOfPhotos } = calculateResults(); + + const getResultLabel = () => { + switch (mode) { + case 'shooting-interval': return "Shooting Interval"; + case 'clip-length': return "Clip Length"; + case 'event-duration': return "Event Duration"; + default: return "Result"; + } + }; return ( @@ -77,44 +95,62 @@ function App() { - + {mode !== 'clip-length' && ( + + )} - + {mode !== 'event-duration' && ( + + )} - + {mode !== 'shooting-interval' && ( + + )} - +
+ + +
diff --git a/src/components/Results.jsx b/src/components/Results.jsx index a369e2a..ceb35f4 100644 --- a/src/components/Results.jsx +++ b/src/components/Results.jsx @@ -1,9 +1,11 @@ import { Statistic, Row, Col } from 'antd'; -export default function Results({ shootingInterval, numberOfPhotos, totalMemory }) { +export default function Results({ label, resultSeconds, numberOfPhotos }) { const formatTime = (seconds) => { + if (!seconds) return '0s'; if (seconds < 60) { - return `${seconds.toFixed(0)}s`; + // For interval, we might want decimals. For others, maybe not strictly required but harmless. + return `${parseFloat(seconds.toFixed(1))}s`; } else if (seconds < 3600) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); @@ -16,38 +18,23 @@ export default function Results({ shootingInterval, numberOfPhotos, totalMemory } }; - const formatMemory = (mb) => { - if (mb < 1024) { - return `${mb.toFixed(2)}MB`; - } else { - return `${(mb / 1024).toFixed(2)}GB`; - } - }; - return (
- + - + - - -
);