Skip to content

Commit b017aa5

Browse files
committed
Various iOS improvements: sync patches folder with documents, fix crash when opening files
1 parent e75d9e6 commit b017aa5

File tree

4 files changed

+175
-13
lines changed

4 files changed

+175
-13
lines changed

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ juce_add_plugin(plugdata_standalone
442442
<dict>
443443
<key>CFBundleIdentifier</key>
444444
<string>com.plugdata.plugdata</string>
445+
<key>LSSupportsOpeningDocumentsInPlace</key>
446+
<true/>
447+
<key>UIFileSharingEnabled</key>
448+
<true/>
445449
</dict>
446450
</plist>]]
447451
)

Source/PluginEditor.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,8 +593,12 @@ DragAndDropTarget* PluginEditor::findNextDragAndDropTarget(Point<int> screenPos)
593593
void PluginEditor::resized()
594594
{
595595
#if JUCE_IOS
596+
static bool alreadyFullscreen = false;
596597
if (auto* window = dynamic_cast<PlugDataWindow*>(getTopLevelComponent())) {
597-
window->setFullScreen(true);
598+
if(!alreadyFullscreen) {
599+
ScopedValueSetter recursionBlock(alreadyFullscreen, true);
600+
window->setFullScreen(true);
601+
}
598602
}
599603
if (auto* peer = getPeer()) {
600604
OSUtils::ScrollTracker::create(peer);

Source/PluginProcessor.cpp

Lines changed: 163 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,131 @@ void PluginProcessor::doubleFlushMessageQueue()
223223
unlockAudioThread();
224224
}
225225

226+
#if JUCE_IOS
227+
void PluginProcessor::syncDirectoryFiles(File const& sourceDir, File const& targetDir, Time lastInitTime, bool deleteIfNotExists)
228+
{
229+
if (!sourceDir.exists() || !targetDir.exists()) {
230+
return;
231+
}
232+
233+
// Cache frequently used values
234+
const bool hasValidLastInitTime = lastInitTime.toMilliseconds() > 0;
235+
const String deleteFlag = deleteIfNotExists ? "true" : "false";
236+
237+
// Get all source files once and build a lookup map for efficiency
238+
auto sourceFiles = sourceDir.findChildFiles(juce::File::TypesOfFileToFind::findFiles, true);
239+
std::unordered_set<String> sourceFileSet;
240+
sourceFileSet.reserve(sourceFiles.size());
241+
242+
// Phase 1: Copy files from source to target (update newer files)
243+
for (const auto& sourceFile : sourceFiles) {
244+
const auto relativePath = sourceFile.getRelativePathFrom(sourceDir);
245+
sourceFileSet.insert(relativePath.toStdString());
246+
247+
const auto targetFile = targetDir.getChildFile(relativePath);
248+
249+
// Create directory structure if needed
250+
const auto parentDir = targetFile.getParentDirectory();
251+
if (!parentDir.exists()) {
252+
parentDir.createDirectory();
253+
}
254+
255+
// Copy if target doesn't exist or source is newer
256+
if (!targetFile.exists()) {
257+
sourceFile.copyFileTo(targetFile);
258+
} else {
259+
const auto sourceModTime = sourceFile.getLastModificationTime();
260+
const auto targetModTime = targetFile.getLastModificationTime();
261+
if (sourceModTime > targetModTime) {
262+
sourceFile.copyFileTo(targetFile);
263+
}
264+
}
265+
}
266+
267+
// Phase 2: Delete files in target that don't exist in source (only if deleteIfNotExists is true)
268+
if (deleteIfNotExists) {
269+
auto targetFiles = targetDir.findChildFiles(juce::File::TypesOfFileToFind::findFiles, true);
270+
271+
// Pre-cache directory modification times to prevent updates during deletion
272+
HashMap<String, Time> directoryModTimes;
273+
std::unordered_set<String> uniqueDirPaths;
274+
275+
for (const auto& targetFile : targetFiles) {
276+
const auto parentPath = targetFile.getParentDirectory().getFullPathName();
277+
if (uniqueDirPaths.find(parentPath.toStdString()) == uniqueDirPaths.end()) {
278+
uniqueDirPaths.insert(parentPath.toStdString());
279+
directoryModTimes.set(parentPath, targetFile.getParentDirectory().getLastModificationTime());
280+
}
281+
}
282+
283+
SmallArray<File> directoriesToCleanup;
284+
std::unordered_set<String> cleanupDirPaths; // Prevent duplicates
285+
286+
for (const auto& targetFile : targetFiles) {
287+
const auto relativePath = targetFile.getRelativePathFrom(targetDir);
288+
289+
// Check if file exists in source using our pre-built set
290+
if (sourceFileSet.find(relativePath.toStdString()) == sourceFileSet.end()) {
291+
// File doesn't exist in source - candidate for deletion
292+
bool shouldDelete = true;
293+
294+
if (hasValidLastInitTime) {
295+
const auto parentPath = targetFile.getParentDirectory().getFullPathName();
296+
const auto dirModTime = directoryModTimes[parentPath];
297+
298+
if (dirModTime > lastInitTime) {
299+
shouldDelete = false;
300+
} else {
301+
}
302+
}
303+
304+
if (shouldDelete) {
305+
const auto parentDir = targetFile.getParentDirectory();
306+
const auto parentPath = parentDir.getFullPathName();
307+
308+
targetFile.deleteFile();
309+
310+
// Add parent directory to cleanup list (avoid duplicates)
311+
if (cleanupDirPaths.find(parentPath.toStdString()) == cleanupDirPaths.end()) {
312+
cleanupDirPaths.insert(parentPath.toStdString());
313+
directoriesToCleanup.add(parentDir);
314+
}
315+
}
316+
}
317+
}
318+
319+
// Clean up empty directories (process from deepest to shallowest)
320+
for (const auto& parentDir : directoriesToCleanup) {
321+
auto currentDir = parentDir;
322+
while (currentDir != targetDir) {
323+
// Check if directory is empty (ignoring hidden files)
324+
const auto filesInDir = currentDir.findChildFiles(juce::File::TypesOfFileToFind::findFiles, false);
325+
bool isEmpty = true;
326+
for (const auto& file : filesInDir) {
327+
if (!file.isHidden()) {
328+
isEmpty = false;
329+
break;
330+
}
331+
}
332+
333+
if (isEmpty) {
334+
const auto parentRelativePath = currentDir.getRelativePathFrom(targetDir);
335+
if (!sourceDir.getChildFile(parentRelativePath).exists()) {
336+
const auto nextParent = currentDir.getParentDirectory();
337+
currentDir.deleteRecursively();
338+
currentDir = nextParent;
339+
} else {
340+
break; // Directory exists in source, keep it
341+
}
342+
} else {
343+
break; // Directory is not empty, stop
344+
}
345+
}
346+
}
347+
}
348+
}
349+
#endif
350+
226351
bool PluginProcessor::initialiseFilesystem()
227352
{
228353
auto const& homeDir = ProjectInfo::appDataDir;
@@ -303,11 +428,6 @@ bool PluginProcessor::initialiseFilesystem()
303428
if (!dekenDir.exists()) {
304429
dekenDir.createDirectory();
305430
}
306-
#if !JUCE_IOS
307-
if (!patchesDir.exists()) {
308-
patchesDir.createDirectory();
309-
}
310-
#endif
311431

312432
auto const testTonePatch = homeDir.getChildFile("testtone.pd");
313433
auto const cpuTestPatch = homeDir.getChildFile("load-meter.pd");
@@ -346,13 +466,12 @@ bool PluginProcessor::initialiseFilesystem()
346466
auto abstractionsPath = versionDataDir.getChildFile("Abstractions").getFullPathName().replaceCharacters("/", "\\");
347467
auto documentationPath = versionDataDir.getChildFile("Documentation").getFullPathName().replaceCharacters("/", "\\");
348468
auto extraPath = versionDataDir.getChildFile("Extra").getFullPathName().replaceCharacters("/", "\\");
349-
auto dekenPath = dekenDir.getFullPathName();
350-
auto patchesPath = patchesDir.getFullPathName();
351469

352470
createLinkWithRetry(homeDir.getChildFile("Abstractions"), versionDataDir.getChildFile("Abstractions"));
353471
createLinkWithRetry(homeDir.getChildFile("Documentation"), versionDataDir.getChildFile("Documentation"));
354472
createLinkWithRetry(homeDir.getChildFile("Extra"), versionDataDir.getChildFile("Extra"));
355473

474+
// TODO: version transition code, remove this later
356475
auto oldlocation = File::getSpecialLocation(File::SpecialLocationType::userDocumentsDirectory).getChildFile("plugdata");
357476
auto backupLocation = File::getSpecialLocation(File::SpecialLocationType::userDocumentsDirectory).getChildFile("plugdata.old");
358477
if (oldlocation.isDirectory() && !backupLocation.isDirectory()) {
@@ -372,13 +491,45 @@ bool PluginProcessor::initialiseFilesystem()
372491
createLinkWithRetry(homeDir.getChildFile("Extra"), versionDataDir.getChildFile("Extra"));
373492

374493
auto docsPatchesDir = File::getSpecialLocation(File::SpecialLocationType::userDocumentsDirectory).getChildFile("Patches");
375-
docsPatchesDir.createDirectory();
376-
if (!patchesDir.isSymbolicLink()) {
494+
if(patchesDir.isSymbolicLink())
495+
{
377496
patchesDir.deleteRecursively();
378-
} else {
379-
patchesDir.deleteFile();
380497
}
381-
docsPatchesDir.createSymbolicLink(patchesDir, true);
498+
if(docsPatchesDir.isSymbolicLink())
499+
{
500+
docsPatchesDir.deleteRecursively();
501+
}
502+
patchesDir.createDirectory();
503+
docsPatchesDir.createDirectory();
504+
505+
// On iOS, only the standalone can actually access the "patches" folder through the file manager. So we have to do this copying step to make sure it gets synced with our shared "Patches" folder
506+
// Thanks to Reinissance for figuring out the copying logic!
507+
if (ProjectInfo::isStandalone) {
508+
// Read last standalone initialization time
509+
auto timestampFile = homeDir.getChildFile(".standalone_last_init");
510+
Time lastInitTime;
511+
if (timestampFile.existsAsFile()) {
512+
auto timestampString = timestampFile.loadFileAsString().trim();
513+
if (timestampString.isNotEmpty()) {
514+
lastInitTime = Time(timestampString.getLargeIntValue());
515+
}
516+
}
517+
else {
518+
// If the file doesn't exist, we assume this is the first run
519+
lastInitTime = Time::getCurrentTime();
520+
// Create the file to store the current time
521+
timestampFile.create();
522+
}
523+
524+
// Sync files both ways: documents -> shared and shared -> documents
525+
// Pass the last init time to prevent deletion of files created by AUv3 apps after standalone was last initialized
526+
syncDirectoryFiles(patchesDir, docsPatchesDir, lastInitTime, true); // Copy newer files from shared to documents and delete files that don't exist there in shared
527+
syncDirectoryFiles(docsPatchesDir, patchesDir, lastInitTime); // Copy newer files from documents to shared
528+
529+
// Store current time as the last initialization time for next run...
530+
auto currentTime = Time::getCurrentTime();
531+
timestampFile.replaceWithText(String(currentTime.toMilliseconds()));
532+
}
382533
#else
383534
createLinkWithRetry(homeDir.getChildFile("Abstractions"), versionDataDir.getChildFile("Abstractions"));
384535
createLinkWithRetry(homeDir.getChildFile("Documentation"), versionDataDir.getChildFile("Documentation"));

Source/PluginProcessor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ class PluginProcessor final : public AudioProcessor
126126
void settingsFileReloaded() override;
127127

128128
static bool initialiseFilesystem();
129+
#if JUCE_IOS
130+
static void syncDirectoryFiles(File const& sourceDir, File const& targetDir, Time lastInitTime = Time(), bool deleteIfNotExists = false);
131+
#endif
129132
void updateSearchPaths();
130133

131134
void sendMidiBuffer(int device, MidiBuffer& buffer);

0 commit comments

Comments
 (0)