diff --git a/CHANGELOG.md b/CHANGELOG.md index e27855f4ad..4838fb4c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [5.0.5] - 2023-12-20 + +### Fixed + +* Fix: Move waiting for lock out of adapters into controller +* fix(NextcloudBookmarks): Use CapacitorHttp to avoid cors errors in capacitor 5 +* fix(native/START_LOGIN_FLOW): migrate to new capacitor http API + +## [5.0.4] - 2023-12-15 + +### Fixed + +* [native] upgrade capacitor-oauth2 +* [native] fix(GoogleDrive): CapacitorHttp no longer encodes x-form-urlencoded +* fix(Import): Request network permissions before import +* fix(GoogleDrive): Request network permissions before login + +## [5.0.3] - 2023-12-12 + +### Fixed + +- [native] Remove capacitor community http Marcel Klehr 36 minutes ago +- [native] fix(DialogImportBookmarks): accept="text/html" +- [android] fix(webdav): Use new builtin CapacitorHttp +- fix(Unlock with credentials): Missing await 🙈 +- fix(Profile import) +- fix(options): Auto-sync option was not saved +- fix(GoogleDrive): Fix permissions.contains syntax +- fix: Always cast to string before comparing item ids +- fix(HtmlSerializer): Try to fix ordering test +- fix(HtmlSerializer): Use Cheerio.text() for getting title + ## [5.0.2] - 2023-12-09 ### Fixed diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5c1b2bb911..929bc438e1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -671,5 +671,11 @@ }, "LabelImportsuccessful": { "message": "Successfully imported profile(s)" + }, + "DescriptionSyncinprogress": { + "message": "Synchronization in progress." + }, + "DescriptionSyncscheduled": { + "message": "This profile will be synced soon. We're either waiting for other devices of yours, or other profiles on this device, to finish syncing." } } diff --git a/_locales/gl/messages.json b/_locales/gl/messages.json index 479cced041..2374490944 100644 --- a/_locales/gl/messages.json +++ b/_locales/gl/messages.json @@ -164,6 +164,9 @@ "LabelCancelsync": { "message": "Cancelar a sincronización" }, + "LabelSyncall": { + "message": "Sincronizar todos os perfís" + }, "LabelAutosync": { "message": "Sincronización automática" }, @@ -185,6 +188,9 @@ "StatusSyncing": { "message": "Sincronizando" }, + "StatusScheduled": { + "message": "Programada" + }, "LabelReset": { "message": "Restabelecer" }, @@ -198,10 +204,10 @@ "message": "Definir un cartafol existente para sincronizar" }, "LabelRemoveaccount": { - "message": "Retirar a conta" + "message": "Retirar o perfil" }, "DescriptionRemoveaccount": { - "message": "Eliminar esta conta (isto non eliminará os marcadores)" + "message": "Eliminar este perfil (isto non eliminará os marcadores)" }, "LabelSyncfromscratch": { "message": "Activar a sincronización dende cero" @@ -261,7 +267,7 @@ "message": "Marcadores de Nextcloud (herdados)" }, "DescriptionAdapternextcloud": { - "message": "A opción herdada é compatíbel polo menos coa versión v0.11 da aplicación Marcadores. Emulará cartafoles usando etiquetas que conteñan a ruta do cartafol. Non se recomenda usar isto para contas novas." + "message": "A opción herdada é compatíbel polo menos coa versión v0.11 da aplicación Marcadores. Emulará cartafoles usando etiquetas que conteñan a ruta do cartafol. Non se recomenda usar isto para novos perfís." }, "LabelAdapterwebdav": { "message": "Compartir con WebDAV" @@ -270,7 +276,7 @@ "message": "A opción WebDAV sincroniza os marcadores almacenándoos nun ficheiro no recurso compartido WebDAV fornecido. Non hai unha interface de usuario web para esta opción e pode usala con calquera servidor compatíbel con WebDAV. Pode sincronizar os marcadores http, ftp, datos, ficheiros e javascript." }, "LabelAddaccount": { - "message": "Engadir unha conta" + "message": "Engadir perfil" }, "LabelOpenintab": { "message": "Abrir na lapela" @@ -319,7 +325,7 @@ }, "LabelOptionsscreen": { "message": "{0} opcións", - "description": "Title of the options screen. The placeholder holds the account type." + "description": "Title of the options screen. The placeholder holds the profile type." }, "LabelPaypal": { "message": "Paypal" @@ -346,7 +352,7 @@ "message": "Faga unha doazón regular a través dos patrocinadores de GitHub para colaborar co proxecto" }, "LegacyAdapterDeprecation": { - "message": "Este tipo de conta herdado está en desuso e vai ser eliminado en pouco tempo. Cambie ao novo método de sincronización de Nextcloud. Agárdanlle melloras de rendemento e precisión." + "message": "Este tipo de perfil herdado está en desuso e vai ser eliminado en pouco tempo. Cambie ao novo método de sincronización de Nextcloud. Agárdanlle melloras de rendemento e precisión." }, "LabelUpdated": { "message": "Floccus foi actualizado" @@ -370,16 +376,16 @@ "message": "Accións perigosas" }, "LabelAccountDeleted": { - "message": "Conta eliminada" + "message": "Perfil eliminado" }, "DescriptionAccountDeleted": { - "message": "Esta conta foi eliminada" + "message": "Este perfil foi eliminado" }, "LabelNoAccount": { - "message": "Aquí non hai ningunha conta" + "message": "Aquí non hai ningún perfil" }, "DescriptionNoAccount": { - "message": "Crea unha conta nova para sincronizar os marcadores ou importar contas dende un dispositivo ou navegador diferente." + "message": "Crea un novo perfil para sincronizar os marcadores ou importar perfís dende un dispositivo ou navegador diferente." }, "LabelLoginFlowStart": { "message": "Acceder con Nextcloud" @@ -391,34 +397,34 @@ "message": "Produciuse n fallo ao acceder a Nextcloud" }, "LabelNewAccount": { - "message": "Nova conta" + "message": "Perfil novo" }, "LabelNestedSync": { - "message": "Contas aniñadas" + "message": "Perfís aniñados" }, "DescriptionNestedSync": { - "message": "Pode aniñar contas para que un cartafol principal pertenza á conta A e un subcartafol á conta A e B. Quere permitir que outras contas sincronicen tamén o cartafol desta conta?" + "message": "Pode aniñar perfís para que un cartafol principal pertenza ao perfil A e un subcartafol ao perfil A e B. Quere permitir que outros perfís sincronicen tamén o cartafol deste perfil?" }, "LabelNestedSyncNo": { - "message": "Non, ignorar o cartafol desta conta noutras contas" + "message": "Non, ignorar o cartafol deste perfil noutros perfís" }, "LabelNestedSyncYes": { - "message": "Si, incluír o cartafol desta conta noutras contas" + "message": "Si, incluír o cartafol deste perfil noutros perfís" }, "LabelImportExport": { - "message": "Importar/Exportar contas" + "message": "Importar/exportar perfís" }, "LabelExport": { - "message": "Exportar contas" + "message": "Exportar perfís" }, "LabelImport": { - "message": "Importar contas" + "message": "Importar perfís" }, "DescriptionExport": { - "message": "Seleccione a seguir as contas que quere exportar a un ficheiro, de xeito que poida volver crear doadamente as mesmas contas nun dispositivo ou navegador diferente." + "message": "Seleccione a seguir os perfís que quere exportar a un ficheiro, de xeito que poida volver crear doadamente os mesmos perfís nun dispositivo ou navegador diferente." }, "DescriptionImport": { - "message": "Importe aquí un ficheiro coas contas exportadas para volver crear contas exportadas nun dispositivo ou navegador diferente. Asegúrese de configurar de novo os cartafoles de sincronización correctos após importar." + "message": "Importe aquí un ficheiro cos perfís exportados para volver crear perfís exportados nun dispositivo ou navegador diferente. Asegúrese de configurar de novo os cartafoles de sincronización correctos após importar." }, "LabelFolderNotFound": { "message": "Non se atopou o cartafol" @@ -475,7 +481,7 @@ "message": "Desactivado. Permitir a eliminación de máis do 50% dos marcadores locais sen preguntar para confirmar." }, "StatusFailsafeoff": { - "message": "A proba de fallos desactivada. Corre o risco de perder datos de xeito involuntario. Recoméndase activar a proba de fallos na configuración da conta." + "message": "A proba de fallos desactivada. Corre o risco de perder datos de xeito involuntario. Recoméndase activar a proba de fallos na configuración do perfíl." }, "LabelAdaptergoogledrive": { "message": "Google Drive" @@ -628,7 +634,10 @@ "message": "Como quere que funcione a sincronización?" }, "LabelAccountcreated": { - "message": "Conta creada" + "message": "Perfil creado" + }, + "DescriptionAccountcreated": { + "message": "Creouse o seu perfil. Xa pode pechar esta lapela." }, "DescriptionNonhttps": { "message": "Introduciu un servidor que usa un protocolo inseguro. Recoméndase utilizar só servidores compatíbeis con HTTPS." @@ -655,9 +664,12 @@ "message": "Exportar marcadores" }, "DescriptionExportBookmarks" : { - "message": "Pode exportar todos os marcadores desta conta como ficheiro HTML compatíbel con todos os principais navegadores." + "message": "Pode exportar todos os marcadores deste perfil como ficheiro HTML compatíbel con todos os principais navegadores." }, "LabelShareitem": { "message": "Compartir" + }, + "LabelImportsuccessful": { + "message": "Perfís importados correctamente" } } diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 365350a2a8..3d35233566 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -90,7 +90,7 @@ "message": "E030: ブックマークファイルの復号に失敗しました。パスフレーズが間違っているか、ファイルが破損しています。" }, "Error031": { - "message": "E031: Google ドライブでクラウドを認証できませんでした。もう一度、floccus を Google アカウントと接続してください。" + "message": "E031: Google ドライブに認証できませんでした。floccus を Google アカウントと再接続してください。" }, "Error032": { "message": "E032: OAuth エラー。トークンの検証でエラーが発生しました。もう一度、Google アカウントと接続してください。" @@ -104,6 +104,9 @@ "Error035": { "message": "E035: 次のブックマークのサーバーへの作成に失敗しました: {0}" }, + "Error036": { + "message": "E036: サーバーと同期する権限がありません" + }, "LabelWebdavurl": { "message": "WebDAV URL" }, @@ -129,16 +132,22 @@ "message": "Googleドライブに保存するブックマークファイルの名前 (ドライブ内でファイル名が重複していないことを確認してください)" }, "LabelServerfolder": { - "message": "サーバーフォルダー" + "message": "サーバーの対象" }, "DescriptionServerfolder": { - "message": "アカウントがサーバー上で動作するためのパスのプレフィックスです。プレフィックスを使用しない場合は空欄のままにしてください。" + "message": "同期するとき、このブラウザーのブックマークはサーバーのこのパスにリンクとして保存されます。このパスは Nextcloud Bookmarks アプリ内のフォルダーを示すもので、Nextcloud Files でのフォルダーを示すものではありません。サーバーの最上位フォルダーにあるすべてのリンクを同期するには、空白のままにしてください。" + }, + "LabelLocaltarget": { + "message": "ローカルの対象" + }, + "DescriptionLocaltarget": { + "message": "ブラウザーのブックマークを同期するか、タブを同期するか選択してください。" }, "LabelLocalfolder": { - "message": "ローカルフォルダー" + "message": "ブックマークフォルダー" }, "DescriptionLocalfolder": { - "message": "このブラウザーからはこのローカル ブックマーク フォルダーがサーバーに同期されます。一部のブラウザー (Firefox、Google Chrome など) はルートフォルダーへの新しいアイテムの作成を許可していないことに注意してください" + "message": "ブックマークフォルダーのブックマークはサーバーにリンクとして保存され、サーバーのリンクはブラウザーのブックマークフォルダーにブックマークとして保存されます。" }, "LabelRootfolder": { "message": "ルートフォルダー" @@ -155,6 +164,9 @@ "LabelCancelsync": { "message": "同期をキャンセル" }, + "LabelSyncall": { + "message": "すべてのプロファイルを同期" + }, "LabelAutosync": { "message": "自動同期" }, @@ -176,6 +188,9 @@ "StatusSyncing": { "message": "同期中" }, + "StatusScheduled": { + "message": "設定済み" + }, "LabelReset": { "message": "リセット" }, @@ -189,10 +204,10 @@ "message": "同期対象の既存のフォルダーを選択する" }, "LabelRemoveaccount": { - "message": "アカウントを削除" + "message": "プロファイルを削除" }, "DescriptionRemoveaccount": { - "message": "このアカウントを削除 (ブックマークは削除しません)" + "message": "このプロファイルを削除 (ブックマークは削除しません)" }, "LabelSyncfromscratch": { "message": "スクラッチしてから同期をトリガー" @@ -252,7 +267,7 @@ "message": "Nextcloud Bookmarks (レガシー)" }, "DescriptionAdapternextcloud": { - "message": "レガシーオプションは、バージョン 0.11 以降のブックマークアプリに対応しています。フォルダーのパスを含むタグを使用してフォルダーをエミュレートします。新しいアカウントを作成してこちらを使用することはおすすめできません。" + "message": "レガシーオプションは、バージョン 0.11 以降のブックマークアプリに対応しています。フォルダーのパスを含むタグを使用してフォルダーをエミュレートします。新しいプロファイルを作成してこちらを使用することはおすすめできません。" }, "LabelAdapterwebdav": { "message": "WebDAV 共有" @@ -261,13 +276,7 @@ "message": "WebDAV オプションは、提供された WebDAV 共有のファイルにブックマークを保存して同期します。ウェブ UI はなく、任意の WebDAV 互換サーバーで使用できます。http、ftp、データ、ファイル、JavaScript のブックマークを同期できます。" }, "LabelAddaccount": { - "message": "アカウントを追加" - }, - "LabelSecurecredentials": { - "message": "認証情報を保護" - }, - "LabelSecuredcredentials": { - "message": "認証情報は保護されています" + "message": "プロファイルを追加" }, "LabelOpenintab": { "message": "タブで開く" @@ -287,12 +296,6 @@ "LabelUntitledfolder": { "message": "名前のないフォルダー" }, - "LabelSetkey": { - "message": "floccus にパスフレーズを設定" - }, - "DescriptionSetkey": { - "message": "パスフレーズを設定すると、ブックマークを同期したいとき、設定を変更したいときなど floccus に関することをするとき、このパスフレーズをブラウザーを起動するごとに入力する必要があります。" - }, "LabelSetkeybutton": { "message": "パスフレーズを設定" }, @@ -322,7 +325,7 @@ }, "LabelOptionsscreen": { "message": "{0} 件のオプション", - "description": "Title of the options screen. The placeholder holds the account type." + "description": "Title of the options screen. The placeholder holds the profile type." }, "LabelPaypal": { "message": "Paypal" @@ -349,7 +352,7 @@ "message": "GitHub sponsors で定期的に寄付をしてプロジェクトを支援する" }, "LegacyAdapterDeprecation": { - "message": "このレガシーアカウントタイプは非推奨になり、間もなく削除されます。新しい Nextcloud の同期方法に変更してください。改善されたパフォーマンスと精度でななたをお待ちしています。" + "message": "このレガシープロファイルタイプは非推奨になり、間もなく削除されます。新しい Nextcloud の同期方法に変更してください。改善されたパフォーマンスと精度でななたをお待ちしています。" }, "LabelUpdated": { "message": "floccus が更新されました" @@ -373,16 +376,16 @@ "message": "危険なアクション" }, "LabelAccountDeleted": { - "message": "アカウントが削除されました" + "message": "プロファイルが削除されました" }, "DescriptionAccountDeleted": { - "message": "このアカウントは削除されました" + "message": "このプロファイルは削除されました" }, "LabelNoAccount": { - "message": "アカウントがありません" + "message": "プロファイルがありません" }, "DescriptionNoAccount": { - "message": "ブックマークを同期するには、新しいアカウントを作成するか、別の端末やブラウザーからアカウントをインポートしてください。" + "message": "新しいプロファイルを作成してブックマークを同期するか、他の端末やブラウザーからプロファイルをインポートしてください。" }, "LabelLoginFlowStart": { "message": "Nextcloud でログイン" @@ -394,40 +397,43 @@ "message": "Nectcloud ログインに失敗しました" }, "LabelNewAccount": { - "message": "新しいアカウント" + "message": "新しいプロファイル" }, "LabelNestedSync": { - "message": "入れ子アカウント" + "message": "入れ子プロファイル" }, "DescriptionNestedSync": { - "message": "親フォルダーがアカウント A に属し、子フォルダーがアカウント A と B に属するというように、アカウントを入れ子にすることができます。他のアカウントにこのアカウントのフォルダーを同期することを許可しますか?" + "message": "親フォルダーがプロファイル A に属し、子フォルダーがプロファイル A と B に属するというように、プロファイルを入れ子にすることができます。他のプロファイルにこのプロファイル内のフォルダーを同期することを許可しますか?" }, "LabelNestedSyncNo": { - "message": "いいえ、他のアカウントではこのアカウントのフォルダーを無視します" + "message": "いいえ、他のプロファイルではこのアカウントのフォルダーを無視します" }, "LabelNestedSyncYes": { - "message": "はい、他のアカウントにこのアカウントのフォルダーを含めます" + "message": "はい、他のプロファイルにこのアカウントのフォルダーを含めます" }, "LabelImportExport": { - "message": "アカウントのインポート/エクスポート" + "message": "プロファイルのインポート/エクスポート" }, "LabelExport": { - "message": "アカウントをエクスポート" + "message": "プロファイルをエクスポート" }, "LabelImport": { - "message": "アカウントをインポート" + "message": "プロファイルをインポート" }, "DescriptionExport": { - "message": "次から、ファイルにエクスポートするアカウントを選択してください。同じアカウントを別の端末やブラウザーで簡単に再作成できるようになります。" + "message": "次から、ファイルにエクスポートするプロファイルを選択してください。同じプロファイルを別の端末やブラウザーで簡単に再設定できるようになります。" }, "DescriptionImport": { - "message": "別の端末やブラウザーでファイルにエクスポートしたアカウントを再作成できます。インポートした後、正しい同期フォルダーを再設定してください。" + "message": "別の端末やブラウザーでファイルにエクスポートしたプロファイルを再作成できます。インポートした後、正しい同期フォルダーを再設定してください。" }, "LabelFolderNotFound": { "message": "フォルダーが見つかりませんでした" }, "LabelSyncTabs": { - "message": "タブを同期" + "message": "ブラウザーのタブ" + }, + "DescriptionSyncTabs": { + "message": "サーバーに保存されているリンクはブラウザーのタブとして開かれ、既に開いているタブはサーバーにリンクとして保存されます。次の同期実行時にすべてのリンクがタブとして開かれるため、サーバーに保存されているリンクの数によってはブラウザーが負荷に耐えられない可能性があります。" }, "LabelTabs": { "message": "タブ" @@ -475,7 +481,7 @@ "message": "無効。確認なしでローカルブックマークの 50% 以上を削除することを許可します。" }, "StatusFailsafeoff": { - "message": "フィールセーフが無効になりました。意図しないデータ消失のリスクがあります。アカウント設定でフィールセーフを有効にすることを推奨します。" + "message": "フィールセーフが無効になりました。意図しないデータ消失のリスクがあります。プロファイル設定でフィールセーフを有効にすることを推奨します。" }, "LabelAdaptergoogledrive": { "message": "Google ドライブ" @@ -628,7 +634,10 @@ "message": "どのように同期しますか?" }, "LabelAccountcreated": { - "message": "アカウントが作成されました" + "message": "プロファイルを作成しました" + }, + "DescriptionAccountcreated": { + "message": "プロファイルが作成されました。このタブは閉じても構いません。" }, "DescriptionNonhttps": { "message": "安全ではないプロトコルを使用するサーバーを入力しました。HTTPS に対応するサーバーの使用を推奨しています。" @@ -655,9 +664,12 @@ "message": "ブックマークをエクスポート" }, "DescriptionExportBookmarks" : { - "message": "このアカウントのブックマークを、すべてのメジャーなブラウザーと互換性のある HTML ファイルとしてエクスポートできます。" + "message": "このプロファイルのブックマークを、すべてのメジャーなブラウザーと互換性のある HTML ファイルとしてエクスポートできます。" }, "LabelShareitem": { "message": "共有" + }, + "LabelImportsuccessful": { + "message": "正常にプロファイルをインポートしました" } } diff --git a/android/app/build.gradle b/android/app/build.gradle index 2813e6599a..9def8c7ef7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "org.handmadeideas.floccus" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 5000002 - versionName "5.0.2" + versionCode 5000005 + versionName "5.0.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index d73deb7748..e954d8b0c6 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -10,7 +10,6 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':byteowls-capacitor-oauth2') - implementation project(':capacitor-community-http') implementation project(':capacitor-app') implementation project(':capacitor-device') implementation project(':capacitor-filesystem') diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 3a228eac75..14d23c99eb 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -3,10 +3,6 @@ "pkg": "@byteowls/capacitor-oauth2", "classpath": "com.byteowls.capacitor.oauth2.OAuth2ClientPlugin" }, - { - "pkg": "@capacitor-community/http", - "classpath": "com.getcapacitor.plugin.http.Http" - }, { "pkg": "@capacitor/app", "classpath": "com.capacitorjs.plugins.app.AppPlugin" diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 55f204d881..af21eaefe7 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -5,9 +5,6 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/ include ':byteowls-capacitor-oauth2' project(':byteowls-capacitor-oauth2').projectDir = new File('../node_modules/@byteowls/capacitor-oauth2/android') -include ':capacitor-community-http' -project(':capacitor-community-http').projectDir = new File('../node_modules/@capacitor-community/http/android') - include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index b5814bee51..779272ddcd 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -12,7 +12,6 @@ def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'ByteowlsCapacitorOauth2', :path => '../../node_modules/@byteowls/capacitor-oauth2' - pod 'CapacitorCommunityHttp', :path => '../../node_modules/@capacitor-community/http' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' diff --git a/manifest.chrome.json b/manifest.chrome.json index af0b3f02d5..6a7f88186d 100644 --- a/manifest.chrome.json +++ b/manifest.chrome.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "floccus bookmarks sync", "short_name": "floccus", - "version": "5.0.2", + "version": "5.0.5", "description": "__MSG_DescriptionExtension__", "icons": { "48": "icons/logo.png", diff --git a/manifest.firefox.json b/manifest.firefox.json index fa60db22ea..ff938cd17a 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "floccus bookmarks sync", "short_name": "floccus", - "version": "5.0.2", + "version": "5.0.5", "description": "__MSG_DescriptionExtension__", "icons": { "48": "icons/logo.png", diff --git a/manifest.json b/manifest.json index af0b3f02d5..6a7f88186d 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "floccus bookmarks sync", "short_name": "floccus", - "version": "5.0.2", + "version": "5.0.5", "description": "__MSG_DescriptionExtension__", "icons": { "48": "icons/logo.png", diff --git a/package-lock.json b/package-lock.json index c06f2aa2c5..374e8c3240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "floccus", - "version": "5.0.2", + "version": "5.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "floccus", - "version": "5.0.2", + "version": "5.0.5", "license": "MPL-2.0", "dependencies": { - "@byteowls/capacitor-oauth2": "4.x", - "@capacitor-community/http": "^1.4.1", + "@byteowls/capacitor-oauth2": "5.x", "@capacitor/android": "^5.0.0", "@capacitor/app": "^5.0.0", "@capacitor/core": "^5.0.0", @@ -1773,54 +1772,11 @@ } }, "node_modules/@byteowls/capacitor-oauth2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@byteowls/capacitor-oauth2/-/capacitor-oauth2-4.0.2.tgz", - "integrity": "sha512-9D8/pXVUf43HzUY0N4CATLGN4ldpsnj3dVfL3RPix6i3hbqitsoOdgHaWTNv4Iwjz4s+onF+DK/2f2z1kHX3Ag==", - "peerDependencies": { - "@capacitor/core": ">=4" - } - }, - "node_modules/@capacitor-community/http": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@capacitor-community/http/-/http-1.4.1.tgz", - "integrity": "sha512-+pCkBXrwfm97UfjOgjV950H/qZ8SE36Mrcb46BlL1ps3VIsGuIO+AulL8GqTC6LewheRVtGJpRspNtneXQotNA==", - "dependencies": { - "@capacitor/android": "^3.0.0", - "@capacitor/core": "^3.0.0", - "@capacitor/filesystem": "^1.0.0", - "@capacitor/ios": "^3.0.0" - } - }, - "node_modules/@capacitor-community/http/node_modules/@capacitor/android": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.9.0.tgz", - "integrity": "sha512-YTPyrh1NozEuYXWGtfqN27TLXUrLbZX9fggyd4JQ1yMaUZTmLPm5dCuznONhQ49aPkJnUJB02JfpHy/qGwa2Lw==", - "peerDependencies": { - "@capacitor/core": "^3.9.0" - } - }, - "node_modules/@capacitor-community/http/node_modules/@capacitor/core": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-3.9.0.tgz", - "integrity": "sha512-j1lL0+/7stY8YhIq1Lm6xixvUqIn89vtyH5ZpJNNmcZ0kwz6K9eLkcG6fvq1UWMDgSVZg9JrRGSFhb4LLoYOsw==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@capacitor-community/http/node_modules/@capacitor/filesystem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-1.1.0.tgz", - "integrity": "sha512-8O3UuvL8HNUEJvZnmn8yUmvgB1evtXfcF0oxIo3YbSlylqywJwS3JTiuhKmsvSxCdpbTy8IaTsutVh3gZgWbKg==", - "peerDependencies": { - "@capacitor/core": "^3.0.0" - } - }, - "node_modules/@capacitor-community/http/node_modules/@capacitor/ios": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.9.0.tgz", - "integrity": "sha512-GezPCJIujRHnF4wbrKJx6Q/mgFz0f9rmh/steTTXQZI+nEl6mHk6NWh8235p7YbhonYi5WD0rFNirrjGg1EaGw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@byteowls/capacitor-oauth2/-/capacitor-oauth2-5.0.0.tgz", + "integrity": "sha512-yW50GypmyPJcH/95NwR2jJcgT78vBN3FYKL2w6A3vrT04bRLQyw2K0fLqfj8Zws6DJy43Ck1wPs0Bcdvbsub7A==", "peerDependencies": { - "@capacitor/core": "^3.9.0" + "@capacitor/core": ">=5" } }, "node_modules/@capacitor/android": { diff --git a/package.json b/package.json index 46cdf58583..3c8a83b2dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "floccus", - "version": "5.0.2", + "version": "5.0.5", "description": "Sync your bookmarks privately across browsers and devices", "scripts": { "build": "gulp", @@ -70,8 +70,7 @@ "webpack-merge": "^4.2.2" }, "dependencies": { - "@byteowls/capacitor-oauth2": "4.x", - "@capacitor-community/http": "^1.4.1", + "@byteowls/capacitor-oauth2": "5.x", "@capacitor/android": "^5.0.0", "@capacitor/app": "^5.0.0", "@capacitor/core": "^5.0.0", diff --git a/src/errors/Error.ts b/src/errors/Error.ts index 9c67899443..0e4bfcd206 100644 --- a/src/errors/Error.ts +++ b/src/errors/Error.ts @@ -305,3 +305,11 @@ export class MissingPermissionsError extends FloccusError { Object.setPrototypeOf(this, MissingPermissionsError.prototype) } } + +export class ResourceLockedError extends FloccusError { + constructor() { + super(`E037: Resource is locked`) + this.code = 37 + Object.setPrototypeOf(this, MissingPermissionsError.prototype) + } +} diff --git a/src/lib/Account.ts b/src/lib/Account.ts index 6b3a1977d2..005ce39274 100644 --- a/src/lib/Account.ts +++ b/src/lib/Account.ts @@ -10,6 +10,7 @@ import { IResource, TLocalTree } from './interfaces/Resource' import { Capacitor } from '@capacitor/core' import IAccount from './interfaces/Account' import Mappings from './Mappings' +import { ResourceLockedError } from '../errors/Error' // register Adapters AdapterFactory.register('nextcloud-folders', async() => (await import('./adapters/NextcloudBookmarks')).default) @@ -146,7 +147,7 @@ export default class Account { Logger.log('Starting sync process for account ' + this.getLabel()) this.syncing = true - await this.setData({ ...this.getData(), syncing: 0.05, error: null }) + await this.setData({ ...this.getData(), syncing: 0.05, scheduled: false, error: null }) if (!(await this.isInitialized())) { await this.init() @@ -163,7 +164,23 @@ export default class Account { if (this.server.onSyncStart) { const needLock = (strategy || this.getData().strategy) !== 'slave' - const status = await this.server.onSyncStart(needLock) + let status + try { + status = await this.server.onSyncStart(needLock) + } catch (e) { + // Resource locked + if (e.code === 37) { + await this.setData({ ...this.getData(), error: null, syncing: false, scheduled: true }) + this.syncing = false + Logger.log( + 'Resource is locked, trying again soon' + ) + await Logger.persist() + return + } else { + throw e + } + } if (status === false) { await this.init() } @@ -210,7 +227,7 @@ export default class Account { } await this.syncProcess.sync() - await this.setData({ ...this.getData(), syncing: 1 }) + await this.setData({ ...this.getData(), scheduled: false, syncing: 1 }) // update cache if (localResource.constructor.name !== 'LocalTabs') { @@ -234,6 +251,7 @@ export default class Account { ...this.getData(), error: null, syncing: false, + scheduled: false, lastSync: Date.now(), }) @@ -250,6 +268,7 @@ export default class Account { ...this.getData(), error: message, syncing: false, + scheduled: false, }) this.syncing = false if (this.server.onSyncFail) { diff --git a/src/lib/Diff.ts b/src/lib/Diff.ts index 8c4cea8517..b1436649ea 100644 --- a/src/lib/Diff.ts +++ b/src/lib/Diff.ts @@ -158,7 +158,7 @@ export default class Diff { const DAG = folderMoves .reduce((DAG, action1) => { DAG[action1.payload.id] = folderMoves.filter(action2 => { - if (action1 === action2 || action1.payload.id === action2.payload.id) { + if (action1 === action2 || String(action1.payload.id) === String(action2.payload.id)) { return false } return ( diff --git a/src/lib/LocalTabs.ts b/src/lib/LocalTabs.ts index 4a3b8a9de3..f651c75043 100644 --- a/src/lib/LocalTabs.ts +++ b/src/lib/LocalTabs.ts @@ -117,7 +117,7 @@ export default class LocalTabs implements IResource { // Not perfect but good enough (Problem: [a,X,c] => insert(b,0) => [b, X, a, c]) if (originalTabs.length !== order.length) { const untouchedChildren = originalTabs.map((tab, i) => [i, tab]).filter(([, tab]) => - !order.some(item => tab.id === item.id) + !order.some(item => String(tab.id) === String(item.id)) ) try { for (const [index, child] of untouchedChildren) { diff --git a/src/lib/Scanner.ts b/src/lib/Scanner.ts index 1c70bc50ca..83630f4ac2 100644 --- a/src/lib/Scanner.ts +++ b/src/lib/Scanner.ts @@ -224,7 +224,7 @@ export default class Scanner { const moves = this.diff.getActions(ActionType.MOVE) const updates = this.diff.getActions(ActionType.UPDATE) updates.forEach(update => { - if (moves.find(move => move.payload.id === update.payload.id)) { + if (moves.find(move => String(move.payload.id) === String(update.payload.id))) { this.diff.retract(update) } }) @@ -265,7 +265,7 @@ export default class Scanner { for (const folderId in targets) { const newFolder = this.newTree.findItem(ItemType.FOLDER, folderId) as Folder - const duplicate = this.diff.getActions(ActionType.REORDER).find(a => a.payload.id === newFolder.id) + const duplicate = this.diff.getActions(ActionType.REORDER).find(a => String(a.payload.id) === String(newFolder.id)) if (duplicate) { this.diff.retract(duplicate) } diff --git a/src/lib/Tree.ts b/src/lib/Tree.ts index 7415229679..b4511f248e 100644 --- a/src/lib/Tree.ts +++ b/src/lib/Tree.ts @@ -363,7 +363,7 @@ export class Folder { static getAncestorsOf(item: TItem, tree: Folder): TItem[] { const ancestors = [item] let parent = item - while (parent.id !== tree.id) { + while (String(parent.id) !== String(tree.id)) { ancestors.push(parent) parent = tree.findItem(ItemType.FOLDER, parent.parentId) if (!parent) { diff --git a/src/lib/adapters/Caching.ts b/src/lib/adapters/Caching.ts index 058828e949..e7da227b6a 100644 --- a/src/lib/adapters/Caching.ts +++ b/src/lib/adapters/Caching.ts @@ -65,7 +65,7 @@ export default class CachingAdapter implements Adapter { } foundBookmark.url = newBm.url foundBookmark.title = newBm.title - if (foundBookmark.parentId === newBm.parentId) { + if (String(foundBookmark.parentId) === String(newBm.parentId)) { return } const foundOldFolder = this.bookmarksCache.findFolder( @@ -154,12 +154,12 @@ export default class CachingAdapter implements Adapter { } order.forEach(item => { const child = folder.findItem(item.type, item.id) - if (!child || child.parentId !== folder.id) { + if (!child || String(child.parentId) !== String(folder.id)) { throw new UnknownFolderItemOrderError(id + ':' + JSON.stringify(item)) } }) folder.children.forEach(child => { - const item = order.find((item) => item.type === child.type && item.id === child.id) + const item = order.find((item) => item.type === child.type && String(item.id) === String(child.id)) if (!item) { throw new MissingItemOrderError( id + ':' + child.inspect() diff --git a/src/lib/adapters/GoogleDrive.ts b/src/lib/adapters/GoogleDrive.ts index bf882ca1b2..814c67defc 100644 --- a/src/lib/adapters/GoogleDrive.ts +++ b/src/lib/adapters/GoogleDrive.ts @@ -8,11 +8,10 @@ import { DecryptionError, FileUnreadableError, GoogleDriveAuthenticationError, InterruptedSyncError, MissingPermissionsError, NetworkError, - OAuthTokenError + OAuthTokenError, ResourceLockedError } from '../../errors/Error' import { OAuth2Client } from '@byteowls/capacitor-oauth2' -import { Capacitor } from '@capacitor/core' -import { Http } from '@capacitor-community/http' +import { Capacitor, CapacitorHttp as Http } from '@capacitor/core' const OAuthConfig = { authorizationBaseUrl: 'https://accounts.google.com/o/oauth2/auth', @@ -69,7 +68,7 @@ export default class GoogleDriveAdapter extends CachingAdapter { if (platform === 'web') { const browser = (await import('../browser-api')).default - const origins = ['https://oauth2.googleapis.com', 'https://www.googleapis.com'] + const origins = ['https://oauth2.googleapis.com/', 'https://www.googleapis.com/'] if (!(await browser.permissions.contains({ origins }))) { throw new MissingPermissionsError() } @@ -143,13 +142,12 @@ export default class GoogleDriveAdapter extends CachingAdapter { async getAccessToken(refreshToken:string) { const platform = Capacitor.getPlatform() - const credentialType = platform const response = await this.request('POST', 'https://oauth2.googleapis.com/token', { refresh_token: refreshToken, - client_id: Credentials[credentialType].client_id, - ...(credentialType === 'web' && {client_secret: Credentials.web.client_secret}), + client_id: Credentials[platform].client_id, + ...(platform === 'web' && {client_secret: Credentials.web.client_secret}), grant_type: 'refresh_token', }, 'application/x-www-form-urlencoded' @@ -193,7 +191,7 @@ export default class GoogleDriveAdapter extends CachingAdapter { }) } - async onSyncStart() { + async onSyncStart(needLock = true) { Logger.log('onSyncStart: begin') if (Capacitor.getPlatform() === 'web') { @@ -206,31 +204,29 @@ export default class GoogleDriveAdapter extends CachingAdapter { this.accessToken = await this.getAccessToken(this.server.refreshToken) - let file - let startDate = Date.now() - const maxTimeout = LOCK_TIMEOUT - const base = 1.25 - for (let i = 0; Date.now() - startDate < maxTimeout; i++) { - const fileList = await this.listFiles('name = ' + "'" + this.server.bookmark_file + "'") - file = fileList.files.filter(file => !file.trashed)[0] - if (file) { - this.fileId = file.id + const fileList = await this.listFiles('name = ' + "'" + this.server.bookmark_file + "'") + const file = fileList.files.filter(file => !file.trashed)[0] + if (file) { + this.fileId = file.id + if (needLock) { const data = await this.getFileMetadata(file.id, 'appProperties') if (data.appProperties && data.appProperties.locked && (data.appProperties.locked === true || JSON.parse(data.appProperties.locked))) { const lockedDate = JSON.parse(data.appProperties.locked) - if (Number.isInteger(lockedDate)) { - startDate = lockedDate + if (!Number.isInteger(lockedDate)) { + throw new ResourceLockedError() + } + if (Date.now() - lockedDate < LOCK_TIMEOUT) { + throw new ResourceLockedError() } - await this.timeout(base ** i * 1000) - continue } } - break } if (file) { this.fileId = file.id - await this.setLock(this.fileId) + if (needLock) { + await this.setLock(this.fileId) + } let xmlDocText = await this.downloadFile(this.fileId) @@ -348,6 +344,14 @@ export default class GoogleDriveAdapter extends CachingAdapter { async requestNative(method: string, url: string, body: any = null, contentType: string = null) : Promise { let res + if (contentType === 'application/x-www-form-urlencoded') { + const params = new URLSearchParams() + for (const [key, value] of Object.entries(body || {})) { + params.set(key, value as string) + } + body = params.toString() + } + try { res = await Http.request({ url, @@ -365,6 +369,8 @@ export default class GoogleDriveAdapter extends CachingAdapter { throw new NetworkError() } + console.log(JSON.stringify(res)) + if (res.status === 401 || res.status === 403) { throw new AuthenticationError() } diff --git a/src/lib/adapters/NextcloudBookmarks.ts b/src/lib/adapters/NextcloudBookmarks.ts index a9a01076c0..629212af79 100644 --- a/src/lib/adapters/NextcloudBookmarks.ts +++ b/src/lib/adapters/NextcloudBookmarks.ts @@ -1,6 +1,6 @@ // Nextcloud ADAPTER // All owncloud specifc stuff goes in here -import { Capacitor } from '@capacitor/core' +import { Capacitor, CapacitorHttp as Http } from '@capacitor/core' import Adapter from '../interfaces/Adapter' import HtmlSerializer from '../serializers/Html' import Logger from '../Logger' @@ -22,7 +22,7 @@ import { NetworkError, ParseResponseError, RedirectError, - RequestTimeoutError, + RequestTimeoutError, ResourceLockedError, UnexpectedServerResponseError, UnknownCreateTargetError, UnknownFolderParentUpdateError, @@ -137,7 +137,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes }) } - async onSyncStart(): Promise { + async onSyncStart(needLock = true): Promise { if (Capacitor.getPlatform() === 'web') { const browser = (await import('../browser-api')).default if (!(await browser.permissions.contains({ origins: [this.server.url + '/'] }))) { @@ -145,18 +145,13 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes } } - this.canceled = false - const startDate = Date.now() - const maxTimeout = LOCK_TIMEOUT - const base = 1.25 - for (let i = 0; Date.now() - startDate < maxTimeout; i++) { - if (await this.acquireLock()) { - break - } else { - Logger.log('Resource is still locked, trying again in ' + (base ** i) + 's') - await this.timeout(base ** i * 1000) + if (needLock) { + if (!(await this.acquireLock())) { + throw new ResourceLockedError() } } + + this.canceled = false this.ended = false this.lockingInterval = setInterval(() => !this.ended && this.acquireLock(), LOCK_INTERVAL) } @@ -395,7 +390,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes } }) } - return recurseChildren(folderId, children).filter(item => item.id !== this.lockId) + return recurseChildren(folderId, children).filter(item => String(item.id) !== String(this.lockId)) } else { // We don't have the children endpoint available, so we have to query all bookmarks that exist :( await this.getBookmarksList() @@ -483,7 +478,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes ) } await recurseChildFolders(tree, childFolders, childrenOrder, childBookmarks, layers) - return tree.children.filter(item => item.id !== this.lockId) + return tree.children.filter(item => String(item.id) !== String(this.lockId)) } } @@ -829,7 +824,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes ) const newFolder = this.tree.findFolder(newBm.parentId) - if (!newFolder.children.find(item => item.id === newBm.id && item.type === 'bookmark')) { + if (!newFolder.children.find(item => String(item.id) === String(newBm.id) && item.type === 'bookmark')) { newFolder.children.push(newBm) } newBm.id = upstreamId + ';' + newBm.parentId @@ -867,9 +862,6 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes const url = this.normalizeServerURL(this.server.url) + relUrl let res let timedOut = false - const authString = Base64.encode( - this.server.username + ':' + this.server.password - ) if (type && type.includes('application/json')) { body = JSON.stringify(body) @@ -881,6 +873,14 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes body = params.toString() } + if (Capacitor.getPlatform() !== 'web') { + return this.sendRequestNative(verb, url, type, body, returnRawResponse) + } + + const authString = Base64.encode( + this.server.username + ':' + this.server.password + ) + try { res = await this.fetchQueue.add(() => Promise.race([ @@ -964,4 +964,60 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes return res.status === 200 } + + private async sendRequestNative(verb: string, url: string, type: string, body: any, returnRawResponse: boolean) { + let res + let timedOut = false + const authString = Base64.encode( + this.server.username + ':' + this.server.password + ) + try { + res = await this.fetchQueue.add(() => + Promise.race([ + Http.request({ + url, + method: verb, + disableRedirects: !this.server.allowRedirects, + headers: { + ...(type && type !== 'multipart/form-data' && { 'Content-type': type }), + Authorization: 'Basic ' + authString, + }, + responseType: 'json', + ...(body && !['get', 'head'].includes(verb.toLowerCase()) && { data: body }), + }), + new Promise((resolve, reject) => + setTimeout(() => { + timedOut = true + reject(new RequestTimeoutError()) + }, TIMEOUT) + ), + ]) + ) + } catch (e) { + if (timedOut) throw e + console.log(e) + throw new NetworkError() + } + + if (res.status < 400 && res.status >= 300) { + throw new RedirectError() + } + + if (returnRawResponse) { + return res + } + + if (res.status === 401 || res.status === 403) { + throw new AuthenticationError() + } + if (res.status === 503 || res.status > 400) { + throw new HttpError(res.status, verb) + } + const json = res.data + if (json.status !== 'success') { + throw new Error('Nextcloud API error: \n' + JSON.stringify(json)) + } + + return json + } } diff --git a/src/lib/adapters/WebDav.ts b/src/lib/adapters/WebDav.ts index 3fa5d5f93f..02194b2cff 100644 --- a/src/lib/adapters/WebDav.ts +++ b/src/lib/adapters/WebDav.ts @@ -10,10 +10,10 @@ import { DecryptionError, FileUnreadableError, HttpError, InterruptedSyncError, LockFileError, MissingPermissionsError, - NetworkError, RedirectError, + NetworkError, RedirectError, ResourceLockedError, SlashError } from '../../errors/Error' -import { Http } from '@capacitor-community/http' +import { CapacitorHttp as Http } from '@capacitor/core' import { Capacitor } from '@capacitor/core' import Html from '../serializers/Html' @@ -92,20 +92,16 @@ export default class WebDavAdapter extends CachingAdapter { } async obtainLock() { - let res - let startDate = Date.now() - const maxTimeout = LOCK_TIMEOUT - const base = 1.25 - for (let i = 0; Date.now() - startDate < maxTimeout; i++) { - res = await this.checkLock() - if (res.status === 200) { - if (res.headers['Last-Modified']) { - const date = new Date(res.headers['Last-Modified']) - startDate = date.valueOf() + const res = await this.checkLock() + if (res.status === 200) { + if (res.headers['Last-Modified']) { + const date = new Date(res.headers['Last-Modified']) + const dateLocked = date.valueOf() + if (Date.now() - dateLocked < LOCK_TIMEOUT) { + throw new ResourceLockedError() } - await this.timeout(base ** i * 1000) - } else if (res.status !== 200) { - break + } else { + throw new ResourceLockedError() } } diff --git a/src/lib/browser/BrowserController.js b/src/lib/browser/BrowserController.js index da3e7d31fe..c32c68d30b 100644 --- a/src/lib/browser/BrowserController.js +++ b/src/lib/browser/BrowserController.js @@ -6,14 +6,9 @@ import Cryptography from '../Crypto' import packageJson from '../../../package.json' import BrowserAccountStorage from './BrowserAccountStorage' import uniqBy from 'lodash/uniqBy' - -import PQueue from 'p-queue' import Account from '../Account' +import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '../interfaces/Controller' -const STATUS_ERROR = Symbol('error') -const STATUS_SYNCING = Symbol('syncing') -const STATUS_ALLGOOD = Symbol('allgood') -const STATUS_DISABLED = Symbol('disabled') const INACTIVITY_TIMEOUT = 7 * 1000 const DEFAULT_SYNC_INTERVAL = 15 @@ -29,6 +24,9 @@ class AlarmManager { const data = account.getData() const lastSync = data.lastSync || 0 const interval = data.syncInterval || DEFAULT_SYNC_INTERVAL + if (data.scheduled) { + this.ctl.scheduleSync(accountId) + } if ( Date.now() > interval * 1000 * 60 + lastSync @@ -42,8 +40,6 @@ class AlarmManager { export default class BrowserController { constructor() { - this.jobs = new PQueue({ concurrency: 1 }) - this.waiting = {} this.schedule = {} this.listeners = [] @@ -93,16 +89,6 @@ export default class BrowserController { currentVersion: packageJson.version }) - // Set flag to switch to new encryption implementation - const oldVersion = d.currentVersion.split('.') - const e = await browser.storage.local.get('accountsLocked') - // eslint-disable-next-line eqeqeq - if (e.accountsLocked && oldVersion[0] === '4' && (oldVersion[1] < 5 || (oldVersion[1] == 5 && oldVersion[2] == 0))) { - await browser.storage.local.set({ - rekeyAfterUpdate: true - }) - } - const packageVersion = packageJson.version.split('.') const lastVersion = d.currentVersion ? d.currentVersion.split('.') : [] if (packageVersion[0] !== lastVersion[0] || packageVersion[1] !== lastVersion[1]) { @@ -176,7 +162,7 @@ export default class BrowserController { // remove encryption this.key = null await browser.storage.local.set({ accountsLocked: null }) - const accountIds = BrowserAccountStorage.getAllAccounts() + const accountIds = await BrowserAccountStorage.getAllAccounts() for (let accountId of accountIds) { const storage = new BrowserAccountStorage(accountId) const data = await storage.getAccountData(key) @@ -275,22 +261,21 @@ export default class BrowserController { return } - if (this.waiting[accountId]) { - console.log('Account is already queued to be synced') + const status = await this.getStatus() + if (status === STATUS_SYNCING) { + await account.setData({ ...account.getData(), scheduled: true }) return } - this.waiting[accountId] = true - await account.setData({ ...account.getData(), scheduled: true }) - - return this.jobs.add(() => this.syncAccount(accountId)) + this.syncAccount(accountId) } async scheduleAll() { const accounts = await Account.getAllAccounts() for (const account of accounts) { - this.scheduleSync(account.id) + await account.setData({...account.getData(), scheduled: true}) } + this.updateStatus() } async cancelSync(accountId, keepEnabled) { @@ -304,13 +289,11 @@ export default class BrowserController { async syncAccount(accountId, strategy) { console.log('Called syncAccount ', accountId) - this.waiting[accountId] = false if (!this.enabled) { console.log('Flocccus controller is not enabled. Not syncing.') return } let account = await Account.get(accountId) - await account.setData({ ...account.getData(), scheduled: false }) if (account.getData().syncing) { console.log('Account is already syncing. Not triggering another sync.') return @@ -339,14 +322,14 @@ export default class BrowserController { } } - async updateBadge() { + async getStatus() { if (!this.unlocked) { - return this.setStatusBadge(STATUS_ERROR) + return STATUS_ERROR } const accounts = await Account.getAllAccounts() let overallStatus = accounts.reduce((status, account) => { const accData = account.getData() - if (status === STATUS_SYNCING || accData.syncing) { + if (status === STATUS_SYNCING || accData.syncing || account.syncing) { return STATUS_SYNCING } else if (status === STATUS_ERROR || (accData.error && !accData.syncing)) { return STATUS_ERROR @@ -361,7 +344,11 @@ export default class BrowserController { } } - this.setStatusBadge(overallStatus) + return overallStatus + } + + async updateBadge() { + await this.setStatusBadge(await this.getStatus()) } async setStatusBadge(status) { @@ -385,7 +372,6 @@ export default class BrowserController { await acc.setData({ ...acc.getData(), syncing: false, - error: false, }) } }) diff --git a/src/lib/browser/BrowserTree.ts b/src/lib/browser/BrowserTree.ts index 3eec7c09b9..b0b34cbeec 100644 --- a/src/lib/browser/BrowserTree.ts +++ b/src/lib/browser/BrowserTree.ts @@ -36,7 +36,7 @@ export default class BrowserTree implements IResource { const recurse = (node, parentId?, rng?) => { if ( allAccounts.some( - acc => acc.getData().localRoot === node.id && node.id !== this.rootId && !acc.getData().nestedSync + acc => acc.getData().localRoot === node.id && String(node.id) !== String(this.rootId) && !acc.getData().nestedSync ) ) { // This is the root folder of a different account and the user doesn't want nested sync @@ -223,8 +223,8 @@ export default class BrowserTree implements IResource { if (realTree.children.length !== order.length) { const untouchedChildren = realTree.children.map((child,i) => [i, child]).filter(([, child]) => child.url - ? !order.some(item => item.type === ItemType.BOOKMARK && item.id === child.id) - : !order.some(item => item.type === ItemType.FOLDER && item.id === child.id) + ? !order.some(item => item.type === ItemType.BOOKMARK && String(item.id) === String(child.id)) + : !order.some(item => item.type === ItemType.FOLDER && String(item.id) === String(child.id)) ) try { Logger.log('Move untouched children back into place', {untouchedChildren: untouchedChildren.map(([i, item]) => [i, item.id])}) diff --git a/src/lib/interfaces/Controller.ts b/src/lib/interfaces/Controller.ts index 545fa6b22a..eb2051fb55 100644 --- a/src/lib/interfaces/Controller.ts +++ b/src/lib/interfaces/Controller.ts @@ -9,3 +9,8 @@ export default interface IController { getUnlocked():Promise; onLoad():void; } + +export const STATUS_ERROR = Symbol('error') +export const STATUS_SYNCING = Symbol('syncing') +export const STATUS_ALLGOOD = Symbol('allgood') +export const STATUS_DISABLED = Symbol('disabled') diff --git a/src/lib/native/NativeController.js b/src/lib/native/NativeController.js index 1dbd92bc60..43b51483c2 100644 --- a/src/lib/native/NativeController.js +++ b/src/lib/native/NativeController.js @@ -2,9 +2,8 @@ import { Preferences as Storage } from '@capacitor/preferences' import { Network } from '@capacitor/network' import Cryptography from '../Crypto' import NativeAccountStorage from './NativeAccountStorage' - -import PQueue from 'p-queue' import Account from '../Account' +import { STATUS_ALLGOOD, STATUS_DISABLED, STATUS_ERROR, STATUS_SYNCING } from '../interfaces/Controller' const INACTIVITY_TIMEOUT = 1000 * 7 const DEFAULT_SYNC_INTERVAL = 15 @@ -13,10 +12,10 @@ class AlarmManager { constructor(ctl) { this.ctl = ctl this.backgroundSyncEnabled = true - setInterval(() => this.checkSync(), 60 * 1000) + setInterval(() => this.checkSync(), 25 * 1000) Network.addListener('networkStatusChange', status => { - if (status.connected && status.connectionType === 'wifi') { + if (status.connected) { this.backgroundSyncEnabled = true } else { this.backgroundSyncEnabled = false @@ -32,12 +31,14 @@ class AlarmManager { for (let accountId of accounts) { const account = await Account.get(accountId) const data = account.getData() + if (data.scheduled) { + this.ctl.scheduleSync(accountId) + } if ( !data.lastSync || Date.now() > (data.syncInterval || DEFAULT_SYNC_INTERVAL) * 1000 * 60 + data.lastSync ) { - // noinspection ES6MissingAwait this.ctl.scheduleSync(accountId) } } @@ -46,8 +47,6 @@ class AlarmManager { export default class NativeController { constructor() { - this.jobs = new PQueue({ concurrency: 1 }) - this.waiting = {} this.schedule = {} this.listeners = [] @@ -118,13 +117,13 @@ export default class NativeController { return } - if (this.waiting[accountId]) { + const status = await this.getStatus() + if (status === STATUS_SYNCING) { + await account.setData({ ...account.getData(), scheduled: true }) return } - this.waiting[accountId] = true - - return this.jobs.add(() => this.syncAccount(accountId)) + this.syncAccount(accountId) } async cancelSync(accountId, keepEnabled) { @@ -137,7 +136,6 @@ export default class NativeController { } async syncAccount(accountId, strategy) { - this.waiting[accountId] = false if (!this.enabled) { return } @@ -158,6 +156,31 @@ export default class NativeController { this.listeners.forEach(fn => fn()) } + async getStatus() { + if (!this.unlocked) { + return STATUS_ERROR + } + const accounts = await Account.getAllAccounts() + let overallStatus = accounts.reduce((status, account) => { + const accData = account.getData() + if (status === STATUS_SYNCING || accData.syncing) { + return STATUS_SYNCING + } else if (status === STATUS_ERROR || (accData.error && !accData.syncing)) { + return STATUS_ERROR + } else { + return STATUS_ALLGOOD + } + }, STATUS_ALLGOOD) + + if (overallStatus === STATUS_ALLGOOD) { + if (accounts.every(account => !account.getData().enabled)) { + overallStatus = STATUS_DISABLED + } + } + + return overallStatus + } + onStatusChange(listener) { this.listeners.push(listener) let unregistered = false @@ -176,7 +199,7 @@ export default class NativeController { await acc.setData({ ...acc.getData(), syncing: false, - error: false, + scheduled: false, }) } }) diff --git a/src/lib/serializers/Html.ts b/src/lib/serializers/Html.ts index d0f8ffcda7..5d2363d7b9 100644 --- a/src/lib/serializers/Html.ts +++ b/src/lib/serializers/Html.ts @@ -12,8 +12,7 @@ class HtmlSerializer implements Serializer { .map(child => { if (child instanceof Bookmark) { return ( - `${indent}
` + - `${child.title}\n` + `${indent}
${child.title}\n` ) } else if (child instanceof Folder) { const nextIndent = indent + ' ' @@ -30,9 +29,9 @@ class HtmlSerializer implements Serializer { } deserialize(html): Folder { - const folders: Folder[] = parseByString(html) - folders.forEach(f => {f.parentId = '0'}) - return new Folder({id: '0', title: 'root', children: folders, location: ItemLocation.SERVER, isRoot: true}) + const items: TItem[] = parseByString(html) + items.forEach(f => { f.parentId = '0' }) + return new Folder({id: '0', title: 'root', children: items, location: ItemLocation.SERVER, isRoot: true}) } } @@ -78,12 +77,12 @@ export const parseByString = (content: string) => { }) const body = $('body') - const root: Folder[] = [] + const root: TItem[] = [] const rdt = getRootFolder(body).children('dt') const parseNode = (node: cheerio.Cheerio, parentId?: string|number) => { const eq0 = node.children().eq(0) - const title = eq0.html() || '' + const title = eq0.text() || '' let url = '' const id = eq0.attr('id') || '' let children: TItem[] = [] @@ -110,7 +109,7 @@ export const parseByString = (content: string) => { rdt.each((_, item) => { const node = $(item) - const child = parseNode(node) as Folder + const child = parseNode(node) root.push(child) }) diff --git a/src/lib/strategies/Default.ts b/src/lib/strategies/Default.ts index ab39dc83ac..08efe4f320 100644 --- a/src/lib/strategies/Default.ts +++ b/src/lib/strategies/Default.ts @@ -446,8 +446,8 @@ export default class SyncProcess { if ( // Don't create duplicates! - targetPlan.getActions(ActionType.MOVE).find(move => move.payload.id === payload.id) || - sourceDiff.getActions(ActionType.MOVE).find(move => move.payload.id === payload.id) || + targetPlan.getActions(ActionType.MOVE).find(move => String(move.payload.id) === String(payload.id)) || + sourceDiff.getActions(ActionType.MOVE).find(move => String(move.payload.id) === String(payload.id)) || // Don't move back into removed territory targetRemovals.find(remove => Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, sourceTree, action.payload, remove)) || sourceRemovals.find(remove => Diff.findChain(mappingsSnapshot, allCreateAndMoveActions, targetTree, action.payload, remove)) @@ -829,12 +829,12 @@ export default class SyncProcess { const reconciled = !cacheItem const changedLocally = (localHash !== cacheHash) || - (cacheItem && localItem.parentId !== cacheItem.parentId) + (cacheItem && String(localItem.parentId) !== String(cacheItem.parentId)) const changedUpstream = (cacheHash !== serverHash) || (cacheItem && - cacheItem.parentId !== - mappingsSnapshot.ServerToLocal.folder[serverItem.parentId]) + String(cacheItem.parentId) !== + String(mappingsSnapshot.ServerToLocal.folder[serverItem.parentId])) return changedLocally || changedUpstream || reconciled } diff --git a/src/lib/strategies/Merge.ts b/src/lib/strategies/Merge.ts index 9411934fee..7b450360c6 100644 --- a/src/lib/strategies/Merge.ts +++ b/src/lib/strategies/Merge.ts @@ -128,8 +128,8 @@ export default class MergeSyncProcess extends Default { oldItem.parentId = Mappings.mapParentId(mappingsSnapshot, oldItem, action.payload.location) if ( - targetPlan.getActions(ActionType.MOVE).find(move => move.payload.id === payload.id) || - sourceDiff.getActions(ActionType.MOVE).find(move => move.payload.id === payload.id) + targetPlan.getActions(ActionType.MOVE).find(move => String(move.payload.id) === String(payload.id)) || + sourceDiff.getActions(ActionType.MOVE).find(move => String(move.payload.id) === String(payload.id)) ) { // Don't create duplicates! return diff --git a/src/test/test.js b/src/test/test.js index 29ff605601..b3d0342bb2 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -3206,11 +3206,18 @@ describe('Floccus', function() { }) await new Promise(resolve => setTimeout(resolve, 60000)) expect(account2.getData().error).to.be.not.ok - expect(resolved).to.equal(false) + expect(account2.getData().scheduled).to.be.true + expect(resolved).to.equal(true) }) console.log('Finished sync with account 1') + sync2 = account2.sync() + sync2.then(() => { + console.log('Finished sync with account 2') + resolved = true + }) await new Promise(resolve => setTimeout(resolve, 60000)) expect(account2.getData().error).to.be.not.ok + expect(account2.getData().scheduled).to.be.false expect(resolved).to.equal(true) }) it('should propagate edits using "last write wins"', async function() { diff --git a/src/ui/components/AccountCard.vue b/src/ui/components/AccountCard.vue index 638c6ca7fb..d6b6e89691 100644 --- a/src/ui/components/AccountCard.vue +++ b/src/ui/components/AccountCard.vue @@ -250,7 +250,10 @@ export default { ) } if (this.account.data.syncing) { - return 'Synchronization in progress.' + return this.t('DescriptionSyncinprogress') + } + if (this.account.data.scheduled) { + return this.t('DescriptionSyncscheduled') } if (this.account.data.lastSync) { return this.t( diff --git a/src/ui/components/OptionsGoogleDrive.vue b/src/ui/components/OptionsGoogleDrive.vue index 36ded98d20..ce4bc10c27 100644 --- a/src/ui/components/OptionsGoogleDrive.vue +++ b/src/ui/components/OptionsGoogleDrive.vue @@ -87,7 +87,7 @@ :label="t('LabelAutosync')" dense class="mt-0 pt-0" - @input="$emit('update:enabled', $event)" /> + @change="$emit('update:enabled', $event)" /> diff --git a/src/ui/components/OptionsNextcloudBookmarks.vue b/src/ui/components/OptionsNextcloudBookmarks.vue index 4152ba4da1..bcc170c34e 100644 --- a/src/ui/components/OptionsNextcloudBookmarks.vue +++ b/src/ui/components/OptionsNextcloudBookmarks.vue @@ -78,7 +78,7 @@ :label="t('LabelAutosync')" dense class="mt-0 pt-0" - @input="$emit('update:enabled', $event)" /> + @change="$emit('update:enabled', $event)" /> diff --git a/src/ui/components/OptionsWebdav.vue b/src/ui/components/OptionsWebdav.vue index 699c494b6b..2b617efca1 100644 --- a/src/ui/components/OptionsWebdav.vue +++ b/src/ui/components/OptionsWebdav.vue @@ -87,7 +87,7 @@ :label="t('LabelAutosync')" dense class="mt-0 pt-0" - @input="$emit('update:enabled', $event)" /> + @change="$emit('update:enabled', $event)" /> diff --git a/src/ui/components/native/DialogImportBookmarks.vue b/src/ui/components/native/DialogImportBookmarks.vue index 80a1614b4a..3f4892a751 100644 --- a/src/ui/components/native/DialogImportBookmarks.vue +++ b/src/ui/components/native/DialogImportBookmarks.vue @@ -12,7 +12,7 @@ ref="filePicker" type="file" class="d-none" - accept="application/html" + accept="text/html" @change="onFileSelect"> import { getIcons } from '../../../lib/getFavicon' -import { Http } from '@capacitor-community/http' +import { CapacitorHttp as Http } from '@capacitor/core' import {Preferences as Storage} from '@capacitor/preferences' export default { diff --git a/src/ui/store/actions.js b/src/ui/store/actions.js index f7538e3313..3d49f5e81c 100644 --- a/src/ui/store/actions.js +++ b/src/ui/store/actions.js @@ -46,7 +46,6 @@ export const actionsDefinition = { return account.id }, async [actions.IMPORT_ACCOUNTS]({commit, dispatch, state}, accounts) { - await browser.permissions.request({origins: ['*://*/*']}) await Account.import(accounts) await dispatch(actions.LOAD_ACCOUNTS) }, diff --git a/src/ui/store/native/actions.js b/src/ui/store/native/actions.js index b95ca97e4a..57479ecc03 100644 --- a/src/ui/store/native/actions.js +++ b/src/ui/store/native/actions.js @@ -4,7 +4,7 @@ import Logger from '../../../lib/Logger' import AdapterFactory from '../../../lib/AdapterFactory' import Controller from '../../../lib/Controller' import { i18n } from '../../../lib/native/I18n' -import { Http } from '@capacitor-community/http' +import { CapacitorHttp as Http } from '@capacitor/core' import { Share } from '@capacitor/share' import Html from '../../../lib/serializers/Html' import { Bookmark, Folder } from '../../../lib/Tree' @@ -200,10 +200,12 @@ export const actionsDefinition = { do { await new Promise(resolve => setTimeout(resolve, 1000)) try { + const data = new URLSearchParams() + data.set('token', json.poll.token) res = await Http.request({ url: json.poll.endpoint, method: 'POST', - data: {token: json.poll.token}, + data: data.toString(), headers: {'Content-type': 'application/x-www-form-urlencoded'} }) } catch (e) { diff --git a/src/ui/views/ImportExport.vue b/src/ui/views/ImportExport.vue index 15d73d6449..e744e9ed7f 100644 --- a/src/ui/views/ImportExport.vue +++ b/src/ui/views/ImportExport.vue @@ -106,8 +106,12 @@ export default { alert(e.message) } }, - onTriggerFilePicker() { + async onTriggerFilePicker() { this.$refs.filePicker.click() + if (this.isBrowser) { + const {default: browser} = await import('../../lib/browser-api') + await browser.permissions.request({ origins: ['*://*/*'] }) + } }, async onFileSelect() { const file = this.$refs.filePicker.files[0] diff --git a/src/ui/views/NewAccount.vue b/src/ui/views/NewAccount.vue index b5b4c3a94d..90aab35ded 100644 --- a/src/ui/views/NewAccount.vue +++ b/src/ui/views/NewAccount.vue @@ -398,6 +398,10 @@ export default { this.isServerTestRunning = false }, async loginGoogleDrive() { + if (this.isBrowser) { + const {default: browser} = await import('../../lib/browser-api') + await browser.permissions.request({ origins: ['*://*/*'] }) + } const GoogleDriveAdapter = (await import('../../lib/adapters/GoogleDrive')).default const { refresh_token, username } = await GoogleDriveAdapter.authorize() if (refresh_token) { diff --git a/src/ui/views/Overview.vue b/src/ui/views/Overview.vue index 138d070c77..17a4ed08bb 100644 --- a/src/ui/views/Overview.vue +++ b/src/ui/views/Overview.vue @@ -56,7 +56,6 @@ mdi-export mdi-sync-circle @@ -84,9 +83,6 @@ export default { loading() { return this.$store.state.loading.accounts }, - canScheduleAll() { - return !(this.$store.state.accounts && Object.values(this.$store.state.accounts).some(account => account.data.scheduled)) - } }, methods: { clickSyncAll() { diff --git a/src/ui/views/native/Home.vue b/src/ui/views/native/Home.vue index 145106c2b8..7e822d838e 100644 --- a/src/ui/views/native/Home.vue +++ b/src/ui/views/native/Home.vue @@ -12,7 +12,7 @@ import { SplashScreen } from '@capacitor/splash-screen' import { SendIntent } from 'send-intent' import packageJson from '../../../../package.json' import { Preferences as Storage } from '@capacitor/preferences' -import { Http } from '@capacitor-community/http' +import { CapacitorHttp as Http } from '@capacitor/core' import Logger from '../../../lib/Logger' export default { diff --git a/src/ui/views/native/Tree.vue b/src/ui/views/native/Tree.vue index ae089832e2..46179202b5 100644 --- a/src/ui/views/native/Tree.vue +++ b/src/ui/views/native/Tree.vue @@ -29,11 +29,11 @@ - mdi-sync + {{ scheduled ? 'mdi-timer-sync-outline' : 'mdi-sync' }} {{ syncError }} + + {{ t('DescriptionSyncscheduled') }} +