[ios] Base implementation of iCloud Synchronization #7641
|
@ -30388,3 +30388,424 @@
|
|||
vi = Xem thực đơn
|
||||
zh-Hans = 查看菜单
|
||||
zh-Hant = 查看選單
|
||||
|
||||
[enable_icloud_synchronization_title]
|
||||
comment = Title for the "Enable iCloud Syncronization" alert.
|
||||
tags = ios
|
||||
en = Enable iCloud Syncronization
|
||||
af = Aktiveer iCloud-sinchronisasie
|
||||
ar = تمكين مزامنة iCloud
|
||||
az = iCloud Sinxronizasiyasını aktivləşdirin
|
||||
be = Уключыць сінхранізацыю з iCloud
|
||||
bg = Активиране на синхронизацията с iCloud
|
||||
ca = Activa la sincronització d'iCloud
|
||||
cs = Povolení synchronizace s iCloudem
|
||||
da = Aktivér iCloud-synkronisering
|
||||
de = Aktiviere die iCloud-Syncronisierung
|
||||
el = Ενεργοποίηση συγχρονισμού iCloud
|
||||
es = Activar la sincronización con iCloud
|
||||
es-MX = Habilitar la sincronización de iCloud
|
||||
et = Lubage iCloudi sünkroniseerimine
|
||||
eu = Gaitu iCloud sinkronizazioa
|
||||
fa = همگام سازی iCloud را فعال کنید
|
||||
fi = Ota iCloud-synkronointi käyttöön
|
||||
fr = Activer la synchronisation iCloud
|
||||
he = הפעל סנכרון iCloud
|
||||
hi = iCloud सिंक्रोनाइज़ेशन सक्षम करें
|
||||
hu = iCloud-szinkronizálás engedélyezése
|
||||
id = Mengaktifkan Sinkronisasi iCloud
|
||||
it = Abilita la sincronizzazione con iCloud
|
||||
ja = iCloud同期を有効にする
|
||||
ko = iCloud 동기화 활성화
|
||||
lt = "iCloud" sinchronizavimo įjungimas
|
||||
mr = iCloud सिंक्रोनाइझेशन सक्षम करा
|
||||
nb = Aktiver iCloud-synkronisering
|
||||
nl = iCloud-synchronisatie inschakelen
|
||||
pl = Włącz synchronizację iCloud
|
||||
pt = Ativar a sincronização do iCloud
|
||||
pt-BR = Ativar a sincronização do iCloud
|
||||
ro = Activați Sincronizarea iCloud
|
||||
ru = Включить синхронизацию с iCloud
|
||||
sk = Povolenie synchronizácie iCloud
|
||||
sv = Aktivera iCloud-synkronisering
|
||||
sw = Washa Usawazishaji wa iCloud
|
||||
th = เปิดใช้งานการซิงโครไนซ์ iCloud
|
||||
tr = iCloud Senkronizasyonunu Etkinleştir
|
||||
uk = Увімкнути синхронізацію з iCloud
|
||||
vi = Kích hoạt đồng bộ hóa iCloud
|
||||
zh-Hans = 启用 iCloud 同步
|
||||
zh-Hant = 啟用 iCloud 同步
|
||||
|
||||
[enable_icloud_synchronization_message]
|
||||
comment = Message for the "Enable iCloud Syncronization" alert.
|
||||
tags = ios
|
||||
en = iCloud synchronization is an experimental feature under development. Make sure that you have made a backup of all your bookmarks and tracks.
|
||||
af = iCloud-sinchronisasie is 'n eksperimentele kenmerk wat ontwikkel word. Maak seker dat jy 'n rugsteun van al jou boekmerke en snitte gemaak het.
|
||||
ar = تعد مزامنة iCloud ميزة تجريبية قيد التطوير. تأكد من عمل نسخة احتياطية لجميع الإشارات المرجعية والمسارات الخاصة بك.
|
||||
az = iCloud sinxronizasiyası inkişaf mərhələsində olan eksperimental xüsusiyyətdir. Bütün əlfəcinlərinizin və treklərinizin ehtiyat nüsxəsini yaratdığınızdan əmin olun.
|
||||
be = Сінхранізацыя з iCloud - гэта эксперыментальная функцыя, якая знаходзіцца ў стадыі распрацоўкі. Пераканайцеся, што вы зрабілі рэзервовую копію ўсіх сваіх закладак і трэкаў.
|
||||
bg = Синхронизацията в iCloud е експериментална функция в процес на разработка. Уверете се, че сте направили резервно копие на всичките си отметки и песни.
|
||||
ca = La sincronització d'iCloud és una característica experimental en desenvolupament. Assegureu-vos que heu fet una còpia de seguretat de tots els vostres marcadors i pistes.
|
||||
cs = Synchronizace iCloudu je experimentální funkce, která se vyvíjí. Ujistěte se, že jste si vytvořili zálohu všech svých záložek a skladeb.
|
||||
da = iCloud-synkronisering er en eksperimentel funktion under udvikling. Sørg for, at du har lavet en backup af alle dine bogmærker og spor.
|
||||
de = Die iCloud-Synchronisierung ist eine experimentelle Funktion, die noch entwickelt wird. Vergewissere dich, dass du eine Sicherungskopie all deiner Lesezeichen und Titel erstellt hast.
|
||||
el = Ο συγχρονισμός iCloud είναι μια πειραματική λειτουργία υπό ανάπτυξη. Βεβαιωθείτε ότι έχετε δημιουργήσει αντίγραφο ασφαλείας όλων των σελιδοδεικτών και των κομματιών σας.
|
||||
es = La sincronización con iCloud es una función experimental en desarrollo. Asegúrate de haber hecho una copia de seguridad de todos tus marcadores y pistas.
|
||||
es-MX = La sincronización de iCloud es una función experimental en desarrollo. Asegúrese de haber realizado una copia de seguridad de todos sus marcadores y pistas.
|
||||
et = iCloudi sünkroniseerimine on eksperimentaalne funktsioon, mis on arendamisel. Veenduge, et olete teinud varukoopia kõigist oma järjehoidjatest ja lugudest.
|
||||
eu = iCloud sinkronizazioa garatzen ari den ezaugarri esperimental bat da. Ziurtatu zure laster-marka eta ibilbide guztien babeskopia egin duzula.
|
||||
fa = همگام سازی iCloud یک ویژگی آزمایشی در دست توسعه است. مطمئن شوید که از تمام نشانکu200cها و آهنگu200cهای خود یک نسخه پشتیبان تهیه کردهu200cاید.
|
||||
fi = iCloud-synkronointi on kehitteillä oleva kokeellinen ominaisuus. Varmista, että olet tehnyt varmuuskopion kaikista kirjanmerkeistäsi ja kappaleistasi.
|
||||
fr = La synchronisation iCloud est une fonctionnalité expérimentale en cours de développement. Assure-toi d'avoir fait une sauvegarde de tous tes signets et pistes.
|
||||
he = סנכרון iCloud הוא תכונה ניסיונית בפיתוח. ודא שעשית גיבוי של כל הסימניות והרצועות שלך.
|
||||
hi = iCloud सिंक्रोनाइज़ेशन विकासाधीन एक प्रायोगिक सुविधा है। सुनिश्चित करें कि आपने अपने सभी बुकमार्क और ट्रैक का बैकअप बना लिया है।
|
||||
hu = Az iCloud-szinkronizálás egy fejlesztés alatt álló kísérleti funkció. Győződjön meg róla, hogy készített biztonsági másolatot az összes könyvjelzőjéről és zeneszámáról.
|
||||
id = Sinkronisasi iCloud adalah fitur eksperimental yang sedang dikembangkan. Pastikan Anda telah membuat cadangan semua penanda dan lagu Anda.
|
||||
it = La sincronizzazione con iCloud è una funzione sperimentale in fase di sviluppo. Assicurati di aver fatto un backup di tutti i tuoi segnalibri e tracce.
|
||||
ja = iCloud同期は開発中の実験的機能である。すべてのブックマークとトラックのバックアップをとっておくこと。
|
||||
ko = iCloud 동기화는 개발 중인 실험적 기능입니다. 모든 북마크와 트랙을 백업해 두었는지 확인하세요.
|
||||
lt = "iCloud" sinchronizavimas yra kuriama eksperimentinė funkcija. Įsitikinkite, kad padarėte atsarginę visų savo žymeklių ir takelių kopiją.
|
||||
mr = iCloud सिंक्रोनाइझेशन हे विकासाधीन प्रायोगिक वैशिष्ट्य आहे. तुम्ही तुमच्या सर्व बुकमार्क्स आणि ट्रॅकचा बॅकअप घेतला असल्याची खात्री करा.
|
||||
nb = iCloud-synkronisering er en eksperimentell funksjon under utvikling. Sørg for at du har tatt en sikkerhetskopi av alle bokmerkene og sporene dine.
|
||||
nl = iCloud synchronisatie is een experimentele functie in ontwikkeling. Zorg ervoor dat je een back-up hebt gemaakt van al je bladwijzers en tracks.
|
||||
pl = Synchronizacja iCloud jest eksperymentalną funkcją w fazie rozwoju. Upewnij się, że wykonałeś kopię zapasową wszystkich zakładek i utworów.
|
||||
pt = A sincronização com o iCloud é uma funcionalidade experimental em desenvolvimento. Certifica-te de que fizeste uma cópia de segurança de todos os teus marcadores e faixas.
|
||||
pt-BR = A sincronização do iCloud é um recurso experimental em desenvolvimento. Certifique-se de que você tenha feito um backup de todos os seus favoritos e faixas.
|
||||
ro = Sincronizarea iCloud este o funcție experimentală aflată în curs de dezvoltare. Asigurați-vă că ați făcut o copie de rezervă a tuturor marcajelor și pieselor dvs.
|
||||
ru = Синхронизация с iCloud - это экспериментальная функция, которая находится в стадии разработки. Убедитесь, что вы сделали резервную копию всех своих закладок и треков.
|
||||
sk = Synchronizácia s iCloudom je experimentálna funkcia, ktorá je vo vývoji. Uistite sa, že ste si vytvorili zálohu všetkých svojich záložiek a skladieb.
|
||||
sv = iCloud-synkronisering är en experimentell funktion under utveckling. Se till att du har gjort en säkerhetskopia av alla dina bokmärken och spår.
|
||||
sw = Usawazishaji wa iCloud ni kipengele cha majaribio kinachoendelezwa. Hakikisha kuwa umefanya nakala rudufu ya alamisho na nyimbo zako zote.
|
||||
th = การซิงโครไนซ์ iCloud เป็นคุณสมบัติทดลองที่อยู่ระหว่างการพัฒนา ตรวจสอบให้แน่ใจว่าคุณได้สำรองข้อมูลบุ๊กมาร์กและแทร็กทั้งหมดของคุณแล้ว
|
||||
tr = iCloud senkronizasyonu geliştirme aşamasında olan deneysel bir özelliktir. Tüm yer imlerinizin ve parçalarınızın yedeğini aldığınızdan emin olun.
|
||||
uk = Синхронізація з iCloud - це експериментальна функція, яка перебуває на стадії розробки. Переконайтеся, що ви зробили резервну копію всіх ваших закладок і треків.
|
||||
vi = Đồng bộ hóa iCloud là một tính năng thử nghiệm đang được phát triển. Đảm bảo rằng bạn đã tạo bản sao lưu tất cả dấu trang và bản nhạc của mình.
|
||||
zh-Hans = iCloud 同步是一项正在开发的实验性功能。请确保备份了所有书签和曲目。
|
||||
zh-Hant = iCloud 同步是一項正在開發中的實驗性功能。確保您已備份所有書籤和曲目。
|
||||
|
||||
[icloud_disabled_title]
|
||||
comment = Title for the "iCloud is Disabled" alert.
|
||||
tags = ios
|
||||
en = iCloud is Disabled
|
||||
af = iCloud is gedeaktiveer
|
||||
ar = تم تعطيل iCloud
|
||||
az = iCloud Deaktivdir
|
||||
be = iCloud адключаны
|
||||
bg = iCloud е деактивиран
|
||||
ca = iCloud està desactivat
|
||||
cs = Služba iCloud je vypnutá
|
||||
da = iCloud er deaktiveret
|
||||
de = iCloud ist deaktiviert
|
||||
el = Το iCloud είναι απενεργοποιημένο
|
||||
es = iCloud está desactivado
|
||||
es-MX = iCloud está desactivado
|
||||
et = iCloud on välja lülitatud
|
||||
eu = iCloud desgaituta dago
|
||||
fa = iCloud غیرفعال است
|
||||
fi = iCloud on poistettu käytöstä
|
||||
fr = iCloud est désactivé
|
||||
he = iCloud מושבת
|
||||
hi = iCloud अक्षम है
|
||||
hu = Az iCloud letiltva
|
||||
id = iCloud Dinonaktifkan
|
||||
it = iCloud è disabilitato
|
||||
ja = iCloudが無効になっている
|
||||
ko = iCloud가 비활성화됨
|
||||
lt = "iCloud" yra išjungtas
|
||||
mr = iCloud अक्षम आहे
|
||||
nb = iCloud er deaktivert
|
||||
nl = iCloud is uitgeschakeld
|
||||
pl = iCloud jest wyłączony
|
||||
pt = O iCloud está desativado
|
||||
ro = iCloud este dezactivat
|
||||
ru = iCloud отключен
|
||||
sk = Služba iCloud je vypnutá
|
||||
sv = iCloud är inaktiverat
|
||||
sw = iCloud Imezimwa
|
||||
th = iCloud ถูกปิดใช้งาน
|
||||
tr = iCloud Devre Dışı
|
||||
uk = iCloud вимкнено
|
||||
vi = iCloud bị vô hiệu hóa
|
||||
zh-Hans = iCloud 已禁用
|
||||
zh-Hant = iCloud 已停用
|
||||
|
||||
[icloud_disabled_message]
|
||||
comment = Message for the "iCloud is Disabled" alert.
|
||||
tags = ios
|
||||
en = Please enable iCloud in your device's settings to use this feature.
|
||||
af = Aktiveer asseblief iCloud in jou toestel se instellings om hierdie kenmerk te gebruik.
|
||||
ar = يرجى تمكين iCloud في إعدادات جهازك لاستخدام هذه الميزة.
|
||||
az = Bu funksiyadan istifadə etmək üçün cihazınızın parametrlərində iCloud-u aktiv edin.
|
||||
be = Каб выкарыстоўваць гэту функцыю, уключыце iCloud у наладах прылады.
|
||||
bg = За да използвате тази функция, разрешете iCloud в настройките на устройството си.
|
||||
ca = Activa iCloud a la configuració del teu dispositiu per utilitzar aquesta funció.
|
||||
cs = Chcete-li tuto funkci používat, povolte v nastavení zařízení službu iCloud.
|
||||
da = Aktivér iCloud i din enheds indstillinger for at bruge denne funktion.
|
||||
de = Bitte aktiviere iCloud in den Einstellungen deines Geräts, um diese Funktion zu nutzen.
|
||||
el = Ενεργοποιήστε το iCloud στις ρυθμίσεις της συσκευής σας για να χρησιμοποιήσετε αυτή τη λειτουργία.
|
||||
es = Activa iCloud en los ajustes de tu dispositivo para utilizar esta función.
|
||||
es-MX = Activa iCloud en los ajustes de tu dispositivo para utilizar esta función.
|
||||
et = Selle funktsiooni kasutamiseks lubage seadme seadetes iCloud.
|
||||
eu = Mesedez, gaitu iCloud zure gailuaren ezarpenetan eginbide hau erabiltzeko.
|
||||
fa = لطفاً iCloud را در تنظیمات دستگاه خود فعال کنید تا از این ویژگی استفاده کنید.
|
||||
fi = Ota iCloud käyttöön laitteesi asetuksissa, jotta voit käyttää tätä ominaisuutta.
|
||||
fr = Active iCloud dans les paramètres de ton appareil pour utiliser cette fonctionnalité.
|
||||
he = אנא הפעל את iCloud בהגדרות המכשיר שלך כדי להשתמש בתכונה זו.
|
||||
hi = कृपया इस सुविधा का उपयोग करने के लिए अपने डिवाइस की सेटिंग में iCloud सक्षम करें।
|
||||
hu = A funkció használatához engedélyezze az iCloudot a készülék beállításaiban.
|
||||
id = Aktifkan iCloud di pengaturan perangkat Anda untuk menggunakan fitur ini.
|
||||
it = Per utilizzare questa funzione, attiva iCloud nelle impostazioni del tuo dispositivo.
|
||||
ja = この機能を使うには、デバイスの設定でiCloudを有効にしてほしい。
|
||||
ko = 이 기능을 사용하려면 기기 설정에서 iCloud를 활성화하세요.
|
||||
lt = Jei norite naudotis šia funkcija, įjunkite "iCloud" įrenginio nustatymuose.
|
||||
mr = कृपया हे वैशिष्ट्य वापरण्यासाठी तुमच्या डिव्हाइसच्या सेटिंग्जमध्ये iCloud सक्षम करा.
|
||||
nb = Aktiver iCloud i enhetens innstillinger for å bruke denne funksjonen.
|
||||
nl = Schakel iCloud in bij de instellingen van je apparaat om deze functie te gebruiken.
|
||||
pl = Aby korzystać z tej funkcji, włącz usługę iCloud w ustawieniach urządzenia.
|
||||
pt = Para utilizar esta funcionalidade, ativa o iCloud nas definições do teu dispositivo.
|
||||
pt-BR = Ative o iCloud nas configurações do seu dispositivo para usar esse recurso.
|
||||
ro = Vă rugăm să activați iCloud în setările dispozitivului dvs. pentru a utiliza această funcție.
|
||||
ru = Чтобы воспользоваться этой функцией, включите iCloud в настройках своего устройства.
|
||||
sk = Ak chcete túto funkciu používať, povoľte iCloud v nastaveniach zariadenia.
|
||||
sv = Aktivera iCloud i enhetens inställningar för att kunna använda denna funktion.
|
||||
sw = Tafadhali wezesha iCloud katika mipangilio ya kifaa chako ili kutumia kipengele hiki.
|
||||
th = โปรดเปิดใช้งาน iCloud ในการตั้งค่าอุปกรณ์ของคุณเพื่อใช้คุณสมบัตินี้
|
||||
tr = Bu özelliği kullanmak için lütfen cihazınızın ayarlarında iCloud'u etkinleştirin.
|
||||
uk = Щоб скористатися цією функцією, увімкніть iCloud у налаштуваннях вашого пристрою.
|
||||
vi = Vui lòng bật iCloud trong cài đặt thiết bị của bạn để sử dụng tính năng này.
|
||||
zh-Hans = 请在设备设置中启用 iCloud,以使用此功能。
|
||||
zh-Hant = 請在您的裝置設定中啟用 iCloud 以使用此功能。
|
||||
|
||||
[enable]
|
||||
comment = Title for the "Enable iCloud Syncronization" alert's "Enable" action button.
|
||||
tags = ios
|
||||
en = Enable
|
||||
af = Aktiveer
|
||||
ar = يُمكَِن
|
||||
az = Aktivləşdirin
|
||||
be = Уключыць
|
||||
bg = Активиране на
|
||||
ca = Activa
|
||||
cs = Povolit
|
||||
da = Aktivér
|
||||
de = Aktiviere
|
||||
el = Ενεργοποίηση
|
||||
es = Activa
|
||||
et = Lubage
|
||||
eu = Gaitu
|
||||
fa = فعال کردن
|
||||
fi = Ota käyttöön
|
||||
fr = Activer
|
||||
he = לְאַפשֵׁר
|
||||
hi = सक्षम
|
||||
hu = Engedélyezze a
|
||||
id = Aktifkan
|
||||
it = Abilitazione
|
||||
ja = 有効にする
|
||||
ko = 사용
|
||||
lt = Įjungti
|
||||
mr = सक्षम करा
|
||||
nb = Aktivere
|
||||
nl = inschakelen
|
||||
pl = Włącz
|
||||
pt = Ativar
|
||||
pt-BR = Ativar
|
||||
ro = Activați
|
||||
ru = Включить
|
||||
sk = Povolenie stránky
|
||||
sv = Aktivera
|
||||
sw = Wezesha
|
||||
th = เปิดใช้งาน
|
||||
tr = Etkinleştir
|
||||
uk = Увімкнути
|
||||
vi = Cho phép
|
||||
zh-Hans = 启用
|
||||
zh-Hant = 使能夠
|
||||
|
||||
[backup]
|
||||
comment = Title for the "Enable iCloud Syncronization" alert's "Backup" action button.
|
||||
tags = ios
|
||||
en = Backup
|
||||
af = Ondersteuning
|
||||
ar = دعم
|
||||
az = Yedəkləmə
|
||||
be = Рэзервовае капіраванне
|
||||
bg = Резервно копие
|
||||
ca = Còpia de seguretat
|
||||
cs = Záloha
|
||||
da = Sikkerhedskopiering
|
||||
de = Sicherung
|
||||
el = Δημιουργία αντιγράφων ασφαλείας
|
||||
es = Copia de seguridad
|
||||
es-MX = Respaldo
|
||||
et = Varukoopia
|
||||
eu = Babeskopia
|
||||
fa = پشتیبان گیری
|
||||
fi = Varmuuskopiointi
|
||||
fr = Sauvegarde
|
||||
he = גיבוי
|
||||
hi = बैकअप
|
||||
hu = Biztonsági mentés
|
||||
id = Cadangan
|
||||
it = Backup
|
||||
ja = バックアップ
|
||||
ko = 백업
|
||||
lt = Atsarginė kopija
|
||||
mr = बॅकअप
|
||||
nb = Sikkerhetskopiering
|
||||
nl = Back-up
|
||||
pl = Kopia zapasowa
|
||||
pt = Cópia de segurança
|
||||
pt-BR = Cópia de segurança
|
||||
ro = Backup
|
||||
ru = Резервная копия
|
||||
sk = Zálohovanie
|
||||
sv = Säkerhetskopiering
|
||||
sw = Hifadhi nakala
|
||||
th = สำรองข้อมูล
|
||||
tr = Yedekleme
|
||||
uk = Резервне копіювання
|
||||
vi = Hỗ trợ
|
||||
zh-Hans = 备份
|
||||
zh-Hant = 备份
|
||||
|
||||
[icloud_synchronization_error_connection_error]
|
||||
comment = iCloud error message: Failed to synchronize due to connection error
|
||||
tags = ios
|
||||
en = Error: Failed to synchronize due to connection error
|
||||
af = Fout: Kon nie sinchroniseer nie weens verbindingsfout
|
||||
ar = خطأ: فشل المزامنة بسبب خطأ في الاتصال
|
||||
az = Xəta: Bağlantı xətası səbəbindən sinxronizasiya alınmadı
|
||||
be = Памылка: не ўдалося сінхранізаваць з-за памылкі злучэння
|
||||
bg = Грешка: Не успя да се синхронизира поради грешка при свързването
|
||||
ca = Error: no s'ha pogut sincronitzar a causa d'un error de connexió
|
||||
cs = Chyba: Nepodařilo se synchronizovat z důvodu chyby připojení
|
||||
da = Fejl: Kunne ikke synkronisere på grund af forbindelsesfejl
|
||||
de = Fehler: Synchronisierung aufgrund eines Verbindungsfehlers fehlgeschlagen
|
||||
el = Σφάλμα: Αποτυχία συγχρονισμού λόγω σφάλματος σύνδεσης
|
||||
es = Error: No se ha podido sincronizar debido a un error de conexión
|
||||
es-MX = Error: No se ha podido sincronizar debido a un error de conexión
|
||||
et = Viga: Sünkroniseerimine ebaõnnestus ühenduse vea tõttu.
|
||||
eu = Errorea: Ezin izan da sinkronizatu konexio-errore baten ondorioz
|
||||
fa = خطا: به دلیل خطای اتصال همگام سازی نشد
|
||||
fi = Virhe: Synkronointi epäonnistui yhteysvirheen vuoksi.
|
||||
fr = Erreur : La synchronisation a échoué en raison d'une erreur de connexion
|
||||
he = שגיאה: הסנכרון נכשל עקב שגיאת חיבור
|
||||
hi = त्रुटि: कनेक्शन त्रुटि के कारण सिंक्रनाइज़ करने में विफल
|
||||
hu = Hiba: A szinkronizálás sikertelen volt a kapcsolat hibája miatt.
|
||||
id = Kesalahan: Gagal menyinkronkan karena kesalahan koneksi
|
||||
it = Errore: Impossibile sincronizzare a causa di un errore di connessione
|
||||
ja = エラー:接続エラーにより同期に失敗した
|
||||
ko = 오류입니다: 연결 오류로 인해 동기화하지 못했습니다.
|
||||
lt = Klaida: Nepavyko sinchronizuoti dėl ryšio klaidos
|
||||
mr = त्रुटी: कनेक्शन त्रुटीमुळे सिंक्रोनाइझ करण्यात अयशस्वी
|
||||
nb = Feil: Kunne ikke synkronisere på grunn av tilkoblingsfeil
|
||||
nl = Fout: Synchronisatie mislukt door verbindingsfout
|
||||
pl = Błąd: Nie udało się zsynchronizować z powodu błędu połączenia
|
||||
pt = Erro: Falha na sincronização devido a um erro de ligação
|
||||
pt-BR = Erro: Falha na sincronização devido a erro de conexão
|
||||
ro = Eroare: Nu s-a reușit sincronizarea din cauza unei erori de conexiune
|
||||
ru = Ошибка: Не удалось выполнить синхронизацию из-за ошибки подключения
|
||||
sk = Chyba: Nepodarilo sa synchronizovať z dôvodu chyby pripojenia
|
||||
sv = Fel: Synkronisering misslyckades på grund av anslutningsfel
|
||||
sw = Hitilafu: Imeshindwa kusawazisha kwa sababu ya hitilafu ya muunganisho
|
||||
th = ข้อผิดพลาด: ไม่สามารถซิงโครไนซ์ได้เนื่องจากข้อผิดพลาดในการเชื่อมต่อ
|
||||
tr = Hata oluştu: Bağlantı hatası nedeniyle senkronizasyon başarısız oldu
|
||||
uk = Помилка: Не вдалося синхронізувати через помилку з'єднання
|
||||
vi = Lỗi: Không đồng bộ được do lỗi kết nối
|
||||
zh-Hans = 错误:由于连接错误,同步失败
|
||||
zh-Hant = 錯誤:由於連線錯誤而無法同步
|
||||
|
||||
[icloud_synchronization_error_quota_exceeded]
|
||||
comment = iCloud error message: Failed to synchronize due to iCloud quota exceeded
|
||||
tags = ios
|
||||
en = Error: Failed to synchronize due to iCloud quota exceeded
|
||||
af = Fout: Kon nie sinchroniseer nie weens iCloud-kwota oorskry
|
||||
ar = خطأ: فشل في المزامنة بسبب تجاوز حصة iCloud النسبية
|
||||
az = Xəta: iCloud kvotasını keçdiyinə görə sinxronizasiya alınmadı
|
||||
be = Памылка: не ўдалося сінхранізаваць з-за перавышэння квоты iCloud
|
||||
bg = Грешка: Неуспешно синхронизиране поради превишена квота на iCloud
|
||||
ca = Error: no s'ha pogut sincronitzar perquè s'ha superat la quota d'iCloud
|
||||
cs = Chyba: Nepodařilo se synchronizovat z důvodu překročení kvóty iCloudu
|
||||
da = Fejl: Kunne ikke synkronisere på grund af overskredet iCloud-kvote
|
||||
de = Fehler: Synchronisierung fehlgeschlagen, da iCloud-Kontingent überschritten
|
||||
el = Σφάλμα: iCloud: Απέτυχε ο συγχρονισμός λόγω υπέρβασης της ποσόστωσης iCloud
|
||||
es = Error: No se ha podido sincronizar porque se ha superado la cuota de iCloud
|
||||
es-MX = Error: No se ha podido sincronizar porque se ha superado la cuota de iCloud
|
||||
et = Viga: iCloudi kvoodi ületamise tõttu ei õnnestunud sünkroniseerimine.
|
||||
eu = Errorea: Ezin izan da sinkronizatu iCloud kuota gainditu delako
|
||||
fa = خطا: به دلیل فراتر رفتن از سهمیه iCloud، همگام سازی انجام نشد
|
||||
fi = Virhe: iCloud-kiintiön ylittymisen vuoksi synkronointi epäonnistui.
|
||||
fr = Erreur : Échec de la synchronisation en raison du dépassement du quota iCloud.
|
||||
he = שגיאה: הסנכרון נכשל עקב חריגה ממכסת iCloud
|
||||
hi = त्रुटि: iCloud कोटा पार हो जाने के कारण सिंक्रनाइज़ करने में विफल
|
||||
hu = Hiba: iCloud-kvóta túllépése miatt nem sikerült szinkronizálni.
|
||||
id = Kesalahan: Gagal menyinkronkan karena kuota iCloud terlampaui
|
||||
it = Errore: Impossibile sincronizzare a causa del superamento della quota iCloud
|
||||
ja = エラー:iCloudのクォータが超過したため、同期に失敗しました。
|
||||
ko = 오류입니다: iCloud 할당량이 초과되어 동기화하지 못했습니다.
|
||||
lt = Klaida: Nepavyko sinchronizuoti dėl viršytos "iCloud" kvotos
|
||||
mr = त्रुटी: iCloud कोटा ओलांडल्यामुळे सिंक्रोनाइझ करण्यात अयशस्वी
|
||||
nb = Feil: Kunne ikke synkronisere på grunn av overskredet iCloud-kvote
|
||||
nl = Fout: Synchronisatie mislukt vanwege overschrijding iCloud-quota
|
||||
pl = Błąd: Nie udało się zsynchronizować z powodu przekroczenia limitu iCloud
|
||||
pt = Erro: Falha na sincronização devido à quota do iCloud ter sido excedida
|
||||
pt-BR = Erro: Falha ao sincronizar devido à cota do iCloud excedida
|
||||
ro = Eroare: Nu s-a reușit sincronizarea din cauza depășirii cotei iCloud
|
||||
ru = Ошибка: Не удалось выполнить синхронизацию из-за превышения квоты iCloud
|
||||
sk = Chyba: Nepodarilo sa synchronizovať z dôvodu prekročenia kvóty iCloudu
|
||||
sv = Fel: Synkronisering misslyckades på grund av att iCloud-kvoten överskreds
|
||||
sw = Hitilafu: Imeshindwa kusawazisha kwa sababu ya mgao wa iCloud uliozidishwa
|
||||
th = ข้อผิดพลาด: ไม่สามารถซิงโครไนซ์ได้เนื่องจากเกินโควต้า iCloud
|
||||
tr = Hata: iCloud kotası aşıldığı için senkronize edilemedi
|
||||
uk = Помилка: Не вдалося синхронізувати через перевищення квоти iCloud
|
||||
vi = Lỗi: Không thể đồng bộ hóa do vượt quá hạn ngạch iCloud
|
||||
zh-Hans = 错误:由于超过 iCloud 配额,同步失败
|
||||
zh-Hant = 錯誤:由於超出 iCloud 配額而無法同步
|
||||
|
||||
[icloud_synchronization_error_cloud_is_unavailable]
|
||||
comment = iCloud error message: iCloud is not available
|
||||
tags = ios
|
||||
en = Error: iCloud is not available
|
||||
af = Fout: iCloud is nie beskikbaar nie
|
||||
ar = خطأ: iCloud غير متوفر
|
||||
az = Xəta: iCloud mövcud deyil
|
||||
be = Памылка: iCloud недаступны
|
||||
bg = Грешка: iCloud не е наличен
|
||||
ca = Error: iCloud no està disponible
|
||||
cs = Chyba: iCloud není k dispozici
|
||||
da = Fejl: iCloud er ikke tilgængelig
|
||||
de = Fehler: iCloud ist nicht verfügbar
|
||||
el = Σφάλμα: Το iCloud δεν είναι διαθέσιμο
|
||||
es = Error: iCloud no está disponible
|
||||
es-MX = Error: iCloud no está disponible
|
||||
et = Viga: iCloud ei ole saadaval
|
||||
eu = Errorea: iCloud ez dago erabilgarri
|
||||
fa = خطا: iCloud در دسترس نیست
|
||||
fi = Virhe: iCloud ei ole käytettävissä
|
||||
fr = Erreur : iCloud n'est pas disponible
|
||||
he = שגיאה: iCloud אינו זמין
|
||||
hi = त्रुटि: iCloud उपलब्ध नहीं है
|
||||
hu = Hiba: Az iCloud nem elérhető
|
||||
id = Kesalahan: iCloud tidak tersedia
|
||||
it = Errore: iCloud non è disponibile
|
||||
ja = エラー:iCloudが利用できない
|
||||
ko = 오류: iCloud를 사용할 수 없습니다.
|
||||
lt = Klaida: "iCloud" nepasiekiamas
|
||||
mr = त्रुटी: iCloud उपलब्ध नाही
|
||||
nb = Feil: iCloud er ikke tilgjengelig
|
||||
nl = Fout: iCloud is niet beschikbaar
|
||||
pl = Błąd: usługa iCloud jest niedostępna
|
||||
pt = Erro: o iCloud não está disponível
|
||||
pt-BR = Erro: o iCloud não está disponível
|
||||
ro = Eroare: iCloud nu este disponibil
|
||||
ru = Ошибка: iCloud недоступен
|
||||
sk = Chyba: iCloud nie je k dispozícii
|
||||
sv = Fel: iCloud är inte tillgängligt
|
||||
sw = Hitilafu: iCloud haipatikani
|
||||
th = ข้อผิดพลาด: iCloud ไม่พร้อมใช้งาน
|
||||
tr = Hata: iCloud kullanılamıyor
|
||||
uk = Помилка: iCloud недоступний
|
||||
vi = Lỗi: iCloud không khả dụng
|
||||
zh-Hans = 错误:iCloud 不可用
|
||||
zh-Hant = 錯誤:iCloud 不可用
|
||||
|
|
|
@ -36,6 +36,8 @@ NS_SWIFT_NAME(BookmarksManager)
|
|||
|
||||
- (BOOL)areBookmarksLoaded;
|
||||
- (void)loadBookmarks;
|
||||
- (void)reloadCategoryAtFilePath:(NSString *)filePath;
|
||||
- (void)deleteCategoryAtFilePath:(NSString *)filePath;
|
||||
|
||||
- (BOOL)areAllCategoriesEmpty;
|
||||
- (BOOL)isCategoryEmpty:(MWMMarkGroupID)groupId;
|
||||
|
@ -61,7 +63,9 @@ NS_SWIFT_NAME(BookmarksManager)
|
|||
- (void)setUserCategoriesVisible:(BOOL)isVisible;
|
||||
- (void)deleteCategory:(MWMMarkGroupID)groupId;
|
||||
- (BOOL)checkCategoryName:(NSString *)name;
|
||||
- (BOOL)hasCategory:(MWMMarkGroupID)groupId;
|
||||
- (BOOL)hasBookmark:(MWMMarkID)bookmarkId;
|
||||
- (BOOL)hasTrack:(MWMTrackID)trackId;
|
||||
- (NSArray<NSNumber *> *)availableSortingTypes:(MWMMarkGroupID)groupId hasMyPosition:(BOOL)hasMyPosition;
|
||||
- (void)sortBookmarks:(MWMMarkGroupID)groupId
|
||||
sortingType:(MWMBookmarksSortingType)sortingType
|
||||
|
|
|
@ -203,6 +203,18 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) {
|
|||
self.bm.LoadBookmarks();
|
||||
}
|
||||
|
||||
- (void)reloadCategoryAtFilePath:(NSString *)filePath
|
||||
{
|
||||
self.bm.ReloadBookmark(filePath.UTF8String);
|
||||
}
|
||||
|
||||
- (void)deleteCategoryAtFilePath:(NSString *)filePath
|
||||
{
|
||||
auto const groupId = self.bm.GetCategoryByFileName(filePath.UTF8String);
|
||||
if (groupId)
|
||||
[self deleteCategory:groupId];
|
||||
}
|
||||
|
||||
#pragma mark - Categories
|
||||
|
||||
- (BOOL)areAllCategoriesEmpty
|
||||
|
@ -350,11 +362,21 @@ static KmlFileType convertFileTypeToCore(MWMKmlFileType fileType) {
|
|||
return !self.bm.IsUsedCategoryName(name.UTF8String);
|
||||
}
|
||||
|
||||
- (BOOL)hasCategory:(MWMMarkGroupID)groupId
|
||||
{
|
||||
return self.bm.HasBmCategory(groupId);
|
||||
}
|
||||
|
||||
- (BOOL)hasBookmark:(MWMMarkID)bookmarkId
|
||||
{
|
||||
return self.bm.HasBookmark(bookmarkId);
|
||||
}
|
||||
|
||||
- (BOOL)hasTrack:(MWMTrackID)trackId
|
||||
{
|
||||
return self.bm.HasTrack(trackId);
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)availableSortingTypes:(MWMMarkGroupID)groupId hasMyPosition:(BOOL)hasMyPosition{
|
||||
auto const availableTypes = self.bm.GetAvailableSortingTypes(groupId, hasMyPosition);
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
|
|
|
@ -33,6 +33,15 @@ import UIKit
|
|||
state = .closed
|
||||
}
|
||||
|
||||
@objc func goBack() {
|
||||
switch state {
|
||||
case .opened:
|
||||
navigationController?.popViewController(animated: true)
|
||||
case .hidden, .closed:
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func hide(categoryId: MWMMarkGroupID) {
|
||||
state = .hidden(categoryId: categoryId)
|
||||
}
|
||||
|
|
|
@ -15,12 +15,21 @@ extension BookmarksListSortingType {
|
|||
}
|
||||
}
|
||||
|
||||
final class BookmarksListInteractor {
|
||||
private let markGroupId: MWMMarkGroupID
|
||||
private var bookmarksManager: BookmarksManager { BookmarksManager.shared() }
|
||||
final class BookmarksListInteractor: NSObject {
|
||||
private var markGroupId: MWMMarkGroupID
|
||||
private var bookmarksManager: BookmarksManager
|
||||
|
||||
var onCategoryReload: ((GroupReloadingResult) -> Void)?
|
||||
|
||||
init(markGroupId: MWMMarkGroupID) {
|
||||
self.markGroupId = markGroupId
|
||||
self.bookmarksManager = BookmarksManager.shared()
|
||||
super.init()
|
||||
self.addToBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeFromBookmarksManagerObserverList()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,4 +160,27 @@ extension BookmarksListInteractor: IBookmarksListInteractor {
|
|||
func finishExportFile() {
|
||||
bookmarksManager.finishShareCategory()
|
||||
}
|
||||
|
||||
func addToBookmarksManagerObserverList() {
|
||||
bookmarksManager.add(self)
|
||||
}
|
||||
|
||||
func removeFromBookmarksManagerObserverList() {
|
||||
bookmarksManager.remove(self)
|
||||
}
|
||||
|
||||
func reloadCategory() {
|
||||
onCategoryReload?(bookmarksManager.hasCategory(markGroupId) ? .success : .notFound)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
extension BookmarksListInteractor: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
reloadCategory()
|
||||
}
|
||||
|
||||
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
|
||||
reloadCategory()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@ enum BookmarkToolbarButtonSource {
|
|||
case more
|
||||
}
|
||||
|
||||
enum GroupReloadingResult {
|
||||
case success
|
||||
case notFound
|
||||
}
|
||||
|
||||
protocol IBookmarksListSectionViewModel {
|
||||
var numberOfItems: Int { get }
|
||||
var sectionTitle: String { get }
|
||||
|
@ -89,6 +94,8 @@ enum BookmarksListSortingType {
|
|||
}
|
||||
|
||||
protocol IBookmarksListInteractor {
|
||||
var onCategoryReload: ((GroupReloadingResult) -> Void)? { get set }
|
||||
|
||||
func getBookmarkGroup() -> BookmarkGroup
|
||||
func hasDescription() -> Bool
|
||||
func prepareForSearch()
|
||||
|
@ -125,6 +132,7 @@ protocol IBookmarksListRouter {
|
|||
delegate: SelectBookmarkGroupViewControllerDelegate?)
|
||||
func editBookmark(bookmarkId: MWMMarkID, completion: @escaping (Bool) -> Void)
|
||||
func editTrack(trackId: MWMTrackID, completion: @escaping (Bool) -> Void)
|
||||
func goBack()
|
||||
}
|
||||
|
||||
protocol IBookmarksListInfoViewModel {
|
||||
|
|
|
@ -5,12 +5,12 @@ protocol BookmarksListDelegate: AnyObject {
|
|||
final class BookmarksListPresenter {
|
||||
private unowned let view: IBookmarksListView
|
||||
private let router: IBookmarksListRouter
|
||||
private let interactor: IBookmarksListInteractor
|
||||
private var interactor: IBookmarksListInteractor
|
||||
private weak var delegate: BookmarksListDelegate?
|
||||
|
||||
private let distanceFormatter = MeasurementFormatter()
|
||||
private let imperialUnits: Bool
|
||||
private let bookmarkGroup: BookmarkGroup
|
||||
private var bookmarkGroup: BookmarkGroup
|
||||
|
||||
private enum EditableItem {
|
||||
case bookmark(MWMMarkID)
|
||||
|
@ -28,9 +28,22 @@ final class BookmarksListPresenter {
|
|||
self.delegate = delegate
|
||||
self.interactor = interactor
|
||||
self.imperialUnits = imperialUnits
|
||||
self.bookmarkGroup = interactor.getBookmarkGroup()
|
||||
self.distanceFormatter.unitOptions = [.providedUnit]
|
||||
self.subscribeOnGroupReloading()
|
||||
}
|
||||
|
||||
bookmarkGroup = interactor.getBookmarkGroup()
|
||||
distanceFormatter.unitOptions = [.providedUnit]
|
||||
private func subscribeOnGroupReloading() {
|
||||
interactor.onCategoryReload = { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .notFound:
|
||||
self.router.goBack()
|
||||
case .success:
|
||||
self.bookmarkGroup = self.interactor.getBookmarkGroup()
|
||||
self.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reload() {
|
||||
|
|
|
@ -79,4 +79,8 @@ extension BookmarksListRouter: IBookmarksListRouter {
|
|||
let editTrackController = EditTrackViewController(trackId: trackId, editCompletion: completion)
|
||||
mapViewController.navigationController?.pushViewController(editTrackController, animated: true)
|
||||
}
|
||||
|
||||
func goBack() {
|
||||
coordinator?.goBack()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,17 +45,11 @@ final class BMCViewController: MWMViewController {
|
|||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
// Disable all notifications in BM on appearance of this view.
|
||||
// It allows to significantly improve performance in case of bookmarks
|
||||
// modification. All notifications will be sent on controller's disappearance.
|
||||
viewModel.setNotificationsEnabled(false)
|
||||
viewModel.addToObserverList()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
// Allow to send all notifications in BM.
|
||||
viewModel.setNotificationsEnabled(true)
|
||||
viewModel.removeFromObserverList()
|
||||
}
|
||||
|
||||
|
|
|
@ -371,7 +371,7 @@ NSString *const kPP2BookmarkEditingSegue = @"PP2BookmarkEditing";
|
|||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
[super viewDidLayoutSubviews];
|
||||
if (!self.mapView.drapeEngineCreated && !MapsAppDelegate.isDrapeDisabled)
|
||||
if (!self.mapView.drapeEngineCreated && !MapsAppDelegate.isTestsEnvironment)
|
||||
[self.mapView createDrapeEngine];
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (NSUInteger)badgeNumber;
|
||||
|
||||
+ (BOOL)isDrapeDisabled;
|
||||
+ (BOOL)isTestsEnvironment;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -121,6 +121,9 @@ using namespace osm_auth_ios;
|
|||
}
|
||||
[self enableTTSForTheFirstTime];
|
||||
|
||||
if (![MapsAppDelegate isTestsEnvironment])
|
||||
[[CloudStorageManager shared] start];
|
||||
|
||||
[[DeepLinkHandler shared] applicationDidFinishLaunching:launchOptions];
|
||||
// application:openUrl:options is called later for deep links if YES is returned.
|
||||
return YES;
|
||||
|
@ -225,8 +228,8 @@ using namespace osm_auth_ios;
|
|||
LOG(LINFO, ("applicationDidBecomeActive - end"));
|
||||
}
|
||||
|
||||
// TODO: Drape enabling is skipped during the test run due to the app crashing in teardown. This is a temporary solution. Drape should be properly disabled instead of merely skipping the enabling process.
|
||||
+ (BOOL)isDrapeDisabled {
|
||||
// TODO: Drape enabling and iCloud sync are skipped during the test run due to the app crashing in teardown. This is a temporary solution. Drape should be properly disabled instead of merely skipping the enabling process.
|
||||
+ (BOOL)isTestsEnvironment {
|
||||
NSProcessInfo * processInfo = [NSProcessInfo processInfo];
|
||||
NSArray<NSString *> * launchArguments = [processInfo arguments];
|
||||
BOOL isTests = [launchArguments containsObject:@"-IsTests"];
|
||||
|
|
|
@ -34,4 +34,7 @@ NS_SWIFT_NAME(Settings)
|
|||
+ (NSString *)donateUrl;
|
||||
+ (BOOL)isNY;
|
||||
|
||||
+ (BOOL)iCLoudSynchronizationEnabled;
|
||||
+ (void)setICLoudSynchronizationEnabled:(BOOL)iCLoudSyncEnabled;
|
||||
|
||||
@end
|
||||
|
|
|
@ -18,6 +18,7 @@ NSString * const kUDAutoNightModeOff = @"AutoNightModeOff";
|
|||
NSString * const kThemeMode = @"ThemeMode";
|
||||
NSString * const kSpotlightLocaleLanguageId = @"SpotlightLocaleLanguageId";
|
||||
NSString * const kUDTrackWarningAlertWasShown = @"TrackWarningAlertWasShown";
|
||||
NSString * const kiCLoudSynchronizationEnabledKey = @"iCLoudSynchronizationEnabled";
|
||||
} // namespace
|
||||
|
||||
@implementation MWMSettings
|
||||
|
@ -156,4 +157,14 @@ NSString * const kUDTrackWarningAlertWasShown = @"TrackWarningAlertWasShown";
|
|||
return settings::Get("NY", isNY) ? isNY : false;
|
||||
}
|
||||
|
||||
+ (BOOL)iCLoudSynchronizationEnabled
|
||||
{
|
||||
return [NSUserDefaults.standardUserDefaults boolForKey:kiCLoudSynchronizationEnabledKey];
|
||||
}
|
||||
|
||||
+ (void)setICLoudSynchronizationEnabled:(BOOL)iCLoudSyncEnabled
|
||||
{
|
||||
[NSUserDefaults.standardUserDefaults setBool:iCLoudSyncEnabled forKey:kiCLoudSynchronizationEnabledKey];
|
||||
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.iCloudSynchronizationDidChangeEnabledState object:nil];
|
||||
}
|
||||
@end
|
||||
|
|
338
iphone/Maps/Core/iCloud/CloudStorageManager.swift
Normal file
|
@ -0,0 +1,338 @@
|
|||
enum VoidResult {
|
||||
case success
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
enum WritingResult {
|
||||
case success
|
||||
case reloadCategoriesAtURLs([URL])
|
||||
case deleteCategoriesAtURLs([URL])
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
typealias VoidResultCompletionHandler = (VoidResult) -> Void
|
||||
typealias WritingResultCompletionHandler = (WritingResult) -> Void
|
||||
|
||||
// TODO: Remove this type and use custom UTTypeIdentifier that is registered into the Info.plist after updating to the iOS >= 14.0.
|
||||
struct FileType {
|
||||
let fileExtension: String
|
||||
let typeIdentifier: String
|
||||
}
|
||||
|
||||
extension FileType {
|
||||
static let kml = FileType(fileExtension: "kml", typeIdentifier: "com.google.earth.kml")
|
||||
}
|
||||
|
||||
let kTrashDirectoryName = ".Trash"
|
||||
private let kBookmarksDirectoryName = "bookmarks"
|
||||
private let kICloudSynchronizationDidChangeEnabledStateNotificationName = "iCloudSynchronizationDidChangeEnabledStateNotification"
|
||||
private let kUDDidFinishInitialCloudSynchronization = "kUDDidFinishInitialCloudSynchronization"
|
||||
|
||||
@objc @objcMembers final class CloudStorageManager: NSObject {
|
||||
|
||||
fileprivate struct Observation {
|
||||
weak var observer: AnyObject?
|
||||
var onErrorCompletionHandler: ((NSError?) -> Void)?
|
||||
}
|
||||
|
||||
let fileManager: FileManager
|
||||
private let localDirectoryMonitor: LocalDirectoryMonitor
|
||||
private let cloudDirectoryMonitor: CloudDirectoryMonitor
|
||||
private let settings: Settings.Type
|
||||
private let bookmarksManager: BookmarksManager
|
||||
private let synchronizationStateManager: SynchronizationStateManager
|
||||
private var fileWriter: SynchronizationFileWriter?
|
||||
private var observers = [ObjectIdentifier: CloudStorageManager.Observation]()
|
||||
private var synchronizationError: SynchronizationError? {
|
||||
didSet { notifyObserversOnSynchronizationError(synchronizationError) }
|
||||
}
|
||||
|
||||
static private var isInitialSynchronization: Bool {
|
||||
return !UserDefaults.standard.bool(forKey: kUDDidFinishInitialCloudSynchronization)
|
||||
}
|
||||
|
||||
static let shared: CloudStorageManager = {
|
||||
let fileManager = FileManager.default
|
||||
let fileType = FileType.kml
|
||||
let cloudDirectoryMonitor = iCloudDocumentsDirectoryMonitor(fileManager: fileManager, fileType: fileType)
|
||||
let synchronizationStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: CloudStorageManager.isInitialSynchronization)
|
||||
do {
|
||||
let localDirectoryMonitor = try DefaultLocalDirectoryMonitor(fileManager: fileManager, directory: fileManager.bookmarksDirectoryUrl, fileType: fileType)
|
||||
let clodStorageManager = try CloudStorageManager(fileManager: fileManager,
|
||||
settings: Settings.self,
|
||||
bookmarksManager: BookmarksManager.shared(),
|
||||
cloudDirectoryMonitor: cloudDirectoryMonitor,
|
||||
localDirectoryMonitor: localDirectoryMonitor,
|
||||
synchronizationStateManager: synchronizationStateManager)
|
||||
return clodStorageManager
|
||||
} catch {
|
||||
fatalError("Failed to create shared iCloud storage manager with error: \(error)")
|
||||
}
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
init(fileManager: FileManager,
|
||||
settings: Settings.Type,
|
||||
bookmarksManager: BookmarksManager,
|
||||
cloudDirectoryMonitor: CloudDirectoryMonitor,
|
||||
localDirectoryMonitor: LocalDirectoryMonitor,
|
||||
synchronizationStateManager: SynchronizationStateManager) throws {
|
||||
guard fileManager === cloudDirectoryMonitor.fileManager, fileManager === localDirectoryMonitor.fileManager else {
|
||||
throw NSError(domain: "CloudStorageManger", code: 0, userInfo: [NSLocalizedDescriptionKey: "File managers should be the same."])
|
||||
}
|
||||
self.fileManager = fileManager
|
||||
self.settings = settings
|
||||
self.bookmarksManager = bookmarksManager
|
||||
self.cloudDirectoryMonitor = cloudDirectoryMonitor
|
||||
self.localDirectoryMonitor = localDirectoryMonitor
|
||||
self.synchronizationStateManager = synchronizationStateManager
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@objc func start() {
|
||||
subscribeToSettingsNotifications()
|
||||
subscribeToApplicationLifecycleNotifications()
|
||||
cloudDirectoryMonitor.delegate = self
|
||||
localDirectoryMonitor.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private extension CloudStorageManager {
|
||||
// MARK: - Synchronization Lifecycle
|
||||
func startSynchronization() {
|
||||
LOG(.debug, "Start synchronization...")
|
||||
switch cloudDirectoryMonitor.state {
|
||||
case .started:
|
||||
LOG(.debug, "Synchronization is already started")
|
||||
return
|
||||
case .paused:
|
||||
resumeSynchronization()
|
||||
case .stopped:
|
||||
cloudDirectoryMonitor.start { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
self.stopSynchronization()
|
||||
self.processError(error)
|
||||
case .success(let cloudDirectoryUrl):
|
||||
self.localDirectoryMonitor.start { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
self.stopSynchronization()
|
||||
self.processError(error)
|
||||
case .success(let localDirectoryUrl):
|
||||
self.fileWriter = SynchronizationFileWriter(fileManager: self.fileManager,
|
||||
localDirectoryUrl: localDirectoryUrl,
|
||||
cloudDirectoryUrl: cloudDirectoryUrl)
|
||||
LOG(.debug, "Synchronization is started successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopSynchronization() {
|
||||
LOG(.debug, "Stop synchronization")
|
||||
localDirectoryMonitor.stop()
|
||||
cloudDirectoryMonitor.stop()
|
||||
synchronizationError = nil
|
||||
fileWriter = nil
|
||||
synchronizationStateManager.resetState()
|
||||
}
|
||||
|
||||
func pauseSynchronization() {
|
||||
LOG(.debug, "Pause synchronization")
|
||||
localDirectoryMonitor.pause()
|
||||
cloudDirectoryMonitor.pause()
|
||||
}
|
||||
|
||||
func resumeSynchronization() {
|
||||
LOG(.debug, "Resume synchronization")
|
||||
localDirectoryMonitor.resume()
|
||||
cloudDirectoryMonitor.resume()
|
||||
}
|
||||
|
||||
// MARK: - App Lifecycle
|
||||
func subscribeToApplicationLifecycleNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
func unsubscribeFromApplicationLifecycleNotifications() {
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
func subscribeToSettingsNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didChangeEnabledState), name: NSNotification.iCloudSynchronizationDidChangeEnabledState, object: nil)
|
||||
}
|
||||
|
||||
@objc func appWillEnterForeground() {
|
||||
guard settings.iCLoudSynchronizationEnabled() else { return }
|
||||
startSynchronization()
|
||||
}
|
||||
|
||||
@objc func appDidEnterBackground() {
|
||||
guard settings.iCLoudSynchronizationEnabled() else { return }
|
||||
pauseSynchronization()
|
||||
}
|
||||
|
||||
@objc func didChangeEnabledState() {
|
||||
settings.iCLoudSynchronizationEnabled() ? startSynchronization() : stopSynchronization()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iCloudStorageManger + LocalDirectoryMonitorDelegate
|
||||
extension CloudStorageManager: LocalDirectoryMonitorDelegate {
|
||||
func didFinishGathering(contents: LocalContents) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didFinishGatheringLocalContents(contents))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didUpdate(contents: LocalContents) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didUpdateLocalContents(contents))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didReceiveLocalMonitorError(_ error: Error) {
|
||||
processError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iCloudStorageManger + CloudDirectoryMonitorDelegate
|
||||
extension CloudStorageManager: CloudDirectoryMonitorDelegate {
|
||||
func didFinishGathering(contents: CloudContents) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didFinishGatheringCloudContents(contents))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didUpdate(contents: CloudContents) {
|
||||
let events = synchronizationStateManager.resolveEvent(.didUpdateCloudContents(contents))
|
||||
processEvents(events)
|
||||
}
|
||||
|
||||
func didReceiveCloudMonitorError(_ error: Error) {
|
||||
processError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private extension CloudStorageManager {
|
||||
func processEvents(_ events: [OutgoingEvent]) {
|
||||
guard !events.isEmpty else {
|
||||
synchronizationError = nil
|
||||
return
|
||||
}
|
||||
|
||||
LOG(.debug, "Start processing events...")
|
||||
events.forEach { [weak self] event in
|
||||
LOG(.debug, "Processing event: \(event)")
|
||||
guard let self, let fileWriter else { return }
|
||||
fileWriter.processEvent(event, completion: writingResultHandler(for: event))
|
||||
}
|
||||
}
|
||||
|
||||
func writingResultHandler(for event: OutgoingEvent) -> WritingResultCompletionHandler {
|
||||
return { [weak self] result in
|
||||
guard let self else { return }
|
||||
DispatchQueue.main.async {
|
||||
switch result {
|
||||
case .success:
|
||||
// Mark that initial synchronization is finished.
|
||||
if case .didFinishInitialSynchronization = event {
|
||||
UserDefaults.standard.set(true, forKey: kUDDidFinishInitialCloudSynchronization)
|
||||
}
|
||||
case .reloadCategoriesAtURLs(let urls):
|
||||
urls.forEach { self.bookmarksManager.reloadCategory(atFilePath: $0.path) }
|
||||
case .deleteCategoriesAtURLs(let urls):
|
||||
urls.forEach { self.bookmarksManager.deleteCategory(atFilePath: $0.path) }
|
||||
case .failure(let error):
|
||||
self.processError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error handling
|
||||
func processError(_ error: Error) {
|
||||
if let synchronizationError = error as? SynchronizationError {
|
||||
LOG(.debug, "Synchronization error: \(error.localizedDescription)")
|
||||
switch synchronizationError {
|
||||
case .fileUnavailable: break
|
||||
case .fileNotUploadedDueToQuota: break
|
||||
case .ubiquityServerNotAvailable: break
|
||||
case .iCloudIsNotAvailable: fallthrough
|
||||
case .failedToOpenLocalDirectoryFileDescriptor: fallthrough
|
||||
case .failedToRetrieveLocalDirectoryContent: fallthrough
|
||||
case .containerNotFound:
|
||||
stopSynchronization()
|
||||
}
|
||||
self.synchronizationError = synchronizationError
|
||||
} else {
|
||||
// TODO: Handle non-synchronization errors
|
||||
LOG(.debug, "Non-synchronization error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CloudStorageManger Observing
|
||||
extension CloudStorageManager {
|
||||
func addObserver(_ observer: AnyObject, onErrorCompletionHandler: @escaping (NSError?) -> Void) {
|
||||
let id = ObjectIdentifier(observer)
|
||||
observers[id] = Observation(observer: observer, onErrorCompletionHandler:onErrorCompletionHandler)
|
||||
// Notify the new observer immediately to handle initial state.
|
||||
observers[id]?.onErrorCompletionHandler?(synchronizationError as NSError?)
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: AnyObject) {
|
||||
let id = ObjectIdentifier(observer)
|
||||
observers.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
private func notifyObserversOnSynchronizationError(_ error: SynchronizationError?) {
|
||||
self.observers.removeUnreachable().forEach { _, observable in
|
||||
DispatchQueue.main.async {
|
||||
observable.onErrorCompletionHandler?(error as NSError?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FileManager + Directories
|
||||
extension FileManager {
|
||||
var bookmarksDirectoryUrl: URL {
|
||||
urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(kBookmarksDirectoryName, isDirectory: true)
|
||||
}
|
||||
|
||||
func trashDirectoryUrl(for baseDirectoryUrl: URL) throws -> URL {
|
||||
let trashDirectory = baseDirectoryUrl.appendingPathComponent(kTrashDirectoryName, isDirectory: true)
|
||||
if !fileExists(atPath: trashDirectory.path) {
|
||||
try createDirectory(at: trashDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
return trashDirectory
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification + iCloudSynchronizationDidChangeEnabledState
|
||||
extension Notification.Name {
|
||||
static let iCloudSynchronizationDidChangeEnabledStateNotification = Notification.Name(kICloudSynchronizationDidChangeEnabledStateNotificationName)
|
||||
}
|
||||
|
||||
@objc extension NSNotification {
|
||||
public static let iCloudSynchronizationDidChangeEnabledState = Notification.Name.iCloudSynchronizationDidChangeEnabledStateNotification
|
||||
}
|
||||
|
||||
// MARK: - Dictionary + RemoveUnreachable
|
||||
private extension Dictionary where Key == ObjectIdentifier, Value == CloudStorageManager.Observation {
|
||||
mutating func removeUnreachable() -> Self {
|
||||
for (id, observation) in self {
|
||||
if observation.observer == nil {
|
||||
removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
216
iphone/Maps/Core/iCloud/DefaultLocalDirectoryMonitor.swift
Normal file
|
@ -0,0 +1,216 @@
|
|||
enum DirectoryMonitorState: CaseIterable, Equatable {
|
||||
case started
|
||||
case stopped
|
||||
case paused
|
||||
}
|
||||
|
||||
protocol DirectoryMonitor: AnyObject {
|
||||
var state: DirectoryMonitorState { get }
|
||||
|
||||
func start(completion: ((Result<URL, Error>) -> Void)?)
|
||||
func stop()
|
||||
func pause()
|
||||
func resume()
|
||||
}
|
||||
|
||||
protocol LocalDirectoryMonitor: DirectoryMonitor {
|
||||
var fileManager: FileManager { get }
|
||||
var directory: URL { get }
|
||||
var delegate: LocalDirectoryMonitorDelegate? { get set }
|
||||
}
|
||||
|
||||
protocol LocalDirectoryMonitorDelegate : AnyObject {
|
||||
func didFinishGathering(contents: LocalContents)
|
||||
func didUpdate(contents: LocalContents)
|
||||
func didReceiveLocalMonitorError(_ error: Error)
|
||||
}
|
||||
|
||||
final class DefaultLocalDirectoryMonitor: LocalDirectoryMonitor {
|
||||
|
||||
typealias Delegate = LocalDirectoryMonitorDelegate
|
||||
|
||||
fileprivate enum DispatchSourceDebounceState {
|
||||
case stopped
|
||||
case started(source: DispatchSourceFileSystemObject)
|
||||
case debounce(source: DispatchSourceFileSystemObject, timer: Timer)
|
||||
}
|
||||
|
||||
let fileManager: FileManager
|
||||
let fileType: FileType
|
||||
private let resourceKeys: [URLResourceKey] = [.nameKey]
|
||||
private var dispatchSource: DispatchSourceFileSystemObject?
|
||||
private var dispatchSourceDebounceState: DispatchSourceDebounceState = .stopped
|
||||
private var dispatchSourceIsSuspended = false
|
||||
private var dispatchSourceIsResumed = false
|
||||
private var didFinishGatheringIsCalled = false
|
||||
|
||||
// MARK: - Public properties
|
||||
let directory: URL
|
||||
private(set) var state: DirectoryMonitorState = .stopped
|
||||
weak var delegate: Delegate?
|
||||
|
||||
init(fileManager: FileManager, directory: URL, fileType: FileType = .kml) throws {
|
||||
self.fileManager = fileManager
|
||||
self.directory = directory
|
||||
self.fileType = fileType
|
||||
try fileManager.createDirectoryIfNeeded(at: directory)
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
func start(completion: ((Result<URL, Error>) -> Void)? = nil) {
|
||||
guard state != .started else { return }
|
||||
|
||||
let nowTimer = Timer.scheduledTimer(withTimeInterval: .zero, repeats: false) { [weak self] _ in
|
||||
LOG(.debug, "LocalMonitor: Initial timer firing...")
|
||||
self?.debounceTimerDidFire()
|
||||
}
|
||||
|
||||
if let dispatchSource {
|
||||
dispatchSourceDebounceState = .debounce(source: dispatchSource, timer: nowTimer)
|
||||
resume()
|
||||
completion?(.success(directory))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let source = try fileManager.source(for: directory)
|
||||
source.setEventHandler { [weak self] in
|
||||
self?.queueDidFire()
|
||||
}
|
||||
dispatchSourceDebounceState = .debounce(source: source, timer: nowTimer)
|
||||
source.activate()
|
||||
dispatchSource = source
|
||||
state = .started
|
||||
completion?(.success(directory))
|
||||
} catch {
|
||||
stop()
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
guard state == .started else { return }
|
||||
LOG(.debug, "LocalMonitor: Stop.")
|
||||
suspendDispatchSource()
|
||||
didFinishGatheringIsCalled = false
|
||||
dispatchSourceDebounceState = .stopped
|
||||
state = .stopped
|
||||
}
|
||||
|
||||
func pause() {
|
||||
guard state == .started else { return }
|
||||
LOG(.debug, "LocalMonitor: Pause.")
|
||||
suspendDispatchSource()
|
||||
state = .paused
|
||||
}
|
||||
|
||||
func resume() {
|
||||
guard state != .started else { return }
|
||||
LOG(.debug, "LocalMonitor: Resume.")
|
||||
resumeDispatchSource()
|
||||
state = .started
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private func queueDidFire() {
|
||||
LOG(.debug, "LocalMonitor: Queue did fire.")
|
||||
let debounceTimeInterval = 0.5
|
||||
switch dispatchSourceDebounceState {
|
||||
case .started(let source):
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: debounceTimeInterval, repeats: false) { [weak self] _ in
|
||||
self?.debounceTimerDidFire()
|
||||
}
|
||||
dispatchSourceDebounceState = .debounce(source: source, timer: timer)
|
||||
case .debounce(_, let timer):
|
||||
timer.fireDate = Date(timeIntervalSinceNow: debounceTimeInterval)
|
||||
// Stay in the `.debounce` state.
|
||||
case .stopped:
|
||||
// This can happen if the read source fired and enqueued a block on the
|
||||
// main queue but, before the main queue got to service that block, someone
|
||||
// called `stop()`. The correct response is to just do nothing.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func debounceTimerDidFire() {
|
||||
LOG(.debug, "LocalMonitor: Debounce timer did fire.")
|
||||
guard state == .started else {
|
||||
LOG(.debug, "LocalMonitor: State is not started. Skip iteration.")
|
||||
return
|
||||
}
|
||||
guard case .debounce(let source, let timer) = dispatchSourceDebounceState else { fatalError() }
|
||||
timer.invalidate()
|
||||
dispatchSourceDebounceState = .started(source: source)
|
||||
|
||||
do {
|
||||
let files = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [], options: [.skipsHiddenFiles], fileExtension: fileType.fileExtension)
|
||||
let contents = files.compactMap { url in
|
||||
do {
|
||||
let metadataItem = try LocalMetadataItem(fileUrl: url)
|
||||
return metadataItem
|
||||
} catch {
|
||||
delegate?.didReceiveLocalMonitorError(error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
let contentMetadataItems = LocalContents(contents)
|
||||
|
||||
if !didFinishGatheringIsCalled {
|
||||
didFinishGatheringIsCalled = true
|
||||
LOG(.debug, "LocalMonitor: didFinishGathering called.")
|
||||
LOG(.debug, "LocalMonitor: contentMetadataItems count: \(contentMetadataItems.count)")
|
||||
delegate?.didFinishGathering(contents: contentMetadataItems)
|
||||
} else {
|
||||
LOG(.debug, "LocalMonitor: didUpdate called.")
|
||||
LOG(.debug, "LocalMonitor: contentMetadataItems count: \(contentMetadataItems.count)")
|
||||
delegate?.didUpdate(contents: contentMetadataItems)
|
||||
}
|
||||
} catch {
|
||||
LOG(.debug, "\(error)")
|
||||
delegate?.didReceiveLocalMonitorError(SynchronizationError.failedToRetrieveLocalDirectoryContent)
|
||||
}
|
||||
}
|
||||
|
||||
private func suspendDispatchSource() {
|
||||
if !dispatchSourceIsSuspended {
|
||||
LOG(.debug, "LocalMonitor: Suspend dispatch source.")
|
||||
dispatchSource?.suspend()
|
||||
dispatchSourceIsSuspended = true
|
||||
dispatchSourceIsResumed = false
|
||||
}
|
||||
}
|
||||
|
||||
private func resumeDispatchSource() {
|
||||
if !dispatchSourceIsResumed {
|
||||
LOG(.debug, "LocalMonitor: Resume dispatch source.")
|
||||
dispatchSource?.resume()
|
||||
dispatchSourceIsResumed = true
|
||||
dispatchSourceIsSuspended = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension FileManager {
|
||||
func source(for directory: URL) throws -> DispatchSourceFileSystemObject {
|
||||
let directoryFileDescriptor = open(directory.path, O_EVTONLY)
|
||||
kirylkaveryn
commented
This code is triggered on every write operation into the directory, not only the creation/renaming/removal because I've set up the source with the
I wrote the test case to check the file contents updating without replacing isolated from the bookmark manager:
We can only add files to the iCloud local container.
This code is triggered on _every write operation_ into the directory, not only the creation/renaming/removal because I've set up the source with the `write` eventMask:
https://developer.apple.com/documentation/dispatch/dispatchsource/filesystemevent/1780646-write
```swift
let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: directoryFileDescriptor, eventMask: [.write], queue: DispatchQueue.main)
```
I wrote the test case to check the file contents _updating without replacing_ isolated from the bookmark manager:
<img width="806" alt="image" src="https://github.com/organicmaps/organicmaps/assets/79797627/feef7adb-0639-4fd8-940c-0175e11e365d">
> Another case to test: what if user uploads some good/bad kml files (and other types too) using Finder/File Sharing into OM's monitored folder?
We can only add files to the iCloud local container.
- Good kmls will be parsed and displayed immediately
- Bad kmls will not be displayed because they will not be parsed - I mentioned in todo that we should add an alert to handle this situation and notify a user.
- Other files (not *.kml) will not be synced (until we implement the support of the nested files/directories).
![]() Is any appending write to the existing file also catched? Can a test for append write be added? Is it possible to avoid detection of appending data? My Files app shows non-kml files. Is it a bug that they are synced to iCloud? Is any appending write to the existing file also catched? Can a test for append write be added?
Is it possible to avoid detection of appending data?
My Files app shows non-kml files. Is it a bug that they are synced to iCloud?

kirylkaveryn
commented
It seems that these are temporary kmls created by the core...
> My Files app shows non-kml files. Is it a bug that they are synced to iCloud?
It seems that these are temporary kmls created by the core...
I'll add filtering to these files.
![]() Another question is why these temporary files are not deleted... Another question is why these temporary files are not deleted...
|
||||
guard directoryFileDescriptor >= 0 else {
|
||||
throw SynchronizationError.failedToOpenLocalDirectoryFileDescriptor
|
||||
}
|
||||
let dispatchSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: directoryFileDescriptor, eventMask: [.write], queue: DispatchQueue.main)
|
||||
dispatchSource.setCancelHandler {
|
||||
close(directoryFileDescriptor)
|
||||
}
|
||||
return dispatchSource
|
||||
}
|
||||
|
||||
func createDirectoryIfNeeded(at url: URL) throws {
|
||||
if !fileExists(atPath: url.path) {
|
||||
try createDirectory(at: url, withIntermediateDirectories: true)
|
||||
}
|
||||
}
|
||||
|
||||
func contentsOfDirectory(at url: URL, includingPropertiesForKeys keys: [URLResourceKey]?, options: FileManager.DirectoryEnumerationOptions, fileExtension: String) throws -> [URL] {
|
||||
let files = try contentsOfDirectory(at: url, includingPropertiesForKeys: keys, options: options)
|
||||
return files.filter { $0.pathExtension == fileExtension }
|
||||
}
|
||||
}
|
156
iphone/Maps/Core/iCloud/MetadataItem.swift
Normal file
|
@ -0,0 +1,156 @@
|
|||
protocol MetadataItem: Equatable, Hashable {
|
||||
var fileName: String { get }
|
||||
var fileUrl: URL { get }
|
||||
var fileSize: Int { get }
|
||||
var contentType: String { get }
|
||||
var creationDate: TimeInterval { get }
|
||||
var lastModificationDate: TimeInterval { get }
|
||||
}
|
||||
|
||||
struct LocalMetadataItem: MetadataItem {
|
||||
let fileName: String
|
||||
let fileUrl: URL
|
||||
let fileSize: Int
|
||||
let contentType: String
|
||||
let creationDate: TimeInterval
|
||||
let lastModificationDate: TimeInterval
|
||||
}
|
||||
|
||||
struct CloudMetadataItem: MetadataItem {
|
||||
let fileName: String
|
||||
let fileUrl: URL
|
||||
let fileSize: Int
|
||||
let contentType: String
|
||||
var isDownloaded: Bool
|
||||
let creationDate: TimeInterval
|
||||
var lastModificationDate: TimeInterval
|
||||
var isRemoved: Bool
|
||||
let downloadingError: NSError?
|
||||
let uploadingError: NSError?
|
||||
let hasUnresolvedConflicts: Bool
|
||||
}
|
||||
|
||||
extension LocalMetadataItem {
|
||||
init(metadataItem: NSMetadataItem) throws {
|
||||
guard let fileName = metadataItem.value(forAttribute: NSMetadataItemFSNameKey) as? String,
|
||||
let fileUrl = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL,
|
||||
let fileSize = metadataItem.value(forAttribute: NSMetadataItemFSSizeKey) as? Int,
|
||||
let contentType = metadataItem.value(forAttribute: NSMetadataItemContentTypeKey) as? String,
|
||||
let creationDate = (metadataItem.value(forAttribute: NSMetadataItemFSCreationDateKey) as? Date)?.timeIntervalSince1970.rounded(.down),
|
||||
let lastModificationDate = (metadataItem.value(forAttribute: NSMetadataItemFSContentChangeDateKey) as? Date)?.timeIntervalSince1970.rounded(.down) else {
|
||||
throw NSError(domain: "LocalMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize LocalMetadataItem from NSMetadataItem"])
|
||||
}
|
||||
self.fileName = fileName
|
||||
self.fileUrl = fileUrl
|
||||
self.fileSize = fileSize
|
||||
self.contentType = contentType
|
||||
self.creationDate = creationDate
|
||||
self.lastModificationDate = lastModificationDate
|
||||
}
|
||||
|
||||
init(fileUrl: URL) throws {
|
||||
let resources = try fileUrl.resourceValues(forKeys: [.fileSizeKey, .typeIdentifierKey, .contentModificationDateKey, .creationDateKey])
|
||||
guard let fileSize = resources.fileSize,
|
||||
let contentType = resources.typeIdentifier,
|
||||
let creationDate = resources.creationDate?.timeIntervalSince1970.rounded(.down),
|
||||
let lastModificationDate = resources.contentModificationDate?.timeIntervalSince1970.rounded(.down) else {
|
||||
throw NSError(domain: "LocalMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize LocalMetadataItem from URL"])
|
||||
}
|
||||
self.fileName = fileUrl.lastPathComponent
|
||||
self.fileUrl = fileUrl
|
||||
self.fileSize = fileSize
|
||||
self.contentType = contentType
|
||||
self.creationDate = creationDate
|
||||
self.lastModificationDate = lastModificationDate
|
||||
}
|
||||
|
||||
func fileData() throws -> Data {
|
||||
try Data(contentsOf: fileUrl)
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudMetadataItem {
|
||||
init(metadataItem: NSMetadataItem) throws {
|
||||
guard let fileName = metadataItem.value(forAttribute: NSMetadataItemFSNameKey) as? String,
|
||||
let fileUrl = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL,
|
||||
let fileSize = metadataItem.value(forAttribute: NSMetadataItemFSSizeKey) as? Int,
|
||||
let contentType = metadataItem.value(forAttribute: NSMetadataItemContentTypeKey) as? String,
|
||||
let downloadStatus = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String,
|
||||
let creationDate = (metadataItem.value(forAttribute: NSMetadataItemFSCreationDateKey) as? Date)?.timeIntervalSince1970.rounded(.down),
|
||||
let lastModificationDate = (metadataItem.value(forAttribute: NSMetadataItemFSContentChangeDateKey) as? Date)?.timeIntervalSince1970.rounded(.down),
|
||||
let hasUnresolvedConflicts = metadataItem.value(forAttribute: NSMetadataUbiquitousItemHasUnresolvedConflictsKey) as? Bool else {
|
||||
throw NSError(domain: "CloudMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize CloudMetadataItem from NSMetadataItem"])
|
||||
}
|
||||
self.fileName = fileName
|
||||
self.fileUrl = fileUrl
|
||||
self.fileSize = fileSize
|
||||
self.contentType = contentType
|
||||
self.isDownloaded = downloadStatus == NSMetadataUbiquitousItemDownloadingStatusCurrent
|
||||
self.creationDate = creationDate
|
||||
self.lastModificationDate = lastModificationDate
|
||||
self.isRemoved = CloudMetadataItem.isInTrash(fileUrl)
|
||||
self.hasUnresolvedConflicts = hasUnresolvedConflicts
|
||||
self.downloadingError = metadataItem.value(forAttribute: NSMetadataUbiquitousItemDownloadingErrorKey) as? NSError
|
||||
self.uploadingError = metadataItem.value(forAttribute: NSMetadataUbiquitousItemUploadingErrorKey) as? NSError
|
||||
}
|
||||
|
||||
init(fileUrl: URL) throws {
|
||||
let resources = try fileUrl.resourceValues(forKeys: [.nameKey, .fileSizeKey, .typeIdentifierKey, .contentModificationDateKey, .creationDateKey, .ubiquitousItemDownloadingStatusKey, .ubiquitousItemHasUnresolvedConflictsKey, .ubiquitousItemDownloadingErrorKey, .ubiquitousItemUploadingErrorKey])
|
||||
guard let fileSize = resources.fileSize,
|
||||
let contentType = resources.typeIdentifier,
|
||||
let creationDate = resources.creationDate?.timeIntervalSince1970.rounded(.down),
|
||||
let downloadStatus = resources.ubiquitousItemDownloadingStatus,
|
||||
let lastModificationDate = resources.contentModificationDate?.timeIntervalSince1970.rounded(.down),
|
||||
let hasUnresolvedConflicts = resources.ubiquitousItemHasUnresolvedConflicts else {
|
||||
throw NSError(domain: "CloudMetadataItem", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to initialize CloudMetadataItem from NSMetadataItem"])
|
||||
}
|
||||
self.fileName = fileUrl.lastPathComponent
|
||||
self.fileUrl = fileUrl
|
||||
self.fileSize = fileSize
|
||||
self.contentType = contentType
|
||||
self.isDownloaded = downloadStatus.rawValue == NSMetadataUbiquitousItemDownloadingStatusCurrent
|
||||
self.creationDate = creationDate
|
||||
self.lastModificationDate = lastModificationDate
|
||||
self.isRemoved = CloudMetadataItem.isInTrash(fileUrl)
|
||||
self.hasUnresolvedConflicts = hasUnresolvedConflicts
|
||||
self.downloadingError = resources.ubiquitousItemDownloadingError
|
||||
self.uploadingError = resources.ubiquitousItemUploadingError
|
||||
}
|
||||
|
||||
static func isInTrash(_ fileUrl: URL) -> Bool {
|
||||
fileUrl.pathComponents.contains(kTrashDirectoryName)
|
||||
}
|
||||
|
||||
func relatedLocalItemUrl(to localContainer: URL) -> URL {
|
||||
localContainer.appendingPathComponent(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalMetadataItem {
|
||||
func relatedCloudItemUrl(to cloudContainer: URL) -> URL {
|
||||
cloudContainer.appendingPathComponent(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: MetadataItem {
|
||||
func containsByName(_ item: any MetadataItem) -> Bool {
|
||||
return contains(where: { $0.fileName == item.fileName })
|
||||
}
|
||||
func firstByName(_ item: any MetadataItem) -> Element? {
|
||||
return first(where: { $0.fileName == item.fileName })
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == CloudMetadataItem {
|
||||
var downloaded: Self {
|
||||
filter { $0.isDownloaded }
|
||||
}
|
||||
|
||||
var notDownloaded: Self {
|
||||
filter { !$0.isDownloaded }
|
||||
}
|
||||
|
||||
func withUnresolvedConflicts(_ hasUnresolvedConflicts: Bool) -> Self {
|
||||
filter { $0.hasUnresolvedConflicts == hasUnresolvedConflicts }
|
||||
}
|
||||
}
|
45
iphone/Maps/Core/iCloud/SynchronizationError.swift
Normal file
|
@ -0,0 +1,45 @@
|
|||
@objc enum SynchronizationError: Int, Error {
|
||||
case fileUnavailable
|
||||
case fileNotUploadedDueToQuota
|
||||
case ubiquityServerNotAvailable
|
||||
case iCloudIsNotAvailable
|
||||
case containerNotFound
|
||||
case failedToOpenLocalDirectoryFileDescriptor
|
||||
case failedToRetrieveLocalDirectoryContent
|
||||
}
|
||||
|
||||
extension SynchronizationError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .fileUnavailable, .ubiquityServerNotAvailable:
|
||||
return L("icloud_synchronization_error_connection_error")
|
||||
case .fileNotUploadedDueToQuota:
|
||||
return L("icloud_synchronization_error_quota_exceeded")
|
||||
case .iCloudIsNotAvailable, .containerNotFound:
|
||||
return L("icloud_synchronization_error_cloud_is_unavailable")
|
||||
case .failedToOpenLocalDirectoryFileDescriptor:
|
||||
return "Failed to open local directory file descriptor"
|
||||
case .failedToRetrieveLocalDirectoryContent:
|
||||
return "Failed to retrieve local directory content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SynchronizationError {
|
||||
static func fromError(_ error: Error) -> SynchronizationError? {
|
||||
let nsError = error as NSError
|
||||
switch nsError.code {
|
||||
// NSURLUbiquitousItemDownloadingErrorKey contains an error with this code when the item has not been uploaded to iCloud by the other devices yet
|
||||
case NSUbiquitousFileUnavailableError:
|
||||
return .fileUnavailable
|
||||
// NSURLUbiquitousItemUploadingErrorKey contains an error with this code when the item has not been uploaded to iCloud because it would make the account go over-quota
|
||||
case NSUbiquitousFileNotUploadedDueToQuotaError:
|
||||
return .fileNotUploadedDueToQuota
|
||||
// NSURLUbiquitousItemDownloadingErrorKey and NSURLUbiquitousItemUploadingErrorKey contain an error with this code when connecting to the iCloud servers failed
|
||||
case NSUbiquitousFileUbiquityServerNotAvailable:
|
||||
return .ubiquityServerNotAvailable
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
273
iphone/Maps/Core/iCloud/SynchronizationFileWriter.swift
Normal file
|
@ -0,0 +1,273 @@
|
|||
final class SynchronizationFileWriter {
|
||||
private let fileManager: FileManager
|
||||
private let backgroundQueue = DispatchQueue(label: "iCloud.app.organicmaps.backgroundQueue", qos: .background)
|
||||
private let fileCoordinator: NSFileCoordinator
|
||||
private let localDirectoryUrl: URL
|
||||
private let cloudDirectoryUrl: URL
|
||||
|
||||
init(fileManager: FileManager = .default,
|
||||
fileCoordinator: NSFileCoordinator = NSFileCoordinator(),
|
||||
localDirectoryUrl: URL,
|
||||
cloudDirectoryUrl: URL) {
|
||||
self.fileManager = fileManager
|
||||
self.fileCoordinator = fileCoordinator
|
||||
self.localDirectoryUrl = localDirectoryUrl
|
||||
self.cloudDirectoryUrl = cloudDirectoryUrl
|
||||
}
|
||||
|
||||
func processEvent(_ event: OutgoingEvent, completion: @escaping WritingResultCompletionHandler) {
|
||||
backgroundQueue.async { [weak self] in
|
||||
guard let self else { return }
|
||||
switch event {
|
||||
case .createLocalItem(let cloudMetadataItem): self.createInLocalContainer(cloudMetadataItem, completion: completion)
|
||||
case .updateLocalItem(let cloudMetadataItem): self.updateInLocalContainer(cloudMetadataItem, completion: completion)
|
||||
case .removeLocalItem(let cloudMetadataItem): self.removeFromLocalContainer(cloudMetadataItem, completion: completion)
|
||||
case .startDownloading(let cloudMetadataItem): self.startDownloading(cloudMetadataItem, completion: completion)
|
||||
case .createCloudItem(let localMetadataItem): self.createInCloudContainer(localMetadataItem, completion: completion)
|
||||
case .updateCloudItem(let localMetadataItem): self.updateInCloudContainer(localMetadataItem, completion: completion)
|
||||
case .removeCloudItem(let localMetadataItem): self.removeFromCloudContainer(localMetadataItem, completion: completion)
|
||||
case .resolveVersionsConflict(let cloudMetadataItem): self.resolveVersionsConflict(cloudMetadataItem, completion: completion)
|
||||
case .resolveInitialSynchronizationConflict(let localMetadataItem): self.resolveInitialSynchronizationConflict(localMetadataItem, completion: completion)
|
||||
case .didFinishInitialSynchronization: completion(.success)
|
||||
case .didReceiveError(let error): completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Read/Write/Downloading/Uploading
|
||||
private func startDownloading(_ cloudMetadataItem: CloudMetadataItem, completion: WritingResultCompletionHandler) {
|
||||
do {
|
||||
LOG(.debug, "Start downloading file: \(cloudMetadataItem.fileName)...")
|
||||
try fileManager.startDownloadingUbiquitousItem(at: cloudMetadataItem.fileUrl)
|
||||
completion(.success)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func createInLocalContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
let targetLocalFileUrl = cloudMetadataItem.relatedLocalItemUrl(to: localDirectoryUrl)
|
||||
guard !fileManager.fileExists(atPath: targetLocalFileUrl.path) else {
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) already exists in the local iCloud container.")
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
writeToLocalContainer(cloudMetadataItem, completion: completion)
|
||||
}
|
||||
|
||||
private func updateInLocalContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
writeToLocalContainer(cloudMetadataItem, completion: completion)
|
||||
}
|
||||
|
||||
private func writeToLocalContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
var coordinationError: NSError?
|
||||
let targetLocalFileUrl = cloudMetadataItem.relatedLocalItemUrl(to: localDirectoryUrl)
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) is downloaded to the local iCloud container. Start coordinating and writing file...")
|
||||
fileCoordinator.coordinate(readingItemAt: cloudMetadataItem.fileUrl, writingItemAt: targetLocalFileUrl, error: &coordinationError) { readingUrl, writingUrl in
|
||||
do {
|
||||
let cloudFileData = try Data(contentsOf: readingUrl)
|
||||
try cloudFileData.write(to: writingUrl, options: .atomic, lastModificationDate: cloudMetadataItem.lastModificationDate)
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) is copied to local directory successfully. Start reloading bookmarks...")
|
||||
completion(.reloadCategoriesAtURLs([writingUrl]))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
if let coordinationError {
|
||||
completion(.failure(coordinationError))
|
||||
}
|
||||
}
|
||||
|
||||
private func removeFromLocalContainer(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
LOG(.debug, "Start removing file \(cloudMetadataItem.fileName) from the local directory...")
|
||||
let targetLocalFileUrl = cloudMetadataItem.relatedLocalItemUrl(to: localDirectoryUrl)
|
||||
guard fileManager.fileExists(atPath: targetLocalFileUrl.path) else {
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) doesn't exist in the local directory and cannot be removed.")
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
completion(.deleteCategoriesAtURLs([targetLocalFileUrl]))
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) is removed from the local directory successfully.")
|
||||
}
|
||||
|
||||
private func createInCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
let targetCloudFileUrl = localMetadataItem.relatedCloudItemUrl(to: cloudDirectoryUrl)
|
||||
guard !fileManager.fileExists(atPath: targetCloudFileUrl.path) else {
|
||||
LOG(.debug, "File \(localMetadataItem.fileName) already exists in the cloud directory.")
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
writeToCloudContainer(localMetadataItem, completion: completion)
|
||||
}
|
||||
|
||||
private func updateInCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
writeToCloudContainer(localMetadataItem, completion: completion)
|
||||
}
|
||||
|
||||
private func writeToCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
LOG(.debug, "Start writing file \(localMetadataItem.fileName) to the cloud directory...")
|
||||
let targetCloudFileUrl = localMetadataItem.relatedCloudItemUrl(to: cloudDirectoryUrl)
|
||||
var coordinationError: NSError?
|
||||
fileCoordinator.coordinate(readingItemAt: localMetadataItem.fileUrl, writingItemAt: targetCloudFileUrl, error: &coordinationError) { readingUrl, writingUrl in
|
||||
do {
|
||||
let fileData = try localMetadataItem.fileData()
|
||||
try fileData.write(to: writingUrl, lastModificationDate: localMetadataItem.lastModificationDate)
|
||||
LOG(.debug, "File \(localMetadataItem.fileName) is copied to the cloud directory successfully.")
|
||||
completion(.success)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
if let coordinationError {
|
||||
completion(.failure(coordinationError))
|
||||
}
|
||||
}
|
||||
|
||||
private func removeFromCloudContainer(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
LOG(.debug, "Start trashing file \(localMetadataItem.fileName)...")
|
||||
do {
|
||||
let targetCloudFileUrl = localMetadataItem.relatedCloudItemUrl(to: cloudDirectoryUrl)
|
||||
try removeDuplicatedFileFromTrashDirectoryIfNeeded(cloudDirectoryUrl: cloudDirectoryUrl, fileName: localMetadataItem.fileName)
|
||||
try self.fileManager.trashItem(at: targetCloudFileUrl, resultingItemURL: nil)
|
||||
LOG(.debug, "File \(localMetadataItem.fileName) was trashed successfully.")
|
||||
completion(.success)
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicated file from iCloud's .Trash directory if needed.
|
||||
// It's important to avoid the duplicating of names in the trash because we can't control the name of the trashed item.
|
||||
private func removeDuplicatedFileFromTrashDirectoryIfNeeded(cloudDirectoryUrl: URL, fileName: String) throws {
|
||||
// There are no ways to retrieve the content of iCloud's .Trash directory on macOS.
|
||||
if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return
|
||||
}
|
||||
LOG(.debug, "Checking if the file \(fileName) is already in the trash directory...")
|
||||
let trashDirectoryUrl = try fileManager.trashDirectoryUrl(for: cloudDirectoryUrl)
|
||||
let fileInTrashDirectoryUrl = trashDirectoryUrl.appendingPathComponent(fileName)
|
||||
let trashDirectoryContent = try fileManager.contentsOfDirectory(at: trashDirectoryUrl,
|
||||
includingPropertiesForKeys: [],
|
||||
options: [.skipsPackageDescendants, .skipsSubdirectoryDescendants])
|
||||
if trashDirectoryContent.contains(fileInTrashDirectoryUrl) {
|
||||
LOG(.debug, "File \(fileName) is already in the trash directory. Removing it...")
|
||||
try fileManager.removeItem(at: fileInTrashDirectoryUrl)
|
||||
LOG(.debug, "File \(fileName) was removed from the trash directory successfully.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Merge conflicts resolving
|
||||
private func resolveVersionsConflict(_ cloudMetadataItem: CloudMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
LOG(.debug, "Start resolving version conflict for file \(cloudMetadataItem.fileName)...")
|
||||
|
||||
guard let versionsInConflict = NSFileVersion.unresolvedConflictVersionsOfItem(at: cloudMetadataItem.fileUrl), !versionsInConflict.isEmpty,
|
||||
let currentVersion = NSFileVersion.currentVersionOfItem(at: cloudMetadataItem.fileUrl) else {
|
||||
LOG(.debug, "No versions in conflict found for file \(cloudMetadataItem.fileName).")
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
|
||||
let sortedVersions = versionsInConflict.sorted { version1, version2 in
|
||||
guard let date1 = version1.modificationDate, let date2 = version2.modificationDate else {
|
||||
return false
|
||||
}
|
||||
return date1 > date2
|
||||
}
|
||||
|
||||
guard let latestVersionInConflict = sortedVersions.first else {
|
||||
LOG(.debug, "No latest version in conflict found for file \(cloudMetadataItem.fileName).")
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
|
||||
let targetCloudFileCopyUrl = generateNewFileUrl(for: cloudMetadataItem.fileUrl)
|
||||
var coordinationError: NSError?
|
||||
fileCoordinator.coordinate(writingItemAt: currentVersion.url,
|
||||
options: [.forReplacing],
|
||||
writingItemAt: targetCloudFileCopyUrl,
|
||||
options: [],
|
||||
error: &coordinationError) { currentVersionUrl, copyVersionUrl in
|
||||
// Check that during the coordination block, the current version of the file have not been already resolved by another process.
|
||||
guard let unresolvedVersions = NSFileVersion.unresolvedConflictVersionsOfItem(at: currentVersionUrl), !unresolvedVersions.isEmpty else {
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) was already resolved.")
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
do {
|
||||
// Check if the file was already resolved by another process. The in-memory versions should be marked as resolved.
|
||||
guard !fileManager.fileExists(atPath: copyVersionUrl.path) else {
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) was already resolved.")
|
||||
try NSFileVersion.removeOtherVersionsOfItem(at: currentVersionUrl)
|
||||
completion(.success)
|
||||
return
|
||||
}
|
||||
|
||||
LOG(.debug, "Duplicate file \(cloudMetadataItem.fileName)...")
|
||||
try latestVersionInConflict.replaceItem(at: copyVersionUrl)
|
||||
// The modification date should be updated to mark files that was involved into the resolving process.
|
||||
try currentVersionUrl.setResourceModificationDate(Date())
|
||||
try copyVersionUrl.setResourceModificationDate(Date())
|
||||
unresolvedVersions.forEach { $0.isResolved = true }
|
||||
try NSFileVersion.removeOtherVersionsOfItem(at: currentVersionUrl)
|
||||
LOG(.debug, "File \(cloudMetadataItem.fileName) was successfully resolved.")
|
||||
completion(.success)
|
||||
return
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if let coordinationError {
|
||||
completion(.failure(coordinationError))
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveInitialSynchronizationConflict(_ localMetadataItem: LocalMetadataItem, completion: @escaping WritingResultCompletionHandler) {
|
||||
LOG(.debug, "Start resolving initial sync conflict for file \(localMetadataItem.fileName) by copying with a new name...")
|
||||
do {
|
||||
let newFileUrl = generateNewFileUrl(for: localMetadataItem.fileUrl, addDeviceName: true)
|
||||
try fileManager.copyItem(at: localMetadataItem.fileUrl, to: newFileUrl)
|
||||
LOG(.debug, "File \(localMetadataItem.fileName) was successfully resolved.")
|
||||
completion(.reloadCategoriesAtURLs([newFileUrl]))
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper methods
|
||||
// Generate a new file URL with a new name for the file with the same name.
|
||||
// This method should generate the same name for the same file on different devices during the simultaneous conflict resolving.
|
||||
private func generateNewFileUrl(for fileUrl: URL, addDeviceName: Bool = false) -> URL {
|
||||
let baseName = fileUrl.deletingPathExtension().lastPathComponent
|
||||
let fileExtension = fileUrl.pathExtension
|
||||
let newBaseName = baseName + "_1"
|
||||
let deviceName = addDeviceName ? "_\(UIDevice.current.name)" : ""
|
||||
let newFileName = newBaseName + deviceName + "." + fileExtension
|
||||
let newFileUrl = fileUrl.deletingLastPathComponent().appendingPathComponent(newFileName)
|
||||
return newFileUrl
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - URL + ResourceValues
|
||||
private extension URL {
|
||||
func setResourceModificationDate(_ date: Date) throws {
|
||||
var url = self
|
||||
var resource = try resourceValues(forKeys:[.contentModificationDateKey])
|
||||
resource.contentModificationDate = date
|
||||
try url.setResourceValues(resource)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
func write(to url: URL, options: Data.WritingOptions = .atomic, lastModificationDate: TimeInterval? = nil) throws {
|
||||
var url = url
|
||||
try write(to: url, options: options)
|
||||
if let lastModificationDate {
|
||||
try url.setResourceModificationDate(Date(timeIntervalSince1970: lastModificationDate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
260
iphone/Maps/Core/iCloud/SynchronizationStateManager.swift
Normal file
|
@ -0,0 +1,260 @@
|
|||
typealias MetadataItemName = String
|
||||
typealias LocalContents = [LocalMetadataItem]
|
||||
typealias CloudContents = [CloudMetadataItem]
|
||||
|
||||
protocol SynchronizationStateManager {
|
||||
var currentLocalContents: LocalContents { get }
|
||||
var currentCloudContents: CloudContents { get }
|
||||
var localContentsGatheringIsFinished: Bool { get }
|
||||
var cloudContentGatheringIsFinished: Bool { get }
|
||||
|
||||
@discardableResult
|
||||
func resolveEvent(_ event: IncomingEvent) -> [OutgoingEvent]
|
||||
func resetState()
|
||||
}
|
||||
|
||||
enum IncomingEvent {
|
||||
case didFinishGatheringLocalContents(LocalContents)
|
||||
case didFinishGatheringCloudContents(CloudContents)
|
||||
case didUpdateLocalContents(LocalContents)
|
||||
case didUpdateCloudContents(CloudContents)
|
||||
}
|
||||
|
||||
enum OutgoingEvent {
|
||||
case createLocalItem(CloudMetadataItem)
|
||||
case updateLocalItem(CloudMetadataItem)
|
||||
case removeLocalItem(CloudMetadataItem)
|
||||
case startDownloading(CloudMetadataItem)
|
||||
case createCloudItem(LocalMetadataItem)
|
||||
case updateCloudItem(LocalMetadataItem)
|
||||
case removeCloudItem(LocalMetadataItem)
|
||||
case didReceiveError(SynchronizationError)
|
||||
case resolveVersionsConflict(CloudMetadataItem)
|
||||
case resolveInitialSynchronizationConflict(LocalMetadataItem)
|
||||
case didFinishInitialSynchronization
|
||||
}
|
||||
|
||||
final class DefaultSynchronizationStateManager: SynchronizationStateManager {
|
||||
|
||||
// MARK: - Public properties
|
||||
private(set) var localContentsGatheringIsFinished = false
|
||||
private(set) var cloudContentGatheringIsFinished = false
|
||||
|
||||
private(set) var currentLocalContents: LocalContents = []
|
||||
private(set) var currentCloudContents: CloudContents = [] {
|
||||
didSet {
|
||||
updateFilteredCloudContents()
|
||||
}
|
||||
}
|
||||
|
||||
// Cached derived arrays
|
||||
private var trashedCloudContents: [CloudMetadataItem] = []
|
||||
private var notTrashedCloudContents: [CloudMetadataItem] = []
|
||||
private var downloadedCloudContents: [CloudMetadataItem] = []
|
||||
private var notDownloadedCloudContents: [CloudMetadataItem] = []
|
||||
|
||||
private var isInitialSynchronization: Bool
|
||||
|
||||
init(isInitialSynchronization: Bool) {
|
||||
self.isInitialSynchronization = isInitialSynchronization
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
@discardableResult
|
||||
func resolveEvent(_ event: IncomingEvent) -> [OutgoingEvent] {
|
||||
let outgoingEvents: [OutgoingEvent]
|
||||
switch event {
|
||||
case .didFinishGatheringLocalContents(let contents):
|
||||
localContentsGatheringIsFinished = true
|
||||
outgoingEvents = resolveDidFinishGathering(localContents: contents, cloudContents: currentCloudContents)
|
||||
case .didFinishGatheringCloudContents(let contents):
|
||||
cloudContentGatheringIsFinished = true
|
||||
outgoingEvents = resolveDidFinishGathering(localContents: currentLocalContents, cloudContents: contents)
|
||||
case .didUpdateLocalContents(let contents):
|
||||
outgoingEvents = resolveDidUpdateLocalContents(contents)
|
||||
case .didUpdateCloudContents(let contents):
|
||||
outgoingEvents = resolveDidUpdateCloudContents(contents)
|
||||
}
|
||||
return outgoingEvents
|
||||
}
|
||||
|
||||
func resetState() {
|
||||
currentLocalContents.removeAll()
|
||||
currentCloudContents.removeAll()
|
||||
localContentsGatheringIsFinished = false
|
||||
cloudContentGatheringIsFinished = false
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func updateFilteredCloudContents() {
|
||||
trashedCloudContents = currentCloudContents.filter { $0.isRemoved }
|
||||
notTrashedCloudContents = currentCloudContents.filter { !$0.isRemoved }
|
||||
}
|
||||
|
||||
private func resolveDidFinishGathering(localContents: LocalContents, cloudContents: CloudContents) -> [OutgoingEvent] {
|
||||
currentLocalContents = localContents
|
||||
currentCloudContents = cloudContents
|
||||
guard localContentsGatheringIsFinished, cloudContentGatheringIsFinished else { return [] }
|
||||
|
||||
var outgoingEvents: [OutgoingEvent]
|
||||
switch (localContents.isEmpty, cloudContents.isEmpty) {
|
||||
case (true, true):
|
||||
outgoingEvents = []
|
||||
case (true, false):
|
||||
outgoingEvents = notTrashedCloudContents.map { .createLocalItem($0) }
|
||||
case (false, true):
|
||||
outgoingEvents = localContents.map { .createCloudItem($0) }
|
||||
case (false, false):
|
||||
var events = [OutgoingEvent]()
|
||||
if isInitialSynchronization {
|
||||
events.append(contentsOf: resolveInitialSynchronizationConflicts(localContents: localContents, cloudContents: cloudContents))
|
||||
}
|
||||
events.append(contentsOf: resolveDidUpdateCloudContents(cloudContents))
|
||||
events.append(contentsOf: resolveDidUpdateLocalContents(localContents))
|
||||
outgoingEvents = events
|
||||
}
|
||||
if isInitialSynchronization {
|
||||
outgoingEvents.append(.didFinishInitialSynchronization)
|
||||
isInitialSynchronization = false
|
||||
}
|
||||
return outgoingEvents
|
||||
}
|
||||
|
||||
private func resolveDidUpdateLocalContents(_ localContents: LocalContents) -> [OutgoingEvent] {
|
||||
let itemsToRemoveFromCloudContainer = Self.getItemsToRemoveFromCloudContainer(currentLocalContents: currentLocalContents,
|
||||
newLocalContents: localContents)
|
||||
let itemsToCreateInCloudContainer = Self.getItemsToCreateInCloudContainer(notTrashedCloudContents: notTrashedCloudContents,
|
||||
trashedCloudContents: trashedCloudContents,
|
||||
localContents: localContents)
|
||||
let itemsToUpdateInCloudContainer = Self.getItemsToUpdateInCloudContainer(notTrashedCloudContents: notTrashedCloudContents,
|
||||
localContents: localContents,
|
||||
isInitialSynchronization: isInitialSynchronization)
|
||||
|
||||
var outgoingEvents = [OutgoingEvent]()
|
||||
itemsToRemoveFromCloudContainer.forEach { outgoingEvents.append(.removeCloudItem($0)) }
|
||||
itemsToCreateInCloudContainer.forEach { outgoingEvents.append(.createCloudItem($0)) }
|
||||
itemsToUpdateInCloudContainer.forEach { outgoingEvents.append(.updateCloudItem($0)) }
|
||||
|
||||
currentLocalContents = localContents
|
||||
return outgoingEvents
|
||||
}
|
||||
|
||||
private func resolveDidUpdateCloudContents(_ cloudContents: CloudContents) -> [OutgoingEvent] {
|
||||
var outgoingEvents = [OutgoingEvent]()
|
||||
currentCloudContents = cloudContents
|
||||
|
||||
// 1. Handle errors
|
||||
let errors = Self.getItemsWithErrors(cloudContents)
|
||||
errors.forEach { outgoingEvents.append(.didReceiveError($0)) }
|
||||
|
||||
// 2. Handle merge conflicts
|
||||
let itemsWithUnresolvedConflicts = Self.getItemsToResolveConflicts(notTrashedCloudContents: notTrashedCloudContents)
|
||||
itemsWithUnresolvedConflicts.forEach { outgoingEvents.append(.resolveVersionsConflict($0)) }
|
||||
|
||||
// Merge conflicts should be resolved at first.
|
||||
guard itemsWithUnresolvedConflicts.isEmpty else {
|
||||
return outgoingEvents
|
||||
}
|
||||
|
||||
let itemsToRemoveFromLocalContainer = Self.getItemsToRemoveFromLocalContainer(notTrashedCloudContents: notTrashedCloudContents,
|
||||
trashedCloudContents: trashedCloudContents,
|
||||
localContents: currentLocalContents)
|
||||
let itemsToCreateInLocalContainer = Self.getItemsToCreateInLocalContainer(notTrashedCloudContents: notTrashedCloudContents,
|
||||
localContents: currentLocalContents)
|
||||
let itemsToUpdateInLocalContainer = Self.getItemsToUpdateInLocalContainer(notTrashedCloudContents: notTrashedCloudContents,
|
||||
localContents: currentLocalContents,
|
||||
isInitialSynchronization: isInitialSynchronization)
|
||||
|
||||
// 3. Handle not downloaded items
|
||||
itemsToCreateInLocalContainer.notDownloaded.forEach { outgoingEvents.append(.startDownloading($0)) }
|
||||
itemsToUpdateInLocalContainer.notDownloaded.forEach { outgoingEvents.append(.startDownloading($0)) }
|
||||
|
||||
// 4. Handle downloaded items
|
||||
itemsToRemoveFromLocalContainer.forEach { outgoingEvents.append(.removeLocalItem($0)) }
|
||||
itemsToCreateInLocalContainer.downloaded.forEach { outgoingEvents.append(.createLocalItem($0)) }
|
||||
itemsToUpdateInLocalContainer.downloaded.forEach { outgoingEvents.append(.updateLocalItem($0)) }
|
||||
|
||||
return outgoingEvents
|
||||
}
|
||||
|
||||
private func resolveInitialSynchronizationConflicts(localContents: LocalContents, cloudContents: CloudContents) -> [OutgoingEvent] {
|
||||
let itemsInInitialConflict = localContents.filter { cloudContents.containsByName($0) }
|
||||
guard !itemsInInitialConflict.isEmpty else {
|
||||
return []
|
||||
}
|
||||
return itemsInInitialConflict.map { .resolveInitialSynchronizationConflict($0) }
|
||||
}
|
||||
|
||||
private static func getItemsToRemoveFromCloudContainer(currentLocalContents: LocalContents, newLocalContents: LocalContents) -> LocalContents {
|
||||
currentLocalContents.filter { !newLocalContents.containsByName($0) }
|
||||
}
|
||||
|
||||
private static func getItemsToCreateInCloudContainer(notTrashedCloudContents: CloudContents, trashedCloudContents: CloudContents, localContents: LocalContents) -> LocalContents {
|
||||
localContents.reduce(into: LocalContents()) { result, localItem in
|
||||
if !notTrashedCloudContents.containsByName(localItem) && !trashedCloudContents.containsByName(localItem) {
|
||||
result.append(localItem)
|
||||
} else if !notTrashedCloudContents.containsByName(localItem),
|
||||
let trashedCloudItem = trashedCloudContents.firstByName(localItem),
|
||||
trashedCloudItem.lastModificationDate < localItem.lastModificationDate {
|
||||
// If Cloud .Trash contains item and it's last modification date is less than the local item's last modification date than file should be recreated.
|
||||
result.append(localItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func getItemsToUpdateInCloudContainer(notTrashedCloudContents: CloudContents, localContents: LocalContents, isInitialSynchronization: Bool) -> LocalContents {
|
||||
guard !isInitialSynchronization else { return [] }
|
||||
// Due to the initial sync all conflicted local items will be duplicated with different name and replaced by the cloud items to avoid a data loss.
|
||||
return localContents.reduce(into: LocalContents()) { result, localItem in
|
||||
if let cloudItem = notTrashedCloudContents.firstByName(localItem),
|
||||
localItem.lastModificationDate > cloudItem.lastModificationDate {
|
||||
result.append(localItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func getItemsWithErrors(_ cloudContents: CloudContents) -> [SynchronizationError] {
|
||||
cloudContents.reduce(into: [SynchronizationError](), { partialResult, cloudItem in
|
||||
if let downloadingError = cloudItem.downloadingError, let synchronizationError = SynchronizationError.fromError(downloadingError) {
|
||||
partialResult.append(synchronizationError)
|
||||
}
|
||||
if let uploadingError = cloudItem.uploadingError, let synchronizationError = SynchronizationError.fromError(uploadingError) {
|
||||
partialResult.append(synchronizationError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private static func getItemsToRemoveFromLocalContainer(notTrashedCloudContents: CloudContents, trashedCloudContents: CloudContents, localContents: LocalContents) -> CloudContents {
|
||||
trashedCloudContents.reduce(into: CloudContents()) { result, cloudItem in
|
||||
// Items shouldn't be removed if newer version of the item isn't in the trash.
|
||||
if let notTrashedCloudItem = notTrashedCloudContents.firstByName(cloudItem), notTrashedCloudItem.lastModificationDate > cloudItem.lastModificationDate {
|
||||
return
|
||||
}
|
||||
if let localItemValue = localContents.firstByName(cloudItem),
|
||||
cloudItem.lastModificationDate >= localItemValue.lastModificationDate {
|
||||
result.append(cloudItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func getItemsToCreateInLocalContainer(notTrashedCloudContents: CloudContents, localContents: LocalContents) -> CloudContents {
|
||||
notTrashedCloudContents.withUnresolvedConflicts(false).filter { !localContents.containsByName($0) }
|
||||
}
|
||||
|
||||
private static func getItemsToUpdateInLocalContainer(notTrashedCloudContents: CloudContents, localContents: LocalContents, isInitialSynchronization: Bool) -> CloudContents {
|
||||
notTrashedCloudContents.withUnresolvedConflicts(false).reduce(into: CloudContents()) { result, cloudItem in
|
||||
if let localItemValue = localContents.firstByName(cloudItem) {
|
||||
// Due to the initial sync all conflicted local items will be duplicated with different name and replaced by the cloud items to avoid a data loss.
|
||||
if isInitialSynchronization {
|
||||
result.append(cloudItem)
|
||||
} else if cloudItem.lastModificationDate > localItemValue.lastModificationDate {
|
||||
result.append(cloudItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func getItemsToResolveConflicts(notTrashedCloudContents: CloudContents) -> CloudContents {
|
||||
notTrashedCloudContents.withUnresolvedConflicts(true)
|
||||
}
|
||||
}
|
269
iphone/Maps/Core/iCloud/iCloudDocumentsDirectoryMonitor.swift
Normal file
|
@ -0,0 +1,269 @@
|
|||
protocol CloudDirectoryMonitor: DirectoryMonitor {
|
||||
var fileManager: FileManager { get }
|
||||
var ubiquitousDocumentsDirectory: URL? { get }
|
||||
var delegate: CloudDirectoryMonitorDelegate? { get set }
|
||||
|
||||
func fetchUbiquityDirectoryUrl(completion: ((Result<URL, Error>) -> Void)?)
|
||||
func isCloudAvailable() -> Bool
|
||||
}
|
||||
|
||||
protocol CloudDirectoryMonitorDelegate : AnyObject {
|
||||
func didFinishGathering(contents: CloudContents)
|
||||
func didUpdate(contents: CloudContents)
|
||||
func didReceiveCloudMonitorError(_ error: Error)
|
||||
}
|
||||
|
||||
private let kUDCloudIdentityKey = "com.apple.organicmaps.UbiquityIdentityToken"
|
||||
private let kDocumentsDirectoryName = "Documents"
|
||||
|
||||
class iCloudDocumentsDirectoryMonitor: NSObject, CloudDirectoryMonitor {
|
||||
|
||||
static let sharedContainerIdentifier: String = {
|
||||
var identifier = "iCloud.app.organicmaps"
|
||||
#if DEBUG
|
||||
identifier.append(".debug")
|
||||
#endif
|
||||
return identifier
|
||||
}()
|
||||
|
||||
let containerIdentifier: String
|
||||
let fileManager: FileManager
|
||||
private let fileType: FileType // TODO: Should be removed when the nested directory support will be implemented
|
||||
private(set) var metadataQuery: NSMetadataQuery?
|
||||
private(set) var ubiquitousDocumentsDirectory: URL?
|
||||
|
||||
// MARK: - Public properties
|
||||
private(set) var state: DirectoryMonitorState = .stopped
|
||||
weak var delegate: CloudDirectoryMonitorDelegate?
|
||||
|
||||
init(fileManager: FileManager = .default, cloudContainerIdentifier: String = iCloudDocumentsDirectoryMonitor.sharedContainerIdentifier, fileType: FileType) {
|
||||
self.fileManager = fileManager
|
||||
self.containerIdentifier = cloudContainerIdentifier
|
||||
self.fileType = fileType
|
||||
super.init()
|
||||
|
||||
fetchUbiquityDirectoryUrl()
|
||||
subscribeOnMetadataQueryNotifications()
|
||||
subscribeOnCloudAvailabilityNotifications()
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
func start(completion: ((Result<URL, Error>) -> Void)? = nil) {
|
||||
guard isCloudAvailable() else {
|
||||
completion?(.failure(SynchronizationError.iCloudIsNotAvailable))
|
||||
return
|
||||
}
|
||||
fetchUbiquityDirectoryUrl { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
case .success(let url):
|
||||
LOG(.debug, "iCloudMonitor: Start")
|
||||
self.startQuery()
|
||||
self.state = .started
|
||||
completion?(.success(url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
guard state != .stopped else { return }
|
||||
LOG(.debug, "iCloudMonitor: Stop")
|
||||
stopQuery()
|
||||
state = .stopped
|
||||
}
|
||||
|
||||
func resume() {
|
||||
guard state != .started else { return }
|
||||
LOG(.debug, "iCloudMonitor: Resume")
|
||||
metadataQuery?.enableUpdates()
|
||||
state = .started
|
||||
}
|
||||
|
||||
func pause() {
|
||||
guard state != .paused else { return }
|
||||
LOG(.debug, "iCloudMonitor: Pause")
|
||||
metadataQuery?.disableUpdates()
|
||||
state = .paused
|
||||
}
|
||||
|
||||
func fetchUbiquityDirectoryUrl(completion: ((Result<URL, Error>) -> Void)? = nil) {
|
||||
if let ubiquitousDocumentsDirectory {
|
||||
completion?(.success(ubiquitousDocumentsDirectory))
|
||||
return
|
||||
}
|
||||
DispatchQueue.global().async {
|
||||
guard let containerUrl = self.fileManager.url(forUbiquityContainerIdentifier: self.containerIdentifier) else {
|
||||
LOG(.debug, "iCloudMonitor: Failed to retrieve container's URL for:\(self.containerIdentifier)")
|
||||
completion?(.failure(SynchronizationError.containerNotFound))
|
||||
return
|
||||
}
|
||||
let documentsContainerUrl = containerUrl.appendingPathComponent(kDocumentsDirectoryName)
|
||||
if !self.fileManager.fileExists(atPath: documentsContainerUrl.path) {
|
||||
LOG(.debug, "iCloudMonitor: Creating directory at path: \(documentsContainerUrl.path)")
|
||||
do {
|
||||
try self.fileManager.createDirectory(at: documentsContainerUrl, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
completion?(.failure(SynchronizationError.containerNotFound))
|
||||
}
|
||||
}
|
||||
LOG(.debug, "iCloudMonitor: Ubiquity directory URL: \(documentsContainerUrl)")
|
||||
self.ubiquitousDocumentsDirectory = documentsContainerUrl
|
||||
completion?(.success(documentsContainerUrl))
|
||||
}
|
||||
}
|
||||
|
||||
func isCloudAvailable() -> Bool {
|
||||
let cloudToken = fileManager.ubiquityIdentityToken
|
||||
guard let cloudToken else {
|
||||
UserDefaults.standard.removeObject(forKey: kUDCloudIdentityKey)
|
||||
LOG(.debug, "iCloudMonitor: Cloud is not available. Cloud token is nil.")
|
||||
return false
|
||||
}
|
||||
do {
|
||||
let data = try NSKeyedArchiver.archivedData(withRootObject: cloudToken, requiringSecureCoding: true)
|
||||
UserDefaults.standard.set(data, forKey: kUDCloudIdentityKey)
|
||||
return true
|
||||
} catch {
|
||||
UserDefaults.standard.removeObject(forKey: kUDCloudIdentityKey)
|
||||
LOG(.debug, "iCloudMonitor: Failed to archive cloud token: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class func buildMetadataQuery(for fileType: FileType) -> NSMetadataQuery {
|
||||
let metadataQuery = NSMetadataQuery()
|
||||
metadataQuery.notificationBatchingInterval = 1
|
||||
metadataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
|
||||
metadataQuery.predicate = NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*.\(fileType.fileExtension)")
|
||||
metadataQuery.sortDescriptors = [NSSortDescriptor(key: NSMetadataItemFSNameKey, ascending: true)]
|
||||
return metadataQuery
|
||||
}
|
||||
|
||||
class func getContentsFromNotification(_ notification: Notification, _ onError: (Error) -> Void) -> CloudContents {
|
||||
guard let metadataQuery = notification.object as? NSMetadataQuery,
|
||||
let metadataItems = metadataQuery.results as? [NSMetadataItem] else {
|
||||
return []
|
||||
}
|
||||
|
||||
let cloudMetadataItems = CloudContents(metadataItems.compactMap { item in
|
||||
do {
|
||||
return try CloudMetadataItem(metadataItem: item)
|
||||
} catch {
|
||||
onError(error)
|
||||
return nil
|
||||
}
|
||||
})
|
||||
return cloudMetadataItems
|
||||
}
|
||||
|
||||
// There are no ways to retrieve the content of iCloud's .Trash directory on the macOS because it uses different file system and place trashed content in the /Users/<user_name>/.Trash which cannot be observed without access.
|
||||
// When we get a new notification and retrieve the metadata from the object the actual list of items in iOS contains both current and deleted files (which is in .Trash/ directory now) but on macOS we only have absence of the file. So there are no way to get list of deleted items on macOS on didFinishGathering state.
|
||||
// Due to didUpdate state we can get the list of deleted items on macOS from the userInfo property but cannot get their new url.
|
||||
class func getTrashContentsFromNotification(_ notification: Notification, _ onError: (Error) -> Void) -> CloudContents {
|
||||
guard let removedItems = notification.userInfo?[NSMetadataQueryUpdateRemovedItemsKey] as? [NSMetadataItem] else { return [] }
|
||||
return CloudContents(removedItems.compactMap { metadataItem in
|
||||
do {
|
||||
var item = try CloudMetadataItem(metadataItem: metadataItem)
|
||||
// on macOS deleted file will not be in the ./Trash directory, but it doesn't mean that it is not removed because it is placed in the NSMetadataQueryUpdateRemovedItems array.
|
||||
item.isRemoved = true
|
||||
return item
|
||||
} catch {
|
||||
onError(error)
|
||||
return nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class func getTrashedContentsFromTrashDirectory(fileManager: FileManager, ubiquitousDocumentsDirectory: URL?, onError: (Error) -> Void) -> CloudContents {
|
||||
// There are no ways to retrieve the content of iCloud's .Trash directory on macOS.
|
||||
if #available(iOS 14.0, *), ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
return []
|
||||
}
|
||||
// On iOS we can get the list of deleted items from the .Trash directory but only when iCloud is enabled.
|
||||
guard let ubiquitousDocumentsDirectory,
|
||||
let trashDirectoryUrl = try? fileManager.trashDirectoryUrl(for: ubiquitousDocumentsDirectory),
|
||||
let removedItems = try? fileManager.contentsOfDirectory(at: trashDirectoryUrl,
|
||||
includingPropertiesForKeys: [.isDirectoryKey],
|
||||
options: [.skipsPackageDescendants, .skipsSubdirectoryDescendants]) else {
|
||||
return []
|
||||
}
|
||||
let removedCloudMetadataItems = CloudContents(removedItems.compactMap { url in
|
||||
do {
|
||||
var item = try CloudMetadataItem(fileUrl: url)
|
||||
item.isRemoved = true
|
||||
return item
|
||||
} catch {
|
||||
onError(error)
|
||||
return nil
|
||||
}
|
||||
})
|
||||
return removedCloudMetadataItems
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private extension iCloudDocumentsDirectoryMonitor {
|
||||
|
||||
func subscribeOnCloudAvailabilityNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(cloudAvailabilityChanged(_:)), name: .NSUbiquityIdentityDidChange, object: nil)
|
||||
}
|
||||
|
||||
// TODO: - Actually this notification was never called. If user disable the iCloud for the current app during the active state the app will be relaunched. Needs to investigate additional cases when this notification can be sent.
|
||||
@objc func cloudAvailabilityChanged(_ notification: Notification) {
|
||||
LOG(.debug, "iCloudMonitor: Cloud availability changed to : \(isCloudAvailable())")
|
||||
isCloudAvailable() ? startQuery() : stopQuery()
|
||||
}
|
||||
|
||||
// MARK: - MetadataQuery
|
||||
func subscribeOnMetadataQueryNotifications() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(queryDidFinishGathering(_:)), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(queryDidUpdate(_:)), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: nil)
|
||||
}
|
||||
|
||||
func startQuery() {
|
||||
metadataQuery = Self.buildMetadataQuery(for: fileType)
|
||||
guard let metadataQuery, !metadataQuery.isStarted else { return }
|
||||
LOG(.debug, "iCloudMonitor: Start metadata query")
|
||||
metadataQuery.start()
|
||||
}
|
||||
|
||||
func stopQuery() {
|
||||
LOG(.debug, "iCloudMonitor: Stop metadata query")
|
||||
metadataQuery?.stop()
|
||||
metadataQuery = nil
|
||||
}
|
||||
|
||||
@objc func queryDidFinishGathering(_ notification: Notification) {
|
||||
guard isCloudAvailable() else { return }
|
||||
metadataQuery?.disableUpdates()
|
||||
LOG(.debug, "iCloudMonitor: Query did finish gathering")
|
||||
let contents = Self.getContentsFromNotification(notification, metadataQueryErrorHandler)
|
||||
let trashedContents = Self.getTrashedContentsFromTrashDirectory(fileManager: fileManager,
|
||||
ubiquitousDocumentsDirectory: ubiquitousDocumentsDirectory,
|
||||
onError: metadataQueryErrorHandler)
|
||||
LOG(.debug, "iCloudMonitor: Cloud contents count: \(contents.count)")
|
||||
LOG(.debug, "iCloudMonitor: Trashed contents count: \(trashedContents.count)")
|
||||
delegate?.didFinishGathering(contents: contents + trashedContents)
|
||||
metadataQuery?.enableUpdates()
|
||||
}
|
||||
|
||||
@objc func queryDidUpdate(_ notification: Notification) {
|
||||
guard isCloudAvailable() else { return }
|
||||
metadataQuery?.disableUpdates()
|
||||
LOG(.debug, "iCloudMonitor: Query did update")
|
||||
let contents = Self.getContentsFromNotification(notification, metadataQueryErrorHandler)
|
||||
let trashedContents = Self.getTrashContentsFromNotification(notification, metadataQueryErrorHandler)
|
||||
LOG(.debug, "iCloudMonitor: Cloud contents count: \(contents.count)")
|
||||
LOG(.debug, "iCloudMonitor: Trashed contents count: \(trashedContents.count)")
|
||||
delegate?.didUpdate(contents: contents + trashedContents)
|
||||
metadataQuery?.enableUpdates()
|
||||
}
|
||||
|
||||
private var metadataQueryErrorHandler: (Error) -> Void {
|
||||
{ [weak self] error in
|
||||
self?.delegate?.didReceiveCloudMonitorError(error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "رابط القائمة";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "تمكين مزامنة iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "تعد مزامنة iCloud ميزة تجريبية قيد التطوير. تأكد من عمل نسخة احتياطية لجميع الإشارات المرجعية والمسارات الخاصة بك.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "تم تعطيل iCloud";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "يرجى تمكين iCloud في إعدادات جهازك لاستخدام هذه الميزة.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "يُمكَِن";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "دعم";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "خطأ: فشل المزامنة بسبب خطأ في الاتصال";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "خطأ: فشل في المزامنة بسبب تجاوز حصة iCloud النسبية";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "خطأ: iCloud غير متوفر";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menyu keçidi";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud Sinxronizasiyasını aktivləşdirin";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud sinxronizasiyası inkişaf mərhələsində olan eksperimental xüsusiyyətdir. Bütün əlfəcinlərinizin və treklərinizin ehtiyat nüsxəsini yaratdığınızdan əmin olun.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud Deaktivdir";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Bu funksiyadan istifadə etmək üçün cihazınızın parametrlərində iCloud-u aktiv edin.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Aktivləşdirin";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Yedəkləmə";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Xəta: Bağlantı xətası səbəbindən sinxronizasiya alınmadı";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Xəta: iCloud kvotasını keçdiyinə görə sinxronizasiya alınmadı";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Xəta: iCloud mövcud deyil";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Спасылка на меню";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Уключыць сінхранізацыю з iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Сінхранізацыя з iCloud - гэта эксперыментальная функцыя, якая знаходзіцца ў стадыі распрацоўкі. Пераканайцеся, што вы зрабілі рэзервовую копію ўсіх сваіх закладак і трэкаў.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud адключаны";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Каб выкарыстоўваць гэту функцыю, уключыце iCloud у наладах прылады.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Уключыць";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Рэзервовае капіраванне";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Памылка: не ўдалося сінхранізаваць з-за памылкі злучэння";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Памылка: не ўдалося сінхранізаваць з-за перавышэння квоты iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Памылка: iCloud недаступны";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Връзка към менюто";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Активиране на синхронизацията с iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Синхронизацията в iCloud е експериментална функция в процес на разработка. Уверете се, че сте направили резервно копие на всичките си отметки и песни.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud е деактивиран";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "За да използвате тази функция, разрешете iCloud в настройките на устройството си.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Активиране на";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Резервно копие";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Грешка: Не успя да се синхронизира поради грешка при свързването";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Грешка: Неуспешно синхронизиране поради превишена квота на iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Грешка: iCloud не е наличен";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Enllaç del menú";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Activa la sincronització d'iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "La sincronització d'iCloud és una característica experimental en desenvolupament. Assegureu-vos que heu fet una còpia de seguretat de tots els vostres marcadors i pistes.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud està desactivat";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Activa iCloud a la configuració del teu dispositiu per utilitzar aquesta funció.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Activa";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Còpia de seguretat";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Error: no s'ha pogut sincronitzar a causa d'un error de connexió";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Error: no s'ha pogut sincronitzar perquè s'ha superat la quota d'iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Error: iCloud no està disponible";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Odkaz na menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Povolení synchronizace s iCloudem";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Synchronizace iCloudu je experimentální funkce, která se vyvíjí. Ujistěte se, že jste si vytvořili zálohu všech svých záložek a skladeb.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "Služba iCloud je vypnutá";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Chcete-li tuto funkci používat, povolte v nastavení zařízení službu iCloud.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Povolit";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Záloha";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Chyba: Nepodařilo se synchronizovat z důvodu chyby připojení";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Chyba: Nepodařilo se synchronizovat z důvodu překročení kvóty iCloudu";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Chyba: iCloud není k dispozici";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Link til menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Aktivér iCloud-synkronisering";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud-synkronisering er en eksperimentel funktion under udvikling. Sørg for, at du har lavet en backup af alle dine bogmærker og spor.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud er deaktiveret";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Aktivér iCloud i din enheds indstillinger for at bruge denne funktion.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Aktivér";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Sikkerhedskopiering";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Fejl: Kunne ikke synkronisere på grund af forbindelsesfejl";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Fejl: Kunne ikke synkronisere på grund af overskredet iCloud-kvote";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Fejl: iCloud er ikke tilgængelig";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Link zur Speisekarte";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Aktiviere die iCloud-Syncronisierung";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Die iCloud-Synchronisierung ist eine experimentelle Funktion, die noch entwickelt wird. Vergewissere dich, dass du eine Sicherungskopie all deiner Lesezeichen und Titel erstellt hast.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud ist deaktiviert";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Bitte aktiviere iCloud in den Einstellungen deines Geräts, um diese Funktion zu nutzen.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Aktiviere";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Sicherung";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Fehler: Synchronisierung aufgrund eines Verbindungsfehlers fehlgeschlagen";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Fehler: Synchronisierung fehlgeschlagen, da iCloud-Kontingent überschritten";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Fehler: iCloud ist nicht verfügbar";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Σύνδεσμος μενού";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Ενεργοποίηση συγχρονισμού iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Ο συγχρονισμός iCloud είναι μια πειραματική λειτουργία υπό ανάπτυξη. Βεβαιωθείτε ότι έχετε δημιουργήσει αντίγραφο ασφαλείας όλων των σελιδοδεικτών και των κομματιών σας.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "Το iCloud είναι απενεργοποιημένο";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Ενεργοποιήστε το iCloud στις ρυθμίσεις της συσκευής σας για να χρησιμοποιήσετε αυτή τη λειτουργία.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Ενεργοποίηση";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Δημιουργία αντιγράφων ασφαλείας";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Σφάλμα: Αποτυχία συγχρονισμού λόγω σφάλματος σύνδεσης";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Σφάλμα: iCloud: Απέτυχε ο συγχρονισμός λόγω υπέρβασης της ποσόστωσης iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Σφάλμα: Το iCloud δεν είναι διαθέσιμο";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menu Link";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Enable iCloud Syncronization";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud synchronization is an experimental feature under development. Make sure that you have made a backup of all your bookmarks and tracks.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud is Disabled";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Please enable iCloud in your device's settings to use this feature.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Enable";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Backup";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Error: Failed to synchronize due to connection error";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Error: Failed to synchronize due to iCloud quota exceeded";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Error: iCloud is not available";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menu Link";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Enable iCloud Syncronization";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud synchronization is an experimental feature under development. Make sure that you have made a backup of all your bookmarks and tracks.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud is Disabled";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Please enable iCloud in your device's settings to use this feature.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Enable";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Backup";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Error: Failed to synchronize due to connection error";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Error: Failed to synchronize due to iCloud quota exceeded";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Error: iCloud is not available";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Enlace al menú";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Habilitar la sincronización de iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "La sincronización de iCloud es una función experimental en desarrollo. Asegúrese de haber realizado una copia de seguridad de todos sus marcadores y pistas.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud está desactivado";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Activa iCloud en los ajustes de tu dispositivo para utilizar esta función.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Activa";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Respaldo";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Error: No se ha podido sincronizar debido a un error de conexión";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Error: No se ha podido sincronizar porque se ha superado la cuota de iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Error: iCloud no está disponible";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Enlace al menú";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Activar la sincronización con iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "La sincronización con iCloud es una función experimental en desarrollo. Asegúrate de haber hecho una copia de seguridad de todos tus marcadores y pistas.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud está desactivado";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Activa iCloud en los ajustes de tu dispositivo para utilizar esta función.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Activa";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Copia de seguridad";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Error: No se ha podido sincronizar debido a un error de conexión";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Error: No se ha podido sincronizar porque se ha superado la cuota de iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Error: iCloud no está disponible";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menüü link";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Lubage iCloudi sünkroniseerimine";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloudi sünkroniseerimine on eksperimentaalne funktsioon, mis on arendamisel. Veenduge, et olete teinud varukoopia kõigist oma järjehoidjatest ja lugudest.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud on välja lülitatud";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Selle funktsiooni kasutamiseks lubage seadme seadetes iCloud.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Lubage";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Varukoopia";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Viga: Sünkroniseerimine ebaõnnestus ühenduse vea tõttu.";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Viga: iCloudi kvoodi ületamise tõttu ei õnnestunud sünkroniseerimine.";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Viga: iCloud ei ole saadaval";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menuaren esteka";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Gaitu iCloud sinkronizazioa";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud sinkronizazioa garatzen ari den ezaugarri esperimental bat da. Ziurtatu zure laster-marka eta ibilbide guztien babeskopia egin duzula.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud desgaituta dago";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Mesedez, gaitu iCloud zure gailuaren ezarpenetan eginbide hau erabiltzeko.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Gaitu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Babeskopia";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Errorea: Ezin izan da sinkronizatu konexio-errore baten ondorioz";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Errorea: Ezin izan da sinkronizatu iCloud kuota gainditu delako";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Errorea: iCloud ez dago erabilgarri";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "لینک منو";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "همگام سازی iCloud را فعال کنید";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "همگام سازی iCloud یک ویژگی آزمایشی در دست توسعه است. مطمئن شوید که از تمام نشانکu200cها و آهنگu200cهای خود یک نسخه پشتیبان تهیه کردهu200cاید.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud غیرفعال است";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "لطفاً iCloud را در تنظیمات دستگاه خود فعال کنید تا از این ویژگی استفاده کنید.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "فعال کردن";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "پشتیبان گیری";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "خطا: به دلیل خطای اتصال همگام سازی نشد";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "خطا: به دلیل فراتر رفتن از سهمیه iCloud، همگام سازی انجام نشد";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "خطا: iCloud در دسترس نیست";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Valikkolinkki";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Ota iCloud-synkronointi käyttöön";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud-synkronointi on kehitteillä oleva kokeellinen ominaisuus. Varmista, että olet tehnyt varmuuskopion kaikista kirjanmerkeistäsi ja kappaleistasi.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud on poistettu käytöstä";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Ota iCloud käyttöön laitteesi asetuksissa, jotta voit käyttää tätä ominaisuutta.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Ota käyttöön";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Varmuuskopiointi";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Virhe: Synkronointi epäonnistui yhteysvirheen vuoksi.";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Virhe: iCloud-kiintiön ylittymisen vuoksi synkronointi epäonnistui.";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Virhe: iCloud ei ole käytettävissä";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Lien vers le menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Activer la synchronisation iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "La synchronisation iCloud est une fonctionnalité expérimentale en cours de développement. Assure-toi d'avoir fait une sauvegarde de tous tes signets et pistes.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud est désactivé";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Active iCloud dans les paramètres de ton appareil pour utiliser cette fonctionnalité.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Activer";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Sauvegarde";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Erreur : La synchronisation a échoué en raison d'une erreur de connexion";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Erreur : Échec de la synchronisation en raison du dépassement du quota iCloud.";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Erreur : iCloud n'est pas disponible";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "קישור לתפריט";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "הפעל סנכרון iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "סנכרון iCloud הוא תכונה ניסיונית בפיתוח. ודא שעשית גיבוי של כל הסימניות והרצועות שלך.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud מושבת";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "אנא הפעל את iCloud בהגדרות המכשיר שלך כדי להשתמש בתכונה זו.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "לְאַפשֵׁר";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "גיבוי";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "שגיאה: הסנכרון נכשל עקב שגיאת חיבור";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "שגיאה: הסנכרון נכשל עקב חריגה ממכסת iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "שגיאה: iCloud אינו זמין";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "मेनू लिंक";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud सिंक्रोनाइज़ेशन सक्षम करें";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud सिंक्रोनाइज़ेशन विकासाधीन एक प्रायोगिक सुविधा है। सुनिश्चित करें कि आपने अपने सभी बुकमार्क और ट्रैक का बैकअप बना लिया है।";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud अक्षम है";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "कृपया इस सुविधा का उपयोग करने के लिए अपने डिवाइस की सेटिंग में iCloud सक्षम करें।";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "सक्षम";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "बैकअप";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "त्रुटि: कनेक्शन त्रुटि के कारण सिंक्रनाइज़ करने में विफल";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "त्रुटि: iCloud कोटा पार हो जाने के कारण सिंक्रनाइज़ करने में विफल";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "त्रुटि: iCloud उपलब्ध नहीं है";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menü link";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud-szinkronizálás engedélyezése";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Az iCloud-szinkronizálás egy fejlesztés alatt álló kísérleti funkció. Győződjön meg róla, hogy készített biztonsági másolatot az összes könyvjelzőjéről és zeneszámáról.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "Az iCloud letiltva";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "A funkció használatához engedélyezze az iCloudot a készülék beállításaiban.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Engedélyezze a";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Biztonsági mentés";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Hiba: A szinkronizálás sikertelen volt a kapcsolat hibája miatt.";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Hiba: iCloud-kvóta túllépése miatt nem sikerült szinkronizálni.";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Hiba: Az iCloud nem elérhető";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Tautan menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Mengaktifkan Sinkronisasi iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Sinkronisasi iCloud adalah fitur eksperimental yang sedang dikembangkan. Pastikan Anda telah membuat cadangan semua penanda dan lagu Anda.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud Dinonaktifkan";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Aktifkan iCloud di pengaturan perangkat Anda untuk menggunakan fitur ini.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Aktifkan";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Cadangan";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Kesalahan: Gagal menyinkronkan karena kesalahan koneksi";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Kesalahan: Gagal menyinkronkan karena kuota iCloud terlampaui";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Kesalahan: iCloud tidak tersedia";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Link al menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Abilita la sincronizzazione con iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "La sincronizzazione con iCloud è una funzione sperimentale in fase di sviluppo. Assicurati di aver fatto un backup di tutti i tuoi segnalibri e tracce.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud è disabilitato";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Per utilizzare questa funzione, attiva iCloud nelle impostazioni del tuo dispositivo.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Abilitazione";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Backup";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Errore: Impossibile sincronizzare a causa di un errore di connessione";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Errore: Impossibile sincronizzare a causa del superamento della quota iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Errore: iCloud non è disponibile";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "メニューリンク";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud同期を有効にする";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud同期は開発中の実験的機能である。すべてのブックマークとトラックのバックアップをとっておくこと。";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloudが無効になっている";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "この機能を使うには、デバイスの設定でiCloudを有効にしてほしい。";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "有効にする";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "バックアップ";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "エラー:接続エラーにより同期に失敗した";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "エラー:iCloudのクォータが超過したため、同期に失敗しました。";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "エラー:iCloudが利用できない";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "메뉴 링크";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud 동기화 활성화";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud 동기화는 개발 중인 실험적 기능입니다. 모든 북마크와 트랙을 백업해 두었는지 확인하세요.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud가 비활성화됨";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "이 기능을 사용하려면 기기 설정에서 iCloud를 활성화하세요.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "사용";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "백업";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "오류입니다: 연결 오류로 인해 동기화하지 못했습니다.";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "오류입니다: iCloud 할당량이 초과되어 동기화하지 못했습니다.";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "오류: iCloud를 사용할 수 없습니다.";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "मेनू लिंक";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud सिंक्रोनाइझेशन सक्षम करा";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud सिंक्रोनाइझेशन हे विकासाधीन प्रायोगिक वैशिष्ट्य आहे. तुम्ही तुमच्या सर्व बुकमार्क्स आणि ट्रॅकचा बॅकअप घेतला असल्याची खात्री करा.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud अक्षम आहे";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "कृपया हे वैशिष्ट्य वापरण्यासाठी तुमच्या डिव्हाइसच्या सेटिंग्जमध्ये iCloud सक्षम करा.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "सक्षम करा";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "बॅकअप";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "त्रुटी: कनेक्शन त्रुटीमुळे सिंक्रोनाइझ करण्यात अयशस्वी";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "त्रुटी: iCloud कोटा ओलांडल्यामुळे सिंक्रोनाइझ करण्यात अयशस्वी";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "त्रुटी: iCloud उपलब्ध नाही";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Lenke til menyen";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Aktiver iCloud-synkronisering";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud-synkronisering er en eksperimentell funksjon under utvikling. Sørg for at du har tatt en sikkerhetskopi av alle bokmerkene og sporene dine.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud er deaktivert";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Aktiver iCloud i enhetens innstillinger for å bruke denne funksjonen.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Aktivere";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Sikkerhetskopiering";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Feil: Kunne ikke synkronisere på grunn av tilkoblingsfeil";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Feil: Kunne ikke synkronisere på grunn av overskredet iCloud-kvote";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Feil: iCloud er ikke tilgjengelig";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menulink";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud-synchronisatie inschakelen";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud synchronisatie is een experimentele functie in ontwikkeling. Zorg ervoor dat je een back-up hebt gemaakt van al je bladwijzers en tracks.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud is uitgeschakeld";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Schakel iCloud in bij de instellingen van je apparaat om deze functie te gebruiken.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "inschakelen";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Back-up";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Fout: Synchronisatie mislukt door verbindingsfout";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Fout: Synchronisatie mislukt vanwege overschrijding iCloud-quota";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Fout: iCloud is niet beschikbaar";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Link do menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Włącz synchronizację iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Synchronizacja iCloud jest eksperymentalną funkcją w fazie rozwoju. Upewnij się, że wykonałeś kopię zapasową wszystkich zakładek i utworów.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud jest wyłączony";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Aby korzystać z tej funkcji, włącz usługę iCloud w ustawieniach urządzenia.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Włącz";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Kopia zapasowa";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Błąd: Nie udało się zsynchronizować z powodu błędu połączenia";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Błąd: Nie udało się zsynchronizować z powodu przekroczenia limitu iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Błąd: usługa iCloud jest niedostępna";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Link do cardápio";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Ativar a sincronização do iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "A sincronização do iCloud é um recurso experimental em desenvolvimento. Certifique-se de que você tenha feito um backup de todos os seus favoritos e faixas.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "O iCloud está desativado";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Ative o iCloud nas configurações do seu dispositivo para usar esse recurso.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Ativar";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Cópia de segurança";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Erro: Falha na sincronização devido a erro de conexão";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Erro: Falha ao sincronizar devido à cota do iCloud excedida";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Erro: o iCloud não está disponível";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Link do menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Ativar a sincronização do iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "A sincronização com o iCloud é uma funcionalidade experimental em desenvolvimento. Certifica-te de que fizeste uma cópia de segurança de todos os teus marcadores e faixas.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "O iCloud está desativado";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Para utilizar esta funcionalidade, ativa o iCloud nas definições do teu dispositivo.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Ativar";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Cópia de segurança";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Erro: Falha na sincronização devido a um erro de ligação";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Erro: Falha na sincronização devido à quota do iCloud ter sido excedida";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Erro: o iCloud não está disponível";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Legătura de meniu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Activați Sincronizarea iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Sincronizarea iCloud este o funcție experimentală aflată în curs de dezvoltare. Asigurați-vă că ați făcut o copie de rezervă a tuturor marcajelor și pieselor dvs.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud este dezactivat";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Vă rugăm să activați iCloud în setările dispozitivului dvs. pentru a utiliza această funcție.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Activați";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Backup";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Eroare: Nu s-a reușit sincronizarea din cauza unei erori de conexiune";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Eroare: Nu s-a reușit sincronizarea din cauza depășirii cotei iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Eroare: iCloud nu este disponibil";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Ссылка на меню";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Включить синхронизацию с iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Синхронизация с iCloud - это экспериментальная функция, которая находится в стадии разработки. Убедитесь, что вы сделали резервную копию всех своих закладок и треков.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud отключен";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Чтобы воспользоваться этой функцией, включите iCloud в настройках своего устройства.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Включить";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Резервная копия";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Ошибка: Не удалось выполнить синхронизацию из-за ошибки подключения";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Ошибка: Не удалось выполнить синхронизацию из-за превышения квоты iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Ошибка: iCloud недоступен";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Odkaz na menu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Povolenie synchronizácie iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Synchronizácia s iCloudom je experimentálna funkcia, ktorá je vo vývoji. Uistite sa, že ste si vytvorili zálohu všetkých svojich záložiek a skladieb.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "Služba iCloud je vypnutá";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Ak chcete túto funkciu používať, povoľte iCloud v nastaveniach zariadenia.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Povolenie stránky";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Zálohovanie";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Chyba: Nepodarilo sa synchronizovať z dôvodu chyby pripojenia";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Chyba: Nepodarilo sa synchronizovať z dôvodu prekročenia kvóty iCloudu";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Chyba: iCloud nie je k dispozícii";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Länk till meny";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Aktivera iCloud-synkronisering";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud-synkronisering är en experimentell funktion under utveckling. Se till att du har gjort en säkerhetskopia av alla dina bokmärken och spår.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud är inaktiverat";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Aktivera iCloud i enhetens inställningar för att kunna använda denna funktion.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Aktivera";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Säkerhetskopiering";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Fel: Synkronisering misslyckades på grund av anslutningsfel";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Fel: Synkronisering misslyckades på grund av att iCloud-kvoten överskreds";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Fel: iCloud är inte tillgängligt";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Kiungo cha menyu";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Washa Usawazishaji wa iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Usawazishaji wa iCloud ni kipengele cha majaribio kinachoendelezwa. Hakikisha kuwa umefanya nakala rudufu ya alamisho na nyimbo zako zote.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud Imezimwa";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Tafadhali wezesha iCloud katika mipangilio ya kifaa chako ili kutumia kipengele hiki.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Wezesha";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Hifadhi nakala";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Hitilafu: Imeshindwa kusawazisha kwa sababu ya hitilafu ya muunganisho";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Hitilafu: Imeshindwa kusawazisha kwa sababu ya mgao wa iCloud uliozidishwa";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Hitilafu: iCloud haipatikani";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "ลิงค์เมนู";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "เปิดใช้งานการซิงโครไนซ์ iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "การซิงโครไนซ์ iCloud เป็นคุณสมบัติทดลองที่อยู่ระหว่างการพัฒนา ตรวจสอบให้แน่ใจว่าคุณได้สำรองข้อมูลบุ๊กมาร์กและแทร็กทั้งหมดของคุณแล้ว";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud ถูกปิดใช้งาน";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "โปรดเปิดใช้งาน iCloud ในการตั้งค่าอุปกรณ์ของคุณเพื่อใช้คุณสมบัตินี้";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "เปิดใช้งาน";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "สำรองข้อมูล";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "ข้อผิดพลาด: ไม่สามารถซิงโครไนซ์ได้เนื่องจากข้อผิดพลาดในการเชื่อมต่อ";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "ข้อผิดพลาด: ไม่สามารถซิงโครไนซ์ได้เนื่องจากเกินโควต้า iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "ข้อผิดพลาด: iCloud ไม่พร้อมใช้งาน";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Menü bağlantısı";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "iCloud Senkronizasyonunu Etkinleştir";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud senkronizasyonu geliştirme aşamasında olan deneysel bir özelliktir. Tüm yer imlerinizin ve parçalarınızın yedeğini aldığınızdan emin olun.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud Devre Dışı";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Bu özelliği kullanmak için lütfen cihazınızın ayarlarında iCloud'u etkinleştirin.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Etkinleştir";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Yedekleme";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Hata oluştu: Bağlantı hatası nedeniyle senkronizasyon başarısız oldu";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Hata: iCloud kotası aşıldığı için senkronize edilemedi";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Hata: iCloud kullanılamıyor";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Посилання на меню";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Увімкнути синхронізацію з iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Синхронізація з iCloud - це експериментальна функція, яка перебуває на стадії розробки. Переконайтеся, що ви зробили резервну копію всіх ваших закладок і треків.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud вимкнено";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Щоб скористатися цією функцією, увімкніть iCloud у налаштуваннях вашого пристрою.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Увімкнути";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Резервне копіювання";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Помилка: Не вдалося синхронізувати через помилку з'єднання";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Помилка: Не вдалося синхронізувати через перевищення квоти iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Помилка: iCloud недоступний";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "Liên kết thực đơn";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "Kích hoạt đồng bộ hóa iCloud";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "Đồng bộ hóa iCloud là một tính năng thử nghiệm đang được phát triển. Đảm bảo rằng bạn đã tạo bản sao lưu tất cả dấu trang và bản nhạc của mình.";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud bị vô hiệu hóa";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "Vui lòng bật iCloud trong cài đặt thiết bị của bạn để sử dụng tính năng này.";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "Cho phép";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "Hỗ trợ";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "Lỗi: Không đồng bộ được do lỗi kết nối";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "Lỗi: Không thể đồng bộ hóa do vượt quá hạn ngạch iCloud";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "Lỗi: iCloud không khả dụng";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "菜单链接";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "启用 iCloud 同步";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud 同步是一项正在开发的实验性功能。请确保备份了所有书签和曲目。";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud 已禁用";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "请在设备设置中启用 iCloud,以使用此功能。";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "启用";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "备份";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "错误:由于连接错误,同步失败";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "错误:由于超过 iCloud 配额,同步失败";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "错误:iCloud 不可用";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -1298,6 +1298,33 @@
|
|||
/* Restaurant or other food place's menu URL field in the Editor */
|
||||
"website_menu" = "選單連結";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_title" = "啟用 iCloud 同步";
|
||||
|
||||
/* Message for the "Enable iCloud Syncronization" alert. */
|
||||
"enable_icloud_synchronization_message" = "iCloud 同步是一項正在開發中的實驗性功能。確保您已備份所有書籤和曲目。";
|
||||
|
||||
/* Title for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_title" = "iCloud 已停用";
|
||||
|
||||
/* Message for the "iCloud is Disabled" alert. */
|
||||
"icloud_disabled_message" = "請在您的裝置設定中啟用 iCloud 以使用此功能。";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Enable" action button. */
|
||||
"enable" = "使能夠";
|
||||
|
||||
/* Title for the "Enable iCloud Syncronization" alert's "Backup" action button. */
|
||||
"backup" = "备份";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to connection error */
|
||||
"icloud_synchronization_error_connection_error" = "錯誤:由於連線錯誤而無法同步";
|
||||
|
||||
/* iCloud error message: Failed to synchronize due to iCloud quota exceeded */
|
||||
"icloud_synchronization_error_quota_exceeded" = "錯誤:由於超出 iCloud 配額而無法同步";
|
||||
|
||||
/* iCloud error message: iCloud is not available */
|
||||
"icloud_synchronization_error_cloud_is_unavailable" = "錯誤:iCloud 不可用";
|
||||
|
||||
|
||||
/********** Types **********/
|
||||
|
||||
|
|
|
@ -471,8 +471,15 @@
|
|||
ED1263AB2B6F99F900AD99F3 /* UIView+AddSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1263AA2B6F99F900AD99F3 /* UIView+AddSeparator.swift */; };
|
||||
ED1ADA332BC6B1B40029209F /* CarPlayServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1ADA322BC6B1B40029209F /* CarPlayServiceTests.swift */; };
|
||||
ED3EAC202B03C88100220A4A /* BottomTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */; };
|
||||
ED63CEB92BDF8F9D006155C4 /* SettingsTableViewiCloudSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED63CEB62BDF8F9C006155C4 /* SettingsTableViewiCloudSwitchCell.swift */; };
|
||||
ED79A5AB2BD7AA9C00952D1F /* LoadingOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5AA2BD7AA9C00952D1F /* LoadingOverlayViewController.swift */; };
|
||||
ED79A5AD2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5AC2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift */; };
|
||||
ED79A5D32BDF8D6100952D1F /* CloudStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CB2BDF8D6100952D1F /* CloudStorageManager.swift */; };
|
||||
ED79A5D42BDF8D6100952D1F /* MetadataItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CC2BDF8D6100952D1F /* MetadataItem.swift */; };
|
||||
ED79A5D52BDF8D6100952D1F /* SynchronizationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CD2BDF8D6100952D1F /* SynchronizationError.swift */; };
|
||||
ED79A5D62BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CE2BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift */; };
|
||||
ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */; };
|
||||
ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */; };
|
||||
ED9966802B94FBC20083CE55 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED99667D2B94FBC20083CE55 /* ColorPicker.swift */; };
|
||||
EDBD68072B625724005DD151 /* LocationServicesDisabledAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */; };
|
||||
EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */; };
|
||||
|
@ -480,6 +487,14 @@
|
|||
EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243D52B6CF3980057369B /* AboutController.swift */; };
|
||||
EDE243E52B6D3F400057369B /* OSMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E42B6D3F400057369B /* OSMView.swift */; };
|
||||
EDE243E72B6D55610057369B /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E02B6D3EA00057369B /* InfoView.swift */; };
|
||||
EDF838842C00B640007E4E67 /* SynchronizationFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838812C00B640007E4E67 /* SynchronizationFileWriter.swift */; };
|
||||
EDF838BE2C00B9D0007E4E67 /* LocalDirectoryMonitorDelegateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838AD2C00B9C7007E4E67 /* LocalDirectoryMonitorDelegateMock.swift */; };
|
||||
EDF838BF2C00B9D0007E4E67 /* SynchronizationStateManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838AF2C00B9C7007E4E67 /* SynchronizationStateManagerTests.swift */; };
|
||||
EDF838C02C00B9D0007E4E67 /* DefaultLocalDirectoryMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838AE2C00B9C7007E4E67 /* DefaultLocalDirectoryMonitorTests.swift */; };
|
||||
EDF838C12C00B9D6007E4E67 /* iCloudDirectoryMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838B22C00B9C7007E4E67 /* iCloudDirectoryMonitorTests.swift */; };
|
||||
EDF838C22C00B9D6007E4E67 /* MetadataItemStubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838B42C00B9C7007E4E67 /* MetadataItemStubs.swift */; };
|
||||
EDF838C32C00B9D6007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838B12C00B9C7007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift */; };
|
||||
EDF838C42C00B9D6007E4E67 /* FileManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF838B32C00B9C7007E4E67 /* FileManagerMock.swift */; };
|
||||
EDFDFB462B7139490013A44C /* AboutInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB452B7139490013A44C /* AboutInfo.swift */; };
|
||||
EDFDFB482B7139670013A44C /* SocialMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB472B7139670013A44C /* SocialMedia.swift */; };
|
||||
EDFDFB4A2B722A310013A44C /* SocialMediaCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB492B722A310013A44C /* SocialMediaCollectionViewCell.swift */; };
|
||||
|
@ -1364,8 +1379,15 @@
|
|||
ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomTabBarButton.swift; sourceTree = "<group>"; };
|
||||
ED48BBB817C2B1E2003E7E92 /* CircleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleView.h; sourceTree = "<group>"; };
|
||||
ED48BBB917C2B1E2003E7E92 /* CircleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleView.m; sourceTree = "<group>"; };
|
||||
ED63CEB62BDF8F9C006155C4 /* SettingsTableViewiCloudSwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewiCloudSwitchCell.swift; sourceTree = "<group>"; };
|
||||
ED79A5AA2BD7AA9C00952D1F /* LoadingOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingOverlayViewController.swift; sourceTree = "<group>"; };
|
||||
ED79A5AC2BD7BA0F00952D1F /* UIApplication+LoadingOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+LoadingOverlay.swift"; sourceTree = "<group>"; };
|
||||
ED79A5CB2BDF8D6100952D1F /* CloudStorageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudStorageManager.swift; sourceTree = "<group>"; };
|
||||
ED79A5CC2BDF8D6100952D1F /* MetadataItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataItem.swift; sourceTree = "<group>"; };
|
||||
ED79A5CD2BDF8D6100952D1F /* SynchronizationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationError.swift; sourceTree = "<group>"; };
|
||||
ED79A5CE2BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iCloudDocumentsDirectoryMonitor.swift; sourceTree = "<group>"; };
|
||||
ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationStateManager.swift; sourceTree = "<group>"; };
|
||||
ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLocalDirectoryMonitor.swift; sourceTree = "<group>"; };
|
||||
ED99667D2B94FBC20083CE55 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = "<group>"; };
|
||||
EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = "<group>"; };
|
||||
EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = "<group>"; };
|
||||
|
@ -1373,6 +1395,14 @@
|
|||
EDE243D52B6CF3980057369B /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
|
||||
EDE243E02B6D3EA00057369B /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = "<group>"; };
|
||||
EDE243E42B6D3F400057369B /* OSMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMView.swift; sourceTree = "<group>"; };
|
||||
EDF838812C00B640007E4E67 /* SynchronizationFileWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationFileWriter.swift; sourceTree = "<group>"; };
|
||||
EDF838AD2C00B9C7007E4E67 /* LocalDirectoryMonitorDelegateMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDirectoryMonitorDelegateMock.swift; sourceTree = "<group>"; };
|
||||
EDF838AE2C00B9C7007E4E67 /* DefaultLocalDirectoryMonitorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultLocalDirectoryMonitorTests.swift; sourceTree = "<group>"; };
|
||||
EDF838AF2C00B9C7007E4E67 /* SynchronizationStateManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizationStateManagerTests.swift; sourceTree = "<group>"; };
|
||||
EDF838B12C00B9C7007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UbiquitousDirectoryMonitorDelegateMock.swift; sourceTree = "<group>"; };
|
||||
EDF838B22C00B9C7007E4E67 /* iCloudDirectoryMonitorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iCloudDirectoryMonitorTests.swift; sourceTree = "<group>"; };
|
||||
EDF838B32C00B9C7007E4E67 /* FileManagerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManagerMock.swift; sourceTree = "<group>"; };
|
||||
EDF838B42C00B9C7007E4E67 /* MetadataItemStubs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataItemStubs.swift; sourceTree = "<group>"; };
|
||||
EDFDFB452B7139490013A44C /* AboutInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInfo.swift; sourceTree = "<group>"; };
|
||||
EDFDFB472B7139670013A44C /* SocialMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMedia.swift; sourceTree = "<group>"; };
|
||||
EDFDFB492B722A310013A44C /* SocialMediaCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMediaCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -1912,6 +1942,7 @@
|
|||
340475281E081A4600C92850 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED79A5CA2BDF8D6100952D1F /* iCloud */,
|
||||
994AEBE323AB763C0079B81F /* Theme */,
|
||||
993F54F6237C622700545511 /* DeepLink */,
|
||||
CDCA27822245090900167D87 /* EventListening */,
|
||||
|
@ -2616,6 +2647,7 @@
|
|||
4B4153B62BF9709100EE4B02 /* Core */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDF838AB2C00B9C7007E4E67 /* iCloudTests */,
|
||||
4B4153B72BF970A000EE4B02 /* TextToSpeech */,
|
||||
);
|
||||
path = Core;
|
||||
|
@ -3025,6 +3057,20 @@
|
|||
path = LoadingOverlay;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED79A5CA2BDF8D6100952D1F /* iCloud */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED79A5CB2BDF8D6100952D1F /* CloudStorageManager.swift */,
|
||||
EDF838812C00B640007E4E67 /* SynchronizationFileWriter.swift */,
|
||||
ED79A5CF2BDF8D6100952D1F /* SynchronizationStateManager.swift */,
|
||||
ED79A5CE2BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift */,
|
||||
ED79A5D02BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift */,
|
||||
ED79A5CC2BDF8D6100952D1F /* MetadataItem.swift */,
|
||||
ED79A5CD2BDF8D6100952D1F /* SynchronizationError.swift */,
|
||||
);
|
||||
path = iCloud;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED99667C2B94FBC20083CE55 /* ColorPicker */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3033,6 +3079,36 @@
|
|||
path = ColorPicker;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDF838AB2C00B9C7007E4E67 /* iCloudTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDF838AC2C00B9C7007E4E67 /* DefaultLocalDirectoryMonitorTests */,
|
||||
EDF838AF2C00B9C7007E4E67 /* SynchronizationStateManagerTests.swift */,
|
||||
EDF838B02C00B9C7007E4E67 /* iCloudDirectoryMonitorTests */,
|
||||
EDF838B42C00B9C7007E4E67 /* MetadataItemStubs.swift */,
|
||||
);
|
||||
path = iCloudTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDF838AC2C00B9C7007E4E67 /* DefaultLocalDirectoryMonitorTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDF838AD2C00B9C7007E4E67 /* LocalDirectoryMonitorDelegateMock.swift */,
|
||||
EDF838AE2C00B9C7007E4E67 /* DefaultLocalDirectoryMonitorTests.swift */,
|
||||
);
|
||||
path = DefaultLocalDirectoryMonitorTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDF838B02C00B9C7007E4E67 /* iCloudDirectoryMonitorTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EDF838B12C00B9C7007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift */,
|
||||
EDF838B22C00B9C7007E4E67 /* iCloudDirectoryMonitorTests.swift */,
|
||||
EDF838B32C00B9C7007E4E67 /* FileManagerMock.swift */,
|
||||
);
|
||||
path = iCloudDirectoryMonitorTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EDFDFB412B7108090013A44C /* AboutController */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3628,6 +3704,7 @@
|
|||
F6E2FD371E097BA00083EBEC /* Cells */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED63CEB62BDF8F9C006155C4 /* SettingsTableViewiCloudSwitchCell.swift */,
|
||||
F6E2FD381E097BA00083EBEC /* SettingsTableViewLinkCell.swift */,
|
||||
F6E2FD391E097BA00083EBEC /* SettingsTableViewSelectableCell.swift */,
|
||||
F6E2FD3A1E097BA00083EBEC /* SettingsTableViewSwitchCell.swift */,
|
||||
|
@ -4037,7 +4114,7 @@
|
|||
6741A9981BF340DE002C974C /* resources-xhdpi_light in Resources */,
|
||||
6741A9611BF340DE002C974C /* resources-xhdpi_dark in Resources */,
|
||||
6741A94D1BF340DE002C974C /* resources-xxhdpi_light in Resources */,
|
||||
3404F49E2028A2430090E401 /* BMCActionsCreateCell.xib in Resources */,
|
||||
3404F49E2028A2430090E401 /* BMCActionsCell.xib in Resources */,
|
||||
6741A9551BF340DE002C974C /* resources-xxhdpi_dark in Resources */,
|
||||
340E1EF51E2F614400CE49BF /* SearchFilters.storyboard in Resources */,
|
||||
340E1EF81E2F614400CE49BF /* Settings.storyboard in Resources */,
|
||||
|
@ -4174,6 +4251,7 @@
|
|||
F6E2FF631E097BA00083EBEC /* MWMTTSLanguageViewController.mm in Sources */,
|
||||
4715273524907F8200E91BBA /* BookmarkColorViewController.swift in Sources */,
|
||||
47E3C7292111E614008B3B27 /* FadeInAnimatedTransitioning.swift in Sources */,
|
||||
ED79A5D42BDF8D6100952D1F /* MetadataItem.swift in Sources */,
|
||||
34AB667D1FC5AA330078E451 /* MWMRoutePreview.mm in Sources */,
|
||||
993DF11B23F6BDB100AC231A /* UIViewRenderer.swift in Sources */,
|
||||
99C964302428C27A00E41723 /* PlacePageHeaderView.swift in Sources */,
|
||||
|
@ -4199,9 +4277,11 @@
|
|||
3454D7D41E07F045004AF2AD /* UIImageView+Coloring.m in Sources */,
|
||||
993DF11D23F6BDB100AC231A /* UIToolbarRenderer.swift in Sources */,
|
||||
99A906E923F6F7030005872B /* WikiDescriptionViewController.swift in Sources */,
|
||||
ED79A5D62BDF8D6100952D1F /* iCloudDocumentsDirectoryMonitor.swift in Sources */,
|
||||
EDFDFB522B726F1A0013A44C /* ButtonsStackView.swift in Sources */,
|
||||
993DF11023F6BDB100AC231A /* MWMButtonRenderer.swift in Sources */,
|
||||
3463BA671DE81DB90082417F /* MWMTrafficButtonViewController.mm in Sources */,
|
||||
ED79A5D52BDF8D6100952D1F /* SynchronizationError.swift in Sources */,
|
||||
993DF10323F6BDB100AC231A /* MainTheme.swift in Sources */,
|
||||
34AB66051FC5AA320078E451 /* MWMNavigationDashboardManager+Entity.mm in Sources */,
|
||||
993DF12A23F6BDB100AC231A /* Style.swift in Sources */,
|
||||
|
@ -4231,6 +4311,7 @@
|
|||
99A906E523F6F7030005872B /* ActionBarViewController.swift in Sources */,
|
||||
47B9065421C7FA400079C85E /* UIImageView+WebImage.m in Sources */,
|
||||
F6E2FF481E097BA00083EBEC /* SettingsTableViewSelectableCell.swift in Sources */,
|
||||
ED63CEB92BDF8F9D006155C4 /* SettingsTableViewiCloudSwitchCell.swift in Sources */,
|
||||
47CA68D4250043C000671019 /* BookmarksListPresenter.swift in Sources */,
|
||||
F6E2FF451E097BA00083EBEC /* SettingsTableViewLinkCell.swift in Sources */,
|
||||
34C9BD0A1C6DBCDA000DC38D /* MWMNavigationController.m in Sources */,
|
||||
|
@ -4340,6 +4421,7 @@
|
|||
F6E2FF661E097BA00083EBEC /* MWMTTSSettingsViewController.mm in Sources */,
|
||||
3454D7C21E07F045004AF2AD /* NSString+Categories.m in Sources */,
|
||||
34E7761F1F14DB48003040B3 /* PlacePageArea.swift in Sources */,
|
||||
ED79A5D82BDF8D6100952D1F /* DefaultLocalDirectoryMonitor.swift in Sources */,
|
||||
4728F69322CF89A400E00028 /* GradientView.swift in Sources */,
|
||||
F6381BF61CD12045004CA943 /* LocaleTranslator.mm in Sources */,
|
||||
9917D17F2397B1D600A7E06E /* IPadModalPresentationController.swift in Sources */,
|
||||
|
@ -4411,6 +4493,7 @@
|
|||
998927402449ECC200260CE2 /* BottomMenuItemCell.swift in Sources */,
|
||||
F6E2FEE21E097BA00083EBEC /* MWMSearchManager.mm in Sources */,
|
||||
F6E2FE221E097BA00083EBEC /* MWMOpeningHoursEditorViewController.mm in Sources */,
|
||||
ED79A5D72BDF8D6100952D1F /* SynchronizationStateManager.swift in Sources */,
|
||||
999FC12B23ABB4B800B0E6F9 /* FontStyleSheet.swift in Sources */,
|
||||
47CA68DA2500469400671019 /* BookmarksListBuilder.swift in Sources */,
|
||||
34D3AFE21E376F7E004100F9 /* UITableView+Updates.swift in Sources */,
|
||||
|
@ -4440,6 +4523,7 @@
|
|||
340475711E081A4600C92850 /* MWMSettings.mm in Sources */,
|
||||
33046832219C57180041F3A8 /* CategorySettingsViewController.swift in Sources */,
|
||||
3404756E1E081A4600C92850 /* MWMSearch.mm in Sources */,
|
||||
EDF838842C00B640007E4E67 /* SynchronizationFileWriter.swift in Sources */,
|
||||
6741AA191BF340DE002C974C /* MWMDownloaderDialogCell.m in Sources */,
|
||||
993DF10823F6BDB100AC231A /* IColors.swift in Sources */,
|
||||
4707E4B12372FE860017DF6E /* PlacePageViewController.swift in Sources */,
|
||||
|
@ -4451,6 +4535,7 @@
|
|||
6741AA1C1BF340DE002C974C /* MWMRoutingDisclaimerAlert.m in Sources */,
|
||||
34D3B0481E389D05004100F9 /* MWMNoteCell.m in Sources */,
|
||||
CD9AD967228067F500EC174A /* MapInfo.swift in Sources */,
|
||||
ED79A5D32BDF8D6100952D1F /* CloudStorageManager.swift in Sources */,
|
||||
6741AA1D1BF340DE002C974C /* MWMDownloadTransitMapAlert.mm in Sources */,
|
||||
471A7BBE2481A3D000A0D4C1 /* EditBookmarkViewController.swift in Sources */,
|
||||
993DF0C923F6BD0600AC231A /* ElevationDetailsBuilder.swift in Sources */,
|
||||
|
@ -4510,8 +4595,15 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
EDF838C12C00B9D6007E4E67 /* iCloudDirectoryMonitorTests.swift in Sources */,
|
||||
EDF838C02C00B9D0007E4E67 /* DefaultLocalDirectoryMonitorTests.swift in Sources */,
|
||||
ED1ADA332BC6B1B40029209F /* CarPlayServiceTests.swift in Sources */,
|
||||
EDF838C32C00B9D6007E4E67 /* UbiquitousDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDF838BE2C00B9D0007E4E67 /* LocalDirectoryMonitorDelegateMock.swift in Sources */,
|
||||
EDF838BF2C00B9D0007E4E67 /* SynchronizationStateManagerTests.swift in Sources */,
|
||||
EDF838C22C00B9D6007E4E67 /* MetadataItemStubs.swift in Sources */,
|
||||
4B4153B52BF9695500EE4B02 /* MWMTextToSpeechTests.mm in Sources */,
|
||||
EDF838C42C00B9D6007E4E67 /* FileManagerMock.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -2,11 +2,28 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:omaps.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.carplay-maps</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.app.organicmaps.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.app.organicmaps.debug</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -2,11 +2,28 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:omaps.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.carplay-maps</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.app.organicmaps</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.app.organicmaps</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -252,5 +252,26 @@
|
|||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSUbiquitousContainers</key>
|
||||
<dict>
|
||||
<key>iCloud.app.organicmaps</key>
|
||||
<dict>
|
||||
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerSupportedFolderLevels</key>
|
||||
<string>ANY</string>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>OrganicMaps</string>
|
||||
</dict>
|
||||
<key>iCloud.app.organicmaps.debug</key>
|
||||
<dict>
|
||||
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerSupportedFolderLevels</key>
|
||||
<string>ANY</string>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>OrganicMapsDEBUG</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
final class DefaultLocalDirectoryMonitorTests: XCTestCase {
|
||||
|
||||
let fileManager = FileManager.default
|
||||
let tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
var directoryMonitor: DefaultLocalDirectoryMonitor!
|
||||
var mockDelegate: LocalDirectoryMonitorDelegateMock!
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
// Setup with a temporary directory and a mock delegate
|
||||
directoryMonitor = try DefaultLocalDirectoryMonitor(fileManager: fileManager, directory: tempDirectory)
|
||||
mockDelegate = LocalDirectoryMonitorDelegateMock()
|
||||
directoryMonitor.delegate = mockDelegate
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
directoryMonitor.stop()
|
||||
mockDelegate = nil
|
||||
try? fileManager.removeItem(at: tempDirectory)
|
||||
try super.tearDownWithError()
|
||||
}
|
||||
|
||||
func testInitialization() {
|
||||
XCTAssertEqual(directoryMonitor.directory, tempDirectory, "Monitor initialized with incorrect directory.")
|
||||
XCTAssertTrue(directoryMonitor.state == .stopped, "Monitor should be stopped initially.")
|
||||
}
|
||||
|
||||
func testStartMonitoring() {
|
||||
let startExpectation = expectation(description: "Start monitoring")
|
||||
directoryMonitor.start { result in
|
||||
switch result {
|
||||
case .success:
|
||||
XCTAssertTrue(self.directoryMonitor.state == .started, "Monitor should be started.")
|
||||
case .failure(let error):
|
||||
XCTFail("Monitoring failed to start with error: \(error)")
|
||||
}
|
||||
startExpectation.fulfill()
|
||||
}
|
||||
wait(for: [startExpectation], timeout: 5.0)
|
||||
}
|
||||
|
||||
func testStopMonitoring() {
|
||||
directoryMonitor.start()
|
||||
directoryMonitor.stop()
|
||||
XCTAssertTrue(directoryMonitor.state == .stopped, "Monitor should be stopped.")
|
||||
}
|
||||
|
||||
func testPauseAndResumeMonitoring() {
|
||||
directoryMonitor.start()
|
||||
directoryMonitor.pause()
|
||||
XCTAssertTrue(directoryMonitor.state == .paused, "Monitor should be paused.")
|
||||
|
||||
directoryMonitor.resume()
|
||||
XCTAssertTrue(directoryMonitor.state == .started, "Monitor should be started.")
|
||||
}
|
||||
|
||||
func testDelegateDidFinishGathering() {
|
||||
mockDelegate.didFinishGatheringExpectation = expectation(description: "didFinishGathering called")
|
||||
directoryMonitor.start()
|
||||
wait(for: [mockDelegate.didFinishGatheringExpectation!], timeout: 5.0)
|
||||
}
|
||||
|
||||
func testDelegateDidReceiveError() {
|
||||
mockDelegate.didReceiveErrorExpectation = expectation(description: "didReceiveLocalMonitorError called")
|
||||
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
|
||||
directoryMonitor.delegate?.didReceiveLocalMonitorError(error)
|
||||
|
||||
wait(for: [mockDelegate.didReceiveErrorExpectation!], timeout: 1.0)
|
||||
}
|
||||
|
||||
func testContentUpdateDetection() {
|
||||
let startExpectation = expectation(description: "Start monitoring")
|
||||
let didFinishGatheringExpectation = expectation(description: "didFinishGathering called")
|
||||
let didUpdateExpectation = expectation(description: "didUpdate called")
|
||||
|
||||
mockDelegate.didFinishGatheringExpectation = didFinishGatheringExpectation
|
||||
mockDelegate.didUpdateExpectation = didUpdateExpectation
|
||||
|
||||
directoryMonitor.start { result in
|
||||
if case .success = result {
|
||||
XCTAssertTrue(self.directoryMonitor.state == .started, "Monitor should be started.")
|
||||
}
|
||||
startExpectation.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [startExpectation], timeout: 5)
|
||||
|
||||
let fileURL = tempDirectory.appendingPathComponent("test.kml")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.fileManager.createFile(atPath: fileURL.path, contents: Data(), attributes: nil)
|
||||
}
|
||||
|
||||
wait(for: [didFinishGatheringExpectation, didUpdateExpectation], timeout: 20)
|
||||
}
|
||||
|
||||
func testFileWithIncorrectExtension() {
|
||||
let startExpectation = expectation(description: "Start monitoring")
|
||||
let didFinishGatheringExpectation = expectation(description: "didFinishGathering called")
|
||||
mockDelegate.didFinishGatheringExpectation = didFinishGatheringExpectation
|
||||
|
||||
let file1URL = tempDirectory.appendingPathComponent("test.kml.tmp")
|
||||
let file2URL = tempDirectory.appendingPathComponent("test2.tmp")
|
||||
let file3URL = tempDirectory.appendingPathComponent("test3.jpg")
|
||||
let correctFileURL = tempDirectory.appendingPathComponent("test.kml")
|
||||
|
||||
var fileData = Data(count: 12)
|
||||
try! fileData.write(to: file1URL, options: .atomic)
|
||||
try! fileData.write(to: file2URL, options: .atomic)
|
||||
try! fileData.write(to: file3URL, options: .atomic)
|
||||
try! fileData.write(to: correctFileURL, options: .atomic)
|
||||
|
||||
directoryMonitor.start { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
XCTFail("Monitoring failed to start with error: \(error)")
|
||||
case .success:
|
||||
XCTAssertTrue(self.directoryMonitor.state == .started, "Monitor should be started.")
|
||||
startExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
wait(for: [startExpectation, didFinishGatheringExpectation], timeout: 5)
|
||||
|
||||
let contents = self.mockDelegate.contents.map { $0.fileUrl }
|
||||
XCTAssertFalse(contents.contains(file1URL), "File with incorrect extension should not be included")
|
||||
XCTAssertFalse(contents.contains(file2URL), "File with incorrect extension should not be included")
|
||||
XCTAssertFalse(contents.contains(file3URL), "File with incorrect extension should not be included")
|
||||
XCTAssertTrue(contents.contains(correctFileURL), "File with correct extension should be included")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
class LocalDirectoryMonitorDelegateMock: LocalDirectoryMonitorDelegate {
|
||||
var contents = LocalContents()
|
||||
|
||||
var didFinishGatheringExpectation: XCTestExpectation?
|
||||
var didUpdateExpectation: XCTestExpectation?
|
||||
var didReceiveErrorExpectation: XCTestExpectation?
|
||||
|
||||
func didFinishGathering(contents: LocalContents) {
|
||||
self.contents = contents
|
||||
didFinishGatheringExpectation?.fulfill()
|
||||
}
|
||||
|
||||
func didUpdate(contents: LocalContents) {
|
||||
self.contents = contents
|
||||
didUpdateExpectation?.fulfill()
|
||||
}
|
||||
|
||||
func didReceiveLocalMonitorError(_ error: Error) {
|
||||
didReceiveErrorExpectation?.fulfill()
|
||||
}
|
||||
}
|
36
iphone/Maps/Tests/Core/iCloudTests/MetadataItemStubs.swift
Normal file
|
@ -0,0 +1,36 @@
|
|||
@testable import Organic_Maps__Debug_
|
||||
|
||||
extension LocalMetadataItem {
|
||||
static func stub(fileName: String,
|
||||
lastModificationDate: TimeInterval) -> LocalMetadataItem {
|
||||
let item = LocalMetadataItem(fileName: fileName,
|
||||
fileUrl: URL(string: "url")!,
|
||||
fileSize: 0,
|
||||
contentType: "",
|
||||
creationDate: Date().timeIntervalSince1970,
|
||||
lastModificationDate: lastModificationDate)
|
||||
return item
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension CloudMetadataItem {
|
||||
static func stub(fileName: String,
|
||||
lastModificationDate: TimeInterval,
|
||||
isInTrash: Bool,
|
||||
isDownloaded: Bool = true,
|
||||
hasUnresolvedConflicts: Bool = false) -> CloudMetadataItem {
|
||||
let item = CloudMetadataItem(fileName: fileName,
|
||||
fileUrl: URL(string: "url")!,
|
||||
fileSize: 0,
|
||||
contentType: "",
|
||||
isDownloaded: isDownloaded,
|
||||
creationDate: Date().timeIntervalSince1970,
|
||||
lastModificationDate: lastModificationDate,
|
||||
isRemoved: isInTrash,
|
||||
downloadingError: nil,
|
||||
uploadingError: nil,
|
||||
hasUnresolvedConflicts: hasUnresolvedConflicts)
|
||||
return item
|
||||
}
|
||||
}
|
|
@ -0,0 +1,752 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
final class SynchronizationStateManagerTests: XCTestCase {
|
||||
|
||||
var syncStateManager: SynchronizationStateManager!
|
||||
var outgoingEvents: [OutgoingEvent] = []
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: false)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
syncStateManager = nil
|
||||
outgoingEvents.removeAll()
|
||||
super.tearDown()
|
||||
}
|
||||
// MARK: - Test didFinishGathering without errors and on initial synchronization
|
||||
func testInitialSynchronization() {
|
||||
syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true)
|
||||
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3)) // Local only item
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2), isInTrash: false) // Conflicting item
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4), isInTrash: false) // Cloud only item
|
||||
|
||||
let localItems: LocalContents = [localItem1, localItem2]
|
||||
let cloudItems: CloudContents = [cloudItem1, cloudItem3]
|
||||
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems)))
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems)))
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { event in
|
||||
if case .resolveInitialSynchronizationConflict(let item) = event, item == localItem1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, "Expected to resolve initial synchronization conflict for localItem1")
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { event in
|
||||
if case .createLocalItem(let item) = event, item == cloudItem3 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, "Expected to create local item for cloudItem3")
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { event in
|
||||
if case .createCloudItem(let item) = event, item == localItem2 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, "Expected to create cloud item for localItem2")
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { event in
|
||||
if case .didFinishInitialSynchronization = event {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, "Expected to finish initial synchronization")
|
||||
}
|
||||
|
||||
func testInitialSynchronizationWithNewerCloudItem() {
|
||||
syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true)
|
||||
|
||||
let localItem = LocalMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1))
|
||||
let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem])))
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem])))
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { if case .resolveInitialSynchronizationConflict(_) = $0 { return true } else { return false } }, "Expected conflict resolution for a newer cloud item")
|
||||
}
|
||||
|
||||
func testInitialSynchronizationWithNewerLocalItem() {
|
||||
syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true)
|
||||
|
||||
let localItem = LocalMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(2))
|
||||
let cloudItem = CloudMetadataItem.stub(fileName: "file", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem])))
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem])))
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { if case .resolveInitialSynchronizationConflict(_) = $0 { return true } else { return false } }, "Expected conflict resolution for a newer local item")
|
||||
}
|
||||
|
||||
func testInitialSynchronizationWithNonConflictingItems() {
|
||||
syncStateManager = DefaultSynchronizationStateManager(isInitialSynchronization: true)
|
||||
|
||||
let localItem = LocalMetadataItem.stub(fileName: "localFile", lastModificationDate: TimeInterval(1))
|
||||
let cloudItem = CloudMetadataItem.stub(fileName: "cloudFile", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringLocalContents([localItem])))
|
||||
outgoingEvents.append(contentsOf: syncStateManager.resolveEvent(.didFinishGatheringCloudContents([cloudItem])))
|
||||
|
||||
XCTAssertTrue(outgoingEvents.contains { if case .createLocalItem(_) = $0 { return true } else { return false } }, "Expected creation of local item for cloudFile")
|
||||
XCTAssertTrue(outgoingEvents.contains { if case .createCloudItem(_) = $0 { return true } else { return false } }, "Expected creation of cloud item for localFile")
|
||||
}
|
||||
|
||||
// MARK: - Test didFinishGathering without errors and after initial synchronization
|
||||
func testDidFinishGatheringWhenCloudAndLocalIsEmpty() {
|
||||
let localItems: LocalContents = []
|
||||
let cloudItems: CloudContents = []
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenOnlyCloudIsEmpty() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let localItems: LocalContents = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems: CloudContents = []
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createCloudItem(let item):
|
||||
XCTAssertTrue(localItems.containsByName(item))
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenOnlyLocalIsEmpty() {
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents()
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createLocalItem(let item):
|
||||
XCTAssertTrue(cloudItems.containsByName(item))
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalIsEmptyAndAllCloudFilesWasDeleted() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2), isInTrash: true)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3), isInTrash: true)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4), isInTrash: true)
|
||||
|
||||
let localItems = [localItem1, localItem2, localItem3]
|
||||
let cloudItems = [cloudItem1, cloudItem2, cloudItem3]
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 3)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .removeLocalItem(let item):
|
||||
XCTAssertTrue(localItems.containsByName(item))
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalIsNotEmptyAndAllCloudFilesWasDeleted() {
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: true)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents()
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalIsEmptyAndSomeCloudFilesWasDeleted() {
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: true)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents()
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createLocalItem(let item):
|
||||
XCTAssertEqual(item, cloudItem3)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndEqual() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndSomeLocalItemsAreNewer() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 2)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .updateCloudItem(let item):
|
||||
XCTAssertTrue([localItem2, localItem3].containsByName(item))
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyAndSomeCloudItemsAreNewer() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(4), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 2)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .updateLocalItem(let item):
|
||||
XCTAssertTrue([cloudItem1, cloudItem3].containsByName(item))
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocalAndCloudAreNotEmptyMixed() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
let localItem4 = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(1))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(4), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3, localItem4])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 4)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .updateLocalItem(let item):
|
||||
XCTAssertEqual(item, cloudItem1)
|
||||
case .removeLocalItem(let item):
|
||||
XCTAssertEqual(item, cloudItem3)
|
||||
case .createCloudItem(let item):
|
||||
XCTAssertEqual(item, localItem4)
|
||||
case .updateCloudItem(let item):
|
||||
XCTAssertEqual(item, localItem2)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenCloudHaveTrashedNewerThanLocal() {
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(8), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([localItem3])
|
||||
let cloudItems = CloudContents([cloudItem3Trashed])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .removeLocalItem(let item):
|
||||
XCTAssertEqual(item, cloudItem3Trashed)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenLocallIsEmptyAndCloudHaveSameFileBothInTrashedAndNotAndTrashedOlder() {
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: false)
|
||||
let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([])
|
||||
let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createLocalItem(let item):
|
||||
XCTAssertEqual(item, cloudItem3)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenCloudHaveSameFileBothInTrashedAndNotAndTrashedOlder() {
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(7), isInTrash: false)
|
||||
let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([localItem3])
|
||||
let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .updateLocalItem(let item):
|
||||
XCTAssertEqual(item, cloudItem3)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenCloudHaveSameFileBothInTrashedAndNotAndTrashedBotLocalIsNewer() {
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(9))
|
||||
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3Trashed = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(6), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([localItem3])
|
||||
let cloudItems = CloudContents([cloudItem3, cloudItem3Trashed])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .updateCloudItem(let item):
|
||||
XCTAssertEqual(item, localItem3)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidFinishGatheringWhenUpdatetLocallyItemSameAsDeletedFromCloudOnTheOtherDevice() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([localItem1])
|
||||
let cloudItems = CloudContents([cloudItem1])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
var localItemsToRemove: LocalContents = []
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .removeLocalItem(let cloudMetadataItem):
|
||||
XCTAssertEqual(cloudMetadataItem, cloudItem1)
|
||||
if let localItemToRemove = localItems.firstByName(cloudMetadataItem) {
|
||||
localItemsToRemove.append(localItemToRemove)
|
||||
}
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(localItemsToRemove))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
}
|
||||
|
||||
// MARK: - Test didFinishGathering MergeConflicts
|
||||
func testDidFinishGatheringMergeConflictWhenUpdatetLocallyItemNewerThanDeletedFromCloudOnTheOtherDevice() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: true)
|
||||
|
||||
let localItems = LocalContents([localItem1])
|
||||
let cloudItems = CloudContents([cloudItem1])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
// Here should be a merge conflict. New Cloud file should be created.
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createCloudItem(let cloudMetadataItem):
|
||||
XCTAssertEqual(cloudMetadataItem, localItem1)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Test didUpdateLocalContents
|
||||
func testDidUpdateLocalContentsWhenContentWasNotChanged() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 3)
|
||||
|
||||
let newLocalItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 3) // Should be equal to the previous results because cloudContent wasn't changed
|
||||
}
|
||||
|
||||
func testDidUpdateLocalContentsWhenNewLocalItemWasAdded() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
let localItem4 = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(4))
|
||||
let newLocalItems = LocalContents([localItem1, localItem2, localItem3, localItem4])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createCloudItem(let item):
|
||||
XCTAssertEqual(item, localItem4)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidUpdateLocalContentsWhenLocalItemWasUpdated() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
let localItem2Updated = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(3))
|
||||
let localItem3Updated = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4))
|
||||
|
||||
let newLocalItems = LocalContents([localItem1, localItem2Updated, localItem3Updated])
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 2)
|
||||
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .updateCloudItem(let item):
|
||||
XCTAssertTrue([localItem2Updated, localItem3Updated].containsByName(item))
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidUpdateLocalContentsWhenLocalItemWasRemoved() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
let newLocalItems = LocalContents([localItem1, localItem2])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .removeCloudItem(let item):
|
||||
XCTAssertEqual(item, localItem3)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidUpdateLocalContentsComplexUpdate() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
let localItem1New = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(2))
|
||||
let localItem3New = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(4))
|
||||
let localItem4New = LocalMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(5))
|
||||
let localItem5New = LocalMetadataItem.stub(fileName: "file5", lastModificationDate: TimeInterval(5))
|
||||
|
||||
let newLocalItems = LocalContents([localItem1New, localItem3New, localItem4New, localItem5New])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(newLocalItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 5)
|
||||
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createCloudItem(let localMetadataItem):
|
||||
XCTAssertTrue([localItem4New, localItem5New].containsByName(localMetadataItem))
|
||||
case .updateCloudItem(let localMetadataItem):
|
||||
XCTAssertTrue([localItem1New, localItem3New].containsByName(localMetadataItem))
|
||||
case .removeCloudItem(let localMetadataItem):
|
||||
XCTAssertEqual(localMetadataItem, localItem2)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Test didUpdateCloudContents
|
||||
func testDidUpdateCloudContentsWhenContentWasNotChanged() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
let cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
let newCloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(newCloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
}
|
||||
|
||||
func testDidUpdateCloudContentsWhenContentItemWasAdded() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
let cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
let cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
let cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
var cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
var cloudItem4 = CloudMetadataItem.stub(fileName: "file4", lastModificationDate: TimeInterval(3), isInTrash: false, isDownloaded: false)
|
||||
cloudItems.append(cloudItem4)
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .startDownloading(let cloudMetadataItem):
|
||||
XCTAssertEqual(cloudMetadataItem, cloudItem4)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
cloudItem4.isDownloaded = true
|
||||
|
||||
// recreate collection
|
||||
cloudItems = [cloudItem1, cloudItem2, cloudItem3, cloudItem4]
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 1)
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .createLocalItem(let cloudMetadataItem):
|
||||
XCTAssertEqual(cloudMetadataItem, cloudItem4)
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDidUpdateCloudContentsWhenAllContentWasTrashed() {
|
||||
let localItem1 = LocalMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1))
|
||||
let localItem2 = LocalMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2))
|
||||
let localItem3 = LocalMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3))
|
||||
|
||||
var cloudItem1 = CloudMetadataItem.stub(fileName: "file1", lastModificationDate: TimeInterval(1), isInTrash: false)
|
||||
var cloudItem2 = CloudMetadataItem.stub(fileName: "file2", lastModificationDate: TimeInterval(2), isInTrash: false)
|
||||
var cloudItem3 = CloudMetadataItem.stub(fileName: "file3", lastModificationDate: TimeInterval(3), isInTrash: false)
|
||||
|
||||
let localItems = LocalContents([localItem1, localItem2, localItem3])
|
||||
var cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringLocalContents(localItems))
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didFinishGatheringCloudContents(cloudItems))
|
||||
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
|
||||
cloudItem1.isRemoved = true
|
||||
cloudItem2.isRemoved = true
|
||||
cloudItem3.isRemoved = true
|
||||
|
||||
cloudItems = CloudContents([cloudItem1, cloudItem2, cloudItem3])
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateCloudContents(cloudItems))
|
||||
XCTAssertEqual(outgoingEvents.count, 3)
|
||||
|
||||
var localItemsToRemove: LocalContents = []
|
||||
outgoingEvents.forEach { event in
|
||||
switch event {
|
||||
case .removeLocalItem(let cloudMetadataItem):
|
||||
XCTAssertTrue(cloudItems.containsByName(cloudMetadataItem))
|
||||
if let localItemToRemove = localItems.firstByName(cloudMetadataItem) {
|
||||
localItemsToRemove.append(localItemToRemove)
|
||||
}
|
||||
default:
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
outgoingEvents = syncStateManager.resolveEvent(.didUpdateLocalContents(localItemsToRemove))
|
||||
// Because all cloud items in .trash and we have removed all local items, we should not have any outgoing events.
|
||||
XCTAssertEqual(outgoingEvents.count, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
class FileManagerMock: FileManager {
|
||||
var stubUbiquityIdentityToken: UbiquityIdentityToken?
|
||||
var shouldReturnContainerURL: Bool = true
|
||||
var stubCloudDirectory: URL?
|
||||
|
||||
override var ubiquityIdentityToken: (any UbiquityIdentityToken)? {
|
||||
return stubUbiquityIdentityToken
|
||||
}
|
||||
|
||||
override func url(forUbiquityContainerIdentifier identifier: String?) -> URL? {
|
||||
return shouldReturnContainerURL ? stubCloudDirectory ?? URL(fileURLWithPath: NSTemporaryDirectory()) : nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
class UbiquitousDirectoryMonitorDelegateMock: CloudDirectoryMonitorDelegate {
|
||||
var didFinishGatheringCalled = false
|
||||
var didUpdateCalled = false
|
||||
var didReceiveErrorCalled = false
|
||||
|
||||
var didFinishGatheringExpectation: XCTestExpectation?
|
||||
var didUpdateExpectation: XCTestExpectation?
|
||||
var didReceiveErrorExpectation: XCTestExpectation?
|
||||
|
||||
var contents = CloudContents()
|
||||
|
||||
func didFinishGathering(contents: CloudContents) {
|
||||
didFinishGatheringCalled = true
|
||||
didFinishGatheringExpectation?.fulfill()
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
func didUpdate(contents: CloudContents) {
|
||||
didUpdateCalled = true
|
||||
didUpdateExpectation?.fulfill()
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
func didReceiveCloudMonitorError(_ error: Error) {
|
||||
didReceiveErrorCalled = true
|
||||
didReceiveErrorExpectation?.fulfill()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
import XCTest
|
||||
@testable import Organic_Maps__Debug_
|
||||
|
||||
typealias UbiquityIdentityToken = NSCoding & NSCopying & NSObjectProtocol
|
||||
|
||||
class iCloudDirectoryMonitorTests: XCTestCase {
|
||||
|
||||
var cloudMonitor: iCloudDocumentsDirectoryMonitor!
|
||||
var mockFileManager: FileManagerMock!
|
||||
var mockDelegate: UbiquitousDirectoryMonitorDelegateMock!
|
||||
var cloudContainerIdentifier: String = "iCloud.app.organicmaps.debug"
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
mockFileManager = FileManagerMock()
|
||||
mockDelegate = UbiquitousDirectoryMonitorDelegateMock()
|
||||
cloudMonitor = iCloudDocumentsDirectoryMonitor(fileManager: mockFileManager, cloudContainerIdentifier: cloudContainerIdentifier, fileType: .kml)
|
||||
cloudMonitor.delegate = mockDelegate
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
cloudMonitor = nil
|
||||
mockFileManager = nil
|
||||
mockDelegate = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testInitialization() {
|
||||
XCTAssertNotNil(cloudMonitor)
|
||||
XCTAssertEqual(cloudMonitor.containerIdentifier, cloudContainerIdentifier)
|
||||
}
|
||||
|
||||
func testCloudAvailability() {
|
||||
mockFileManager.stubUbiquityIdentityToken = NSString(string: "mockToken")
|
||||
XCTAssertTrue(cloudMonitor.isCloudAvailable())
|
||||
|
||||
mockFileManager.stubUbiquityIdentityToken = nil
|
||||
XCTAssertFalse(cloudMonitor.isCloudAvailable())
|
||||
}
|
||||
|
||||
func testStartWhenCloudAvailable() {
|
||||
mockFileManager.stubUbiquityIdentityToken = NSString(string: "mockToken")
|
||||
let startExpectation = expectation(description: "startExpectation")
|
||||
cloudMonitor.start { result in
|
||||
if case .success = result {
|
||||
startExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
XCTAssertTrue(cloudMonitor.state == .started, "Monitor should be started when the cloud is available.")
|
||||
}
|
||||
|
||||
|
||||
|
||||
func testStartWhenCloudNotAvailable() {
|
||||
mockFileManager.stubUbiquityIdentityToken = nil
|
||||
let startExpectation = expectation(description: "startExpectation")
|
||||
cloudMonitor.start { result in
|
||||
if case .failure(let error) = result, case SynchronizationError.iCloudIsNotAvailable = error {
|
||||
startExpectation.fulfill()
|
||||
}
|
||||
}
|
||||
waitForExpectations(timeout: 5)
|
||||
XCTAssertTrue(cloudMonitor.state == .stopped, "Monitor should not start when the cloud is not available.")
|
||||
}
|
||||
|
||||
func testStopAfterStart() {
|
||||
testStartWhenCloudAvailable()
|
||||
cloudMonitor.stop()
|
||||
XCTAssertTrue(cloudMonitor.state == .stopped, "Monitor should not be started after stopping.")
|
||||
}
|
||||
|
||||
func testPauseAndResume() {
|
||||
testStartWhenCloudAvailable()
|
||||
cloudMonitor.pause()
|
||||
XCTAssertTrue(cloudMonitor.state == .paused, "Monitor should be paused.")
|
||||
|
||||
cloudMonitor.resume()
|
||||
XCTAssertTrue(cloudMonitor.state == .started, "Monitor should not be paused after resuming.")
|
||||
}
|
||||
|
||||
func testFetchUbiquityDirectoryUrl() {
|
||||
let expectation = self.expectation(description: "Fetch Ubiquity Directory URL")
|
||||
mockFileManager.shouldReturnContainerURL = true
|
||||
cloudMonitor.fetchUbiquityDirectoryUrl { result in
|
||||
if case .success = result {
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
wait(for: [expectation], timeout: 5.0)
|
||||
}
|
||||
|
||||
func testDelegateMethods() {
|
||||
testStartWhenCloudAvailable()
|
||||
|
||||
guard let metadataQuery = cloudMonitor.metadataQuery else {
|
||||
XCTFail("Metadata query should not be nil")
|
||||
return
|
||||
}
|
||||
|
||||
let didFinishGatheringExpectation = expectation(description: "didFinishGathering")
|
||||
mockDelegate.didFinishGatheringExpectation = didFinishGatheringExpectation
|
||||
NotificationCenter.default.post(name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: metadataQuery)
|
||||
wait(for: [didFinishGatheringExpectation], timeout: 5.0)
|
||||
|
||||
let didUpdateExpectation = expectation(description: "didUpdate")
|
||||
mockDelegate.didUpdateExpectation = didUpdateExpectation
|
||||
NotificationCenter.default.post(name: NSNotification.Name.NSMetadataQueryDidUpdate, object: metadataQuery)
|
||||
wait(for: [didUpdateExpectation], timeout: 5.0)
|
||||
}
|
||||
|
||||
// MARK: - Delegate
|
||||
|
||||
func testDelegateDidFinishGathering() {
|
||||
testStartWhenCloudAvailable()
|
||||
|
||||
guard let metadataQuery = cloudMonitor.metadataQuery else {
|
||||
XCTFail("Metadata query should not be nil")
|
||||
return
|
||||
}
|
||||
|
||||
let didFinishGatheringExpectation = expectation(description: "didFinishGathering")
|
||||
mockDelegate.didFinishGatheringExpectation = didFinishGatheringExpectation
|
||||
NotificationCenter.default.post(name: .NSMetadataQueryDidFinishGathering, object: metadataQuery)
|
||||
wait(for: [didFinishGatheringExpectation], timeout: 5.0)
|
||||
XCTAssertTrue(mockDelegate.didFinishGatheringCalled, "Delegate's didFinishGathering should be called.")
|
||||
}
|
||||
|
||||
func testDelegateDidUpdate() {
|
||||
testStartWhenCloudAvailable()
|
||||
|
||||
guard let metadataQuery = cloudMonitor.metadataQuery else {
|
||||
XCTFail("Metadata query should not be nil")
|
||||
return
|
||||
}
|
||||
let didUpdateExpectation = expectation(description: "didUpdate")
|
||||
mockDelegate.didUpdateExpectation = didUpdateExpectation
|
||||
NotificationCenter.default.post(name: NSNotification.Name.NSMetadataQueryDidUpdate, object: metadataQuery)
|
||||
wait(for: [didUpdateExpectation], timeout: 5.0)
|
||||
XCTAssertTrue(mockDelegate.didUpdateCalled, "Delegate's didUpdate should be called.")
|
||||
}
|
||||
|
||||
func testDelegateDidReceiveError() {
|
||||
testStartWhenCloudAvailable()
|
||||
let didReceiveErrorExpectation = expectation(description: "didReceiveError")
|
||||
mockDelegate.didReceiveErrorExpectation = didReceiveErrorExpectation
|
||||
cloudMonitor.delegate?.didReceiveCloudMonitorError(SynchronizationError.containerNotFound)
|
||||
wait(for: [didReceiveErrorExpectation], timeout: 5.0)
|
||||
XCTAssertTrue(mockDelegate.didReceiveErrorCalled, "Delegate's didReceiveError should be called.")
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ final class CarPlayMapViewController: MWMViewController {
|
|||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
if mapView?.drapeEngineCreated == false && !MapsAppDelegate.isDrapeDisabled() {
|
||||
if mapView?.drapeEngineCreated == false && !MapsAppDelegate.isTestsEnvironment() {
|
||||
mapView?.createDrapeEngine()
|
||||
}
|
||||
updateVisibleViewPortState(viewPortState)
|
||||
|
|
|
@ -28,6 +28,7 @@ final class EditBookmarkViewController: MWMTableViewController {
|
|||
private var bookmarkGroupId = FrameworkHelper.invalidCategoryId()
|
||||
private var newBookmarkGroupId = FrameworkHelper.invalidCategoryId()
|
||||
private var bookmarkColor: BookmarkColor!
|
||||
private let bookmarksManager = BookmarksManager.shared()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -44,26 +45,36 @@ final class EditBookmarkViewController: MWMTableViewController {
|
|||
tableView.registerNib(cell: BookmarkTitleCell.self)
|
||||
tableView.registerNib(cell: MWMButtonCell.self)
|
||||
tableView.registerNib(cell: MWMNoteCell.self)
|
||||
|
||||
addToBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
func configure(with bookmarkId: MWMMarkID, editCompletion completion: @escaping (Bool) -> Void) {
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
updateBookmarkIfNeeded()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeFromBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
func configure(with bookmarkId: MWMMarkID, editCompletion completion: ((Bool) -> Void)?) {
|
||||
self.bookmarkId = bookmarkId
|
||||
|
||||
let bm = BookmarksManager.shared()
|
||||
let bookmark = bm.bookmark(withId: bookmarkId)
|
||||
let bookmark = bookmarksManager.bookmark(withId: bookmarkId)
|
||||
|
||||
bookmarkTitle = bookmark.bookmarkName
|
||||
bookmarkColor = bookmark.bookmarkColor
|
||||
|
||||
bookmarkDescription = bm.description(forBookmarkId: bookmarkId)
|
||||
bookmarkDescription = bookmarksManager.description(forBookmarkId: bookmarkId)
|
||||
|
||||
let bookmarkGroup = bm.category(forBookmarkId: bookmarkId)
|
||||
let bookmarkGroup = bookmarksManager.category(forBookmarkId: bookmarkId)
|
||||
bookmarkGroupId = bookmarkGroup.categoryId
|
||||
bookmarkGroupTitle = bookmarkGroup.title
|
||||
|
||||
|
||||
editingCompleted = completion
|
||||
}
|
||||
|
||||
|
||||
@objc(configureWithPlacePageData:)
|
||||
func configure(with placePageData: PlacePageData) {
|
||||
guard let bookmarkData = placePageData.bookmarkData else { fatalError("placePageData and bookmarkData can't be nil") }
|
||||
|
@ -152,6 +163,23 @@ final class EditBookmarkViewController: MWMTableViewController {
|
|||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateBookmarkIfNeeded() {
|
||||
// Skip for the regular place page.
|
||||
guard bookmarkId != FrameworkHelper.invalidBookmarkId() else { return }
|
||||
// TODO: Update the bookmark content on the Edit screen instead of closing it when the bookmark gets updated from cloud.
|
||||
if !bookmarksManager.hasBookmark(bookmarkId) {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
private func addToBookmarksManagerObserverList() {
|
||||
bookmarksManager.add(self)
|
||||
}
|
||||
|
||||
private func removeFromBookmarksManagerObserverList() {
|
||||
bookmarksManager.remove(self)
|
||||
}
|
||||
|
||||
@objc private func onSave() {
|
||||
view.endEditing(true)
|
||||
|
||||
|
@ -225,3 +253,16 @@ extension EditBookmarkViewController: SelectBookmarkGroupViewControllerDelegate
|
|||
tableView.reloadRows(at: [IndexPath(row: InfoSectionRows.bookmarkGroup.rawValue, section: Sections.info.rawValue)], with: .none)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
extension EditBookmarkViewController: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
updateBookmarkIfNeeded()
|
||||
}
|
||||
|
||||
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
|
||||
if bookmarkGroupId == groupId {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,22 +42,34 @@ final class EditTrackViewController: MWMTableViewController {
|
|||
editingCompleted = completion
|
||||
|
||||
super.init(style: .grouped)
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
updateTrackIfNeeded()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeFromBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
title = L("track_title")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save,
|
||||
target: self,
|
||||
action: #selector(onSave))
|
||||
|
||||
|
||||
tableView.registerNib(cell: BookmarkTitleCell.self)
|
||||
tableView.registerNib(cell: MWMButtonCell.self)
|
||||
|
||||
addToBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
@ -123,7 +135,22 @@ final class EditTrackViewController: MWMTableViewController {
|
|||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private func updateTrackIfNeeded() {
|
||||
// TODO: Update the track content on the Edit screen instead of closing it when the track gets updated from cloud.
|
||||
if !bookmarksManager.hasTrack(trackId) {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
|
||||
private func addToBookmarksManagerObserverList() {
|
||||
bookmarksManager.add(self)
|
||||
}
|
||||
|
||||
private func removeFromBookmarksManagerObserverList() {
|
||||
bookmarksManager.remove(self)
|
||||
}
|
||||
|
||||
@objc private func onSave() {
|
||||
view.endEditing(true)
|
||||
BookmarksManager.shared().updateTrack(trackId, setGroupId: trackGroupId, color: trackColor, title: trackTitle ?? "")
|
||||
|
@ -185,3 +212,16 @@ extension EditTrackViewController: SelectBookmarkGroupViewControllerDelegate {
|
|||
with: .none)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
extension EditTrackViewController: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
updateTrackIfNeeded()
|
||||
}
|
||||
|
||||
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
|
||||
if trackGroupId == groupId {
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,27 +2,42 @@ protocol PlacePageInteractorProtocol: AnyObject {
|
|||
func updateTopBound(_ bound: CGFloat, duration: TimeInterval)
|
||||
}
|
||||
|
||||
class PlacePageInteractor {
|
||||
class PlacePageInteractor: NSObject {
|
||||
weak var presenter: PlacePagePresenterProtocol?
|
||||
weak var viewController: UIViewController?
|
||||
weak var mapViewController: MapViewController?
|
||||
|
||||
private let bookmarksManager = BookmarksManager.shared()
|
||||
private var placePageData: PlacePageData
|
||||
|
||||
init (viewController: UIViewController, data: PlacePageData, mapViewController: MapViewController) {
|
||||
init(viewController: UIViewController, data: PlacePageData, mapViewController: MapViewController) {
|
||||
self.placePageData = data
|
||||
self.viewController = viewController
|
||||
self.mapViewController = mapViewController
|
||||
super.init()
|
||||
addToBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeFromBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
private func updateBookmarkIfNeeded() {
|
||||
guard let bookmarkId = placePageData.bookmarkData?.bookmarkId else { return }
|
||||
if !BookmarksManager.shared().hasBookmark(bookmarkId) {
|
||||
guard bookmarksManager.hasBookmark(bookmarkId) else {
|
||||
presenter?.closeAnimated()
|
||||
return
|
||||
}
|
||||
FrameworkHelper.updatePlacePageData()
|
||||
placePageData.updateBookmarkStatus()
|
||||
}
|
||||
|
||||
private func addToBookmarksManagerObserverList() {
|
||||
bookmarksManager.add(self)
|
||||
}
|
||||
|
||||
private func removeFromBookmarksManagerObserverList() {
|
||||
bookmarksManager.remove(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension PlacePageInteractor: PlacePageInteractorProtocol {
|
||||
|
@ -224,3 +239,18 @@ extension PlacePageInteractor: PlacePageHeaderViewControllerDelegate {
|
|||
presenter?.showNextStop()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
extension PlacePageInteractor: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
updateBookmarkIfNeeded()
|
||||
}
|
||||
|
||||
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
|
||||
guard let bookmarkGroupId = placePageData.bookmarkData?.bookmarkGroupId else { return }
|
||||
if bookmarkGroupId == groupId {
|
||||
presenter?.closeAnimated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ protocol SettingsTableViewSwitchCellDelegate {
|
|||
}
|
||||
|
||||
@objc
|
||||
final class SettingsTableViewSwitchCell: MWMTableViewCell {
|
||||
class SettingsTableViewSwitchCell: MWMTableViewCell {
|
||||
|
||||
private let switchButton = UISwitch()
|
||||
let switchButton = UISwitch()
|
||||
|
||||
@IBOutlet weak var delegate: SettingsTableViewSwitchCellDelegate?
|
||||
|
||||
|
@ -24,6 +24,11 @@ final class SettingsTableViewSwitchCell: MWMTableViewCell {
|
|||
set { switchButton.isOn = newValue }
|
||||
}
|
||||
|
||||
@objc
|
||||
func setOn(_ isOn: Bool, animated: Bool) {
|
||||
switchButton.setOn(isOn, animated: animated)
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
setupCell()
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
final class SettingsTableViewiCloudSwitchCell: SettingsTableViewSwitchCell {
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
styleDetail()
|
||||
}
|
||||
|
||||
@objc
|
||||
func updateWithError(_ error: NSError?) {
|
||||
if let error = error as? SynchronizationError {
|
||||
switch error {
|
||||
case .fileUnavailable, .fileNotUploadedDueToQuota, .ubiquityServerNotAvailable:
|
||||
accessoryView = switchButton
|
||||
case .iCloudIsNotAvailable, .containerNotFound:
|
||||
accessoryView = nil
|
||||
accessoryType = .detailButton
|
||||
default:
|
||||
break
|
||||
}
|
||||
detailTextLabel?.text = error.localizedDescription
|
||||
} else {
|
||||
accessoryView = switchButton
|
||||
detailTextLabel?.text?.removeAll()
|
||||
}
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
private func styleDetail() {
|
||||
let detailTextLabelStyle = "regular12:blackSecondaryText"
|
||||
detailTextLabel?.setStyleAndApply(detailTextLabelStyle)
|
||||
detailTextLabel?.numberOfLines = 0
|
||||
detailTextLabel?.lineBreakMode = .byWordWrapping
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
#import "MWMAuthorizationCommon.h"
|
||||
#import "MWMTextToSpeech+CPP.h"
|
||||
#import "SwiftBridge.h"
|
||||
#import "MWMActivityViewController.h"
|
||||
|
||||
#import <CoreApi/CoreApi.h>
|
||||
|
||||
|
@ -9,6 +10,8 @@
|
|||
|
||||
using namespace power_management;
|
||||
|
||||
static NSString * const kUDDidShowICloudSynchronizationEnablingAlert = @"kUDDidShowICloudSynchronizationEnablingAlert";
|
||||
|
||||
@interface MWMSettingsViewController ()<SettingsTableViewSwitchCellDelegate>
|
||||
|
||||
@property(weak, nonatomic) IBOutlet SettingsTableViewLinkCell *profileCell;
|
||||
|
@ -29,6 +32,7 @@ using namespace power_management;
|
|||
@property(weak, nonatomic) IBOutlet SettingsTableViewSwitchCell *autoZoomCell;
|
||||
@property(weak, nonatomic) IBOutlet SettingsTableViewLinkCell *voiceInstructionsCell;
|
||||
@property(weak, nonatomic) IBOutlet SettingsTableViewLinkCell *drivingOptionsCell;
|
||||
@property(weak, nonatomic) IBOutlet SettingsTableViewiCloudSwitchCell *iCloudSynchronizationCell;
|
||||
|
||||
|
||||
@end
|
||||
|
@ -180,6 +184,14 @@ using namespace power_management;
|
|||
break;
|
||||
}
|
||||
[self.nightModeCell configWithTitle:L(@"pref_appearance_title") info:nightMode];
|
||||
|
||||
BOOL isICLoudSynchronizationEnabled = [MWMSettings iCLoudSynchronizationEnabled];
|
||||
[self.iCloudSynchronizationCell configWithDelegate:self
|
||||
title:@"iCloud Synchronization (Beta)"
|
||||
isOn:isICLoudSynchronizationEnabled];
|
||||
[CloudStorageManager.shared addObserver:self onErrorCompletionHandler:^(NSError * _Nullable error) {
|
||||
[self.iCloudSynchronizationCell updateWithError:error];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)show3dBuildingsAlert:(UITapGestureRecognizer *)recognizer {
|
||||
|
@ -209,6 +221,68 @@ using namespace power_management;
|
|||
[self.drivingOptionsCell configWithTitle:L(@"driving_options_title") info:@""];
|
||||
}
|
||||
|
||||
- (void)showICloudSynchronizationEnablingAlert:(void (^)(BOOL))isEnabled {
|
||||
UIAlertController * alertController = [UIAlertController alertControllerWithTitle:L(@"enable_icloud_synchronization_title")
|
||||
message:L(@"enable_icloud_synchronization_message")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction * enableButton = [UIAlertAction actionWithTitle:L(@"enable")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[self setICloudSynchronizationEnablingAlertIsShown];
|
||||
isEnabled(YES);
|
||||
}];
|
||||
UIAlertAction * backupButton = [UIAlertAction actionWithTitle:L(@"backup")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
[MWMBookmarksManager.sharedManager shareAllCategoriesWithCompletion:^(MWMBookmarksShareStatus status, NSURL * _Nonnull url) {
|
||||
switch (status) {
|
||||
case MWMBookmarksShareStatusSuccess: {
|
||||
MWMActivityViewController * shareController = [MWMActivityViewController shareControllerForURL:url message:L(@"share_bookmarks_email_body") completionHandler:^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) {
|
||||
[self setICloudSynchronizationEnablingAlertIsShown];
|
||||
isEnabled(completed);
|
||||
}];
|
||||
[shareController presentInParentViewController:self anchorView:self.iCloudSynchronizationCell];
|
||||
break;
|
||||
}
|
||||
case MWMBookmarksShareStatusEmptyCategory:
|
||||
[[MWMToast toastWithText:L(@"bookmarks_error_title_share_empty")] show];
|
||||
isEnabled(NO);
|
||||
break;
|
||||
case MWMBookmarksShareStatusArchiveError:
|
||||
case MWMBookmarksShareStatusFileError:
|
||||
[[MWMToast toastWithText:L(@"dialog_routing_system_error")] show];
|
||||
isEnabled(NO);
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}];
|
||||
UIAlertAction * cancelButton = [UIAlertAction actionWithTitle:L(@"cancel")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction * action) {
|
||||
isEnabled(NO);
|
||||
}];
|
||||
|
||||
[alertController addAction:cancelButton];
|
||||
if (![MWMBookmarksManager.sharedManager areAllCategoriesEmpty])
|
||||
[alertController addAction:backupButton];
|
||||
[alertController addAction:enableButton];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)setICloudSynchronizationEnablingAlertIsShown {
|
||||
[NSUserDefaults.standardUserDefaults setBool:YES forKey:kUDDidShowICloudSynchronizationEnablingAlert];
|
||||
}
|
||||
|
||||
- (void)showICloudIsDisabledAlert {
|
||||
UIAlertController * alertController = [UIAlertController alertControllerWithTitle:L(@"icloud_disabled_title")
|
||||
message:L(@"icloud_disabled_message")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertAction * okButton = [UIAlertAction actionWithTitle:L(@"ok")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil];
|
||||
[alertController addAction:okButton];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - SettingsTableViewSwitchCellDelegate
|
||||
|
||||
|
@ -241,12 +315,22 @@ using namespace power_management;
|
|||
auto &f = GetFramework();
|
||||
f.AllowAutoZoom(value);
|
||||
f.SaveAutoZoom(value);
|
||||
} else if (cell == self.iCloudSynchronizationCell) {
|
||||
if (![NSUserDefaults.standardUserDefaults boolForKey:kUDDidShowICloudSynchronizationEnablingAlert]) {
|
||||
[self showICloudSynchronizationEnablingAlert:^(BOOL isEnabled) {
|
||||
[self.iCloudSynchronizationCell setOn:isEnabled animated:YES];
|
||||
[MWMSettings setICLoudSynchronizationEnabled:isEnabled];
|
||||
}];
|
||||
} else {
|
||||
[MWMSettings setICLoudSynchronizationEnabled:value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:true];
|
||||
auto cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
if (cell == self.profileCell) {
|
||||
[self performSegueWithIdentifier:@"SettingsToProfileSegue" sender:nil];
|
||||
|
@ -267,6 +351,13 @@ using namespace power_management;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
|
||||
auto cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
if (cell == self.iCloudSynchronizationCell) {
|
||||
[self showICloudIsDisabledAlert];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
|
|
|
@ -284,6 +284,31 @@
|
|||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="SettingsTableViewSwitchCell" textLabel="QEX-D4-ejR" detailTextLabel="Iy7-de-pvk" style="IBUITableViewCellStyleSubtitle" id="E6M-av-wQu" userLabel="iCloud synchronization" customClass="SettingsTableViewiCloudSwitchCell" customModule="Organic_Maps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="586" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="E6M-av-wQu" id="PbS-4v-dzK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="iCloud Synchronization (Beta)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QEX-D4-ejR">
|
||||
<rect key="frame" x="20" y="0.0" width="374" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Error" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Iy7-de-pvk">
|
||||
<rect key="frame" x="20" y="22.5" width="28" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="НАВИГАЦИЯ" id="E4E-hs-9xW">
|
||||
|
@ -392,6 +417,7 @@
|
|||
<outlet property="compassCalibrationCell" destination="P5e-67-f4k" id="KcB-EC-S2y"/>
|
||||
<outlet property="drivingOptionsCell" destination="KrE-Sc-fI1" id="XOl-eI-xJX"/>
|
||||
<outlet property="fontScaleCell" destination="pri-6G-9Zb" id="rHJ-ZT-lwM"/>
|
||||
<outlet property="iCloudSynchronizationCell" destination="E6M-av-wQu" id="05q-Wq-SQa"/>
|
||||
<outlet property="is3dCell" destination="0Lf-xU-P2U" id="obI-bL-FLh"/>
|
||||
<outlet property="mobileInternetCell" destination="6NC-QX-WiF" id="L1V-gS-sTe"/>
|
||||
<outlet property="nightModeCell" destination="QNt-XC-xma" id="nSn-Jr-KuZ"/>
|
||||
|
|
|
@ -1994,7 +1994,7 @@ void BookmarkManager::LoadBookmark(std::string const & filePath, bool isTemporar
|
|||
// Defer bookmark loading in case of another asynchronous process.
|
||||
if (!m_loadBookmarksFinished || m_asyncLoadingInProgress)
|
||||
{
|
||||
m_bookmarkLoadingQueue.emplace_back(filePath, isTemporaryFile);
|
||||
m_bookmarkLoadingQueue.emplace_back(filePath, isTemporaryFile, false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2002,6 +2002,18 @@ void BookmarkManager::LoadBookmark(std::string const & filePath, bool isTemporar
|
|||
LoadBookmarkRoutine(filePath, isTemporaryFile);
|
||||
}
|
||||
|
||||
void BookmarkManager::ReloadBookmark(std::string const & filePath)
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
if (!m_loadBookmarksFinished || m_asyncLoadingInProgress)
|
||||
{
|
||||
m_bookmarkLoadingQueue.emplace_back(filePath, false, true);
|
||||
return;
|
||||
}
|
||||
NotifyAboutStartAsyncLoading();
|
||||
ReloadBookmarkRoutine(filePath);
|
||||
}
|
||||
|
||||
void BookmarkManager::LoadBookmarkRoutine(std::string const & filePath, bool isTemporaryFile)
|
||||
{
|
||||
GetPlatform().RunTask(Platform::Thread::File, [this, filePath, isTemporaryFile]()
|
||||
|
@ -2053,6 +2065,36 @@ void BookmarkManager::LoadBookmarkRoutine(std::string const & filePath, bool isT
|
|||
});
|
||||
}
|
||||
|
||||
void BookmarkManager::ReloadBookmarkRoutine(std::string const & filePath)
|
||||
{
|
||||
GetPlatform().RunTask(Platform::Thread::File, [this, filePath]()
|
||||
{
|
||||
if (m_needTeardown)
|
||||
return;
|
||||
|
||||
auto const ext = GetLowercaseFileExt(filePath);
|
||||
std::unique_ptr<kml::FileData> kmlData;
|
||||
if (ext == kKmlExtension)
|
||||
kmlData = LoadKmlFile(filePath, KmlFileType::Text);
|
||||
else if (ext == kGpxExtension)
|
||||
kmlData = LoadKmlFile(filePath, KmlFileType::Gpx);
|
||||
else
|
||||
ASSERT(false, ("Unsupported bookmarks extension", ext));
|
||||
|
||||
if (m_needTeardown)
|
||||
return;
|
||||
|
||||
auto collection = std::make_shared<KMLDataCollection>();
|
||||
if (kmlData)
|
||||
collection->emplace_back(std::move(filePath), std::move(kmlData));
|
||||
|
||||
if (m_needTeardown)
|
||||
return;
|
||||
|
||||
NotifyAboutFinishAsyncLoading(std::move(collection));
|
||||
});
|
||||
}
|
||||
|
||||
void BookmarkManager::NotifyAboutStartAsyncLoading()
|
||||
{
|
||||
if (m_needTeardown)
|
||||
|
@ -2088,7 +2130,10 @@ void BookmarkManager::NotifyAboutFinishAsyncLoading(KMLDataCollectionPtr && coll
|
|||
if (!m_bookmarkLoadingQueue.empty())
|
||||
{
|
||||
ASSERT(m_asyncLoadingInProgress, ());
|
||||
LoadBookmarkRoutine(m_bookmarkLoadingQueue.front().m_filename,
|
||||
if (m_bookmarkLoadingQueue.front().m_isReloading)
|
||||
ReloadBookmarkRoutine(m_bookmarkLoadingQueue.front().m_filename);
|
||||
else
|
||||
LoadBookmarkRoutine(m_bookmarkLoadingQueue.front().m_filename,
|
||||
m_bookmarkLoadingQueue.front().m_isTemporaryFile);
|
||||
m_bookmarkLoadingQueue.pop_front();
|
||||
}
|
||||
|
@ -2296,6 +2341,13 @@ bool BookmarkManager::HasBookmark(kml::MarkId markId) const
|
|||
return (GetBookmark(markId) != nullptr);
|
||||
}
|
||||
|
||||
bool BookmarkManager::HasTrack(kml::TrackId trackId) const
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
ASSERT(IsBookmark(trackId), ());
|
||||
return (GetTrack(trackId) != nullptr);
|
||||
}
|
||||
|
||||
void BookmarkManager::UpdateBmGroupIdList()
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
|
@ -2409,7 +2461,7 @@ void BookmarkManager::CheckAndResetLastIds()
|
|||
idStorage.ResetTrackId();
|
||||
}
|
||||
|
||||
bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId)
|
||||
bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId, bool deleteFile)
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
auto it = m_categories.find(groupId);
|
||||
|
@ -2419,7 +2471,8 @@ bool BookmarkManager::DeleteBmCategory(kml::MarkGroupId groupId)
|
|||
ClearGroup(groupId);
|
||||
m_changesTracker.OnDeleteGroup(groupId);
|
||||
|
||||
FileWriter::DeleteFileX(it->second->GetFileName());
|
||||
if (deleteFile)
|
||||
FileWriter::DeleteFileX(it->second->GetFileName());
|
||||
|
||||
DeleteCompilations(it->second->GetCategoryData().m_compilationIds);
|
||||
m_categories.erase(it);
|
||||
|
@ -3498,7 +3551,7 @@ void BookmarkManager::EditSession::SetCategoryCustomProperty(kml::MarkGroupId ca
|
|||
|
||||
bool BookmarkManager::EditSession::DeleteBmCategory(kml::MarkGroupId groupId)
|
||||
{
|
||||
return m_bmManager.DeleteBmCategory(groupId);
|
||||
return m_bmManager.DeleteBmCategory(groupId, true);
|
||||
}
|
||||
|
||||
void BookmarkManager::EditSession::NotifyChanges()
|
||||
|
|
|
@ -273,6 +273,7 @@ public:
|
|||
|
||||
std::string GetCategoryName(kml::MarkGroupId categoryId) const;
|
||||
std::string GetCategoryFileName(kml::MarkGroupId categoryId) const;
|
||||
kml::MarkGroupId GetCategoryByFileName(std::string const & fileName) const;
|
||||
m2::RectD GetCategoryRect(kml::MarkGroupId categoryId, bool addIconsSize) const;
|
||||
kml::CategoryData const & GetCategoryData(kml::MarkGroupId categoryId) const;
|
||||
|
||||
|
@ -283,6 +284,7 @@ public:
|
|||
size_t GetBmGroupsCount() const { return m_unsortedBmGroupsIdList.size(); };
|
||||
bool HasBmCategory(kml::MarkGroupId groupId) const;
|
||||
bool HasBookmark(kml::MarkId markId) const;
|
||||
bool HasTrack(kml::TrackId trackId) const;
|
||||
kml::MarkGroupId LastEditedBMCategory();
|
||||
kml::PredefinedColor LastEditedBMColor() const;
|
||||
|
||||
|
@ -300,6 +302,7 @@ public:
|
|||
/// Scans and loads all kml files with bookmarks.
|
||||
void LoadBookmarks();
|
||||
void LoadBookmark(std::string const & filePath, bool isTemporaryFile);
|
||||
void ReloadBookmark(std::string const & filePath);
|
||||
|
||||
/// Uses the same file name from which was loaded, or
|
||||
/// creates unique file name on first save and uses it every time.
|
||||
|
@ -573,7 +576,7 @@ private:
|
|||
void SetCategoryTags(kml::MarkGroupId categoryId, std::vector<std::string> const & tags);
|
||||
void SetCategoryAccessRules(kml::MarkGroupId categoryId, kml::AccessRules accessRules);
|
||||
void SetCategoryCustomProperty(kml::MarkGroupId categoryId, std::string const & key, std::string const & value);
|
||||
bool DeleteBmCategory(kml::MarkGroupId groupId);
|
||||
bool DeleteBmCategory(kml::MarkGroupId groupId, bool deleteFile);
|
||||
void ClearCategories();
|
||||
|
||||
void MoveBookmark(kml::MarkId bmID, kml::MarkGroupId curGroupID, kml::MarkGroupId newGroupID);
|
||||
|
@ -609,6 +612,7 @@ private:
|
|||
void NotifyAboutFinishAsyncLoading(KMLDataCollectionPtr && collection);
|
||||
void NotifyAboutFile(bool success, std::string const & filePath, bool isTemporaryFile);
|
||||
void LoadBookmarkRoutine(std::string const & filePath, bool isTemporaryFile);
|
||||
void ReloadBookmarkRoutine(std::string const & filePath);
|
||||
|
||||
using BookmarksChecker = std::function<bool(kml::FileData const &)>;
|
||||
KMLDataCollectionPtr LoadBookmarks(std::string const & dir, std::string_view ext,
|
||||
|
@ -628,8 +632,6 @@ private:
|
|||
kml::MarkGroupId CheckAndCreateDefaultCategory();
|
||||
void CheckAndResetLastIds();
|
||||
|
||||
kml::MarkGroupId GetCategoryByFileName(std::string const & fileName) const;
|
||||
|
||||
std::unique_ptr<kml::FileData> CollectBmGroupKMLData(BookmarkCategory const * group) const;
|
||||
KMLDataCollectionPtr PrepareToSaveBookmarks(kml::GroupIdCollection const & groupIdCollection);
|
||||
|
||||
|
@ -761,12 +763,13 @@ private:
|
|||
bool m_asyncLoadingInProgress = false;
|
||||
struct BookmarkLoaderInfo
|
||||
{
|
||||
BookmarkLoaderInfo(std::string const & filename, bool isTemporaryFile)
|
||||
: m_filename(filename), m_isTemporaryFile(isTemporaryFile)
|
||||
BookmarkLoaderInfo(std::string const & filename, bool isTemporaryFile, bool isReloading)
|
||||
: m_filename(filename), m_isTemporaryFile(isTemporaryFile), m_isReloading(isReloading)
|
||||
{}
|
||||
|
||||
std::string m_filename;
|
||||
bool m_isTemporaryFile = false;
|
||||
bool m_isReloading = false;
|
||||
};
|
||||
std::list<BookmarkLoaderInfo> m_bookmarkLoadingQueue;
|
||||
|
||||
|
|
This code monitor only directory changes, not file changes. E.g. if you edit/overwrite some file in-place, it won't fire. If you add a new file, delete, change file name, then it fires.
Our bookmarks saving code should create a new temporary file, and move it to overwrite the old file atomically, that's why it works.
However, there could be some other cases when files are just overwritten/appended, without modifying the directory.
Another case to test: what if user uploads some good/bad kml files (and other types too) using Finder/File Sharing into OM's monitored folder?