When I use npm start and run the app normally, I can add and view all images and videos without any issues. However, when I build the app, everything else gets saved correctly, but the images fail to load. Apart from loading images or videos, everything else works fine. When I check the database in local files, the image field appears empty. By the way, I’m using a local database (db-local).
Additionally, if you notice any potential issues beyond what I’ve mentioned, I’d appreciate it if you could point them out. I’m primarily a back-end developer, and this is my first front-end and Electron project.
react.js part
import React from 'react'
import Typography from '@mui/material/Typography';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Paper from '@mui/material/Paper';
import Chip from '@mui/material/Chip';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';
import { useDispatch } from 'react-redux';
import { startloading } from '../store/slices/userSlice';
import { Controller, useForm } from 'react-hook-form';
import AddIcon from '@mui/icons-material/AddRounded';
import IconButton from '@mui/material/IconButton';
import Attachment from '@mui/icons-material/AttachmentRounded';
import MediaPreview from '../components/MediaPreview';
import { Container, Switch } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import ArrowBackIos from '@mui/icons-material/ArrowBackIosRounded';
const AddRule = () => {
const [type, setType] = React.useState('');
const [newRule, setNewRule] = React.useState('');
const [controlledRules, setControlledRules] = React.useState([]);
const [preview, setPreview] = React.useState(null);
const [previewOpen, setPreviewOpen] = React.useState(true);
const [error, setError] = React.useState(null);
const ruleInputRef = React.useRef(null);
const dispatch = useDispatch();
const {
control,
handleSubmit,
getValues,
reset,
setValue,
formState: { errors },
} = useForm({
values: React.useMemo(() => ({
rules: [],
rule_title: "",
rule_timeout_unit: "seconds",
rule_timeout: 1,
rule_delay: 1,
rule_response: "",
image: ""
}), [])
})
const addRuleHandler = (rule) => {
if (rule.trim() === '') return setError(prevError => ({ ...prevError, rules: "Kural boş bırakılamaz." }));
if (controlledRules.includes(rule)) return setError(prevError => ({ ...prevError, rules: "Her kural bir kere girilmelidir." }))
setControlledRules([...controlledRules, rule])
setError(prevError => ({ ...prevError, rules: "" }))
setNewRule('');
}
React.useEffect(() => {
setValue("rules", controlledRules);
}, [controlledRules, setValue]);
const deleteRuleHandler = (rule) => {
setControlledRules(controlledRules.filter(r => r !== rule))
}
const handleFileChange = (file, field) => {
if (file && (file.type === "video/mp4" || file.type === "image/png" || file.type === "image/jpeg")) {
const maxSize = 15 * 1024 * 1024;
if (file.size > maxSize) {
return setError(prevError => ({ ...prevError, file: "Medya boyutu 15MB'dan büyük olamaz." }));
}
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(",")[1];
field.onChange(base64);
setValue("image", base64)
};
reader.readAsDataURL(file);
setType(file.type);
setPreview({ url: URL.createObjectURL(file), type: file.type });
setError(null);
} else {
setError(prevError => ({ ...prevError, file: "Sadece .png, .jpg veya .mp4 dosyaları yüklenebilir." }));
field.onChange("");
setPreview(null);
}
};
const onSubmit = data => {
if (controlledRules.length == 0) return setError(prevError => ({ ...prevError, rules: "Kural boş bırakılamaz." }))
dispatch({
type: 'websocket/sendMessage', payload: {
data_type: 'add_rule', formdata: { ...data, type }
}
});
dispatch(startloading());
dispatch({ type: 'websocket/sendMessage', payload: { data_type: 'get_rules' } });
setControlledRules([]);
setPreview(null);
fileInputRef.current.value = "";
reset()
};
const fileInputRef = React.useRef(null);
const handleFileDelete = () => {
setError(null);
setPreview(null);
setValue("image", "");
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
setType("");
}
const navigate = useNavigate();
return (
<Container>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<IconButton onClick={() => navigate('/')}>
<ArrowBackIos />
</IconButton>
<Typography sx={{ fontWeight: '400', margin: '40px 0', fontSize: '32px', textAlign: 'center' }} variant='h5'>Kural Ekleyin</Typography>
<Box />
</Box>
<Controller name="rule_title"
control={control}
rules={{
required: "Kural başlığı gereklidir",
minLength: { value: 3, message: "Başlık en az 3 karakter olmalı" },
maxLength: { value: 25, message: "Başlık en fazla 25 karakter olabilir" },
}}
render={({ field, fieldState }) => (
<TextField
{...field}
sx={{ width: "100%" }}
type="text"
label="Kural başlığı"
placeholder="Kural başlığı girin."
variant="outlined"
error={!!fieldState.error}
helperText={!!fieldState.error ? fieldState.error?.message : ''}
/>
)}
/>
<Paper sx={{ padding: "10px", marginTop: '10px' }}>
<Typography sx={{ fontSize: '16px', fontWeight: '400', display: 'block', textAlign: 'center' }} variant='h6'>Cevap Verme Kuralları</Typography>
<Box sx={{ display: 'flex', gap: '5px', flexWrap: 'wrap', padding: "10px", marginTop: '5px' }}>
{controlledRules.map((subRule, subIndex) =>
<Chip onDelete={() => deleteRuleHandler(subRule)} size='small' key={subIndex} label={subRule} />
)}
</Box>
<TextField
size='small'
value={newRule}
sx={{ width: "100%", marginTop: '15px' }}
label="Yeni Kural"
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
addRuleHandler(newRule);
}
}}
placeholder='Kural girin.'
ref={ruleInputRef}
onChange={e => setNewRule(e.target.value)}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton type='button' onClick={() => addRuleHandler(newRule)}>
<AddIcon />
</IconButton>
</InputAdornment>
)
}
}}
error={!!error?.rules}
helperText={!!error?.rules ? error.rules : ''}
/>
</Paper>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: '5px' }}>
<Controller name="rule_timeout"
control={control}
rules={{
min: { value: 1, message: "1 veya daha yüksek bir sayı seçiniz." },
max: {
value: getValues().rule_timeout_unit === "seconds" || getValues().rule_timeout_unit === "minutes" ? 59
: getValues().rule_timeout_unit === "hours"
? 23
: 1, message: "Saniye veya Dakika seçili ise en fazla 59, saat seçili ise en fazla 23, güne seçili ise en fazla 1 olmalıdır."
},
}}
render={({ field, fieldState }) => (
<TextField
{...field}
sx={{ width: '100%', marginTop: '20px', }}
type="number"
label='Tekrar kullanım aralığı'
placeholder='Tekrar kullanım aralığını girin.'
variant="outlined"
error={!!fieldState.error}
helperText={!!fieldState.error ? fieldState.error?.message : ''}
/>
)}
/>
<Controller name="rule_timeout_unit"
control={control}
render={({ field }) => (
<Select defaultValue={"seconds"} sx={{ flex: "1", marginTop: '20px' }} {...field} fullWidth>
<MenuItem value="seconds">Saniye</MenuItem>
<MenuItem value="minutes">Dakika</MenuItem>
<MenuItem value="hours">Saat</MenuItem>
<MenuItem value="day">Gün</MenuItem>
<MenuItem value="onetime">Tek Seferlik</MenuItem>
</Select>
)}
/>
</Box>
<Controller name="rule_delay"
control={control}
rules={{
min: { value: 1, message: "Cevap gecikmesi en az 1 saniye olabilir." },
max: { value: 59, message: "Cevap gecikmesi en fazla 59 saniye olabilir." }
}}
render={({ field, fieldState }) => (
<TextField
{...field}
sx={{ width: '100%', marginTop: '20px', }}
type="number"
label='Cevap gecikmesi (Saniye)'
placeholder='Cevap gecikmesini girin. (Saniye)'
variant="outlined"
error={!!fieldState.error}
helperText={!!fieldState.error ? fieldState.error?.message : ''}
/>
)}
/>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Controller name="image"
control={control}
render={({ field }) => (
<Box sx={{ marginTop: '10px' }}>
<>
<label htmlFor="file-upload1">
<Button disabled={getValues().image ? true : false} startIcon={<Attachment />} variant="text" component="span">
Bot cevabına bir medya ekleyin.
</Button>
</label>
<input
disabled={getValues().image ? true : false}
id="file-upload1"
accept=".mp4,.jpg,.png"
multiple={false}
type="file"
ref={fileInputRef}
hidden
onChange={(e) => {
const file = e.target.files[0];
handleFileChange(file, field);
}}
/>
</>
</Box>
)}
/>
{preview &&
<div style={{ marginTop: '5px' }}>
<Switch checked={previewOpen} size='small' onChange={() => setPreviewOpen(!!!previewOpen)} />
<Typography variant='body2' component={'span'}>Medyayı Önizle</Typography>
</div>
}
</Box>
{error?.file && <Typography color='error' variant='caption'>{error.file}</Typography>}
{(preview && previewOpen) &&
<MediaPreview setRemove={handleFileDelete} media={preview} />
}
<Controller
name="rule_response"
control={control}
rules={{
required: { value: true, message: "Bot cevabı gereklidir." },
minLength: { value: 3, message: "Bot cevabı en az 3 karakter olabilir." },
maxLength: { value: 64000, message: "Bot cevabı en fazla 64.000 karakter olabilir." }
}}
render={({ field, fieldState }) => (
<TextField
multiline
rows={10}
{...field}
sx={{ width: '100%', marginTop: '20px', }}
type="number"
label='Bot cevabı'
placeholder='Bot cevabını girin.'
variant="outlined"
error={!!fieldState.error}
helperText={!!fieldState.error ? fieldState.error?.message : ''}
/>
)}
/>
<Button sx={{ marginTop: '20px' }} size='large' variant='contained' color='success' type='submit'>Kaydet</Button>
<Button onClick={() => { reset(); setPreview(null); setControlledRules([]); }} sx={{ marginTop: '20px', marginLeft: '5px' }} size='large' color='warning' variant='contained'>Sıfırla</Button>
</form>
</Container>
)
}
export default AddRule
This is electron.js ws part
case "add_rule": {
object.formdata.rule_delay = parseInt(object.formdata.rule_delay)
object.formdata.rule_timeout = parseInt(object.formdata.rule_timeout)
const item = await command.create({ ...object.formdata, image: '' }).save();
if (object.formdata.image) {
const buffer = Buffer.from(object.formdata.image, 'base64');
const filePath = path.join(__dirname, 'images', `${item._id + '_' + Date.now()}.${object.formdata.type.split('/')[1]}`);
await fs.promises.writeFile(filePath, buffer);
const outputFile = 'images/temp_output.mp4';
if (object.formdata.type.startsWith('video/')) {
ffmpeg(filePath)
.videoCodec('libx264')
.audioCodec('aac')
.audioBitrate(128)
.outputOptions('-crf', '23')
.outputOptions('-preset', 'fast')
.on('start', (commandLine) => {
console.log('FFmpeg komutu başladı:', commandLine);
})
.on('progress', (progress) => {
console.log(`İlerleme: ${progress.percent.toFixed(2)}% tamamlandı.`);
})
.on('end', () => {
console.log('FFmpeg işlemi tamamlandı, dosya değiştiriliyor...');
fs.unlink(filePath, (err) => {
if (err) {
console.error('Eski dosya silinirken hata oluştu:', err.message);
} else {
fs.rename(outputFile, filePath, (err) => {
if (err) {
console.error('Yeni dosya taşınırken hata oluştu:', err.message);
} else {
console.log('Dosya başarıyla güncellendi!');
}
});
}
});
})
.on('error', (err) => {
console.error('Hata oluştu:', err.message);
if (fs.existsSync(outputFile)) {
fs.unlink(outputFile, () => {
console.log('Geçici dosya silindi.');
});
}
})
.save(outputFile);
}
object.formdata.image = filePath;
let komut = await command.findOne({ _id: item._id });
komut.image = filePath;
komut.save();
}
wss.clients.forEach((wsClient) =>
wsClient.send(JSON.stringify({ data_type: 'rule_added', message: 'Kural başarıyla eklendi.', status: true }))
)
break;
}