Skip to content

Commit

Permalink
Duplicate bug (#216)
Browse files Browse the repository at this point in the history
* Clone image handle

* Dispose of image on removal

* Close project after pop animation

* Dispose of deleted images

* Duplicate frame method

* Add duplicate path counter

* Implement make duplicate path

* Schedule dispose task

* Dispose in scheduled task

* Extract duplicate scene / scene layer

* Bump version
  • Loading branch information
ruskakimov authored Aug 9, 2021
1 parent 91af8ff commit 09d094e
Show file tree
Hide file tree
Showing 19 changed files with 191 additions and 91 deletions.
15 changes: 11 additions & 4 deletions lib/common/data/io/disk_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:ui';
import 'dart:io';

import 'package:equatable/equatable.dart';
import 'package:mooltik/common/data/io/make_duplicate_path.dart';
import 'package:mooltik/common/data/io/png.dart';

/// Manages a single image file.
Expand All @@ -22,10 +23,6 @@ class DiskImage with EquatableMixin {

int? get height => _snapshot?.height;

void changeSnapshot(Image? newSnapshot) {
_snapshot = newSnapshot;
}

Future<void> loadSnapshot() async {
_snapshot = await pngRead(file);
}
Expand All @@ -35,6 +32,16 @@ class DiskImage with EquatableMixin {
await pngWrite(file, _snapshot!);
}

Future<DiskImage> duplicate() async {
final duplicatePath = makeDuplicatePath(file.path);
final duplicateFile = await file.copy(duplicatePath);

return DiskImage(
file: duplicateFile,
snapshot: snapshot?.clone(),
);
}

factory DiskImage.fromJson(Map<String, dynamic> json) => DiskImage(
file: File(json[_filePathKey]),
);
Expand Down
32 changes: 32 additions & 0 deletions lib/common/data/io/make_duplicate_path.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:path/path.dart' as p;

/// Returns a new path for a duplicate file.
/// `example/path/image.png` -> `example/path/image_1.png`
/// `example/path/image_1.png` -> `example/path/image_2.png`
String makeDuplicatePath(String path) {
final dir = p.dirname(path);
final name = p.basenameWithoutExtension(path);
final ext = p.extension(path);

final newName =
_hasCounter(name) ? _incrementCounter(name) : _createCounter(name);

return p.join(dir, newName + ext);
}

bool _hasCounter(String name) {
return RegExp(r'_\d+$').hasMatch(name);
}

String _createCounter(String name) {
return name + '_1';
}

String _incrementCounter(String name) {
final parts = name.split('_');

final newCounterValue = int.parse(parts.last) + 1;
parts.last = newCounterValue.toString();

return parts.join('_');
}
5 changes: 5 additions & 0 deletions lib/common/data/project/composite_frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class CompositeFrame extends TimeSpan with EquatableMixin {
_durationKey: duration.toString(),
};

@override
void dispose() {
compositeImage.layers.forEach((layerImage) => layerImage.dispose());
}

static const String _imageKey = 'image';
static const String _durationKey = 'duration';
}
11 changes: 11 additions & 0 deletions lib/common/data/project/scene.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ class Scene extends TimeSpan {
}
}

Future<Scene> duplicate() async {
final duplicateLayers =
await Future.wait(layers.map((layer) => layer.duplicate()));
return copyWith(layers: duplicateLayers);
}

factory Scene.fromJson(Map<String, dynamic> json, String frameDirPath) =>
Scene(
layers: (json[_layersKey] as List<dynamic>)
Expand Down Expand Up @@ -111,6 +117,11 @@ class Scene extends TimeSpan {
'',
(previousValue, layer) => previousValue + layer.toString(),
);

@override
void dispose() {
layers.forEach((layer) => layer.dispose());
}
}

const String _layersKey = 'layers';
Expand Down
11 changes: 11 additions & 0 deletions lib/common/data/project/scene_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ class SceneLayer {
_name = value;
}

Future<SceneLayer> duplicate() async {
final duplicateFrames = await Future.wait(
frameSeq.iterable.map((frame) => frame.duplicate()),
);
return SceneLayer(Sequence(duplicateFrames), playMode);
}

