feat: Implement timelapse calculator application with core logic, UI components, and Gitea deployment workflow.
Some checks failed
Deploy to Private Server / deploy (push) Failing after 2s
Some checks failed
Deploy to Private Server / deploy (push) Failing after 2s
This commit is contained in:
@@ -32,3 +32,15 @@ jobs:
|
||||
timelapse-calc:latest
|
||||
|
||||
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 }}
|
||||
122
src/App.jsx
122
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);
|
||||
}
|
||||
}, [clipLength, eventDuration, fps, imageSize]);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<ConfigProvider
|
||||
@@ -68,6 +81,11 @@ function App() {
|
||||
InputNumber: {
|
||||
controlHeight: 44,
|
||||
},
|
||||
Segmented: {
|
||||
itemSelectedBg: '#F4D03F',
|
||||
itemSelectedColor: '#2C3E50',
|
||||
trackBg: '#F8F9FA'
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -77,6 +95,7 @@ function App() {
|
||||
<Card className="calculator-card">
|
||||
<CalculatorMode value={mode} onChange={setMode} />
|
||||
|
||||
{mode !== 'clip-length' && (
|
||||
<TimeInput
|
||||
label="Clip length"
|
||||
hours={clipLength.hours}
|
||||
@@ -84,7 +103,9 @@ function App() {
|
||||
seconds={clipLength.seconds}
|
||||
onChange={setClipLength}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode !== 'event-duration' && (
|
||||
<TimeInput
|
||||
label="Event duration"
|
||||
hours={eventDuration.hours}
|
||||
@@ -92,29 +113,44 @@ function App() {
|
||||
seconds={eventDuration.seconds}
|
||||
onChange={setEventDuration}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode !== 'shooting-interval' && (
|
||||
<NumberInput
|
||||
label="Frames per second"
|
||||
value={fps}
|
||||
onChange={setFps}
|
||||
unit="fps"
|
||||
min={1}
|
||||
step={1}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Image size"
|
||||
value={imageSize}
|
||||
onChange={setImageSize}
|
||||
unit="MB"
|
||||
label="Shooting Interval (sec)"
|
||||
value={intervalInput}
|
||||
onChange={setIntervalInput}
|
||||
unit="s"
|
||||
min={0.1}
|
||||
step={0.1}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Frames per second</label>
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: '24', value: 24 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '60', value: 60 },
|
||||
]}
|
||||
value={fps}
|
||||
onChange={setFps}
|
||||
block
|
||||
size="large"
|
||||
style={{
|
||||
backgroundColor: '#F8F9FA',
|
||||
padding: 4,
|
||||
borderRadius: 8,
|
||||
border: '1px solid #DEE2E6'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Results
|
||||
shootingInterval={shootingInterval}
|
||||
label={getResultLabel()}
|
||||
resultSeconds={resultSeconds}
|
||||
numberOfPhotos={numberOfPhotos}
|
||||
totalMemory={totalMemory}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="results-section">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Statistic
|
||||
title="Shooting interval"
|
||||
value={formatTime(shootingInterval)}
|
||||
title={label}
|
||||
value={formatTime(resultSeconds)}
|
||||
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Statistic
|
||||
title="Number of photos"
|
||||
value={numberOfPhotos}
|
||||
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Statistic
|
||||
title="Total memory usage"
|
||||
value={formatMemory(totalMemory)}
|
||||
valueStyle={{ color: '#2C3E50', fontSize: '1.5rem', fontWeight: 600 }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user