From 24617aa8f364161f6f38b27a7c87e9ee68af6f93 Mon Sep 17 00:00:00 2001 From: 403F <4o3f@proton.me> Date: Thu, 29 Feb 2024 23:04:04 +0800 Subject: [PATCH] feat: custom schema support (#516) * feat(custom-scheme): add custom scheme functionality (WIP) * feat(custom-schema): implement custom schema for auto import * fix(custom-schema): fix missing deep link prepare * fix(custom-schema): fix typo and better handling --- .gitignore | 1 + backend/Cargo.lock | 56 ++++++ .../.github/workflows/audit.yml | 26 +++ .../.github/workflows/format.yml | 24 +++ .../.github/workflows/lint.yml | 27 +++ .../.github/workflows/release.yml | 33 ++++ backend/tauri-plugin-deep-link/.gitignore | 2 + backend/tauri-plugin-deep-link/CHANGELOG.md | 34 ++++ backend/tauri-plugin-deep-link/Cargo.toml | 29 +++ .../tauri-plugin-deep-link/LICENSE_APACHE-2.0 | 177 +++++++++++++++++ backend/tauri-plugin-deep-link/LICENSE_MIT | 21 ++ backend/tauri-plugin-deep-link/README.md | 20 ++ backend/tauri-plugin-deep-link/cliff.toml | 71 +++++++ .../tauri-plugin-deep-link/example/Info.plist | 21 ++ .../tauri-plugin-deep-link/example/main.rs | 39 ++++ backend/tauri-plugin-deep-link/renovate.json | 3 + backend/tauri-plugin-deep-link/src/lib.rs | 63 ++++++ backend/tauri-plugin-deep-link/src/linux.rs | 141 ++++++++++++++ backend/tauri-plugin-deep-link/src/macos.rs | 184 ++++++++++++++++++ .../src/template.desktop | 7 + backend/tauri-plugin-deep-link/src/windows.rs | 145 ++++++++++++++ backend/tauri/Cargo.toml | 1 + backend/tauri/Info.plist | 19 ++ backend/tauri/src/main.rs | 15 +- src/components/profile/profile-viewer.tsx | 3 +- src/pages/_layout.tsx | 15 +- src/pages/profiles.tsx | 17 +- 27 files changed, 1189 insertions(+), 5 deletions(-) create mode 100644 backend/tauri-plugin-deep-link/.github/workflows/audit.yml create mode 100644 backend/tauri-plugin-deep-link/.github/workflows/format.yml create mode 100644 backend/tauri-plugin-deep-link/.github/workflows/lint.yml create mode 100644 backend/tauri-plugin-deep-link/.github/workflows/release.yml create mode 100644 backend/tauri-plugin-deep-link/.gitignore create mode 100644 backend/tauri-plugin-deep-link/CHANGELOG.md create mode 100644 backend/tauri-plugin-deep-link/Cargo.toml create mode 100644 backend/tauri-plugin-deep-link/LICENSE_APACHE-2.0 create mode 100644 backend/tauri-plugin-deep-link/LICENSE_MIT create mode 100644 backend/tauri-plugin-deep-link/README.md create mode 100644 backend/tauri-plugin-deep-link/cliff.toml create mode 100644 backend/tauri-plugin-deep-link/example/Info.plist create mode 100644 backend/tauri-plugin-deep-link/example/main.rs create mode 100644 backend/tauri-plugin-deep-link/renovate.json create mode 100644 backend/tauri-plugin-deep-link/src/lib.rs create mode 100644 backend/tauri-plugin-deep-link/src/linux.rs create mode 100644 backend/tauri-plugin-deep-link/src/macos.rs create mode 100644 backend/tauri-plugin-deep-link/src/template.desktop create mode 100644 backend/tauri-plugin-deep-link/src/windows.rs create mode 100644 backend/tauri/Info.plist diff --git a/.gitignore b/.gitignore index bcddfa6027..51588ec2e7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ scripts/_env.sh tauri.nightly.conf.json +.idea \ No newline at end of file diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 05865f70b6..c03f07fd66 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -712,6 +712,7 @@ dependencies = [ "sysproxy", "tauri", "tauri-build", + "tauri-plugin-deep-link", "tempfile", "thiserror", "tokio", @@ -2355,6 +2356,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version 0.4.0", + "to_method", + "winapi", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -3147,6 +3161,28 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + [[package]] name = "objc_exception" version = "0.1.2" @@ -5191,6 +5227,20 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +dependencies = [ + "dirs 5.0.1", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.52.0", + "winreg 0.52.0", +] + [[package]] name = "tauri-runtime" version = "0.14.2" @@ -5451,6 +5501,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.36.0" diff --git a/backend/tauri-plugin-deep-link/.github/workflows/audit.yml b/backend/tauri-plugin-deep-link/.github/workflows/audit.yml new file mode 100644 index 0000000000..59952e51f1 --- /dev/null +++ b/backend/tauri-plugin-deep-link/.github/workflows/audit.yml @@ -0,0 +1,26 @@ +name: Audit + +on: + schedule: + - cron: "0 0 * * *" + push: + branches: + - main + paths: + - "**/Cargo.lock" + - "**/Cargo.toml" + pull_request: + branches: + - main + paths: + - "**/Cargo.lock" + - "**/Cargo.toml" + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/backend/tauri-plugin-deep-link/.github/workflows/format.yml b/backend/tauri-plugin-deep-link/.github/workflows/format.yml new file mode 100644 index 0000000000..6f7db438b6 --- /dev/null +++ b/backend/tauri-plugin-deep-link/.github/workflows/format.yml @@ -0,0 +1,24 @@ +name: Format + +on: + push: + branches: + - main + pull_request: + branches: + - main + - dev + +jobs: + format: + runs-on: ubuntu-latest + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check diff --git a/backend/tauri-plugin-deep-link/.github/workflows/lint.yml b/backend/tauri-plugin-deep-link/.github/workflows/lint.yml new file mode 100644 index 0000000000..26f18d1060 --- /dev/null +++ b/backend/tauri-plugin-deep-link/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Clippy + +on: + push: + branches: + - main + pull_request: + branches: + - main + - dev + +jobs: + clippy: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/backend/tauri-plugin-deep-link/.github/workflows/release.yml b/backend/tauri-plugin-deep-link/.github/workflows/release.yml new file mode 100644 index 0000000000..7358b25eaa --- /dev/null +++ b/backend/tauri-plugin-deep-link/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Publish + +on: + push: + tags: + - "v*.*.*" + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Release to crates.io + run: | + cargo login ${{ secrets.CRATES_IO }} + cargo publish + + - name: Generate Changelog + uses: orhun/git-cliff-action@v2 + id: git-cliff + with: + config: cliff.toml + args: -vv --latest --strip header + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + body: ${{ steps.git-cliff.outputs.content }} diff --git a/backend/tauri-plugin-deep-link/.gitignore b/backend/tauri-plugin-deep-link/.gitignore new file mode 100644 index 0000000000..4fffb2f89c --- /dev/null +++ b/backend/tauri-plugin-deep-link/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/backend/tauri-plugin-deep-link/CHANGELOG.md b/backend/tauri-plugin-deep-link/CHANGELOG.md new file mode 100644 index 0000000000..373e11ea21 --- /dev/null +++ b/backend/tauri-plugin-deep-link/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.2] - 2023-08-15 + +**This plugin will be migrated to https://github.com/tauri-apps/plugins-workspace/.** Therefore, this will likely be the last release in this repository. + +### Miscellaneous Tasks + +- Update rust crate objc2 to 0.4.0 (#30) +- Update rust crate objc2 to 0.4.1 (#31) + +## [0.1.1] - 2023-04-04 + +### Bug Fixes + +- Info.plist formatting (#22) +- Fixed inability to focus when launched from a Windows notification. (#27) + +### Documentation + +- Add env::args getter to example + +### Miscellaneous Tasks + +- Update rust crate winreg to 0.50.0 (#28) +- Switch from dirs-next to dirs + +## [0.1.0] - 2023-02-27 + +### Features + +- Initial release diff --git a/backend/tauri-plugin-deep-link/Cargo.toml b/backend/tauri-plugin-deep-link/Cargo.toml new file mode 100644 index 0000000000..e3282684f2 --- /dev/null +++ b/backend/tauri-plugin-deep-link/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tauri-plugin-deep-link" +version = "0.1.2" +authors = ["FabianLars "] +description = "A Tauri plugin for deep linking support" +repository = "https://github.com/FabianLars/tauri-plugin-deep-link" +edition = "2021" +rust-version = "1.64" +license = "MIT OR Apache-2.0" +readme = "README.md" +include = ["src/**", "Cargo.toml", "LICENSE_*"] + +[dependencies] +dirs = "5" +log = "0.4" +once_cell = "1" +tauri-utils = { version = "1" } + +[target.'cfg(windows)'.dependencies] +interprocess = { version = "1.2", default-features = false } +windows-sys = { version = "0.52.0", features = [ + "Win32_Foundation", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_WindowsAndMessaging", +] } +winreg = "0.52.0" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.4.1" diff --git a/backend/tauri-plugin-deep-link/LICENSE_APACHE-2.0 b/backend/tauri-plugin-deep-link/LICENSE_APACHE-2.0 new file mode 100644 index 0000000000..4947287f7b --- /dev/null +++ b/backend/tauri-plugin-deep-link/LICENSE_APACHE-2.0 @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/backend/tauri-plugin-deep-link/LICENSE_MIT b/backend/tauri-plugin-deep-link/LICENSE_MIT new file mode 100644 index 0000000000..01c355f865 --- /dev/null +++ b/backend/tauri-plugin-deep-link/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 - Present FabianLars + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/tauri-plugin-deep-link/README.md b/backend/tauri-plugin-deep-link/README.md new file mode 100644 index 0000000000..6ca6f07646 --- /dev/null +++ b/backend/tauri-plugin-deep-link/README.md @@ -0,0 +1,20 @@ +# Deep link plugin for Tauri + +[![](https://img.shields.io/crates/v/tauri-plugin-deep-link.svg)](https://crates.io/crates/tauri-plugin-deep-link) [![](https://img.shields.io/docsrs/tauri-plugin-deep-link)](https://docs.rs/tauri-plugin-deep-link) + +**This plugin will be migrated to https://github.com/tauri-apps/plugins-workspace/.** `0.1.2` will be the last release in this repo. + +~~Temporary solution until https://github.com/tauri-apps/tauri/issues/323 lands.~~ + +Depending on your use case, for example a `Login with Google` button, you may want to take a look at https://github.com/FabianLars/tauri-plugin-oauth instead. It uses a minimalistic localhost server for the OAuth process instead of custom uri schemes because some oauth providers, like the aforementioned Google, require this setup. Personally, I think it's easier to use too. + +Check out the [`example/`](https://github.com/FabianLars/tauri-plugin-deep-link/tree/main/example) directory for a minimal example. You must copy it into an actual tauri app first! + +## macOS + +In case you're one of the very few people that didn't know this already: macOS hates developers! Not only is that why the macOS implementation took me so long, it also means _you_ have to be a bit more careful if your app targets macOS: + +- Read through the methods' platform-specific notes. +- On macOS you need to register the schemes in a `Info.plist` file at build time, the plugin can't change the schemes at runtime. +- macOS apps are in single-instance by default so this plugin will not manually shut down secondary instances in release mode. + - To make development via `tauri dev` a little bit more pleasant, the plugin will work similar-ish to Linux and Windows _in debug mode_ but you will see secondary instances show on the screen for a split second and the event will trigger twice in the primary instance (one of these events will be an empty string). You still have to install a `.app` bundle you got from `tauri build --debug` for this to work! diff --git a/backend/tauri-plugin-deep-link/cliff.toml b/backend/tauri-plugin-deep-link/cliff.toml new file mode 100644 index 0000000000..1c186416d6 --- /dev/null +++ b/backend/tauri-plugin-deep-link/cliff.toml @@ -0,0 +1,71 @@ +# configuration file for git-cliff +# see https://github.com/orhun/git-cliff#configuration-file + +[changelog] +# changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://tera.netlify.app/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = "" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"}, # replace issue numbers + +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^chore\\(release\\): [Pp]repare for", skip = true }, + { message = "^chore", group = "Miscellaneous Tasks" }, + { message = "^ci", group = "CI", skip = true }, + { body = ".*security", group = "Security" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# glob pattern for matching git tags +tag_pattern = "v[0-9]*" +# regex for skipping tags +# skip_tags = "v0.1.0-beta.1" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" +# limit the number of commits included in the changelog. +# limit_commits = 42 diff --git a/backend/tauri-plugin-deep-link/example/Info.plist b/backend/tauri-plugin-deep-link/example/Info.plist new file mode 100644 index 0000000000..ae51fe52f2 --- /dev/null +++ b/backend/tauri-plugin-deep-link/example/Info.plist @@ -0,0 +1,21 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + + de.fabianlars.deep-link-test + CFBundleURLSchemes + + + myapp + myscheme + + + + + diff --git a/backend/tauri-plugin-deep-link/example/main.rs b/backend/tauri-plugin-deep-link/example/main.rs new file mode 100644 index 0000000000..af1b08a2f9 --- /dev/null +++ b/backend/tauri-plugin-deep-link/example/main.rs @@ -0,0 +1,39 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use tauri::Manager; + +fn main() { + // prepare() checks if it's a single instance and tries to send the args otherwise. + // It should always be the first line in your main function (with the exception of loggers or similar) + tauri_plugin_deep_link::prepare("de.fabianlars.deep-link-test"); + // It's expected to use the identifier from tauri.conf.json + // Unfortuenetly getting it is pretty ugly without access to sth that implements `Manager`. + + tauri::Builder::default() + .setup(|app| { + // If you need macOS support this must be called in .setup() ! + // Otherwise this could be called right after prepare() but then you don't have access to tauri APIs + let handle = app.handle(); + tauri_plugin_deep_link::register( + "my-scheme", + move |request| { + dbg!(&request); + handle.emit_all("scheme-request-received", request).unwrap(); + }, + ) + .unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */); + + // If you also need the url when the primary instance was started by the custom scheme, you currently have to read it yourself + /* + #[cfg(not(target_os = "macos"))] // on macos the plugin handles this (macos doesn't use cli args for the url) + if let Some(url) = std::env::args().nth(1) { + app.emit_all("scheme-request-received", url).unwrap(); + } + */ + + Ok(()) + }) + // .plugin(tauri_plugin_deep_link::init()) // consider adding a js api later + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/backend/tauri-plugin-deep-link/renovate.json b/backend/tauri-plugin-deep-link/renovate.json new file mode 100644 index 0000000000..297b7b5d3c --- /dev/null +++ b/backend/tauri-plugin-deep-link/renovate.json @@ -0,0 +1,3 @@ +{ + "extends": ["config:base", ":semanticCommitTypeAll(chore)"] +} diff --git a/backend/tauri-plugin-deep-link/src/lib.rs b/backend/tauri-plugin-deep-link/src/lib.rs new file mode 100644 index 0000000000..2cdc550d39 --- /dev/null +++ b/backend/tauri-plugin-deep-link/src/lib.rs @@ -0,0 +1,63 @@ +use std::io::{ErrorKind, Result}; + +use once_cell::sync::OnceCell; + +#[cfg(target_os = "windows")] +#[path = "windows.rs"] +mod platform_impl; +#[cfg(target_os = "linux")] +#[path = "linux.rs"] +mod platform_impl; +#[cfg(target_os = "macos")] +#[path = "macos.rs"] +mod platform_impl; + +static ID: OnceCell = OnceCell::new(); + +/// This function is meant for use-cases where the default [`prepare()`] function can't be used. +/// +/// # Errors +/// If ID was already set this functions returns an AlreadyExists error. +pub fn set_identifier(identifier: &str) -> Result<()> { + ID.set(identifier.to_string()) + .map_err(|_| ErrorKind::AlreadyExists.into()) +} + +// Consider adding a function to register without starting the listener. + +/// Registers a handler for the given scheme. +/// +/// ## Platform-specific: +/// +/// - **macOS**: On macOS schemes must be defined in an Info.plist file, therefore this function only calls [`listen()`] without registering the scheme. This function can only be called once on macOS. +pub fn register(scheme: &str, handler: F) -> Result<()> { + platform_impl::register(scheme, handler) +} + +/// Starts the event listener without registering any schemes. +/// +/// ## Platform-specific: +/// +/// - **macOS**: This function can only be called once on macOS. +pub fn listen(handler: F) -> Result<()> { + platform_impl::listen(handler) +} + +/// Unregister a previously registered scheme. +/// +/// ## Platform-specific: +/// +/// - **macOS**: This function has no effect on macOS. +pub fn unregister(scheme: &str) -> Result<()> { + platform_impl::unregister(scheme) +} + +/// Checks if current instance is the primary instance. +/// Also sends the URL event data to the primary instance and stops the process afterwards. +/// +/// ## Platform-specific: +/// +/// - **macOS**: Only registers the identifier (only relevant in debug mode). It does not interact with the primary instance and does not exit the app. +pub fn prepare(identifier: &str) { + platform_impl::prepare(identifier) +} diff --git a/backend/tauri-plugin-deep-link/src/linux.rs b/backend/tauri-plugin-deep-link/src/linux.rs new file mode 100644 index 0000000000..0f849a2908 --- /dev/null +++ b/backend/tauri-plugin-deep-link/src/linux.rs @@ -0,0 +1,141 @@ +use std::{ + fs::{create_dir_all, remove_file, File}, + io::{Error, ErrorKind, Read, Result, Write}, + os::unix::net::{UnixListener, UnixStream}, + process::Command, +}; + +use dirs::data_dir; + +use crate::ID; + +pub fn register(scheme: &str, handler: F) -> Result<()> { + listen(handler)?; + + let mut target = data_dir() + .ok_or_else(|| Error::new(ErrorKind::NotFound, "data directory not found."))? + .join("applications"); + + create_dir_all(&target)?; + + let exe = tauri_utils::platform::current_exe()?; + + let file_name = format!( + "{}-handler.desktop", + exe.file_name() + .ok_or_else(|| Error::new( + ErrorKind::NotFound, + "Couldn't get file name of curent executable.", + ))? + .to_string_lossy() + ); + + target.push(&file_name); + + let mime_types = format!("x-scheme-handler/{};", scheme); + + let mut file = File::create(&target)?; + file.write_all( + format!( + include_str!("template.desktop"), + name = ID + .get() + .expect("Called register() before prepare()") + .split('.') + .last() + .unwrap(), + exec = std::env::var("APPIMAGE").unwrap_or_else(|_| exe.display().to_string()), + mime_types = mime_types + ) + .as_bytes(), + )?; + + target.pop(); + + Command::new("update-desktop-database") + .arg(target) + .status()?; + + Command::new("xdg-mime") + .args(["default", &file_name, scheme]) + .status()?; + + Ok(()) +} + +pub fn unregister(_scheme: &str) -> Result<()> { + let mut target = + data_dir().ok_or_else(|| Error::new(ErrorKind::NotFound, "data directory not found."))?; + + target.push("applications"); + target.push(format!( + "{}-handler.desktop", + tauri_utils::platform::current_exe()? + .file_name() + .ok_or_else(|| Error::new( + ErrorKind::NotFound, + "Couldn't get file name of curent executable.", + ))? + .to_string_lossy() + )); + + remove_file(&target)?; + + Ok(()) +} + +pub fn listen(mut handler: F) -> Result<()> { + std::thread::spawn(move || { + let addr = format!( + "/tmp/{}-deep-link.sock", + ID.get().expect("listen() called before prepare()") + ); + + let listener = UnixListener::bind(addr).expect("Can't create listener"); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let mut buffer = String::new(); + if let Err(io_err) = stream.read_to_string(&mut buffer) { + log::error!("Error reading incoming connection: {}", io_err.to_string()); + }; + + handler(dbg!(buffer)); + } + Err(err) => { + log::error!("Incoming connection failed: {}", err); + continue; + } + } + } + }); + + Ok(()) +} + +pub fn prepare(identifier: &str) { + let addr = format!("/tmp/{}-deep-link.sock", identifier); + + match UnixStream::connect(&addr) { + Ok(mut stream) => { + if let Err(io_err) = + stream.write_all(std::env::args().nth(1).unwrap_or_default().as_bytes()) + { + log::error!( + "Error sending message to primary instance: {}", + io_err.to_string() + ); + }; + std::process::exit(0); + } + Err(err) => { + log::error!("Error creating socket listener: {}", err.to_string()); + if err.kind() == ErrorKind::ConnectionRefused { + let _ = remove_file(&addr); + } + } + }; + ID.set(identifier.to_string()) + .expect("prepare() called more than once with different identifiers."); +} diff --git a/backend/tauri-plugin-deep-link/src/macos.rs b/backend/tauri-plugin-deep-link/src/macos.rs new file mode 100644 index 0000000000..b1206e7eda --- /dev/null +++ b/backend/tauri-plugin-deep-link/src/macos.rs @@ -0,0 +1,184 @@ +use std::{ + fs::remove_file, + io::{ErrorKind, Read, Result, Write}, + os::unix::net::{UnixListener, UnixStream}, + sync::Mutex, +}; + +use objc2::{ + class, declare_class, msg_send, msg_send_id, + mutability::Immutable, + rc::Id, + runtime::{AnyObject, NSObject}, + sel, ClassType, +}; +use once_cell::sync::OnceCell; + +use crate::ID; + +type THandler = OnceCell>>; + +// If the Mutex turns out to be a problem, or FnMut turns out to be useless, we can remove the Mutex and turn FnMut into Fn +static HANDLER: THandler = OnceCell::new(); + +pub fn register(_scheme: &str, handler: F) -> Result<()> { + listen(handler)?; + + Ok(()) +} + +pub fn unregister(_scheme: &str) -> Result<()> { + Ok(()) +} + +// kInternetEventClass +const EVENT_CLASS: u32 = 0x4755524c; +// kAEGetURL +const EVENT_GET_URL: u32 = 0x4755524c; + +// Adapted from https://github.com/mrmekon/fruitbasket/blob/aad14e400d710d1d46317c0d8c55ff742bfeaadd/src/osx.rs#L848 +fn parse_url_event(event: *mut AnyObject) -> Option { + if event as u64 == 0u64 { + return None; + } + unsafe { + let class: u32 = msg_send![event, eventClass]; + let id: u32 = msg_send![event, eventID]; + if class != EVENT_CLASS || id != EVENT_GET_URL { + return None; + } + + let subevent: *mut AnyObject = msg_send![event, paramDescriptorForKeyword: 0x2d2d2d2d_u32]; + let nsstring: *mut AnyObject = msg_send![subevent, stringValue]; + let cstr: *const i8 = msg_send![nsstring, UTF8String]; + if !cstr.is_null() { + Some(std::ffi::CStr::from_ptr(cstr).to_string_lossy().to_string()) + } else { + None + } + } +} + +declare_class!( + struct Handler; + + unsafe impl ClassType for Handler { + type Super = NSObject; + type Mutability = Immutable; + const NAME: &'static str = "TauriPluginDeepLinkHandler"; + } + + unsafe impl Handler { + #[method(handleEvent:withReplyEvent:)] + fn handle_event(&self, event: *mut AnyObject, _replace: *const AnyObject) { + let s = parse_url_event(event).unwrap_or_default(); + let mut cb = HANDLER.get().unwrap().lock().unwrap(); + cb(s); + } + } +); + +impl Handler { + pub fn new() -> Id { + let cls = Self::class(); + unsafe { msg_send_id![msg_send_id![cls, alloc], init] } + } +} + +#[cfg(debug_assertions)] +fn secondary_handler(s: String) { + let addr = format!( + "/tmp/{}-deep-link.sock", + ID.get() + .expect("URL event received before prepare() was called") + ); + if let Ok(mut stream) = UnixStream::connect(addr) { + if let Err(io_err) = stream.write_all(s.as_bytes()) { + log::error!( + "Error sending message to primary instance: {}", + io_err.to_string() + ); + }; + } + std::process::exit(0); +} + +pub fn listen(handler: F) -> Result<()> { + #[cfg(debug_assertions)] + let addr = format!( + "/tmp/{}-deep-link.sock", + ID.get().expect("listen() called before prepare()") + ); + + #[cfg(debug_assertions)] + if HANDLER + .set(match UnixStream::connect(&addr) { + Ok(_) => Mutex::new(Box::new(secondary_handler)), + Err(err) => { + log::error!("Error creating socket listener: {}", err.to_string()); + if err.kind() == ErrorKind::ConnectionRefused { + let _ = remove_file(&addr); + } + Mutex::new(Box::new(handler)) + } + }) + .is_err() + { + return Err(std::io::Error::new( + ErrorKind::AlreadyExists, + "Handler was already set", + )); + } + + #[cfg(not(debug_assertions))] + if HANDLER.set(Mutex::new(Box::new(handler))).is_err() { + return Err(std::io::Error::new( + ErrorKind::AlreadyExists, + "Handler was already set", + )); + } + + unsafe { + let event_manager: Id = + msg_send_id![class!(NSAppleEventManager), sharedAppleEventManager]; + + let handler = Handler::new(); + let handler_boxed = Box::into_raw(Box::new(handler)); + + let _: () = msg_send![&event_manager, + setEventHandler: &**handler_boxed + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass:EVENT_CLASS + andEventID:EVENT_GET_URL]; + } + + #[cfg(debug_assertions)] + std::thread::spawn(move || { + let listener = UnixListener::bind(addr).expect("Can't create listener"); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let mut buffer = String::new(); + if let Err(io_err) = stream.read_to_string(&mut buffer) { + log::error!("Error reading incoming connection: {}", io_err.to_string()); + }; + + let mut cb = HANDLER.get().unwrap().lock().unwrap(); + cb(buffer); + } + Err(err) => { + log::error!("Incoming connection failed: {}", err); + continue; + } + } + } + }); + + Ok(()) +} + +pub fn prepare(identifier: &str) { + ID.set(identifier.to_string()) + .expect("prepare() called more than once with different identifiers."); +} diff --git a/backend/tauri-plugin-deep-link/src/template.desktop b/backend/tauri-plugin-deep-link/src/template.desktop new file mode 100644 index 0000000000..9e790c7893 --- /dev/null +++ b/backend/tauri-plugin-deep-link/src/template.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name={name} +Exec={exec} %u +Terminal=false +MimeType={mime_types} +NoDisplay=true \ No newline at end of file diff --git a/backend/tauri-plugin-deep-link/src/windows.rs b/backend/tauri-plugin-deep-link/src/windows.rs new file mode 100644 index 0000000000..93fba2bf49 --- /dev/null +++ b/backend/tauri-plugin-deep-link/src/windows.rs @@ -0,0 +1,145 @@ +use std::{ + io::{BufRead, BufReader, Result, Write}, + path::Path, +}; + +use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; +use windows_sys::Win32::UI::{ + Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT}, + WindowsAndMessaging::{AllowSetForegroundWindow, ASFW_ANY}, +}; +use winreg::{enums::HKEY_CURRENT_USER, RegKey}; + +use crate::ID; + +pub fn register(scheme: &str, handler: F) -> Result<()> { + listen(handler)?; + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let base = Path::new("Software").join("Classes").join(scheme); + + let exe = tauri_utils::platform::current_exe()? + .display() + .to_string() + .replace("\\\\?\\", ""); + + let (key, _) = hkcu.create_subkey(&base)?; + key.set_value( + "", + &format!( + "URL:{}", + ID.get().expect("register() called before prepare()") + ), + )?; + key.set_value("URL Protocol", &"")?; + + let (icon, _) = hkcu.create_subkey(base.join("DefaultIcon"))?; + icon.set_value("", &format!("\"{}\",0", &exe))?; + + let (cmd, _) = hkcu.create_subkey(base.join("shell").join("open").join("command"))?; + + cmd.set_value("", &format!("\"{}\" \"%1\"", &exe))?; + + Ok(()) +} + +pub fn unregister(scheme: &str) -> Result<()> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let base = Path::new("Software").join("Classes").join(scheme); + + hkcu.delete_subkey_all(base)?; + + Ok(()) +} + +pub fn listen(mut handler: F) -> Result<()> { + std::thread::spawn(move || { + let listener = + LocalSocketListener::bind(ID.get().expect("listen() called before prepare()").as_str()) + .expect("Can't create listener"); + + for conn in listener.incoming().filter_map(|c| { + c.map_err(|error| log::error!("Incoming connection failed: {}", error)) + .ok() + }) { + // Listen for the launch arguments + let mut conn = BufReader::new(conn); + let mut buffer = String::new(); + if let Err(io_err) = conn.read_line(&mut buffer) { + log::error!("Error reading incoming connection: {}", io_err.to_string()); + }; + buffer.pop(); + + handler(buffer); + } + }); + + Ok(()) +} + +pub fn prepare(identifier: &str) { + if let Ok(mut conn) = LocalSocketStream::connect(identifier) { + // We are the secondary instance. + // Prep to activate primary instance by allowing another process to take focus. + + // A workaround to allow AllowSetForegroundWindow to succeed - press a key. + // This was originally used by Chromium: https://bugs.chromium.org/p/chromium/issues/detail?id=837796 + dummy_keypress(); + + let primary_instance_pid = conn.peer_pid().unwrap_or(ASFW_ANY); + unsafe { + let success = AllowSetForegroundWindow(primary_instance_pid) != 0; + if !success { + log::warn!("AllowSetForegroundWindow failed."); + } + } + + if let Err(io_err) = conn.write_all(std::env::args().nth(1).unwrap_or_default().as_bytes()) + { + log::error!( + "Error sending message to primary instance: {}", + io_err.to_string() + ); + }; + let _ = conn.write_all(b"\n"); + std::process::exit(0); + }; + ID.set(identifier.to_string()) + .expect("prepare() called more than once with different identifiers."); +} + +/// Send a dummy keypress event so AllowSetForegroundWindow can succeed +fn dummy_keypress() { + let keyboard_input_down = KEYBDINPUT { + wVk: 0, // This doesn't correspond to any actual keyboard key, but should still function for the workaround. + dwExtraInfo: 0, + wScan: 0, + time: 0, + dwFlags: 0, + }; + + let mut keyboard_input_up = keyboard_input_down; + keyboard_input_up.dwFlags = 0x0002; // KEYUP flag + + let input_down_u = INPUT_0 { + ki: keyboard_input_down, + }; + let input_up_u = INPUT_0 { + ki: keyboard_input_up, + }; + + let input_down = INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: input_down_u, + }; + + let input_up = INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: input_up_u, + }; + + let ipsize = std::mem::size_of::() as i32; + unsafe { + SendInput(2, [input_down, input_up].as_ptr(), ipsize); + }; +} diff --git a/backend/tauri/Cargo.toml b/backend/tauri/Cargo.toml index aa8e6498aa..2db1c32883 100644 --- a/backend/tauri/Cargo.toml +++ b/backend/tauri/Cargo.toml @@ -82,6 +82,7 @@ tracing-log = { version = "0.2" } tracing-appender = { version = "0.2", features = ["parking_lot"] } base64 = "0.21" single-instance = "0.3.3" +tauri-plugin-deep-link = { path = "../tauri-plugin-deep-link", version = "0.1.2" } [target.'cfg(windows)'.dependencies] deelevate = "0.2.0" diff --git a/backend/tauri/Info.plist b/backend/tauri/Info.plist new file mode 100644 index 0000000000..fdfd592bda --- /dev/null +++ b/backend/tauri/Info.plist @@ -0,0 +1,19 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + + Clash Nyanpasu + CFBundleURLSchemes + + clash-nyanpasu + + + + + \ No newline at end of file diff --git a/backend/tauri/src/main.rs b/backend/tauri/src/main.rs index ca52854646..81db650867 100644 --- a/backend/tauri/src/main.rs +++ b/backend/tauri/src/main.rs @@ -15,7 +15,7 @@ use crate::{ utils::{init, resolve}, }; use anyhow::Context; -use tauri::{api, SystemTray}; +use tauri::{api, Manager, SystemTray}; rust_i18n::i18n!("../../locales"); @@ -46,6 +46,9 @@ fn main() -> std::io::Result<()> { #[cfg(feature = "deadlock-detection")] deadlock_detection(); + // Should be in first place in order prevent single instance check block everything + tauri_plugin_deep_link::prepare("moe.elaina.clash.nyanpasu"); + // 单例检测 let single_instance_result: anyhow::Result<()> = single_instance::SingleInstance::new(utils::dirs::APP_NAME) @@ -87,6 +90,16 @@ fn main() -> std::io::Result<()> { .system_tray(SystemTray::new()) .setup(|app| { resolve::resolve_setup(app); + // setup custom scheme + let handle = app.handle().clone(); + log_err!(tauri_plugin_deep_link::register( + "clash-nyanpasu", + move |request| { + log::info!(target: "app", "scheme request received: {:?}", &request); + resolve::create_window(&handle.clone()); // create window if not exists + handle.emit_all("scheme-request-received", request).unwrap(); + } + )); Ok(()) }) .on_system_tray_event(core::tray::Tray::on_system_tray_event) diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index 7e3f60f2f5..cfc06af446 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -27,6 +27,7 @@ import MDYSwitch from "../common/mdy-switch"; interface Props { onChange: () => void; + url?: string; } export interface ProfileViewerRef { @@ -51,7 +52,7 @@ export const ProfileViewer = forwardRef( type: "remote", name: "Remote File", desc: "", - url: "", + url: props.url ?? "", option: { // user_agent: "", with_proxy: false, diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index ef6c31adc2..60c21ff17d 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -19,7 +19,7 @@ import { AnimatePresence } from "framer-motion"; import i18next from "i18next"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation, useRoutes } from "react-router-dom"; +import { useLocation, useNavigate, useRoutes } from "react-router-dom"; import { SWRConfig, mutate } from "swr"; import { routers } from "./_routers"; @@ -35,6 +35,7 @@ export default function Layout() { const { verge } = useVerge(); const { theme_blur, language } = verge || {}; + const navigate = useNavigate(); const location = useLocation(); const routes = useRoutes(routers); if (!routes) return null; @@ -87,6 +88,18 @@ export default function Layout() { mutate("getProviders"); }); + listen("scheme-request-received", (req) => { + const message: string = req.payload as string; + const url = new URL(message); + switch (url.pathname) { + case "//subscribe-remote-profile": + case "//subscribe-remote-profile/": + navigate("/profile", { + state: { scheme: url.searchParams.get("url") }, + }); + } + }); + setTimeout(() => { appWindow.show(); appWindow.unminimize(); diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index b2a2237d19..9eb0e59e8a 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -43,13 +43,15 @@ import { LoadingButton } from "@mui/lab"; import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; import { useLockFn } from "ahooks"; import { throttle } from "lodash-es"; -import { useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useSetRecoilState } from "recoil"; import useSWR, { mutate } from "swr"; +import { useLocation } from "react-router-dom"; export default function ProfilePage() { const { t } = useTranslation(); + const location = useLocation(); const [url, setUrl] = useState(""); const [disabled, setDisabled] = useState(false); @@ -97,6 +99,13 @@ export default function ProfilePage() { return { regularItems, enhanceItems }; }, [profiles]); + useEffect(() => { + if (location.state != null) { + console.log(location.state.scheme); + viewerRef.current?.create(); + } + }, []); + const onImport = async () => { if (!url) return; setLoading(true); @@ -412,7 +421,11 @@ export default function ProfilePage() { )} - mutateProfiles()} /> + mutateProfiles()} + /> );