1414#include < QSaveFile>
1515#include < QUrl>
1616#include < algorithm>
17+ #include < cmath>
1718
1819namespace AviQtl ::Core {
1920
20- inline constexpr int PROJECT_VERSION = 2 ;
21+ inline constexpr int PROJECT_VERSION = 3 ;
2122
2223namespace {
2324constexpr int kMaxDimension = 32768 ;
2425constexpr double kMaxFps = 1000.0 ;
2526constexpr int kMaxSampleRate = 192000 ;
27+ constexpr double kMaxGridBpm = 1000.0 ;
28+ constexpr double kMaxGridOffset = 86400.0 ;
29+ constexpr int kMaxGridInterval = 1'000'000 ;
30+ constexpr int kMaxGridSubdivision = 128 ;
31+ constexpr int kMaxMagneticSnapRange = 100 ;
2632
2733int clampDimension (int value, int fallback) {
2834 return (value <= 0 || value > kMaxDimension ) ? fallback : value;
@@ -33,6 +39,15 @@ double clampFps(double value, double fallback) {
3339int clampSampleRate (int value, int fallback) {
3440 return (value <= 0 || value > kMaxSampleRate ) ? fallback : value;
3541}
42+ double clampPositiveGridValue (double value, double maximum, double fallback) {
43+ return (!std::isfinite (value) || value <= 0.0 || value > maximum) ? fallback : value;
44+ }
45+ double clampGridOffset (double value, double fallback) {
46+ return (!std::isfinite (value) || value < 0.0 || value > kMaxGridOffset ) ? fallback : value;
47+ }
48+ int clampPositiveGridValue (int value, int maximum, int fallback) {
49+ return (value <= 0 || value > maximum) ? fallback : value;
50+ }
3651} // namespace
3752
3853static QString toRelativePath (const QString &absolutePath, const QString &baseDir) {
@@ -76,6 +91,44 @@ static void convertMediaPaths(QVariantMap ¶ms, const QString &baseDir, bool
7691 }
7792}
7893
94+ static void convertEffectMediaPath (const QString &effectId, QVariantMap ¶ms, const QString &baseDir, bool toRelative) {
95+ QString pathKey;
96+ if (effectId == QLatin1String (" video" ) || effectId == QLatin1String (" image" )) {
97+ pathKey = QStringLiteral (" path" );
98+ } else if (effectId == QLatin1String (" audio" )) {
99+ pathKey = QStringLiteral (" source" );
100+ } else {
101+ return ;
102+ }
103+
104+ auto pathIt = params.find (pathKey);
105+ if (pathIt == params.end () || pathIt->toString ().isEmpty ()) {
106+ return ;
107+ }
108+ *pathIt = toRelative ? toRelativePath (pathIt->toString (), baseDir) : toAbsolutePath (pathIt->toString (), baseDir);
109+ }
110+
111+ static auto layerSetToJson (const QSet<int > &layers) -> QJsonArray {
112+ QList<int > sortedLayers (layers.cbegin (), layers.cend ());
113+ std::sort (sortedLayers.begin (), sortedLayers.end ());
114+ QJsonArray result;
115+ for (int layer : std::as_const (sortedLayers)) {
116+ result.append (layer);
117+ }
118+ return result;
119+ }
120+
121+ static auto layerSetFromJson (const QJsonValue &value) -> QSet<int> {
122+ QSet<int > result;
123+ for (const QJsonValue &layer : value.toArray ()) {
124+ const int index = layer.toInt (-1 );
125+ if (index >= 0 && index <= 127 ) {
126+ result.insert (index);
127+ }
128+ }
129+ return result;
130+ }
131+
79132auto ProjectSerializer::save (const QString &fileUrl, const UI ::TimelineService *timeline, const UI ::ProjectService *project, QString *errorMessage) -> bool {
80133 QString path = QUrl (fileUrl).toLocalFile ();
81134 if (path.isEmpty ()) {
@@ -104,6 +157,16 @@ auto ProjectSerializer::save(const QString &fileUrl, const UI::TimelineService *
104157 sObj .insert (QStringLiteral (" fps" ), scene.fps );
105158 sObj .insert (QStringLiteral (" start" ), scene.startFrame );
106159 sObj .insert (QStringLiteral (" duration" ), scene.totalFrames );
160+ sObj .insert (QStringLiteral (" nestedDuration" ), scene.durationFrames );
161+ sObj .insert (QStringLiteral (" lockedLayers" ), layerSetToJson (scene.lockedLayers ));
162+ sObj .insert (QStringLiteral (" hiddenLayers" ), layerSetToJson (scene.hiddenLayers ));
163+ sObj .insert (QStringLiteral (" gridMode" ), scene.gridMode );
164+ sObj .insert (QStringLiteral (" gridBpm" ), scene.gridBpm );
165+ sObj .insert (QStringLiteral (" gridOffset" ), scene.gridOffset );
166+ sObj .insert (QStringLiteral (" gridInterval" ), scene.gridInterval );
167+ sObj .insert (QStringLiteral (" gridSubdivision" ), scene.gridSubdivision );
168+ sObj .insert (QStringLiteral (" enableSnap" ), scene.enableSnap );
169+ sObj .insert (QStringLiteral (" magneticSnapRange" ), scene.magneticSnapRange );
107170 scenesArray.append (sObj );
108171 }
109172 root.insert (QStringLiteral (" scenes" ), scenesArray);
@@ -143,7 +206,9 @@ auto ProjectSerializer::save(const QString &fileUrl, const UI::TimelineService *
143206 eObj.insert (QStringLiteral (" id" ), eff->id ());
144207 eObj.insert (QStringLiteral (" name" ), eff->name ());
145208 eObj.insert (QStringLiteral (" enabled" ), eff->isEnabled ());
146- eObj.insert (QStringLiteral (" params" ), QJsonObject::fromVariantMap (eff->params ()));
209+ QVariantMap effectParams = eff->params ();
210+ convertEffectMediaPath (eff->id (), effectParams, projectDir, true );
211+ eObj.insert (QStringLiteral (" params" ), QJsonObject::fromVariantMap (effectParams));
147212 eObj.insert (QStringLiteral (" keyframes" ), QJsonObject::fromVariantMap (eff->keyframeTracks ()));
148213 effArray.append (eObj);
149214 }
@@ -194,8 +259,15 @@ auto ProjectSerializer::load(const QString &fileUrl, UI::TimelineService *timeli
194259 return false ;
195260 }
196261
197- auto jsonData = file.readAll ();
198- QJsonDocument doc = QJsonDocument::fromJson (jsonData);
262+ const QByteArray jsonData = file.readAll ();
263+ QJsonParseError parseError;
264+ const QJsonDocument doc = QJsonDocument::fromJson (jsonData, &parseError);
265+ if (parseError.error != QJsonParseError::NoError || !doc.isObject ()) {
266+ if (errorMessage != nullptr ) {
267+ *errorMessage = parseError.error != QJsonParseError::NoError ? parseError.errorString () : QStringLiteral (" Project root must be a JSON object" );
268+ }
269+ return false ;
270+ }
199271 QJsonObject root = doc.object ();
200272
201273 const int version = root.value (QStringLiteral (" version" )).toInt (1 );
@@ -237,6 +309,18 @@ auto ProjectSerializer::load(const QString &fileUrl, UI::TimelineService *timeli
237309 scene.startFrame = sobj.value (QStringLiteral (" start" )).toInt (0 );
238310 const int totalFrames = sobj.value (QStringLiteral (" duration" )).toInt (AviQtl::kDefaultTotalFrames );
239311 scene.totalFrames = totalFrames > 0 ? totalFrames : AviQtl::kDefaultTotalFrames ;
312+ if (version >= 3 ) {
313+ scene.durationFrames = std::max (0 , sobj.value (QStringLiteral (" nestedDuration" )).toInt (0 ));
314+ scene.lockedLayers = layerSetFromJson (sobj.value (QStringLiteral (" lockedLayers" )));
315+ scene.hiddenLayers = layerSetFromJson (sobj.value (QStringLiteral (" hiddenLayers" )));
316+ scene.gridMode = sobj.value (QStringLiteral (" gridMode" )).toString (QStringLiteral (" Auto" ));
317+ scene.gridBpm = clampPositiveGridValue (sobj.value (QStringLiteral (" gridBpm" )).toDouble (120.0 ), kMaxGridBpm , 120.0 );
318+ scene.gridOffset = clampGridOffset (sobj.value (QStringLiteral (" gridOffset" )).toDouble (0.0 ), 0.0 );
319+ scene.gridInterval = clampPositiveGridValue (sobj.value (QStringLiteral (" gridInterval" )).toInt (10 ), kMaxGridInterval , 10 );
320+ scene.gridSubdivision = clampPositiveGridValue (sobj.value (QStringLiteral (" gridSubdivision" )).toInt (4 ), kMaxGridSubdivision , 4 );
321+ scene.enableSnap = sobj.value (QStringLiteral (" enableSnap" )).toBool (true );
322+ scene.magneticSnapRange = clampPositiveGridValue (sobj.value (QStringLiteral (" magneticSnapRange" )).toInt (10 ), kMaxMagneticSnapRange , 10 );
323+ }
240324 tempScenes.append (scene);
241325 maxSceneId = std::max (scene.id , maxSceneId);
242326 }
@@ -292,7 +376,11 @@ auto ProjectSerializer::load(const QString &fileUrl, UI::TimelineService *timeli
292376 continue ;
293377 }
294378 QString displayName = meta.name .isEmpty () ? eObj.value (QStringLiteral (" name" )).toString () : meta.name ;
295- auto *eff = new UI::EffectModel (effId, displayName, meta.kind , meta.categories , eObj.value (QStringLiteral (" params" )).toObject ().toVariantMap (), meta.qmlSource , meta.uiDefinition , timeline);
379+ QVariantMap effectParams = eObj.value (QStringLiteral (" params" )).toObject ().toVariantMap ();
380+ if (version >= 2 ) {
381+ convertEffectMediaPath (effId, effectParams, projectDir, false );
382+ }
383+ auto *eff = new UI::EffectModel (effId, displayName, meta.kind , meta.categories , effectParams, meta.qmlSource , meta.uiDefinition , timeline);
296384 eff->setEnabled (eObj.value (QStringLiteral (" enabled" )).toBool (true ));
297385 auto it = eObj.find (QStringLiteral (" keyframes" ));
298386 if (it != eObj.end ()) {
0 commit comments