factory SceneLayer.fromJson(Map<String, dynamic> json, String frameDirPath) =>
SceneLayer(
Sequence<Frame>((json[_framesKey] as List<dynamic>)
Expand All @@ -132,6 +139,10 @@ class SceneLayer {
'',
(previousValue, frame) => previousValue + frame.toString(),
);

void dispose() {
frameSeq.iterable.forEach((frame) => frame.dispose());
}
}

const String _framesKey = 'frames';
Expand Down
5 changes: 3 additions & 2 deletions lib/common/data/sequence/sequence.dart
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,14 @@ class Sequence<T extends TimeSpan> extends ChangeNotifier {
notifyListeners();
}

void removeAt(int index) {
T removeAt(int index) {
_validateIndex(index);
if (_spans.length <= 1) {
throw Exception('Cannot remove last span in sequence.');
}

final removedDuration = _spans[index].duration;
_spans.removeAt(index);
final removedSpan = _spans.removeAt(index);

_totalDuration -= removedDuration;

Expand All @@ -164,6 +164,7 @@ class Sequence<T extends TimeSpan> extends ChangeNotifier {
}
}
notifyListeners();
return removedSpan;
}

void swapSpanAt(int index, T newSpan) {
Expand Down
2 changes: 2 additions & 0 deletions lib/common/data/sequence/time_span.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ abstract class TimeSpan {
final Duration _duration;

TimeSpan copyWith({Duration? duration});

void dispose();
}
9 changes: 9 additions & 0 deletions lib/drawing/data/frame/frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class Frame extends TimeSpan with EquatableMixin {

final DiskImage image;

Future<Frame> duplicate() async {
return this.copyWith(image: await image.duplicate());
}

factory Frame.fromJson(Map<String, dynamic> json, String frameDirPath) =>
Frame(
image: DiskImage(
Expand All @@ -38,6 +42,11 @@ class Frame extends TimeSpan with EquatableMixin {
duration: duration ?? this.duration,
);

@override
void dispose() {
image.dispose();
}

@override
List<Object> get props => [image.file.path, duration];

Expand Down
16 changes: 10 additions & 6 deletions lib/drawing/data/frame_reel_model.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:mooltik/common/data/io/disk_image.dart';
import 'package:flutter/scheduler.dart';
import 'package:mooltik/common/data/project/project.dart';
import 'package:mooltik/common/data/sequence/sequence.dart';
import 'package:mooltik/drawing/data/frame/frame.dart';
Expand Down Expand Up @@ -42,21 +42,25 @@ class FrameReelModel extends ChangeNotifier {
}

Future<void> duplicateCurrent() async {
final duplicateDiskImage = (await _createNewFrame()).image;
duplicateDiskImage.changeSnapshot(currentFrame.image.snapshot);
duplicateDiskImage.saveSnapshot();
if (currentFrame.image.snapshot == null) return;

frameSeq.insert(
_currentIndex + 1,
currentFrame.copyWith(image: duplicateDiskImage),
await currentFrame.duplicate(),
);
notifyListeners();
}

bool get canDeleteCurrent => frameSeq.length > 1;

void deleteCurrent() {
frameSeq.removeAt(_currentIndex);
final removedFrame = frameSeq.removeAt(_currentIndex);

SchedulerBinding.instance?.scheduleTask(
() => removedFrame.dispose(),
Priority.idle,
);

_currentIndex = _currentIndex.clamp(0, frameSeq.length - 1);
notifyListeners();
}
Expand Down
8 changes: 7 additions & 1 deletion lib/drawing/data/reel_stack_model.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:mooltik/common/data/project/project.dart';
import 'package:mooltik/common/data/project/scene.dart';
import 'package:mooltik/common/data/project/scene_layer.dart';
Expand Down Expand Up @@ -72,7 +73,12 @@ class ReelStackModel extends ChangeNotifier {
final activeReelBefore = activeReel;

reels.removeAt(layerIndex);
_scene.layers.removeAt(layerIndex);
final removedLayer = _scene.layers.removeAt(layerIndex);

SchedulerBinding.instance?.scheduleTask(
() => removedLayer.dispose(),
Priority.idle,
);

if (layerIndex == _activeReelIndex) {
_activeReelIndex = _activeReelIndex.clamp(0, reels.length - 1);
Expand Down
39 changes: 12 additions & 27 deletions lib/editing/data/timeline_view_model.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:mooltik/common/data/project/project.dart';
import 'package:flutter/scheduler.dart';
import 'package:mooltik/common/data/project/scene.dart';
import 'package:mooltik/common/data/project/scene_layer.dart';
import 'package:mooltik/common/data/project/sound_clip.dart';
Expand All @@ -23,7 +23,6 @@ class TimelineViewModel extends ChangeNotifier {
required TimelineModel timeline,
required List<SoundClip>? soundClips,
required SharedPreferences? sharedPreferences,
required this.createNewFrame,
}) : _timeline = timeline,
_soundClips = soundClips ?? [],
_preferences = sharedPreferences,
Expand All @@ -41,7 +40,6 @@ class TimelineViewModel extends ChangeNotifier {
SharedPreferences? _preferences;
final TimelineModel _timeline;
final List<SoundClip> _soundClips;
final CreateNewFrame? createNewFrame;

bool get isEditingScene => _sceneEdit;
bool _sceneEdit = false;
Expand Down Expand Up @@ -366,42 +364,29 @@ class TimelineViewModel extends ChangeNotifier {
void deleteSelected() {
if (_selectedSliverId == null) return;
if (!canDeleteSelected) return;
selectedSliverSequence!.removeAt(_selectedSliverId!.spanIndex);

final removedSliver =
selectedSliverSequence!.removeAt(_selectedSliverId!.spanIndex);

SchedulerBinding.instance?.scheduleTask(
() => removedSliver.dispose(),
Priority.idle,
);

removeSliverSelection();
notifyListeners();
}

Future<void> duplicateSelected() async {
if (_selectedSliverId == null) return;
final duplicate = isEditingScene
? await _duplicateFrame(selectedFrame)
: await _duplicateScene(selectedScene);
? await selectedFrame.duplicate()
: await selectedScene.duplicate();
selectedSliverSequence!.insert(_selectedSliverId!.spanIndex + 1, duplicate);
removeSliverSelection();
notifyListeners();
}

Future<Frame> _duplicateFrame(Frame frame) async {
final duplicateDiskImage = (await createNewFrame!()).image;
duplicateDiskImage.changeSnapshot(frame.image.snapshot);
duplicateDiskImage.saveSnapshot();

return frame.copyWith(image: duplicateDiskImage);
}

Future<Scene> _duplicateScene(Scene scene) async {
final duplicateLayers =
await Future.wait(scene.layers.map((layer) => _duplicateLayer(layer)));
return scene.copyWith(layers: duplicateLayers);
}

Future<SceneLayer> _duplicateLayer(SceneLayer sceneLayer) async {
final duplicateFrames = await Future.wait(
sceneLayer.frameSeq.iterable.map((frame) => _duplicateFrame(frame)),
);
return SceneLayer(Sequence(duplicateFrames), sceneLayer.playMode);
}

Duration get selectedSliverStartTime => isEditingScene
? sceneStart +
selectedSliverSequence!.startTimeOf(_selectedSliverId!.spanIndex)
Expand Down
6 changes: 0 additions & 6 deletions lib/editing/editing_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ class _EditingPageState extends State<EditingPage>
timeline: context.read<TimelineModel>(),
soundClips: context.read<Project>().soundClips,
sharedPreferences: context.read<SharedPreferences>(),
createNewFrame: context.read<Project>().createNewFrame,
),
),
],
Expand All @@ -71,11 +70,6 @@ class _EditingPageState extends State<EditingPage>
onWillPop: () async {
final timeline = context.read<TimelineModel>();
if (timeline.isPlaying) return false;

final project = context.read<Project>();
WidgetsBinding.instance!.addPostFrameCallback((_) {
project.saveAndClose();
});
return true;
},
child: Scaffold(
Expand Down
35 changes: 35 additions & 0 deletions lib/home/data/gallery_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import 'dart:io';
import 'package:io/io.dart';
import 'package:flutter/material.dart';
import 'package:mooltik/common/data/project/project.dart';
import 'package:mooltik/editing/editing_page.dart';
import 'package:path/path.dart' as p;
import 'package:provider/provider.dart';

class GalleryModel extends ChangeNotifier {
late Directory _directory;
Expand Down Expand Up @@ -40,6 +42,39 @@ class GalleryModel extends ChangeNotifier {
return project;
}

Project? _openedProject;

/// Opens one project at a time.
/// Returns a future when the current project has been closed and another project can be opened.
Future<void> openProject(Project project, BuildContext context) async {
if (_openedProject != null) return;

_openedProject = project;

try {
await project.open();

final route = MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<Project>.value(
value: project,
child: EditingPage(),
),
);

Navigator.of(context).push(route);

// Await for pop animation to finish.
await route.completed;

// Don't await for save to finish. Allow other projects to be opened immediately.
project.saveAndClose();
} catch (e) {
rethrow;
} finally {
_openedProject = null;
}
}

Future<void> moveProjectToBin(Project project) async {
if (!_projects.contains(project)) {
throw Exception('Project instance not found.');
Expand Down
Loading

0 comments on commit 09d094e

Please sign in to comment.