├── .all-contributorsrc ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── dependabot-approve.yml │ ├── issues.yml │ ├── lock-threads.yml │ ├── stale.yml │ └── tests.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CONSIDERATIONS.md ├── LICENSE.txt ├── PRIVACY_POLICY.md ├── README.md ├── _locales ├── cs │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fi │ └── messages.json ├── fr │ └── messages.json ├── gl │ └── messages.json ├── ja │ └── messages.json ├── ko_KR │ └── messages.json ├── nl_NL │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── pt_BR │ └── messages.json ├── ru │ └── messages.json ├── sv │ └── messages.json ├── tr │ └── messages.json ├── tr_TR │ └── messages.json ├── zh │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── android ├── .gitignore ├── .idea │ ├── compiler.xml │ └── jarRepositories.xml ├── app │ ├── .gitignore │ ├── build.gradle │ ├── capacitor.build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ ├── capacitor.config.json │ │ └── capacitor.plugins.json │ │ ├── java │ │ └── org │ │ │ └── handmadeideas │ │ │ └── floccus │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable-land-hdpi │ │ └── splash.png │ │ ├── drawable-land-mdpi │ │ └── splash.png │ │ ├── drawable-land-xhdpi │ │ └── splash.png │ │ ├── drawable-land-xxhdpi │ │ └── splash.png │ │ ├── drawable-land-xxxhdpi │ │ └── splash.png │ │ ├── drawable-port-hdpi │ │ └── splash.png │ │ ├── drawable-port-mdpi │ │ └── splash.png │ │ ├── drawable-port-xhdpi │ │ └── splash.png │ │ ├── drawable-port-xxhdpi │ │ └── splash.png │ │ ├── drawable-port-xxxhdpi │ │ └── splash.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── splash.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── config.xml │ │ ├── file_paths.xml │ │ └── network_security_config.xml ├── build.gradle ├── capacitor.settings.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── variables.gradle ├── capacitor.config.json ├── doc └── Adapters.md ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 03.png │ │ └── 04.png │ └── short_description.txt ├── google-api.credentials.json ├── gulpfile.js ├── html ├── background.html ├── index.html ├── options.html └── test.html ├── icons ├── disabled_128.png ├── disabled_32.png ├── disabled_64.png ├── error_128.png ├── error_32.png ├── error_64.png ├── logo.png ├── logo.svg ├── logo_128.png ├── logo_128_white.png ├── logo_32.png ├── logo_512.png ├── logo_64.png ├── syncing_128.png ├── syncing_32.png ├── syncing_64.png └── tree-swing.svg ├── img ├── promotional-tile-marquee.png ├── promotional-tile-medium.png ├── promotional-tile-medium.xcf ├── promotional-tile-medium2.png ├── promotional-tile-medium2.xcf ├── promotional-tile-small.png ├── promotional-tile-small.xcf ├── screen_chrome_account.png ├── screen_chrome_folderpicker.png ├── screen_chrome_options.png ├── screen_chrome_wide.png ├── screen_firefox_account.png ├── screen_firefox_folderpicker.png └── screen_firefox_options.png ├── ios ├── .gitignore └── App │ ├── App.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── Floccus New Bookmark.xcscheme │ │ └── Floccus.xcscheme │ ├── App.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ ├── App │ ├── App.entitlements │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon-512@2x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── Splash.imageset │ │ │ ├── Contents.json │ │ │ ├── splash-2732x2732-1.png │ │ │ ├── splash-2732x2732-2.png │ │ │ └── splash-2732x2732.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── capacitor.config.json │ └── config.xml │ ├── Floccus New Bookmark │ ├── Base.lproj │ │ └── MainInterface.storyboard │ ├── Floccus New Bookmark.entitlements │ ├── Info.plist │ └── ShareViewController.swift │ ├── Floccus.entitlements │ ├── Podfile │ ├── Podfile.lock │ └── PrivacyInfo.xcprivacy ├── lib └── gulp-crx.js ├── manifest-firefox-override.sh ├── manifest.chrome.json ├── manifest.firefox.json ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── entries │ ├── background-script.js │ ├── native.js │ ├── options.js │ └── test.js ├── errors │ └── Error.ts ├── lib │ ├── Account.ts │ ├── AdapterFactory.ts │ ├── CacheTree.ts │ ├── CachingTreeWrapper.ts │ ├── Controller.ts │ ├── Crypto.ts │ ├── DefunctCrypto.js │ ├── Diff.ts │ ├── IndexedDB.ts │ ├── LocalTabs.ts │ ├── Logger.js │ ├── Mappings.ts │ ├── PathHelper.js │ ├── Scanner.ts │ ├── Tree.ts │ ├── adapters │ │ ├── Caching.ts │ │ ├── Fake.js │ │ ├── Git.ts │ │ ├── GoogleDrive.ts │ │ ├── Karakeep.ts │ │ ├── Linkwarden.ts │ │ ├── NextcloudBookmarks.ts │ │ └── WebDav.ts │ ├── browser-api.js │ ├── browser │ │ ├── BrowserAccount.ts │ │ ├── BrowserAccountStorage.js │ │ ├── BrowserController.js │ │ ├── BrowserDetection.ts │ │ └── BrowserTree.ts │ ├── getFavicon.js │ ├── interfaces │ │ ├── Account.ts │ │ ├── AccountStorage.ts │ │ ├── Adapter.ts │ │ ├── Controller.ts │ │ ├── Ordering.ts │ │ ├── Resource.ts │ │ └── Serializer.ts │ ├── isTest.ts │ ├── native │ │ ├── I18n.ts │ │ ├── NativeAccount.ts │ │ ├── NativeAccountStorage.js │ │ ├── NativeController.js │ │ └── NativeTree.ts │ ├── serializers │ │ ├── Html.ts │ │ └── Xbel.ts │ └── strategies │ │ ├── Default.ts │ │ ├── Merge.ts │ │ └── Unidirectional.ts ├── test │ ├── index.js │ ├── reporter.js │ └── test.js └── ui │ ├── App.vue │ ├── NativeApp.vue │ ├── NativeRouter.js │ ├── components │ ├── AccountCard.vue │ ├── NextcloudLogin.vue │ ├── OptionAllowRedirects.vue │ ├── OptionAutoSync.vue │ ├── OptionClientCert.vue │ ├── OptionDeleteAccount.vue │ ├── OptionDownloadLogs.vue │ ├── OptionExportBookmarks.vue │ ├── OptionFailsafe.vue │ ├── OptionFileType.vue │ ├── OptionNestedSync.vue │ ├── OptionPassphrase.vue │ ├── OptionResetCache.vue │ ├── OptionSyncFolder.vue │ ├── OptionSyncInterval.vue │ ├── OptionSyncStrategy.vue │ ├── OptionsFake.vue │ ├── OptionsGit.vue │ ├── OptionsGoogleDrive.vue │ ├── OptionsKarakeep.vue │ ├── OptionsLinkwarden.vue │ ├── OptionsNextcloudBookmarks.vue │ ├── OptionsNextcloudLegacy.vue │ ├── OptionsWebdav.vue │ └── native │ │ ├── DialogChooseFolder.vue │ │ ├── DialogEditBookmark.vue │ │ ├── DialogEditFolder.vue │ │ ├── DialogImportBookmarks.vue │ │ ├── Drawer.vue │ │ ├── FaviconImage.vue │ │ └── OptionAllowNetwork.vue │ ├── index.js │ ├── native-public-path.js │ ├── native.js │ ├── plugins │ ├── capacitor.js │ ├── i18n.js │ └── vuetify.js │ ├── router.js │ ├── store │ ├── actions.js │ ├── definitions.js │ ├── index.js │ ├── mutations.js │ └── native │ │ ├── actions.js │ │ ├── index.js │ │ └── mutations.js │ └── views │ ├── AccountOptions.vue │ ├── Donate.vue │ ├── ImportExport.vue │ ├── NewAccount.vue │ ├── Overview.vue │ ├── Telemetry.vue │ ├── Update.vue │ └── native │ ├── About.vue │ ├── AddBookmarkIntent.vue │ ├── Home.vue │ ├── ImportExport.vue │ ├── Options.vue │ └── Tree.vue ├── test ├── apache-vhost.conf ├── apcu.ini ├── save-stats.js └── selenium-runner.js ├── transifex.yml ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: marcelklehr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: marcelklehr 5 | open_collective: floccus 6 | liberapay: marcelklehr 7 | ko_fi: marcelklehr 8 | custom: https://www.paypal.com/donate/?hosted_button_id=R3SDCC7AFSYZU 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report for floccus 3 | labels: ['bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. 8 | - type: markdown 9 | attributes: 10 | value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions. 11 | - type: input 12 | attributes: 13 | label: Which version of floccus are you using? 14 | description: 'Please specify the exact version instead of "latest". For example: 4.14.0' 15 | validations: 16 | required: true 17 | - type: input 18 | attributes: 19 | label: How many bookmarks do you have, roughly? 20 | description: 'e.g. 10, 300 or 12k' 21 | validations: 22 | required: true 23 | - type: input 24 | attributes: 25 | label: Are you using other means to sync bookmarks in parallel to floccus? 26 | description: 'e.g. "No" or "Yes, I also sync via Mozilla account"' 27 | validations: 28 | required: true 29 | - type: dropdown 30 | attributes: 31 | label: Sync method 32 | description: Which sync method are you using? 33 | multiple: false 34 | options: 35 | - Nextcloud Bookmarks 36 | - Linkwarden 37 | - WebDAV 38 | - Google Drive 39 | - Git 40 | validations: 41 | required: true 42 | - type: input 43 | attributes: 44 | label: Which browser are you using? In case you are using the phone App, specify the Android or iOS version and device please. 45 | description: 'Please specify the exact version instead of "latest". For example: Chrome 100.0.4878.0 or ' 46 | - type: input 47 | attributes: 48 | label: Which version of Nextcloud Bookmarks are you using? (if relevant) 49 | description: 'For example: v10.1.0' 50 | - type: input 51 | attributes: 52 | label: Which version of Nextcloud? (if relevant) 53 | description: 'For example: v23.0.1' 54 | - type: textarea 55 | attributes: 56 | label: What kind of WebDAV server are you using? (if relevant) 57 | description: Describe the setup of your WebDAV server 58 | - type: textarea 59 | attributes: 60 | label: Describe the Bug 61 | description: A clear and concise description of what the bug is. 62 | validations: 63 | required: true 64 | - type: textarea 65 | attributes: 66 | label: Expected Behavior 67 | description: A clear and concise description of what you expected to happen. 68 | validations: 69 | required: true 70 | - type: textarea 71 | attributes: 72 | label: To Reproduce 73 | description: Steps to reproduce the behavior, please provide a clear number of steps that always reproduces the issue. Screenshots can be provided in the issue body below. 74 | validations: 75 | required: true 76 | - type: markdown 77 | attributes: 78 | value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. 79 | - type: markdown 80 | attributes: 81 | value: Contributors should be able to follow the steps provided in order to reproduce the bug. 82 | - type: markdown 83 | attributes: 84 | value: It is often useful to provide a debug log file along with the issue. You can obtain a (redacted) debug log of the most recent sync run in the account settings of your floccus account. 85 | - type: markdown 86 | attributes: 87 | value: You can also let floccus automatically redact your debug logs. 88 | - type: checkboxes 89 | attributes: 90 | label: Debug log provided 91 | options: 92 | - label: I have provided a debug log file 93 | required: false 94 | - type: markdown 95 | attributes: 96 | value: "Please note: To continue development and maintenance of this project in a sustainable way, I ask that you donate to the project when opening a ticket (or at least once your issue is resolved), if you're not a donor already. You can find donation options at . Thank you!" 97 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/floccusAddon/floccus/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Create a feature request for floccus 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. 8 | - type: textarea 9 | attributes: 10 | label: Describe the feature you'd like to request 11 | description: A clear and concise description of what you want and what your use case is. 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Describe the solution you'd like 17 | description: A clear and concise description of what you want to happen. 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Describe alternatives you've considered 23 | description: A clear and concise description of any alternative solutions or features you've considered. 24 | validations: 25 | required: true 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ develop, master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ develop ] 20 | schedule: 21 | - cron: '37 13 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | node-versions: [14.x] 37 | npm-versions: [8.x] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | - name: Set up node ${{ matrix.node-versions }} 59 | uses: actions/setup-node@v1 60 | with: 61 | node-version: ${{ matrix.node-versions }} 62 | 63 | - name: Set up npm ${{ matrix.npm-version }} 64 | run: npm i -g npm@"${{ matrix.npm-version }}" 65 | 66 | # ℹ️ Command-line programs to run using the OS shell. 67 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 68 | 69 | # If the Autobuild fails above, remove it and uncomment the following three lines. 70 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 71 | 72 | # - run: | 73 | # echo "Run, Build Application using script" 74 | # ./location_of_script_within_repo/buildscript.sh 75 | 76 | - name: Perform CodeQL Analysis 77 | uses: github/codeql-action/analyze@v2 78 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto approve 2 | on: pull_request 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: hmarr/auto-approve-action@v2.0.0 9 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' 10 | with: 11 | github-token: "${{ secrets.GITHUB_TOKEN }}" 12 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | name: New issue workflow 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | env: 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | 9 | jobs: 10 | assign_one_project: 11 | runs-on: ubuntu-latest 12 | name: Assign to One Project 13 | steps: 14 | - name: Assign new issues and pull requests to project 1 Backlog 15 | uses: srggrs/assign-one-project-github-action@1.2.0 16 | with: 17 | project: 'https://github.com/floccusaddon/floccus/projects/1' 18 | column_name: 'Backlog' 19 | 20 | first_comment: 21 | runs-on: ubuntu-latest 22 | name: Add first comment 23 | steps: 24 | - uses: ben-z/actions-comment-on-issue@1.0.3 25 | with: 26 | message: | 27 | Hello :wave: 28 | 29 | Thank you for taking the time to open this issue with floccus. I know it's frustrating when software 30 | causes problems. You have made the right choice to come here and open an issue to make sure your problem gets looked at 31 | and if possible solved. Let me give you a short introduction on what to expect from this issue tracker to avoid misunderstandings. 32 | I'm Marcel. I created floccus a few years ago, and have been maintaining it since. I currently work for Nextcloud 33 | which leaves me with less time for side projects like this one than I used to have. 34 | I still try to answer all issues and if possible fix all bugs here, but it sometimes takes a while until I get to it. 35 | Until then, please be patient. It helps when you stick around to answer follow up questions I may have, 36 | as very few bugs can be fixed directly from the first bug report, without any interaction. If information is missing in your bug report 37 | and the issue cannot be solved without it, I will have to close the issue after a while. 38 | Note also that GitHub in general is a place where people meet to make software better *together*. Nobody here is under any obligation 39 | to help you, solve your problems or deliver on any expectations or demands you may have, but if enough people come together we can 40 | collaborate to make this software better. For everyone. 41 | Thus, if you can, you could also have a look at other issues to see whether you can help other people with your knowledge 42 | and experience. If you have coding experience it would also be awesome if you could step up to dive into the code and 43 | try to fix the odd bug yourself. Everyone will be thankful for extra helping hands! 44 | If you cannot lend a helping hand, to continue the development and maintenance of this project in a sustainable way, 45 | I ask that you donate to the project when opening an issue (or at least once your issue is solved), if you're not a donor already. 46 | You can find donation options at . Thank you! 47 | 48 | One last word: If you feel, at any point, like you need to vent, this is not the place for it; you can go to the Nextcloud forum, 49 | to twitter or somewhere else. But this is a technical issue tracker, so please make sure to 50 | focus on the tech and keep your opinions to yourself. 51 | 52 | Thank you for reading through this primer. I look forward to working with you on this issue! 53 | Cheers :blue_heart: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /.github/workflows/lock-threads.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v4 20 | with: 21 | issue-comment: > 22 | This issue has been automatically locked since there 23 | has not been any recent activity after it was closed. 24 | Please open a new issue for related bugs. 25 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | stale-issue-message: | 13 | Hello :wave: 14 | This issue appears to have had no activity for 3 months. We cannot keep track of whether individual issues 15 | have resolved themselves or still require attention without user interaction. We're thus adding the stale label to this issue to schedule 16 | it for getting closed in 5 days time. If you believe this issue is still valid and should be fixed, you can add a comment 17 | or remove the label to avoid it getting closed. 18 | 19 | Cheers :blue_heart: 20 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 21 | days-before-issue-stale: 90 22 | days-before-issue-close: 5 23 | days-before-pr-close: -1 24 | only-labels: 'waiting for more information' 25 | exempt-issue-labels: 'enhancement' 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw* 2 | dist 3 | node_modules 4 | builds 5 | key.pem 6 | .idea 7 | .DS_STORE 8 | 9 | # Sentry Config File 10 | .env.sentry-build-plugin 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "jsxBracketSameLine": true 5 | } 6 | -------------------------------------------------------------------------------- /CONSIDERATIONS.md: -------------------------------------------------------------------------------- 1 | # Considerations aka. Is this a good idea? 2 | 3 | As there have been debates about whether this software product is a good idea, I've made a little section here with my considerations. 4 | 5 | ### Goals 6 | 7 | The goals of this piece of software 8 | 9 | - provide an open cross-platform sync solution for browser data with a self-hosted server 10 | - performance is a plus, but not necessary 11 | - (eventual) consistency is more important than intention preservation (i.e. when ever a mistake happens during sync, it's guaranteed to be eventually consistent on all sites) 12 | 13 | ### Current status and Limitations 14 | 15 | The WebExtensions bookmarks API has a few limitations: 16 | 17 | 1. No support for batching or transactions 18 | 2. Record GUIDs can change, but are only known to change when Firefox Sync is used. 19 | 3. The data format doesn't represent descriptions, tags or separators 20 | 4. No way to create a per-device folder 21 | 5. It's impossible to express safe operations, because there are no compare-and-set primitives. 22 | 6. Triggering a sync after the first change, causing repeated syncs and inconsistency to spread to other devices. 23 | 24 | Nonetheless, I've chosen to utilize the WebExtensions API for implementing this sync client. As I'm aware, this decision has (at least) the following consequences: 25 | 26 | 1. No transaction support (\#1) leads to bad performance 27 | 2. No support for transactions (\#1) also can potentially cause intermediate states to be synced. However, all necessary precautions are taken to prevent this and even in the case that this happens, all sites will be eventually consistent, allowing you to manually resolve possible problems after the fact. 28 | 3. Due to the modification of GUIDs (\#2), usage of Firefox Sync along with Floccus is discouraged. 29 | 4. The incomplete data format (\#3) is an open problem, but doesn't impact the synchronization of the remaining accessible data. 30 | 5. The inability to exclude folders from sync in 3rd-party extensions (\#4) is a problem, but manageable when users are able to manually choose folders to ignore. (Currently not implemented) 31 | 6. The lack of safe write operations (\#5) can be dealt with similarly to the missing transaction support: Changes made during sync could lead to an unintended but consistent state, which can be resolved manually. Additionally, precautions are taken to prevent this. 32 | 7. In order to avoid syncing prematurely (\#6) floccus employs a timeout to wait until all pending bookmarks operations are done. 33 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy policy 2 | 3 | The Floccus browser extension ("Floccus") provides browser bookmarks synchronization functionality (“Functionality”) that works with privately controlled or otherwise accessible servers ("Third-party services") to store your bookmarks for synchronization. Your bookmarks are as secure as the Third-party Services you choose to store them on. 4 | 5 | ## Information you provide 6 | 7 | **Account Information.** You provide usernames and credentials to Third-party Services for all accounts you create. 8 | 9 | **Bookmarks.** All bookmarks that are stored in your browser are accessible to Floccus in order to provide the Functionality. Any third-party services you choose to store your bookmarks on will have access to your bookmarks. 10 | 11 | ## Information floccus collects 12 | 13 | **Debug log.** Floccus creates a debug log of all its actions, which is only accessible to you and may be shared by you at your sole discretion with the authors in order to aid in debugging. 14 | 15 | ## Information shared with others 16 | 17 | Neither the authors of Floccus nor the publisher receive any of the data you provide to Floccus. The authors cannot make any assurances about how the Third-party services you choose to store your bookmarks on will handle the data you provide. 18 | 19 | ## License 20 | 21 | Please also read the License which also governs the use of floccus as well as liability of its authors. 22 | 23 | ## Contact Us 24 | 25 | If you have questions about this Privacy Policy please contact me at mklehr@gmx.net. 26 | 27 | Marcel Klehr 28 | Natruper Straße 211A 29 | 49090 30 | Germany 31 | 32 | Effective as of Sep 28, 2018 33 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore 2 | 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | # Android Profiling 90 | *.hprof 91 | 92 | # Cordova plugins for Capacitor 93 | capacitor-cordova-android-plugins 94 | 95 | # Copied web assets 96 | app/src/main/assets/public 97 | -------------------------------------------------------------------------------- /android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.npmkeep 3 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace "org.handmadeideas.floccus" 5 | compileSdk rootProject.ext.compileSdkVersion 6 | defaultConfig { 7 | applicationId "org.handmadeideas.floccus" 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | targetSdkVersion rootProject.ext.targetSdkVersion 10 | versionCode 5005004 11 | versionName "5.5.4" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | aaptOptions { 14 | // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. 15 | // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 16 | ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' 17 | } 18 | manifestPlaceholders = [ 19 | "appAuthRedirectScheme": "org.handmadeideas.floccus" 20 | ] 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | dependenciesInfo { 29 | // Disables dependency metadata when building APKs. 30 | includeInApk = false 31 | // Disables dependency metadata when building Android App Bundles. 32 | includeInBundle = false 33 | } 34 | } 35 | 36 | repositories { 37 | flatDir{ 38 | dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" 44 | implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" 45 | implementation fileTree(include: ['*.jar'], dir: 'libs') 46 | implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" 47 | implementation project(':capacitor-android') 48 | testImplementation "junit:junit:$junitVersion" 49 | androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" 50 | androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" 51 | implementation project(':capacitor-cordova-android-plugins') 52 | implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" 53 | } 54 | 55 | apply from: 'capacitor.build.gradle' 56 | -------------------------------------------------------------------------------- /android/app/capacitor.build.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility JavaVersion.VERSION_21 6 | targetCompatibility JavaVersion.VERSION_21 7 | } 8 | } 9 | 10 | apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" 11 | dependencies { 12 | implementation project(':byteowls-capacitor-oauth2') 13 | implementation project(':capacitor-app') 14 | implementation project(':capacitor-browser') 15 | implementation project(':capacitor-device') 16 | implementation project(':capacitor-filesystem') 17 | implementation project(':capacitor-network') 18 | implementation project(':capacitor-preferences') 19 | implementation project(':capacitor-share') 20 | implementation project(':capacitor-splash-screen') 21 | implementation project(':send-intent') 22 | 23 | } 24 | 25 | 26 | if (hasProperty('postBuildExtras')) { 27 | postBuildExtras() 28 | } 29 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /android/app/src/main/assets/capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "org.handmadeideas.floccus", 3 | "appName": "floccus", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "dist", 7 | "loggingBehavior": "production", 8 | "server": { 9 | "androidScheme": "http" 10 | }, 11 | "plugins": { 12 | "SplashScreen": { 13 | "launchAutoHide": false, 14 | "androidScaleType": "CENTER_INSIDE", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "CapacitorHttp": { 18 | "enabled": true 19 | } 20 | }, 21 | "cordova": {} 22 | } 23 | -------------------------------------------------------------------------------- /android/app/src/main/assets/capacitor.plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pkg": "@byteowls/capacitor-oauth2", 4 | "classpath": "com.byteowls.capacitor.oauth2.OAuth2ClientPlugin" 5 | }, 6 | { 7 | "pkg": "@capacitor/app", 8 | "classpath": "com.capacitorjs.plugins.app.AppPlugin" 9 | }, 10 | { 11 | "pkg": "@capacitor/browser", 12 | "classpath": "com.capacitorjs.plugins.browser.BrowserPlugin" 13 | }, 14 | { 15 | "pkg": "@capacitor/device", 16 | "classpath": "com.capacitorjs.plugins.device.DevicePlugin" 17 | }, 18 | { 19 | "pkg": "@capacitor/filesystem", 20 | "classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin" 21 | }, 22 | { 23 | "pkg": "@capacitor/network", 24 | "classpath": "com.capacitorjs.plugins.network.NetworkPlugin" 25 | }, 26 | { 27 | "pkg": "@capacitor/preferences", 28 | "classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin" 29 | }, 30 | { 31 | "pkg": "@capacitor/share", 32 | "classpath": "com.capacitorjs.plugins.share.SharePlugin" 33 | }, 34 | { 35 | "pkg": "@capacitor/splash-screen", 36 | "classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin" 37 | }, 38 | { 39 | "pkg": "send-intent", 40 | "classpath": "de.mindlib.sendIntent.SendIntent" 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /android/app/src/main/java/org/handmadeideas/floccus/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.handmadeideas.floccus; 2 | 3 | import android.content.Intent; 4 | import android.webkit.ValueCallback; 5 | 6 | import com.getcapacitor.BridgeActivity; 7 | 8 | public class MainActivity extends BridgeActivity { 9 | 10 | @Override 11 | protected void onNewIntent(Intent intent) { 12 | super.onNewIntent(intent); 13 | this.handleIntent(intent); 14 | } 15 | 16 | private void handleIntent(Intent intent) { 17 | String action = intent.getAction(); 18 | String type = intent.getType(); 19 | if (Intent.ACTION_SEND.equals(action) && type != null) { 20 | bridge.getActivity().setIntent(intent); 21 | bridge.eval("window.dispatchEvent(new Event('sendIntentReceived'))", s -> { 22 | // no op 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-land-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-land-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-land-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-land-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-land-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-land-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-port-hdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-port-mdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-port-xhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-port-xxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-port-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable-port-xxxhdpi/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/drawable/splash.png -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | floccus bookmark sync 4 | floccus 5 | org.handmadeideas.floccus 6 | auth 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.7.2' 11 | } 12 | } 13 | 14 | apply from: "variables.gradle" 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | mavenCentral() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /android/capacitor.settings.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | include ':capacitor-android' 3 | project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 4 | 5 | include ':byteowls-capacitor-oauth2' 6 | project(':byteowls-capacitor-oauth2').projectDir = new File('../node_modules/@byteowls/capacitor-oauth2/android') 7 | 8 | include ':capacitor-app' 9 | project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') 10 | 11 | include ':capacitor-browser' 12 | project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android') 13 | 14 | include ':capacitor-device' 15 | project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/device/android') 16 | 17 | include ':capacitor-filesystem' 18 | project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') 19 | 20 | include ':capacitor-network' 21 | project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android') 22 | 23 | include ':capacitor-preferences' 24 | project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') 25 | 26 | include ':capacitor-share' 27 | project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') 28 | 29 | include ':capacitor-splash-screen' 30 | project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android') 31 | 32 | include ':send-intent' 33 | project(':send-intent').projectDir = new File('../node_modules/send-intent/android') 34 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | 24 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Mar 19 14:08:52 CET 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':capacitor-cordova-android-plugins' 3 | project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') 4 | 5 | apply from: 'capacitor.settings.gradle' -------------------------------------------------------------------------------- /android/variables.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | minSdkVersion = 23 3 | compileSdkVersion = 35 4 | targetSdkVersion = 35 5 | androidxActivityVersion = '1.9.2' 6 | androidxAppCompatVersion = '1.7.0' 7 | androidxCoordinatorLayoutVersion = '1.2.0' 8 | androidxCoreVersion = '1.15.0' 9 | androidxFragmentVersion = '1.8.4' 10 | coreSplashScreenVersion = '1.0.1' 11 | androidxWebkitVersion = '1.12.1' 12 | junitVersion = '4.13.2' 13 | androidxJunitVersion = '1.2.1' 14 | androidxEspressoCoreVersion = '3.6.1' 15 | cordovaAndroidVersion = '10.1.1' 16 | } 17 | -------------------------------------------------------------------------------- /capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "org.handmadeideas.floccus", 3 | "appName": "floccus", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "dist", 7 | "loggingBehavior": "production", 8 | "server": { 9 | "androidScheme": "http" 10 | }, 11 | "plugins": { 12 | "SplashScreen": { 13 | "launchAutoHide": false, 14 | "androidScaleType": "CENTER_INSIDE", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "CapacitorHttp": { 18 | "enabled": true 19 | } 20 | }, 21 | "cordova": {} 22 | } 23 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Manage and synchronize your bookmarks via Nextcloud, or any WebDAV service, or any Git service, or Google Drive, end-to-end encrypted, if you want. 2 | 3 | This is the standalone bookmarks manager android app variant of floccus. You can also install floccus on your Desktop browsers to sync bookmarks with them. This App, due to technical reasons, cannot access bookmarks in your mobile browser apps directly, which is why you can only view them in the app or import and export them as a html file. 4 | 5 | More information on https://floccus.org 6 | 7 | Support 8 | 9 | If you'd like to support the creation and maintenance of this software, you can donate here: 10 | https://floccus.org/donate/ 11 | 12 | Problems? 13 | 14 | Please file bug reports or requests for help over on Github: 15 | https://github.com/floccusaddon/floccus 16 | 17 | Legal 18 | 19 | License: https://github.com/marcelklehr/floccus/blob/master/LICENSE.txt 20 | 21 | Privacy Policy: https://github.com/marcelklehr/floccus/blob/master/PRIVACY_POLICY.md -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/fastlane/metadata/android/en-US/images/phoneScreenshots/01.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/fastlane/metadata/android/en-US/images/phoneScreenshots/02.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/fastlane/metadata/android/en-US/images/phoneScreenshots/03.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/fastlane/metadata/android/en-US/images/phoneScreenshots/04.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | Sync your bookmarks privately across browsers and devices 2 | -------------------------------------------------------------------------------- /google-api.credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "web":{ 3 | "client_id":"305459871054-4rr6n0jmsdvvprtjqbma5oeksshis2bn.apps.googleusercontent.com","project_id":"floccus-1613668481464","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"-2C9DALj2JYEGhZMZTvzN2ZE","redirect_uris":["https://mbepccofdnoepgicagpchfmafecckdam.chromiumapp.org/","https://76a380c4950986998208e7bb9dbd8fea94c91504.extensions.allizom.org/"] 4 | }, 5 | "android":{ 6 | "client_id":"305459871054-05e7kf9q9kkbeovaf380ldsb248psc2d.apps.googleusercontent.com","project_id":"floccus-1613668481464","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"] 7 | }, 8 | "ios": { 9 | "client_id": "305459871054-ovvunbhc8jf8g467gtpsbnap5el302gq.apps.googleusercontent.com" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /html/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Floccus 6 | 7 | 8 | 9 | {{ scripts }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | Floccus bookmark sync 14 | 15 | 16 |
17 | {{ scripts }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Floccus bookmark sync 10 | 23 | 24 | 25 |
26 | {{ scripts }} 27 | 28 | 29 | -------------------------------------------------------------------------------- /html/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 |
9 | 10 | 11 | {{ scripts }} 12 | 13 | 14 | -------------------------------------------------------------------------------- /icons/disabled_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/disabled_128.png -------------------------------------------------------------------------------- /icons/disabled_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/disabled_32.png -------------------------------------------------------------------------------- /icons/disabled_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/disabled_64.png -------------------------------------------------------------------------------- /icons/error_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/error_128.png -------------------------------------------------------------------------------- /icons/error_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/error_32.png -------------------------------------------------------------------------------- /icons/error_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/error_64.png -------------------------------------------------------------------------------- /icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/logo.png -------------------------------------------------------------------------------- /icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 34 | 54 | 58 | 62 | 63 | -------------------------------------------------------------------------------- /icons/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/logo_128.png -------------------------------------------------------------------------------- /icons/logo_128_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/logo_128_white.png -------------------------------------------------------------------------------- /icons/logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/logo_32.png -------------------------------------------------------------------------------- /icons/logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/logo_512.png -------------------------------------------------------------------------------- /icons/logo_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/logo_64.png -------------------------------------------------------------------------------- /icons/syncing_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/syncing_128.png -------------------------------------------------------------------------------- /icons/syncing_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/syncing_32.png -------------------------------------------------------------------------------- /icons/syncing_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/icons/syncing_64.png -------------------------------------------------------------------------------- /img/promotional-tile-marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-marquee.png -------------------------------------------------------------------------------- /img/promotional-tile-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-medium.png -------------------------------------------------------------------------------- /img/promotional-tile-medium.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-medium.xcf -------------------------------------------------------------------------------- /img/promotional-tile-medium2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-medium2.png -------------------------------------------------------------------------------- /img/promotional-tile-medium2.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-medium2.xcf -------------------------------------------------------------------------------- /img/promotional-tile-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-small.png -------------------------------------------------------------------------------- /img/promotional-tile-small.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/promotional-tile-small.xcf -------------------------------------------------------------------------------- /img/screen_chrome_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_chrome_account.png -------------------------------------------------------------------------------- /img/screen_chrome_folderpicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_chrome_folderpicker.png -------------------------------------------------------------------------------- /img/screen_chrome_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_chrome_options.png -------------------------------------------------------------------------------- /img/screen_chrome_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_chrome_wide.png -------------------------------------------------------------------------------- /img/screen_firefox_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_firefox_account.png -------------------------------------------------------------------------------- /img/screen_firefox_folderpicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_firefox_folderpicker.png -------------------------------------------------------------------------------- /img/screen_firefox_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/img/screen_firefox_options.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | App/build 2 | App/Pods 3 | App/App/public 4 | DerivedData 5 | xcuserdata 6 | 7 | # Cordova plugins for Capacitor 8 | capacitor-cordova-ios-plugins 9 | 10 | -------------------------------------------------------------------------------- /ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/App/App.xcodeproj/xcshareddata/xcschemes/Floccus New Bookmark.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 78 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /ios/App/App.xcodeproj/xcshareddata/xcschemes/Floccus.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ios/App/App.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/App/App.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/App/App/App.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.org.handmadeideas.floccus 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon-512@2x.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/Splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "splash-2732x2732-2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "splash-2732x2732-1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "splash-2732x2732.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png -------------------------------------------------------------------------------- /ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floccusaddon/floccus/0a46b5c6db3ae55a2130ea66d282ee358d576ec1/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png -------------------------------------------------------------------------------- /ios/App/App/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ios/App/App/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /ios/App/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | floccus 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIBackgroundModes 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | CFBundleURLTypes 53 | 54 | 55 | CFBundleTypeRole 56 | Viewer 57 | CFBundleURLName 58 | 59 | CFBundleURLSchemes 60 | 61 | org.handmadeideas.floccus 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /ios/App/App/capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "org.handmadeideas.floccus", 3 | "appName": "floccus", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "dist", 7 | "loggingBehavior": "production", 8 | "server": { 9 | "androidScheme": "http" 10 | }, 11 | "plugins": { 12 | "SplashScreen": { 13 | "launchAutoHide": false, 14 | "androidScaleType": "CENTER_INSIDE", 15 | "backgroundColor": "#ffffff" 16 | }, 17 | "CapacitorHttp": { 18 | "enabled": true 19 | } 20 | }, 21 | "cordova": {}, 22 | "packageClassList": [ 23 | "OAuth2ClientPlugin", 24 | "AppPlugin", 25 | "CAPBrowserPlugin", 26 | "DevicePlugin", 27 | "FilesystemPlugin", 28 | "CAPNetworkPlugin", 29 | "PreferencesPlugin", 30 | "SharePlugin", 31 | "SplashScreenPlugin", 32 | "SendIntentPlugin" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /ios/App/App/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/App/Floccus New Bookmark/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/App/Floccus New Bookmark/Floccus New Bookmark.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.org.handmadeideas.floccus 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/App/Floccus New Bookmark/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsFileWithMaxCount 12 | 5 13 | NSExtensionActivationSupportsImageWithMaxCount 14 | 5 15 | NSExtensionActivationSupportsMovieWithMaxCount 16 | 5 17 | NSExtensionActivationSupportsText 18 | 19 | NSExtensionActivationSupportsWebPageWithMaxCount 20 | 1 21 | NSExtensionActivationSupportsWebURLWithMaxCount 22 | 1 23 | NSExtensionActivationUsesStrictMatching 24 | 25 | 26 | 27 | NSExtensionMainStoryboard 28 | MainInterface 29 | NSExtensionPointIdentifier 30 | com.apple.share-services 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ios/App/Floccus.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.org.handmadeideas.floccus 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/App/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' 2 | 3 | platform :ios, '14.0' 4 | use_frameworks! 5 | 6 | # workaround to avoid Xcode caching of Pods that requires 7 | # Product -> Clean Build Folder after new Cordova plugins installed 8 | # Requires CocoaPods 1.6 or newer 9 | install! 'cocoapods', :disable_input_output_paths => true 10 | 11 | def capacitor_pods 12 | pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' 13 | pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' 14 | pod 'ByteowlsCapacitorOauth2', :path => '../../node_modules/@byteowls/capacitor-oauth2' 15 | pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' 16 | pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser' 17 | pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' 18 | pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' 19 | pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' 20 | pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' 21 | pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' 22 | pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' 23 | pod 'SendIntent', :path => '../../node_modules/send-intent' 24 | end 25 | 26 | target 'Floccus' do 27 | capacitor_pods 28 | # Add your Pods here 29 | end 30 | 31 | post_install do |installer| 32 | assertDeploymentTarget(installer) 33 | end 34 | -------------------------------------------------------------------------------- /ios/App/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ByteowlsCapacitorOauth2 (5.0.0): 3 | - Capacitor 4 | - OAuthSwift (= 2.2.0) 5 | - Capacitor (6.1.1): 6 | - CapacitorCordova 7 | - CapacitorApp (6.0.0): 8 | - Capacitor 9 | - CapacitorBrowser (6.0.1): 10 | - Capacitor 11 | - CapacitorCordova (6.1.1) 12 | - CapacitorDevice (6.0.0): 13 | - Capacitor 14 | - CapacitorFilesystem (6.0.0): 15 | - Capacitor 16 | - CapacitorNetwork (6.0.1): 17 | - Capacitor 18 | - CapacitorPreferences (6.0.1): 19 | - Capacitor 20 | - CapacitorShare (6.0.1): 21 | - Capacitor 22 | - CapacitorSplashScreen (6.0.1): 23 | - Capacitor 24 | - OAuthSwift (2.2.0) 25 | - SendIntent (0.0.1): 26 | - Capacitor 27 | 28 | DEPENDENCIES: 29 | - "ByteowlsCapacitorOauth2 (from `../../node_modules/@byteowls/capacitor-oauth2`)" 30 | - "Capacitor (from `../../node_modules/@capacitor/ios`)" 31 | - "CapacitorApp (from `../../node_modules/@capacitor/app`)" 32 | - "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)" 33 | - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" 34 | - "CapacitorDevice (from `../../node_modules/@capacitor/device`)" 35 | - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" 36 | - "CapacitorNetwork (from `../../node_modules/@capacitor/network`)" 37 | - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" 38 | - "CapacitorShare (from `../../node_modules/@capacitor/share`)" 39 | - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" 40 | - SendIntent (from `../../node_modules/send-intent`) 41 | 42 | SPEC REPOS: 43 | trunk: 44 | - OAuthSwift 45 | 46 | EXTERNAL SOURCES: 47 | ByteowlsCapacitorOauth2: 48 | :path: "../../node_modules/@byteowls/capacitor-oauth2" 49 | Capacitor: 50 | :path: "../../node_modules/@capacitor/ios" 51 | CapacitorApp: 52 | :path: "../../node_modules/@capacitor/app" 53 | CapacitorBrowser: 54 | :path: "../../node_modules/@capacitor/browser" 55 | CapacitorCordova: 56 | :path: "../../node_modules/@capacitor/ios" 57 | CapacitorDevice: 58 | :path: "../../node_modules/@capacitor/device" 59 | CapacitorFilesystem: 60 | :path: "../../node_modules/@capacitor/filesystem" 61 | CapacitorNetwork: 62 | :path: "../../node_modules/@capacitor/network" 63 | CapacitorPreferences: 64 | :path: "../../node_modules/@capacitor/preferences" 65 | CapacitorShare: 66 | :path: "../../node_modules/@capacitor/share" 67 | CapacitorSplashScreen: 68 | :path: "../../node_modules/@capacitor/splash-screen" 69 | SendIntent: 70 | :path: "../../node_modules/send-intent" 71 | 72 | SPEC CHECKSUMS: 73 | ByteowlsCapacitorOauth2: 9e7cdae2bf251463a6ad89493e27fb288bf694d7 74 | Capacitor: 8941aba4364ba9d1b22188569001f2ce45cc2b00 75 | CapacitorApp: 9d53aec7101f7b030a950c5bdc4df8612576b279 76 | CapacitorBrowser: 473c7fd70ddbe541608ff09ec1be14da0078279e 77 | CapacitorCordova: 8f2cc8d8d3619c566e9418fe8772064a94266106 78 | CapacitorDevice: f8fd88f9edd1261c55a109f32015b09bbbfdc4a0 79 | CapacitorFilesystem: 60e59ba274c234a979e7a3be2552feaadcee4263 80 | CapacitorNetwork: 5c94acfdddc22043f2ffaff224ce9b4aa5a179f0 81 | CapacitorPreferences: 72909b165bc7807103778ddbb86d5d8ce06abf71 82 | CapacitorShare: 02222f2457ff003e642370a9c1ecd101baaa27c8 83 | CapacitorSplashScreen: 61645214e8f955ff2b80f16a6a3648960fe4c89f 84 | OAuthSwift: 75efbb5bd9a4b2b71a37bd7e986bf3f55ddd54c6 85 | SendIntent: 0a17b6984c4f27e9dfa56513267ba2c044a5a6c8 86 | 87 | PODFILE CHECKSUM: ec4a5e49843d3546e8e3d2415a3cc07be4758a27 88 | 89 | COCOAPODS: 1.16.2 90 | -------------------------------------------------------------------------------- /ios/App/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/gulp-crx.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var ChromeExtension = require('crx') 4 | var through = require('through2') 5 | var merge = require('merge') 6 | var File = require('vinyl') 7 | var PluginError = require('plugin-error') 8 | 9 | module.exports = function(opt) { 10 | var files = [] 11 | function transform(file, encoding, done) { 12 | files.push(file.path) 13 | done() 14 | } 15 | 16 | return through.obj(transform, function(done) { 17 | // TODO proper support for streaming files 18 | // if (!file.isNull()) return done(null, file); 19 | 20 | var options = merge({}, opt) 21 | 22 | var that = this 23 | var crx = new ChromeExtension(options) 24 | 25 | var onError = function(err) { 26 | console.error(err) 27 | done(new PluginError('gulp-crx', err)) 28 | } 29 | 30 | crx 31 | .load(files) 32 | .then(function() { 33 | crx 34 | .pack() 35 | .then(function(crxBuffer) { 36 | that.push( 37 | new File({ 38 | path: options.filename, 39 | contents: crxBuffer 40 | }) 41 | ) 42 | 43 | if (options.updateXmlFilename) { 44 | var xmlBuffer = crx.generateUpdateXML() 45 | that.push( 46 | new File({ 47 | path: options.updateXmlFilename, 48 | contents: xmlBuffer 49 | }) 50 | ) 51 | } 52 | done() 53 | }) 54 | .catch(onError) 55 | }) 56 | .catch(onError) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /manifest-firefox-override.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | # Local development workaround for Firefox. 4 | # 5 | # Once you build this extension (with `npm install` and `npm run build`, as per ), to 6 | # load it, visit Firefox special URL about:debugging#/runtime/this-firefox. Click button "Load 7 | # Temporary Add-on..." That button shows a file/folder picker. 8 | # 9 | # Problem: The file picker does allow you to select `manifest.firefox.json`, BUT it will not load 10 | # it. It loads `manifest.json` (from the directory where you selected `manifest.firefox.json`) 11 | # instead. (Indeed, a Firefox defect - but life is too short for us to waste it on Mozilla's 12 | # bugzilla....) 13 | # 14 | # Firefox doesn't allow to use symlinks to workaround the above problem (see 15 | # https://bugzilla.mozilla.org/show_bug.cgi?id=803999 - symlinks are a security problem). 16 | # 17 | # Workaround: This script 18 | # 1. copies manifest.firefox.json over manifest.json 19 | # 2. prevents that change from being accidentally committed to GIT. 20 | 21 | # Enter the directory where this script is (in case we call it from somewhere else). 22 | cd "${0%/*}" 23 | 24 | # Invoking `/usr/bin/cp` directly, in case there's an alias that warns about overriding existing 25 | # files. 26 | /usr/bin/cp manifest.firefox.json manifest.json 27 | 28 | # See also 29 | # https://stackoverflow.com/questions/13630849/git-difference-between-assume-unchanged-and-skip-worktree 30 | git update-index --skip-worktree manifest.json 31 | -------------------------------------------------------------------------------- /manifest.chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "floccus bookmarks sync", 4 | "short_name": "floccus", 5 | "version": "5.5.4", 6 | "description": "__MSG_DescriptionExtension__", 7 | "icons": { 8 | "48": "icons/logo.png", 9 | "64": "icons/logo_64.png", 10 | "128": "icons/logo_128.png" 11 | }, 12 | 13 | "default_locale": "en", 14 | 15 | "permissions": ["alarms", "bookmarks", "storage", "unlimitedStorage", "tabs", "identity"], 16 | "optional_permissions": ["history"], 17 | "host_permissions": [ 18 | "*://*/*" 19 | ], 20 | "content_security_policy": { 21 | "extension_pages": "script-src 'self'; object-src 'self';" 22 | }, 23 | 24 | "options_ui": { 25 | "page": "dist/html/options.html", 26 | "browser_style": false 27 | }, 28 | 29 | "action": { 30 | "browser_style": false, 31 | "default_icon": { 32 | "48": "icons/logo.png" 33 | }, 34 | "default_title": "Open Floccus options", 35 | "default_popup": "dist/html/options.html" 36 | }, 37 | 38 | "background": { 39 | "service_worker": "dist/js/background-script.js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /manifest.firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "floccus bookmarks sync", 4 | "short_name": "floccus", 5 | "version": "5.5.4", 6 | "description": "__MSG_DescriptionExtension__", 7 | "icons": { 8 | "48": "icons/logo.png", 9 | "64": "icons/logo_64.png", 10 | "128": "icons/logo_128.png" 11 | }, 12 | 13 | "applications": { 14 | "gecko": { 15 | "id": "floccus@handmadeideas.org", 16 | "strict_min_version": "58.0" 17 | } 18 | }, 19 | 20 | "default_locale": "en", 21 | 22 | "permissions": ["*://*/*", "alarms", "bookmarks", "storage", "unlimitedStorage", "tabs", "identity"], 23 | "optional_permissions": ["history"], 24 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", 25 | 26 | "options_ui": { 27 | "page": "dist/html/options.html", 28 | "browser_style": false, 29 | "chrome_style": false 30 | }, 31 | 32 | "browser_action": { 33 | "browser_style": false, 34 | "chrome_style": false, 35 | "default_icon": { 36 | "48": "icons/logo.png" 37 | }, 38 | "default_title": "Open Floccus options", 39 | "default_popup": "dist/html/options.html" 40 | }, 41 | 42 | "background": { 43 | "page": "dist/html/background.html" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "floccus bookmarks sync", 4 | "short_name": "floccus", 5 | "version": "5.4.4", 6 | "description": "__MSG_DescriptionExtension__", 7 | "icons": { 8 | "48": "icons/logo.png", 9 | "64": "icons/logo_64.png", 10 | "128": "icons/logo_128.png" 11 | }, 12 | 13 | "default_locale": "en", 14 | 15 | "permissions": ["alarms", "bookmarks", "storage", "unlimitedStorage", "tabs", "identity"], 16 | "optional_permissions": ["history"], 17 | "host_permissions": [ 18 | "*://*/*" 19 | ], 20 | "content_security_policy": { 21 | "extension_pages": "script-src 'self'; object-src 'self';" 22 | }, 23 | 24 | "options_ui": { 25 | "page": "dist/html/options.html", 26 | "browser_style": false 27 | }, 28 | 29 | "action": { 30 | "browser_style": false, 31 | "default_icon": { 32 | "48": "icons/logo.png" 33 | }, 34 | "default_title": "Open Floccus options", 35 | "default_popup": "dist/html/options.html" 36 | }, 37 | 38 | "background": { 39 | "service_worker": "dist/js/background-script.js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/entries/background-script.js: -------------------------------------------------------------------------------- 1 | import BrowserController from '../lib/browser/BrowserController' 2 | 3 | const controller = new BrowserController 4 | controller.onLoad() 5 | -------------------------------------------------------------------------------- /src/entries/native.js: -------------------------------------------------------------------------------- 1 | import app from '../ui/native.js' 2 | 3 | app() 4 | -------------------------------------------------------------------------------- /src/entries/options.js: -------------------------------------------------------------------------------- 1 | import app from '../ui' 2 | app() 3 | -------------------------------------------------------------------------------- /src/entries/test.js: -------------------------------------------------------------------------------- 1 | require('../test/index.js') 2 | -------------------------------------------------------------------------------- /src/lib/AdapterFactory.ts: -------------------------------------------------------------------------------- 1 | import { TAdapter } from './interfaces/Adapter' 2 | import { IAccountData } from './interfaces/AccountStorage' 3 | 4 | export default { 5 | registry: {}, 6 | register(type:string, adapter: any):void { 7 | this.registry[type] = adapter 8 | }, 9 | async factory(data: any): Promise { 10 | if ('type' in data) { 11 | const adapter = await this.registry[data.type]() 12 | return new adapter(data) 13 | } 14 | }, 15 | async getDefaultValues(type:string):Promise { 16 | const adapter = await this.registry[type]() 17 | return { 18 | ...adapter.getDefaultValues(), 19 | enabled: true, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/CacheTree.ts: -------------------------------------------------------------------------------- 1 | import CachingAdapter from './adapters/Caching' 2 | import { IResource } from './interfaces/Resource' 3 | import { Folder, ItemLocation, TItemLocation } from './Tree' 4 | 5 | export default class CacheTree extends CachingAdapter implements IResource { 6 | protected location: TItemLocation = ItemLocation.LOCAL 7 | 8 | constructor() { 9 | super({}) 10 | } 11 | 12 | public setTree(tree: Folder) { 13 | this.bookmarksCache = tree.clone(false) 14 | this.bookmarksCache.createIndex() 15 | } 16 | 17 | async getBookmarksTree(): Promise> { 18 | const tree = await super.getBookmarksTree() 19 | tree.createIndex() 20 | return tree as Folder 21 | } 22 | 23 | isAvailable(): Promise { 24 | return Promise.resolve(true) 25 | } 26 | } -------------------------------------------------------------------------------- /src/lib/CachingTreeWrapper.ts: -------------------------------------------------------------------------------- 1 | import { CachingResource, OrderFolderResource } from './interfaces/Resource' 2 | import { Bookmark, Folder, ItemLocation } from './Tree' 3 | import CacheTree from './CacheTree' 4 | import Ordering from './interfaces/Ordering' 5 | 6 | export default class CachingTreeWrapper implements OrderFolderResource, CachingResource { 7 | private innerTree: OrderFolderResource 8 | private cacheTree: CacheTree 9 | 10 | constructor(innerTree: OrderFolderResource) { 11 | this.innerTree = innerTree 12 | this.cacheTree = new CacheTree() 13 | } 14 | 15 | async getBookmarksTree(): Promise> { 16 | const tree = await this.innerTree.getBookmarksTree() 17 | this.cacheTree.setTree(tree) 18 | return tree 19 | } 20 | 21 | async createBookmark(bookmark:Bookmark): Promise { 22 | const id = await this.innerTree.createBookmark(bookmark) 23 | const cacheId = await this.cacheTree.createBookmark(bookmark.copy(false)) 24 | const cacheBookmark = this.cacheTree.bookmarksCache.findBookmark(cacheId) 25 | cacheBookmark.id = id 26 | cacheBookmark.parentId = bookmark.parentId 27 | this.cacheTree.bookmarksCache.createIndex() 28 | return id 29 | } 30 | 31 | async updateBookmark(bookmark:Bookmark):Promise { 32 | await this.innerTree.updateBookmark(bookmark) 33 | await this.cacheTree.updateBookmark(bookmark.copy(false)) 34 | } 35 | 36 | async removeBookmark(bookmark:Bookmark): Promise { 37 | await this.innerTree.removeBookmark(bookmark) 38 | await this.cacheTree.removeBookmark(bookmark) 39 | } 40 | 41 | async createFolder(folder:Folder): Promise { 42 | const id = await this.innerTree.createFolder(folder) 43 | const cacheId = await this.cacheTree.createFolder(folder.copy(false)) 44 | const cacheFolder = this.cacheTree.bookmarksCache.findFolder(cacheId) 45 | cacheFolder.id = id 46 | cacheFolder.parentId = folder.parentId 47 | this.cacheTree.bookmarksCache.createIndex() 48 | return id 49 | } 50 | 51 | async orderFolder(id:string|number, order:Ordering): Promise { 52 | await this.innerTree.orderFolder(id, order) 53 | await this.cacheTree.orderFolder(id, order) 54 | } 55 | 56 | async updateFolder(folder:Folder): Promise { 57 | await this.innerTree.updateFolder(folder) 58 | await this.cacheTree.updateFolder(folder.copy(false)) 59 | } 60 | 61 | async removeFolder(folder:Folder): Promise { 62 | await this.innerTree.removeFolder(folder) 63 | await this.cacheTree.removeFolder(folder) 64 | } 65 | 66 | isAvailable(): Promise { 67 | return this.innerTree.isAvailable() 68 | } 69 | 70 | async isUsingBrowserTabs() { 71 | return this.innerTree.isUsingBrowserTabs?.() 72 | } 73 | 74 | getCacheTree(): Promise> { 75 | return this.cacheTree.getBookmarksTree() 76 | } 77 | } -------------------------------------------------------------------------------- /src/lib/Crypto.ts: -------------------------------------------------------------------------------- 1 | import { fromUint8Array, toUint8Array } from 'js-base64' 2 | 3 | export default class Crypto { 4 | static iterations = 250000 5 | static ivLength = 16 6 | 7 | static async sha256(message: string): Promise { 8 | const msgBuffer = new TextEncoder().encode(message) // encode as UTF-8 9 | const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer) // hash the message 10 | const hashHex = this.bufferToHexstr(new Uint8Array(hashBuffer)) // convert bytes to hex string 11 | return hashHex 12 | } 13 | 14 | static bufferToHexstr(buffer: Uint8Array): string { 15 | return Array.from(new Uint8Array(buffer)) 16 | .map(b => ('00' + b.toString(16)).slice(-2)) 17 | .join('') // convert bytes to hex string 18 | } 19 | 20 | static hexstrToBuffer(hex: string): Uint8Array { 21 | for ( 22 | var bytes = new Uint8Array(hex.length / 2), c = 0; 23 | c < hex.length; 24 | c += 2 25 | ) { 26 | bytes[c / 2] = parseInt(hex.substr(c, 2), 16) 27 | } 28 | return bytes 29 | } 30 | 31 | static async prepareKey(passphrase: string, salt: string): Promise { 32 | const enc = new TextEncoder() 33 | const passphraseBytes = enc.encode(passphrase) 34 | const saltBytes = enc.encode(salt) 35 | const key = await crypto.subtle.importKey('raw', passphraseBytes, 'PBKDF2', false, ['deriveKey']) 36 | return crypto.subtle.deriveKey( 37 | { 38 | name: 'PBKDF2', 39 | hash: 'SHA-256', 40 | salt: saltBytes, 41 | iterations: Crypto.iterations 42 | }, 43 | key, 44 | { 45 | name: 'AES-GCM', 46 | length: 256 47 | }, 48 | false, 49 | ['encrypt', 'decrypt'] 50 | ) 51 | } 52 | 53 | static async decryptAES(key: string, payload: string, salt: string) : Promise { 54 | const cryptoKey = await this.prepareKey(key, salt) 55 | const buffer = toUint8Array(payload) 56 | const iv = buffer.slice(0, this.ivLength) 57 | const ciphertext = buffer.slice(this.ivLength) 58 | 59 | const plaintextBytes = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, ciphertext) 60 | return new TextDecoder().decode(plaintextBytes) 61 | } 62 | 63 | static async encryptAES(key: string, message: string, salt: string): Promise { 64 | // Generate a random 16 byte initialization vector 65 | const iv = this.getRandomBytes(this.ivLength) 66 | const messageBytes = new TextEncoder().encode(message) 67 | const cryptoKey = await this.prepareKey(key, salt) 68 | const ciphertext = await crypto.subtle.encrypt( 69 | { name: 'AES-GCM', iv }, 70 | cryptoKey, 71 | messageBytes 72 | ) 73 | 74 | const resultBytes = this.concatBytes(iv, new Uint8Array(ciphertext)) 75 | 76 | return fromUint8Array(resultBytes) 77 | } 78 | 79 | static concatBytes(array1: Uint8Array, array2: Uint8Array): Uint8Array { 80 | const result = new Uint8Array(array1.length + array2.length) 81 | result.set(array1, 0) 82 | result.set(array2, array1.length) 83 | return result 84 | } 85 | 86 | static getRandomBytes(bytelength: number) : Uint8Array { 87 | const rand = new Uint8Array(bytelength) 88 | crypto.getRandomValues(rand) 89 | return rand 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/DefunctCrypto.js: -------------------------------------------------------------------------------- 1 | export default class Crypto { 2 | static async sha256(message) { 3 | const msgBuffer = new TextEncoder('utf-8').encode(message) // encode as UTF-8 4 | const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer) // hash the message 5 | const hashHex = this.bufferToHexstr(hashBuffer) // convert bytes to hex string 6 | return hashHex 7 | } 8 | 9 | static bufferToHexstr(buffer) { 10 | return Array.from(new Uint8Array(buffer)) 11 | .map(b => ('00' + b.toString(16)).slice(-2)) 12 | .join('') // convert bytes to hex string 13 | } 14 | 15 | static hexstrToBuffer(hex) { 16 | for ( 17 | var bytes = new Uint8Array(hex.length / 2), c = 0; 18 | c < hex.length; 19 | c += 2 20 | ) { 21 | bytes[c / 2] = parseInt(hex.substr(c, 2), 16) 22 | } 23 | return bytes 24 | } 25 | 26 | static async prepareKey(key) { 27 | const keyBuffer = await crypto.subtle.digest( 28 | 'SHA-256', 29 | new TextEncoder('utf-8').encode(key) 30 | ) // hash the key 31 | let cryptoKey = await crypto.subtle.importKey( 32 | 'raw', 33 | keyBuffer, 34 | { name: 'AES-CBC' }, 35 | false, 36 | ['decrypt', 'encrypt'] 37 | ) 38 | return cryptoKey 39 | } 40 | 41 | static async decryptAES(key, iv, ciphertext) { 42 | return new TextDecoder().decode( 43 | await crypto.subtle.decrypt( 44 | { name: 'AES-CBC', iv: Uint8Array.from(Crypto.iv) }, 45 | await this.prepareKey(key), 46 | this.hexstrToBuffer(ciphertext) 47 | ) 48 | ) 49 | } 50 | 51 | static async encryptAES(key, iv, message) { 52 | return this.bufferToHexstr( 53 | await crypto.subtle.encrypt( 54 | { name: 'AES-CBC', iv: Uint8Array.from(Crypto.iv) }, 55 | await this.prepareKey(key), 56 | new TextEncoder().encode(message) 57 | ) 58 | ) 59 | } 60 | 61 | static getRandomBytes(bytelength) { 62 | let rand = new Int8Array(bytelength) 63 | crypto.getRandomValues(rand) 64 | return rand 65 | } 66 | } 67 | 68 | // A default initialization vector for the key hash 69 | Crypto.iv = [ 70 | 58, 71 | 14, 72 | 9, 73 | 204, 74 | 174, 75 | 93, 76 | 77, 77 | 98, 78 | 12, 79 | 248, 80 | 11, 81 | 160, 82 | 143, 83 | 24, 84 | 119, 85 | 20 86 | ] 87 | -------------------------------------------------------------------------------- /src/lib/IndexedDB.ts: -------------------------------------------------------------------------------- 1 | import Dexie, { type EntityTable } from 'dexie' 2 | 3 | interface LogMessage { 4 | id: number; 5 | dateTime: number; 6 | message: string; 7 | } 8 | 9 | const db = new Dexie('floccus') as Dexie & { 10 | logs: EntityTable< 11 | LogMessage, 12 | 'id' // primary key "id" (for the typings only) 13 | >; 14 | } 15 | 16 | db.version(1).stores({ 17 | logs: '++id, dateTime, message' 18 | }) 19 | 20 | export { db } 21 | export { LogMessage } 22 | 23 | const MAX_STORAGE_SIZE = 50 * 1024 * 1024 // 50MB 24 | 25 | export async function freeStorageIfNecessary() { 26 | if (navigator.storage && navigator.storage.estimate) { 27 | let {usage, quota} = await navigator.storage.estimate() 28 | if (usage / quota > 0.9 || usage > MAX_STORAGE_SIZE) { 29 | const oneWeekAgo = Date.now() - 60 * 60 * 1000 * 24 * 7 30 | 31 | await db.logs 32 | .where('dateTime').below(oneWeekAgo) 33 | .delete() 34 | } 35 | 36 | ({usage, quota} = await navigator.storage.estimate()) 37 | if (usage / quota > 0.6 || usage > MAX_STORAGE_SIZE) { 38 | const oneDayAgo = Date.now() - 60 * 60 * 1000 * 24 39 | 40 | await db.logs 41 | .where('dateTime').below(oneDayAgo) 42 | .delete() 43 | } 44 | 45 | ({usage, quota} = await navigator.storage.estimate()) 46 | if (usage / quota > 0.6 || usage > MAX_STORAGE_SIZE) { 47 | const lastHour = Date.now() - 60 * 60 * 1000 48 | 49 | await db.logs 50 | .where('dateTime').below(lastHour) 51 | .delete() 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/lib/Mappings.ts: -------------------------------------------------------------------------------- 1 | import { TItem, TItemLocation, TItemType } from './Tree' 2 | 3 | type InternalItemTypeMapping = { LocalToServer: Record, ServerToLocal: Record } 4 | 5 | export type Mapping = Record> 6 | 7 | export type MappingSnapshot = { 8 | ServerToLocal: Mapping, 9 | LocalToServer: Mapping 10 | } 11 | 12 | export default class Mappings { 13 | private folders: InternalItemTypeMapping 14 | private bookmarks: InternalItemTypeMapping 15 | private storage: any 16 | 17 | constructor(storageAdapter:any, mappingsData:any) { 18 | this.storage = storageAdapter 19 | this.folders = mappingsData.folders 20 | this.bookmarks = mappingsData.bookmarks 21 | } 22 | 23 | getSnapshot():MappingSnapshot { 24 | return { 25 | ServerToLocal: { 26 | bookmark: {...this.bookmarks.ServerToLocal}, 27 | folder: {...this.folders.ServerToLocal} 28 | }, 29 | LocalToServer: { 30 | bookmark: {...this.bookmarks.LocalToServer}, 31 | folder: {...this.folders.LocalToServer} 32 | } 33 | } 34 | } 35 | 36 | async addFolder({ localId, remoteId }: { localId?:string|number, remoteId?:string|number }):Promise { 37 | Mappings.add(this.folders, { localId, remoteId }) 38 | } 39 | 40 | async removeFolder({ localId, remoteId }: { localId?:string|number, remoteId?:string|number }):Promise { 41 | Mappings.remove(this.folders, { localId, remoteId }) 42 | } 43 | 44 | async addBookmark({ localId, remoteId }: { localId?:string|number, remoteId?:string|number }):Promise { 45 | Mappings.add(this.bookmarks, { localId, remoteId }) 46 | } 47 | 48 | async removeBookmark({ localId, remoteId }: { localId?:string|number, remoteId?:string|number }):Promise { 49 | Mappings.remove(this.bookmarks, { localId, remoteId }) 50 | } 51 | 52 | async persist():Promise { 53 | await this.storage.setMappings({ 54 | folders: this.folders, 55 | bookmarks: this.bookmarks 56 | }) 57 | } 58 | 59 | private static add(mappings, { localId, remoteId }: { localId?:string|number, remoteId?:string|number }) { 60 | if (typeof localId === 'undefined' || typeof remoteId === 'undefined') { 61 | throw new Error('Cannot add empty mapping') 62 | } 63 | mappings.LocalToServer[localId] = remoteId 64 | mappings.ServerToLocal[remoteId] = localId 65 | } 66 | 67 | private static remove(mappings, { localId, remoteId }: { localId?:string|number, remoteId?:string|number }):InternalItemTypeMapping { 68 | if (localId && remoteId && mappings.LocalToServer[localId] !== remoteId) { 69 | mappings = this.remove(mappings, { localId }) 70 | return this.remove(mappings, { remoteId }) 71 | } 72 | 73 | if (typeof localId !== 'undefined') { 74 | delete mappings.ServerToLocal[mappings.LocalToServer[localId]] 75 | delete mappings.LocalToServer[localId] 76 | } else { 77 | delete mappings.LocalToServer[mappings.ServerToLocal[remoteId]] 78 | delete mappings.ServerToLocal[remoteId] 79 | } 80 | } 81 | 82 | static mapId(mappingsSnapshot:MappingSnapshot, item: TItem, target: TItemLocation) : string|number { 83 | if (item.location === target) { 84 | return item.id 85 | } 86 | return mappingsSnapshot[item.location + 'To' + target][item.type][item.id] 87 | } 88 | 89 | static mapParentId(mappingsSnapshot:MappingSnapshot, item: TItem, target: TItemLocation) : string|number { 90 | if (item.location === target) { 91 | return item.parentId 92 | } 93 | return mappingsSnapshot[item.location + 'To' + target].folder[item.parentId] 94 | } 95 | 96 | static mappable(mappingsSnapshot: MappingSnapshot, item1: TItem, item2: TItem) : boolean { 97 | if (Mappings.mapId(mappingsSnapshot, item1, item2.location) === item2.id) { 98 | return true 99 | } 100 | if (Mappings.mapId(mappingsSnapshot, item2, item1.location) === item1.id) { 101 | return true 102 | } 103 | return false 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/lib/PathHelper.js: -------------------------------------------------------------------------------- 1 | export default class PathHelper { 2 | static reverseStr(str) { 3 | return str 4 | .split('') 5 | .reverse() 6 | .join('') 7 | } 8 | 9 | static pathToArray(path) { 10 | return this.reverseStr(path) 11 | .split(/[/](?![\\])|[/](?=\\\\)/) 12 | .map(value => this.reverseStr(value)) 13 | .map(value => value.replace(/\\[/]/g, '/')) 14 | .map(value => value.replace(/\\\\/g, '\\')) 15 | .reverse() 16 | } 17 | 18 | static arrayToPath(array) { 19 | return array 20 | .map(value => value.replace(/\\/g, '\\\\')) 21 | .map(value => value.replace(/[/]/g, '\\/')) 22 | .join('/') 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/adapters/Fake.js: -------------------------------------------------------------------------------- 1 | import CachingAdapter from './Caching' 2 | 3 | export default class FakeAdapter extends CachingAdapter { 4 | constructor(server) { 5 | super() 6 | this.server = server 7 | } 8 | 9 | static getDefaultValues() { 10 | return { 11 | type: 'fake' 12 | } 13 | } 14 | 15 | setData(data) { 16 | this.server = data 17 | } 18 | 19 | getData() { 20 | return JSON.parse(JSON.stringify(this.server)) 21 | } 22 | 23 | getLabel() { 24 | return 'Fake account (floccus)' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/browser-api.js: -------------------------------------------------------------------------------- 1 | /* global chrome browser */ 2 | 3 | const ChromePromise = (function(root) { 4 | 'use strict' 5 | var push = Array.prototype.push, 6 | hasOwnProperty = Object.prototype.hasOwnProperty 7 | 8 | function ChromePromise(chrome, Promise) { 9 | chrome = chrome || root.chrome 10 | Promise = Promise || root.Promise 11 | 12 | var runtime = chrome.runtime 13 | 14 | fillProperties(chrome, this) 15 | 16 | /// ///////////// 17 | 18 | function setPromiseFunction(fn, thisArg) { 19 | return function() { 20 | var args = arguments 21 | 22 | return new Promise(function(resolve, reject) { 23 | function callback() { 24 | var err = runtime.lastError 25 | if (err) { 26 | reject(err) 27 | } else { 28 | resolve.apply(null, arguments) 29 | } 30 | } 31 | 32 | push.call(args, callback) 33 | 34 | fn.apply(thisArg, args) 35 | }) 36 | } 37 | } 38 | 39 | function fillProperties(source, target) { 40 | for (var key in source) { 41 | if (hasOwnProperty.call(source, key)) { 42 | var val = source[key] 43 | var type = typeof val 44 | 45 | if (type === 'object' && !(val instanceof ChromePromise) && key.indexOf('on') !== 0) { 46 | target[key] = {} 47 | fillProperties(val, target[key]) 48 | } else if (type === 'function') { 49 | target[key] = setPromiseFunction(val, source) 50 | } else { 51 | target[key] = val 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | return ChromePromise 59 | })(typeof window !== 'undefined' ? window : self) 60 | 61 | let b 62 | if (typeof browser === 'undefined' && typeof chrome !== 'undefined') { 63 | b = new ChromePromise(chrome, Promise) 64 | b.alarms = chrome.alarms // Don't promisify alarms -- don't make sense, yo! 65 | b.browserAction = chrome.browserAction // apparently, they provide no callbacks for these 66 | b.action = chrome.action // apparently, they provide no callbacks for these 67 | b.i18n = chrome.i18n 68 | } else { 69 | b = browser 70 | } 71 | 72 | export default b 73 | -------------------------------------------------------------------------------- /src/lib/browser/BrowserDetection.ts: -------------------------------------------------------------------------------- 1 | export const isVivaldi = async() => { 2 | const {default: browser} = await import('../browser-api.js') 3 | const tabs = await browser.tabs.query({ active: true, currentWindow: true }) 4 | return Boolean(tabs?.[0]?.['vivExtData']) 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/getFavicon.js: -------------------------------------------------------------------------------- 1 | export function getIcons(html, pageUrl) { 2 | const url = new URL(pageUrl) 3 | const parser = new DOMParser() 4 | const document = parser.parseFromString(html, 'text/html') 5 | var links = document.getElementsByTagName('link') 6 | var icons = [] 7 | 8 | for (var i = 0; i < links.length; i++) { 9 | var link = links[i] 10 | 11 | // Technically it could be null / undefined if someone didn't set it! 12 | // People do weird things when building pages! 13 | var rel = link.getAttribute('rel') 14 | if (rel) { 15 | // I don't know why people don't use indexOf more often 16 | // It is faster than regex for simple stuff like this 17 | // Lowercase comparison for safety 18 | if (rel.toLowerCase().indexOf('icon') > -1) { 19 | var href = link.getAttribute('href') 20 | 21 | // Make sure href is not null / undefined 22 | if (href) { 23 | // Relative 24 | // Lowercase comparison in case some idiot decides to put the 25 | // https or http in caps 26 | // Also check for absolute url with no protocol 27 | if (href.toLowerCase().indexOf('https:') === -1 && href.toLowerCase().indexOf('http:') === -1 && 28 | href.indexOf('//') !== 0) { 29 | // This is of course assuming the script is executing in the browser 30 | // Node.js is a different story! As I would be using cheerio.js for parsing the html instead of document. 31 | // Also you would use the response.headers object for Node.js below. 32 | 33 | var absoluteHref = url.protocol + '//' + url.host 34 | 35 | if (url.port) { 36 | absoluteHref += ':' + url.port 37 | } 38 | 39 | // We already have a forward slash 40 | // On the front of the href 41 | if (href.indexOf('/') === 0) { 42 | absoluteHref += href 43 | } else { 44 | // We don't have a forward slash 45 | // It is really relative! 46 | var path = url.pathname.split('/') 47 | path.pop() 48 | var finalPath = path.join('/') 49 | 50 | absoluteHref += finalPath + '/' + href 51 | } 52 | icons.push(absoluteHref) 53 | } else if (href.indexOf('//') === 0) { 54 | // Absolute url with no protocol 55 | var absoluteUrl = url.protocol + href 56 | icons.push(absoluteUrl) 57 | } else { 58 | // Absolute 59 | icons.push(href) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | icons.push(url.protocol + '//' + url.host + '/favicon.ico') 67 | 68 | return icons 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/interfaces/Account.ts: -------------------------------------------------------------------------------- 1 | import { IAccountData } from './AccountStorage' 2 | import Account from '../Account' 3 | 4 | export default interface IAccount { 5 | get(id:string):Promise 6 | create(data: IAccountData):Promise 7 | import(accounts:IAccountData[]):Promise 8 | export(accountIds:string[]):Promise 9 | getAllAccounts():Promise 10 | getAccountsContainingLocalId(localId:string, ancestors:string[], allAccounts:Account[]):Promise 11 | stringifyError(er: any): Promise 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/interfaces/AccountStorage.ts: -------------------------------------------------------------------------------- 1 | import Mappings from '../Mappings' 2 | import { Folder, ItemLocation } from '../Tree' 3 | import { ISerializedSyncProcess } from '../strategies/Default' 4 | 5 | export type TAccountStrategy = 'default' | 'overwrite' | 'slave' 6 | 7 | export interface IAccountData { 8 | localRoot?: string 9 | strategy?: TAccountStrategy 10 | syncInterval?: number 11 | nestedSync?: boolean 12 | failsafe?: boolean 13 | username?: string 14 | password?: string 15 | label?: string 16 | errorCount?: number 17 | clickCountEnabled?: boolean 18 | [p:string]: any 19 | } 20 | 21 | export default interface IAccountStorage { 22 | accountId: string; 23 | getAccountData(key): Promise; 24 | setAccountData(data:IAccountData, key:string): Promise; 25 | deleteAccountData(): Promise 26 | initCache(): Promise 27 | getCache(): Promise> 28 | setCache(data): Promise 29 | deleteCache(): Promise 30 | initMappings(): Promise; 31 | getMappings(): Promise; 32 | setMappings(data): Promise; 33 | deleteMappings(): Promise; 34 | getCurrentContinuation(): Promise; 35 | setCurrentContinuation(continuation: ISerializedSyncProcess|null): Promise; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/interfaces/Adapter.ts: -------------------------------------------------------------------------------- 1 | import { Bookmark, ItemLocation, TItemLocation } from '../Tree' 2 | import TResource from './Resource' 3 | import { IAccountData } from './AccountStorage' 4 | 5 | export default interface IAdapter { 6 | setData(data: IAccountData): void 7 | getData() :IAccountData 8 | getLabel(): string 9 | acceptsBookmark(bookmark:Bookmark): boolean 10 | onSyncStart(needLock?:boolean, forceLock?: boolean):Promise 11 | onSyncComplete():Promise 12 | onSyncFail():Promise 13 | cancel():void 14 | } 15 | 16 | export type TAdapter = IAdapter & TResource 17 | -------------------------------------------------------------------------------- /src/lib/interfaces/Controller.ts: -------------------------------------------------------------------------------- 1 | export default interface IController { 2 | setEnabled(enabled:boolean): void; 3 | unlock(key):Promise; 4 | scheduleSync(accountId, wait):Promise; 5 | scheduleAll():Promise; 6 | cancelSync(accountId, keepEnabled):Promise; 7 | syncAccount(accountId, strategy, forceSync):Promise; 8 | onStatusChange(listener):()=>void; 9 | getUnlocked():Promise; 10 | onLoad():Promise; 11 | } 12 | 13 | export const STATUS_ERROR = Symbol('error') 14 | export const STATUS_SYNCING = Symbol('syncing') 15 | export const STATUS_ALLGOOD = Symbol('allgood') 16 | export const STATUS_DISABLED = Symbol('disabled') 17 | -------------------------------------------------------------------------------- /src/lib/interfaces/Ordering.ts: -------------------------------------------------------------------------------- 1 | import { TItemLocation, TItemType } from '../Tree' 2 | 3 | export interface OrderingItem { 4 | type: TItemType 5 | id: string|number 6 | } 7 | 8 | type Ordering = OrderingItem[] 9 | export default Ordering 10 | -------------------------------------------------------------------------------- /src/lib/interfaces/Resource.ts: -------------------------------------------------------------------------------- 1 | import { Bookmark, Folder, ItemLocation, TItem, TItemLocation } from '../Tree' 2 | import Ordering from './Ordering' 3 | 4 | export interface IResource { 5 | getBookmarksTree(loadAll?: boolean):Promise> 6 | createBookmark(bookmark: Bookmark):Promise 7 | updateBookmark(bookmark: Bookmark):Promise 8 | removeBookmark(bookmark:Bookmark):Promise 9 | 10 | createFolder(folder:Folder):Promise 11 | updateFolder(folder:Folder):Promise 12 | removeFolder(folder:Folder):Promise 13 | isAvailable():Promise 14 | isUsingBrowserTabs?: () => Promise 15 | } 16 | 17 | export interface CachingResource extends IResource { 18 | getCacheTree():Promise> 19 | } 20 | 21 | export interface BulkImportResource extends IResource { 22 | bulkImportFolder(id: number|string, folder:Folder):Promise> 23 | } 24 | 25 | export interface LoadFolderChildrenResource extends IResource { 26 | loadFolderChildren(id: number|string, all?: boolean):Promise[]> 27 | } 28 | 29 | export interface OrderFolderResource extends IResource { 30 | orderFolder(id: number|string, order:Ordering):Promise 31 | } 32 | 33 | export interface ClickCountResource extends IResource { 34 | countClick(url:string):Promise 35 | } 36 | 37 | export type TLocalTree = IResource & OrderFolderResource 38 | 39 | type TResource = IResource|BulkImportResource|LoadFolderChildrenResource|OrderFolderResource 40 | export default TResource -------------------------------------------------------------------------------- /src/lib/interfaces/Serializer.ts: -------------------------------------------------------------------------------- 1 | import { Folder, ItemLocation } from '../Tree' 2 | 3 | export default interface Serializer { 4 | serialize(folder:Folder): string 5 | deserialize(data:string):Folder 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/isTest.ts: -------------------------------------------------------------------------------- 1 | export const isTest = typeof window !== 'undefined' && (new URL(window.location.href)).pathname.includes('test') 2 | -------------------------------------------------------------------------------- /src/lib/native/I18n.ts: -------------------------------------------------------------------------------- 1 | import IntlMessageFormat from 'intl-messageformat' 2 | import DEFAULT_MESSAGES from '../../../_locales/en/messages.json' 3 | 4 | // hehe, ignore all the things... 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | const context = require.context( 8 | '../../../_locales', 9 | true, 10 | /^.*?\.json$/, 11 | 'lazy' 12 | ) 13 | 14 | interface TranslationEntry { 15 | message: string; 16 | } 17 | interface Messages { 18 | [key: string]: TranslationEntry; 19 | } 20 | 21 | export default class I18n { 22 | private locales: string[]; 23 | private locale = 'en' 24 | private messages: Messages | undefined; 25 | private defaultMessages: Messages; 26 | constructor(locale: string) { 27 | this.locales = [locale] 28 | this.defaultMessages = DEFAULT_MESSAGES 29 | this.messages = DEFAULT_MESSAGES 30 | } 31 | 32 | setLocales(locales:string[]):void { 33 | this.locales = locales 34 | } 35 | 36 | async load():Promise { 37 | for (const locale of this.locales) { 38 | try { 39 | const fileName = './' + locale.replace('-', '_') + '/messages.json' 40 | const imported = await context(fileName) 41 | console.log(imported) 42 | this.messages = imported 43 | this.locale = locale 44 | break 45 | } catch (error) { 46 | console.warn(error) 47 | } 48 | try { 49 | const fileName = './' + locale.split('-')[0] + '/messages.json' 50 | const imported = await context(fileName) 51 | console.log(imported) 52 | this.messages = imported 53 | this.locale = locale.split('-')[0] 54 | break 55 | } catch (error) { 56 | console.warn(error) 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Get a formatted message with the given name 63 | */ 64 | public getMessage(messageName: string, content?: any, formats?: any): string { 65 | const string = this.doGetMessage(messageName) 66 | if (string) { 67 | const message = new IntlMessageFormat(string.message, this.locale, formats).format(content) 68 | if (!message) { 69 | return messageName 70 | } 71 | if (Array.isArray(message)) { 72 | return message.join('') 73 | } 74 | return message 75 | } 76 | return messageName 77 | } 78 | 79 | /** 80 | * Get message with given name from the default locale 81 | */ 82 | private getDefaultLocaleMessage(messageName: string): TranslationEntry | null { 83 | if (!Object.hasOwnProperty.call(this.defaultMessages, messageName)) { 84 | console.warn(`WARN: No message found with name ${messageName} in default locale en`) 85 | return null 86 | } 87 | return this.defaultMessages[messageName] 88 | } 89 | 90 | /** 91 | * Get message with given name 92 | */ 93 | private doGetMessage(messageName: string): TranslationEntry | null { 94 | if (!this.messages || !Object.hasOwnProperty.call(this.messages, messageName)) { 95 | console.warn(`No message found with name ${messageName} in locale ${this.locale}. Using default locale 'en'`) 96 | return this.getDefaultLocaleMessage(messageName) 97 | } 98 | return this.messages[messageName] 99 | } 100 | } 101 | 102 | export const i18n = new I18n('en') 103 | -------------------------------------------------------------------------------- /src/lib/serializers/Html.ts: -------------------------------------------------------------------------------- 1 | import Serializer from '../interfaces/Serializer' 2 | import { Bookmark, Folder, ItemLocation, TItem } from '../Tree' 3 | import * as cheerio from 'cheerio' 4 | 5 | class HtmlSerializer implements Serializer { 6 | serialize(folder) { 7 | return `

\n${this._serializeFolder(folder, '')}

\n` 8 | } 9 | 10 | _htmlentities_encode(string) { 11 | return string.replace(/[<>&"']/g, char => '&#' + char.charCodeAt(0) + ';') 12 | } 13 | 14 | _serializeFolder(folder, indent) { 15 | return folder.children 16 | .map(child => { 17 | if (child instanceof Bookmark) { 18 | return ( 19 | `${indent}

${this._htmlentities_encode(child.title)}\n` 20 | ) 21 | } else if (child instanceof Folder) { 22 | const nextIndent = indent + ' ' 23 | return ( 24 | `${indent}

${this._htmlentities_encode(child.title)}

\n` + 25 | `${indent}

\n${this._serializeFolder( 26 | child, 27 | nextIndent 28 | )}${indent}

\n` 29 | ) 30 | } 31 | }) 32 | .join('') 33 | } 34 | 35 | deserialize(html): Folder { 36 | const items: TItem[] = parseByString(html) 37 | items.forEach(f => { f.parentId = '0' }) 38 | return new Folder({id: '0', title: 'root', children: items, location: ItemLocation.SERVER, isRoot: true}) 39 | } 40 | } 41 | 42 | export default new HtmlSerializer() 43 | 44 | // The following code is based on https://github.com/hold-baby/bookmark-file-parser 45 | // Copyright (c) 2019 hold-baby 46 | // MIT License 47 | 48 | export const getRootFolder = (body: cheerio.Cheerio) => { 49 | const h3 = body.find('h3').first() 50 | 51 | const isChrome = typeof h3.attr('personal_toolbar_folder') === 'string' 52 | 53 | if (isChrome) { 54 | return body.children('dl').first() 55 | } 56 | 57 | const isSafari = typeof h3.attr('folded') === 'string' 58 | 59 | if (isSafari) { 60 | return body 61 | } 62 | 63 | const isIE = typeof h3.attr('item_id') === 'string' 64 | 65 | if (isIE) { 66 | return body.children('dl').first() 67 | } 68 | 69 | const isFireFox = h3.text() === 'Mozilla Firefox' 70 | 71 | if (isFireFox) { 72 | return body.children('dl').first() 73 | } 74 | 75 | return body.children('dl').first() 76 | } 77 | 78 | export const parseByString = (content: string) => { 79 | const $ = cheerio.load(content) 80 | 81 | const body = $('body') 82 | const root: TItem[] = [] 83 | const rdt = getRootFolder(body).children('dt') 84 | 85 | const parseNode = (node: cheerio.Cheerio, parentId?: string|number) => { 86 | const eq0 = node.children().eq(0) 87 | const title = typeof eq0.text() !== 'undefined' ? eq0.text() : '' 88 | let url = '' 89 | const id = typeof eq0.attr('id') !== 'undefined' ? eq0.attr('id') : '' 90 | let children: TItem[] = [] 91 | 92 | switch (eq0[0].name) { 93 | case 'h3': 94 | // folder 95 | const dl = node.children('dl').first() 96 | const dts = dl.children() 97 | 98 | const ls = dts.toArray().map((ele) => { 99 | if (ele.name !== 'dt') return null 100 | return parseNode($(ele), id) 101 | }) 102 | children = ls.filter((item) => item !== null) as TItem[] 103 | return new Folder({id, title, parentId, children, location: ItemLocation.SERVER}) 104 | case 'a': 105 | // site 106 | url = eq0.attr('href') || '' 107 | return new Bookmark({id, title, url, parentId, location: ItemLocation.SERVER}) 108 | } 109 | throw new Error('Failed to parse') 110 | } 111 | 112 | rdt.each((_, item) => { 113 | const node = $(item) 114 | const child = parseNode(node) 115 | root.push(child) 116 | }) 117 | 118 | return root 119 | } 120 | -------------------------------------------------------------------------------- /src/lib/serializers/Xbel.ts: -------------------------------------------------------------------------------- 1 | import Serializer from '../interfaces/Serializer' 2 | import { Bookmark, Folder, ItemLocation } from '../Tree' 3 | import { XMLParser, XMLBuilder } from 'fast-xml-parser' 4 | 5 | class XbelSerializer implements Serializer { 6 | serialize(folder: Folder) { 7 | const xbelObj = this._serializeFolder(folder) 8 | const xmlBuilder = new XMLBuilder({format: true, preserveOrder: true, ignoreAttributes: false}) 9 | return xmlBuilder.build(xbelObj) 10 | } 11 | 12 | deserialize(xbel: string) { 13 | const parser = new XMLParser({ 14 | preserveOrder: true, 15 | ignorePiTags: true, 16 | ignoreAttributes: false, 17 | parseTagValue: false, 18 | }) 19 | const xmlObj = parser.parse(xbel) 20 | 21 | if (!Array.isArray(xmlObj[0].xbel)) { 22 | throw new Error( 23 | 'Parse Error: ' + xbel 24 | ) 25 | } 26 | 27 | const rootFolder = new Folder({ id: 0, title: 'root', location: ItemLocation.SERVER }) 28 | try { 29 | this._parseFolder(xmlObj[0].xbel, rootFolder) 30 | } catch (e) { 31 | throw new Error( 32 | 'Parse Error: ' + e.message 33 | ) 34 | } 35 | return rootFolder 36 | } 37 | 38 | _parseFolder(xbelObj, folder: Folder) { 39 | /* parse depth first */ 40 | 41 | xbelObj 42 | .forEach(node => { 43 | let item 44 | if (typeof node.bookmark !== 'undefined') { 45 | item = new Bookmark({ 46 | id: parseInt(node[':@']['@_id']), 47 | parentId: folder.id, 48 | url: node[':@']['@_href'], 49 | title: '' + (typeof node.bookmark?.[0]?.title?.[0]?.['#text'] !== 'undefined' ? node.bookmark?.[0]?.title?.[0]?.['#text'] : ''), // cast to string 50 | location: ItemLocation.SERVER, 51 | }) 52 | } else if (typeof node.folder !== 'undefined') { 53 | item = new Folder({ 54 | id: parseInt(node[':@']?.['@_id']), 55 | title: '' + (typeof node.folder?.[0]?.title?.[0]?.['#text'] !== 'undefined' ? node.folder?.[0]?.title?.[0]?.['#text'] : ''), // cast to string 56 | parentId: folder.id, 57 | location: ItemLocation.SERVER, 58 | }) 59 | this._parseFolder(node.folder, item) 60 | } else { 61 | return 62 | } 63 | 64 | folder.children.push(item) 65 | }) 66 | } 67 | 68 | _serializeFolder(folder: Folder) { 69 | return folder.children 70 | .map(child => { 71 | if (child instanceof Bookmark) { 72 | return { 73 | bookmark: [ 74 | {title: [{'#text': child.title}]} 75 | ], 76 | ':@': { 77 | '@_href': child.url, 78 | '@_id': String(child.id) 79 | } 80 | } 81 | } 82 | 83 | if (child instanceof Folder) { 84 | return { 85 | folder: [ 86 | {title: [{'#text': child.title}]}, 87 | ...this._serializeFolder(child) 88 | ], 89 | ':@': { 90 | ...('id' in child && {'@_id': String(child.id)}), 91 | } 92 | } 93 | } 94 | }) 95 | } 96 | } 97 | 98 | export default new XbelSerializer() 99 | -------------------------------------------------------------------------------- /src/test/index.js: -------------------------------------------------------------------------------- 1 | import { createWebdriverAndHtmlReporter } from './reporter' 2 | import util from 'util' 3 | 4 | // Make logs accessible to travis selenium runner 5 | window.floccusTestLogs = [] 6 | const consoleLog = console.log 7 | console.log = function() { 8 | consoleLog.apply(console, arguments) 9 | window.floccusTestLogs.push(util.format.apply(util, arguments)) 10 | } 11 | 12 | window.addEventListener('DOMContentLoaded', () => { 13 | mocha.setup('bdd') 14 | import('./test').then(() => { 15 | mocha.reporter(createWebdriverAndHtmlReporter(mocha._reporter)) 16 | mocha.run() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/test/reporter.js: -------------------------------------------------------------------------------- 1 | /* global Mocha */ 2 | const { 3 | EVENT_RUN_END, 4 | EVENT_TEST_BEGIN, 5 | EVENT_TEST_FAIL, 6 | EVENT_TEST_PASS, 7 | EVENT_SUITE_BEGIN 8 | } = Mocha.Runner.constants 9 | 10 | export function createWebdriverAndHtmlReporter(html_reporter) { 11 | return function(runner) { 12 | Mocha.reporters.Base.call(this, runner) 13 | 14 | // report on the selenium screen, too 15 | // fucking eslint forced me to create this prop 16 | runner.html = new html_reporter(runner) 17 | 18 | // build a summary 19 | const summary = [] 20 | 21 | let mocha = document.querySelector('#mocha') 22 | runner.on(EVENT_TEST_BEGIN, test => { 23 | console.log('\n### ' + test.title + ' ###\n') 24 | // Scroll down test display after each test 25 | mocha.scrollTop = mocha.scrollHeight 26 | }) 27 | 28 | runner.on(EVENT_SUITE_BEGIN, suite => { 29 | if (suite.root) return 30 | console.log('\n## ' + suite.title + ' ## \n') 31 | summary.push('## ' + suite.title) 32 | }) 33 | 34 | runner.on(EVENT_TEST_FAIL, test => { 35 | console.log('->', 'FAILED :', test.title, stringifyException(test.err)) 36 | summary.push(Mocha.reporters.Base.symbols.err + ' ' + test.title) 37 | }) 38 | runner.on(EVENT_TEST_PASS, test => { 39 | const status = `${test.title} (${test.duration / 1000}s)` 40 | console.log('->', 'PASSED :', status) 41 | summary.push(Mocha.reporters.Base.symbols.ok + ' ' + status) 42 | }) 43 | 44 | runner.on(EVENT_RUN_END, () => { 45 | const minutes = Math.floor(runner.stats.duration / 1000 / 60) 46 | const seconds = Math.round((runner.stats.duration / 1000) % 60) 47 | 48 | console.log('\n' + summary.join('\n')) 49 | 50 | console.log( 51 | 'FINISHED ' + (runner.stats.failures > 0 ? 'FAILED' : 'PASSED') + ' -', 52 | runner.stats.passes, 53 | 'tests passed,', 54 | runner.stats.failures, 55 | 'tests failed, duration: ' + minutes + ':' + seconds 56 | ) 57 | }) 58 | } 59 | } 60 | 61 | function stringifyException(exception) { 62 | if (exception.list) { 63 | return exception.list 64 | .map(e => { 65 | return stringifyException(e) 66 | }) 67 | .join('\n') 68 | } 69 | let err = exception.stack || exception.toString() 70 | 71 | // FF / Opera do not add the message 72 | if (!~err.indexOf(exception.message)) { 73 | err = exception.message + '\n' + err 74 | } 75 | 76 | // Safari doesn't give you a stack. Let's at least provide a source line. 77 | if (!exception.stack && exception.sourceURL && exception.line !== undefined) { 78 | err += '\n(' + exception.sourceURL + ':' + exception.line + ')' 79 | } 80 | 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /src/ui/NativeApp.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | 64 | -------------------------------------------------------------------------------- /src/ui/NativeRouter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Tree from './views/native/Tree' 4 | import Home from './views/native/Home' 5 | import NewAccount from './views/NewAccount' 6 | import AddBookmarkIntent from './views/native/AddBookmarkIntent' 7 | import ImportExport from './views/native/ImportExport' 8 | import About from './views/native/About' 9 | 10 | Vue.use(Router) 11 | 12 | export const routes = { 13 | HOME: 'HOME', 14 | TREE: 'TREE', 15 | ACCOUNT_OPTIONS: 'ACCOUNT_OPTIONS', 16 | NEW_ACCOUNT: 'NEW_ACCOUNT', 17 | ADD_BOOKMARK: 'ADD_BOOKMARK', 18 | FUNDING: 'FUNDING', 19 | UPDATE: 'UPDATE', 20 | IMPORTEXPORT: 'IMPORTEXPORT', 21 | DONATE: 'DONATE', 22 | ABOUT: 'ABOUT' 23 | 24 | } 25 | 26 | export const router = new Router({ 27 | linkActiveClass: 'active', 28 | routes: [ 29 | { 30 | path: '/', 31 | name: routes.HOME, 32 | component: Home, 33 | }, 34 | { 35 | path: '/tree/:accountId', 36 | name: routes.TREE, 37 | component: Tree, 38 | }, 39 | { 40 | path: '/options/:accountId', 41 | name: routes.ACCOUNT_OPTIONS, 42 | component: () => import(/* webpackPrefetch: true */ './views/native/Options'), 43 | }, 44 | { 45 | path: '/new', 46 | name: routes.NEW_ACCOUNT, 47 | component: NewAccount, 48 | }, 49 | { 50 | path: '/update', 51 | name: routes.UPDATE, 52 | component: () => import(/* webpackPrefetch: true */ './views/Update'), 53 | }, 54 | { 55 | path: '/newBookmark/:accountId/:url/:text?', 56 | name: routes.ADD_BOOKMARK, 57 | component: AddBookmarkIntent, 58 | }, 59 | { 60 | path: '/importexport', 61 | name: routes.IMPORTEXPORT, 62 | component: ImportExport, 63 | }, 64 | { 65 | path: '/about', 66 | name: routes.ABOUT, 67 | component: About, 68 | }, 69 | ], 70 | }) 71 | -------------------------------------------------------------------------------- /src/ui/components/NextcloudLogin.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 92 | 93 | 96 | -------------------------------------------------------------------------------- /src/ui/components/OptionAllowRedirects.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /src/ui/components/OptionAutoSync.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /src/ui/components/OptionClientCert.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /src/ui/components/OptionDeleteAccount.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | 32 | 35 | -------------------------------------------------------------------------------- /src/ui/components/OptionDownloadLogs.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /src/ui/components/OptionExportBookmarks.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /src/ui/components/OptionFailsafe.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/ui/components/OptionFileType.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/ui/components/OptionNestedSync.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 33 | 34 | 37 | -------------------------------------------------------------------------------- /src/ui/components/OptionPassphrase.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 64 | 65 | 68 | -------------------------------------------------------------------------------- /src/ui/components/OptionResetCache.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /src/ui/components/OptionSyncInterval.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /src/ui/components/OptionSyncStrategy.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /src/ui/components/OptionsFake.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /src/ui/components/native/DialogChooseFolder.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 136 | 137 | -------------------------------------------------------------------------------- /src/ui/components/native/DialogEditFolder.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 129 | 130 | 133 | -------------------------------------------------------------------------------- /src/ui/components/native/DialogImportBookmarks.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 70 | 71 | 74 | -------------------------------------------------------------------------------- /src/ui/components/native/Drawer.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 123 | 124 | 127 | -------------------------------------------------------------------------------- /src/ui/components/native/FaviconImage.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 70 | 71 | 74 | -------------------------------------------------------------------------------- /src/ui/components/native/OptionAllowNetwork.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | 30 | 33 | -------------------------------------------------------------------------------- /src/ui/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import vuetify from './plugins/vuetify' 3 | import capacitor from './plugins/capacitor' 4 | import App from './App' 5 | import { router } from './router' 6 | import store from './store' 7 | import i18nPlugin from './plugins/i18n' 8 | import {i18n} from '../lib/native/I18n' 9 | import '@mdi/font/css/materialdesignicons.css' 10 | 11 | Vue.mixin(i18nPlugin) 12 | Vue.mixin(capacitor) 13 | 14 | const app = () => { 15 | i18n.setLocales(navigator.languages) 16 | i18n.load().then(() => { 17 | window['floccus'] = global['Floccus'] = new Vue({ 18 | el: '#app', 19 | router, 20 | store, 21 | vuetify, 22 | render: (h) => h(App), 23 | }) 24 | }) 25 | } 26 | 27 | export default app 28 | -------------------------------------------------------------------------------- /src/ui/native-public-path.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | __webpack_public_path__ = '/js/' 3 | -------------------------------------------------------------------------------- /src/ui/native.js: -------------------------------------------------------------------------------- 1 | import './native-public-path' 2 | import Vue from 'vue' 3 | import vuetify from './plugins/vuetify' 4 | import capacitor from './plugins/capacitor' 5 | import App from './NativeApp' 6 | import store from './store/native' 7 | import i18nPlugin from './plugins/i18n' 8 | import { router } from './NativeRouter' 9 | import {i18n} from '../lib/native/I18n' 10 | import '@mdi/font/css/materialdesignicons.css' 11 | 12 | Vue.mixin(i18nPlugin) 13 | Vue.mixin(capacitor) 14 | 15 | const app = () => { 16 | i18n.setLocales(navigator.languages) 17 | i18n.load().then(() => { 18 | window['floccus'] = global['Floccus'] = new Vue({ 19 | el: '#app', 20 | router, 21 | store, 22 | vuetify, 23 | render: (h) => h(App), 24 | }) 25 | }) 26 | } 27 | 28 | export default app 29 | -------------------------------------------------------------------------------- /src/ui/plugins/capacitor.js: -------------------------------------------------------------------------------- 1 | import { Capacitor } from '@capacitor/core' 2 | import { App } from '@capacitor/app' 3 | 4 | let backButtonListener = null 5 | 6 | export default { 7 | computed: { 8 | isBrowser() { 9 | return Capacitor.getPlatform() === 'web' || !Capacitor.getPlatform() 10 | }, 11 | }, 12 | mounted() { 13 | if (this.$options.backButton) { 14 | if (backButtonListener) { 15 | backButtonListener.remove() 16 | } 17 | backButtonListener = App.addListener('backButton', () => this.$options.backButton.call(this)) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ui/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import {i18n} from '../../lib/native/I18n' 2 | 3 | export default { 4 | methods: { 5 | t(messageName, substitutions) { 6 | return i18n.getMessage(messageName, substitutions) 7 | }, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import 'vuetify/dist/vuetify.min.css' 4 | 5 | Vue.use(Vuetify) 6 | 7 | const opts = { 8 | theme: { 9 | dark: Boolean(window.matchMedia('(prefers-color-scheme: dark)').matches) 10 | } 11 | } 12 | 13 | export default new Vuetify(opts) 14 | -------------------------------------------------------------------------------- /src/ui/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Overview from './views/Overview' 4 | import NewAccount from './views/NewAccount' 5 | import Update from './views/Update' 6 | import ImportExport from './views/ImportExport' 7 | import Donate from './views/Donate' 8 | import About from './views/native/About' 9 | import Telemetry from './views/Telemetry.vue' 10 | 11 | Vue.use(Router) 12 | 13 | export const routes = { 14 | OVERVIEW: 'OVERVIEW', 15 | ACCOUNT_OPTIONS: 'ACCOUNT_OPTIONS', 16 | NEW_ACCOUNT: 'NEW_ACCOUNT', 17 | SET_KEY: 'SET_KEY', 18 | FUNDING: 'FUNDING', 19 | UPDATE: 'UPDATE', 20 | IMPORTEXPORT: 'IMPORTEXPORT', 21 | DONATE: 'DONATE', 22 | TELEMETRY: 'TELEMETRY', 23 | 24 | } 25 | 26 | export const router = new Router({ 27 | linkActiveClass: 'active', 28 | routes: [ 29 | { 30 | path: '/', 31 | name: routes.OVERVIEW, 32 | component: Overview, 33 | }, 34 | 35 | { 36 | path: '/options/:accountId', 37 | name: routes.ACCOUNT_OPTIONS, 38 | component: () => import(/* webpackPrefetch: true */ './views/AccountOptions'), 39 | }, 40 | { 41 | path: '/new', 42 | name: routes.NEW_ACCOUNT, 43 | component: NewAccount, 44 | }, 45 | { 46 | path: '/about', 47 | name: 'ABOUT', 48 | component: About, 49 | }, 50 | { 51 | path: '/update', 52 | name: routes.UPDATE, 53 | component: Update, 54 | }, 55 | { 56 | path: '/importexport', 57 | name: routes.IMPORTEXPORT, 58 | component: ImportExport, 59 | }, 60 | { 61 | path: '/donate', 62 | name: routes.DONATE, 63 | component: Donate, 64 | }, 65 | { 66 | path: '/telemetry', 67 | name: routes.TELEMETRY, 68 | component: Telemetry, 69 | }, 70 | ], 71 | }) 72 | -------------------------------------------------------------------------------- /src/ui/store/definitions.js: -------------------------------------------------------------------------------- 1 | export const actions = { 2 | LOAD_LOCKED: 'LOAD_LOCKED', 3 | UNLOCK: 'UNLOCK', 4 | SET_KEY: 'SET_KEY', 5 | UNSET_KEY: 'UNSET_KEY', 6 | LOAD_ACCOUNTS: 'LOAD_ACCOUNTS', 7 | SELECT_ACCOUNT: 'SELECT_ACCOUNT', 8 | LOAD_TREE: 'LOAD_TREE', 9 | LOAD_TREE_FROM_DISK: 'LOAD_TREE_FROM_DISK', 10 | CREATE_BOOKMARK: 'CREATE_BOOKMARK', 11 | EDIT_BOOKMARK: 'EDIT_BOOKMARK', 12 | DELETE_BOOKMARK: 'DELETE_BOOKMARK', 13 | SHARE_BOOKMARK: 'SHARE_BOOKMARK', 14 | CREATE_FOLDER: 'CREATE_FOLDER', 15 | EDIT_FOLDER: 'EDIT_FOLDER', 16 | DELETE_FOLDER: 'DELETE_FOLDER', 17 | IMPORT_BOOKMARKS: 'IMPORT_BOOKMARKS', 18 | CREATE_ACCOUNT: 'CREATE_ACCOUNT', 19 | IMPORT_ACCOUNTS: 'IMPORT_ACCOUNTS', 20 | EXPORT_ACCOUNTS: 'EXPORT_ACCOUNTS', 21 | DELETE_ACCOUNT: 'DELETE_ACCOUNT', 22 | RESET_ACCOUNT: 'RESET_ACCOUNT', 23 | STORE_ACCOUNT: 'STORE_ACCOUNT', 24 | TRIGGER_SYNC: 'TRIGGER_SYNC', 25 | FORCE_SYNC: 'FORCE_SYNC', 26 | TRIGGER_SYNC_ALL: 'TRIGGER_SYNC_ALL', 27 | TRIGGER_SYNC_UP: 'TRIGGER_SYNC_UP', 28 | TRIGGER_SYNC_DOWN: 'TRIGGER_SYNC_DOWN', 29 | CANCEL_SYNC: 'CANCEL_SYNC', 30 | DOWNLOAD_LOGS: 'DOWNLOAD_LOGS', 31 | EXPORT_BOOKMARKS: 'EXPORT_BOOKMARKS', 32 | TEST_WEBDAV_SERVER: 'TEST_WEBDAV_SERVER', 33 | TEST_NEXTCLOUD_SERVER: 'TEST_NEXTCLOUD_SERVER', 34 | TEST_LINKWARDEN_SERVER: 'TEST_LINKWARDEN_SERVER', 35 | TEST_KARAKEEP_SERVER: 'TEST_KARAKEEP_SERVER', 36 | START_LOGIN_FLOW: 'START_LOGIN_FLOW', 37 | STOP_LOGIN_FLOW: 'STOP_LOGIN_FLOW', 38 | REQUEST_NETWORK_PERMISSIONS: 'REQUEST_NETWORK_PERMISSIONS', 39 | COUNT_BOOKMARK_CLICK: 'COUNT_BOOKMARK_CLICK', 40 | REQUEST_HISTORY_PERMISSIONS: 'REQUEST_HISTORY_PERMISSIONS', 41 | SET_SORTBY: 'SET_SORTBY', 42 | } 43 | 44 | export const mutations = { 45 | LOADING_START: 'LOADING_START', 46 | LOADING_END: 'LOADING_END', 47 | SET_LOCKED: 'SET_LOCKED', 48 | LOAD_ACCOUNTS: 'LOAD_ACCOUNTS', 49 | STORE_ACCOUNT_DATA: 'STORE_ACCOUNT_DATA', 50 | REMOVE_ACCOUNT: 'REMOVE_ACCOUNT', 51 | LOAD_TREE: 'LOAD_TREE', 52 | SET_LOGIN_FLOW_STATE: 'SET_LOGIN_FLOW_STATE', 53 | SET_LAST_FOLDER: 'SET_LAST_FOLDER', 54 | } 55 | -------------------------------------------------------------------------------- /src/ui/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex, { Store } from 'vuex' 3 | import { mutationsDefinition } from './mutations' 4 | import { actionsDefinition } from './actions' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Store({ 9 | mutations: mutationsDefinition, 10 | actions: actionsDefinition, 11 | state: { 12 | locked: false, 13 | accounts: {}, 14 | loginFlow: { 15 | isRunning: false 16 | }, 17 | loading: { 18 | accounts: true, 19 | } 20 | }, 21 | getters: {}, 22 | }) 23 | -------------------------------------------------------------------------------- /src/ui/store/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mutations } from './definitions' 3 | 4 | export const mutationsDefinition = { 5 | [mutations.SET_LOCKED](state, locked) { 6 | state.locked = locked 7 | }, 8 | [mutations.LOAD_ACCOUNTS](state, accounts) { 9 | state.accounts = accounts 10 | }, 11 | [mutations.STORE_ACCOUNT_DATA](state, {id, data}) { 12 | Vue.set(state.accounts[id], 'data', data) 13 | }, 14 | [mutations.REMOVE_ACCOUNT](state, id) { 15 | Vue.delete(state.accounts, id) 16 | }, 17 | [mutations.SET_LOGIN_FLOW_STATE](state, running) { 18 | Vue.set(state.loginFlow, 'isRunning', running) 19 | }, 20 | [mutations.LOADING_START](state, label) { 21 | Vue.set(state.loading, label, true) 22 | }, 23 | [mutations.LOADING_END](state, label) { 24 | Vue.set(state.loading, label, false) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/store/native/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex, { Store } from 'vuex' 3 | import { mutationsDefinition } from './mutations' 4 | import { actionsDefinition } from './actions' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Store({ 9 | mutations: mutationsDefinition, 10 | actions: actionsDefinition, 11 | state: { 12 | locked: false, 13 | accounts: {}, 14 | tree: null, 15 | loginFlow: { 16 | isRunning: false 17 | }, 18 | loading: { 19 | accounts: true, 20 | }, 21 | lastFolders: {}, 22 | }, 23 | getters: {}, 24 | }) 25 | -------------------------------------------------------------------------------- /src/ui/store/native/mutations.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mutations } from '../definitions' 3 | 4 | export const mutationsDefinition = { 5 | [mutations.SET_LOCKED](state, locked) { 6 | state.locked = locked 7 | }, 8 | [mutations.LOAD_ACCOUNTS](state, accounts) { 9 | state.accounts = accounts 10 | }, 11 | [mutations.STORE_ACCOUNT_DATA](state, {id, data}) { 12 | Vue.set(state.accounts[id], 'data', data) 13 | }, 14 | [mutations.REMOVE_ACCOUNT](state, id) { 15 | Vue.delete(state.accounts, id) 16 | }, 17 | [mutations.LOAD_TREE](state, tree) { 18 | state.tree = tree 19 | }, 20 | [mutations.SET_LOGIN_FLOW_STATE](state, running) { 21 | Vue.set(state.loginFlow, 'isRunning', running) 22 | }, 23 | [mutations.LOADING_START](state, label) { 24 | Vue.set(state.loading, label, true) 25 | }, 26 | [mutations.LOADING_END](state, label) { 27 | Vue.set(state.loading, label, false) 28 | }, 29 | [mutations.SET_LAST_FOLDER](state, {accountId, folderId}) { 30 | Vue.set(state.lastFolders, accountId, folderId) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/views/Donate.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 82 | 83 | 89 | -------------------------------------------------------------------------------- /src/ui/views/Overview.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 94 | 95 | 102 | -------------------------------------------------------------------------------- /src/ui/views/Telemetry.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 70 | 71 | 77 | -------------------------------------------------------------------------------- /src/ui/views/native/ImportExport.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /test/apache-vhost.conf: -------------------------------------------------------------------------------- 1 | 2 | # [...] 3 | 4 | DocumentRoot %TRAVIS_BUILD_DIR% 5 | 6 | 7 | Options FollowSymLinks MultiViews ExecCGI 8 | AllowOverride All 9 | Require all granted 10 | 11 | 12 | # Wire up Apache to use Travis CI's php-fpm. 13 | 14 | AddHandler php5-fcgi .php 15 | Action php5-fcgi /php5-fcgi 16 | Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi 17 | FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -pass-header Authorization 18 | 19 | 20 | Require all granted 21 | 22 | 23 | 24 | # [...] 25 | 26 | -------------------------------------------------------------------------------- /test/apcu.ini: -------------------------------------------------------------------------------- 1 | extension=apcu.so 2 | apc.enabled=true 3 | apc.enable_cli=true -------------------------------------------------------------------------------- /test/save-stats.js: -------------------------------------------------------------------------------- 1 | const GistClient = require('gist-client') 2 | const gistClient = new GistClient() 3 | 4 | const GIST_ID = '51b4015641802f4f275574ca98beed61' 5 | 6 | async function save(sha, label, data) { 7 | if (!process.env.GIST_TOKEN) return 8 | gistClient.setToken(process.env['GIST_TOKEN']) 9 | const gist = await gistClient.getOneById(GIST_ID) 10 | const db = JSON.parse(gist.files['index.json'].content) 11 | if (!db[sha]) { 12 | db[sha] = {} 13 | } 14 | db[sha][label] = data 15 | await gistClient.update(GIST_ID, { 16 | files: { 'index.json': { content: JSON.stringify(db) } } 17 | }) 18 | } 19 | 20 | module.exports = save 21 | -------------------------------------------------------------------------------- /transifex.yml: -------------------------------------------------------------------------------- 1 | git: 2 | filters: 3 | - filter_type: file 4 | file_format: CHROME 5 | source_language: en 6 | source_file: _locales/en/messages.json 7 | translation_files_expression: '_locales//messages.json' 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true, 6 | "allowJs": true, 7 | "outDir": "./dist/", 8 | "jsx": "react", 9 | "noImplicitThis": true, 10 | "esModuleInterop": true, 11 | "resolveJsonModule": true, 12 | "lib": [ 13 | "DOM", 14 | "es2015", 15 | "es2016", 16 | "es2017", 17 | "es2019", 18 | "webworker" 19 | ] 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "dist" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { VueLoaderPlugin } = require('vue-loader') 3 | const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin') 4 | const webpack = require('webpack') 5 | 6 | module.exports = { 7 | entry: { 8 | 'background-script': path.join( 9 | __dirname, 10 | 'src', 11 | 'entries', 12 | 'background-script.js' 13 | ), 14 | options: path.join(__dirname, 'src', 'entries', 'options.js'), 15 | test: path.join(__dirname, 'src', 'entries', 'test.js'), 16 | native: path.join(__dirname, 'src', 'entries', 'native.js'), 17 | }, 18 | output: { 19 | path: path.resolve(__dirname, 'dist', 'js'), 20 | publicPath: '/dist/js/', 21 | filename: `[name].js`, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|vue)$/, 27 | use: 'eslint-loader', 28 | exclude: /node_modules/, 29 | enforce: 'pre', 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: ['vue-style-loader', 'css-loader'], 34 | }, 35 | { 36 | test: /\.s(c|a)ss$/, 37 | use: [ 38 | 'vue-style-loader', 39 | 'css-loader', 40 | { 41 | loader: 'sass-loader', 42 | // Requires sass-loader@^8.0.0 43 | options: { 44 | implementation: require('sass'), 45 | sassOptions: { 46 | fiber: false, 47 | indentedSyntax: true, // optional 48 | }, 49 | }, 50 | }, 51 | ], 52 | }, 53 | { 54 | test: /\.vue$/, 55 | loader: 'vue-loader', 56 | exclude: /node_modules/, 57 | }, 58 | { 59 | test: /\.tsx?$/, 60 | use: 'ts-loader', 61 | exclude: /node_modules/, 62 | }, 63 | { 64 | test: /\.js$/, 65 | exclude: /node_modules/, 66 | use: { 67 | loader: 'babel-loader', 68 | options: { 69 | presets: [ 70 | [ 71 | '@babel/preset-env', 72 | { 73 | useBuiltIns: 'usage', 74 | corejs: { version: '3.19', proposals: true }, 75 | shippedProposals: true, 76 | }, 77 | ], 78 | ], 79 | // https://github.com/dexie/Dexie.js/issues/2143 80 | plugins: [ 81 | '@babel/plugin-transform-async-to-generator' 82 | ] 83 | }, 84 | }, 85 | }, 86 | ], 87 | }, 88 | plugins: [ 89 | new VueLoaderPlugin(), 90 | new VuetifyLoaderPlugin(), 91 | new webpack.ProvidePlugin({ 92 | Buffer: ['buffer', 'Buffer'], 93 | }), 94 | new webpack.ProvidePlugin({ 95 | process: 'process/browser.js', 96 | }), 97 | ], 98 | resolve: { 99 | extensions: ['.js', '.vue', '.ts', '.json'], 100 | fallback: { 101 | buffer: require.resolve('buffer'), 102 | process: require.resolve('process/browser.js'), 103 | stream: require.resolve('stream-browserify'), 104 | }, 105 | }, 106 | } 107 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | const webpack = require('webpack') 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'cheap-module-source-map', 8 | optimization: { 9 | splitChunks: { chunks: 'async' }, 10 | }, 11 | plugins: [ 12 | new webpack.DefinePlugin({ 13 | 'DEBUG': JSON.stringify(true) 14 | }) 15 | ] 16 | }) 17 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { 2 | sentryWebpackPlugin 3 | } = require("@sentry/webpack-plugin") 4 | const merge = require('webpack-merge') 5 | const common = require('./webpack.common.js') 6 | const webpack = require('webpack') 7 | const packageJSON = require('./package.json') 8 | 9 | module.exports = merge(common, { 10 | mode: 'production', 11 | devtool: 'source-map', 12 | optimization: { 13 | splitChunks: { chunks: 'all' }, 14 | }, 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | DEBUG: JSON.stringify(false) 18 | }), 19 | sentryWebpackPlugin({ 20 | authToken: process.env.SENTRY_AUTH_TOKEN, 21 | org: "marcel-klehr", 22 | project: "floccus", 23 | release: { 24 | name: packageJSON.version 25 | }, 26 | sourcemaps: { 27 | disable: true, 28 | } 29 | }), 30 | ] 31 | }) 32 | --------------------------------------------------------------------------------