Compare commits
1 Commits
windows-dn
...
flutter-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
555f5233cc |
@@ -1,130 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Ideas & Feature Requests
|
||||
|
||||
Use this category for feature requests, enhancements, integrations, and product ideas.
|
||||
|
||||
NetBird uses community traction in discussions — upvotes, replies, affected users, and use-case detail — as an input when deciding what should become a maintainer-curated issue or roadmap item. A clear problem statement is more useful than a solution-only request.
|
||||
|
||||
Please search first and add your use case to an existing discussion when one already exists.
|
||||
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before posting
|
||||
options:
|
||||
- label: I searched existing discussions and issues for similar requests.
|
||||
required: true
|
||||
- label: I checked the documentation to confirm this is not already supported.
|
||||
required: true
|
||||
- label: This is a product idea or enhancement request, not a support question.
|
||||
required: true
|
||||
- label: I removed or anonymized sensitive details from examples and screenshots.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Product area
|
||||
description: Select every area this request touches.
|
||||
multiple: true
|
||||
options:
|
||||
- Client / Agent
|
||||
- CLI
|
||||
- Desktop UI
|
||||
- Mobile app
|
||||
- Dashboard / Admin UI
|
||||
- Management service / API
|
||||
- Signal service
|
||||
- Relay
|
||||
- DNS
|
||||
- Routes / Exit nodes
|
||||
- NetBird SSH
|
||||
- Access control policies
|
||||
- Posture checks
|
||||
- Identity provider / SSO
|
||||
- Self-hosting / Deployment
|
||||
- Kubernetes / Operator
|
||||
- Terraform / Automation
|
||||
- Documentation
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem or use case
|
||||
description: What are you trying to accomplish, and what is difficult or impossible today?
|
||||
placeholder: |
|
||||
As a ...
|
||||
I want to ...
|
||||
Because ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe the behavior, workflow, API, UI, or integration you would like to see.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives or workarounds considered
|
||||
description: What have you tried today? Why is the current workaround not enough?
|
||||
|
||||
- type: textarea
|
||||
id: impact
|
||||
attributes:
|
||||
label: Community impact and priority
|
||||
description: Help us understand who benefits and how urgent this is.
|
||||
placeholder: |
|
||||
- Number of users/teams/peers affected:
|
||||
- Deployment type: Cloud / self-hosted / both
|
||||
- Frequency: daily / weekly / occasional
|
||||
- Blocking production adoption? yes/no
|
||||
- Related comments, discussions, or customer requests:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Examples from other tools or products
|
||||
description: If another tool solves this well, link or describe the behavior.
|
||||
|
||||
- type: textarea
|
||||
id: security
|
||||
attributes:
|
||||
label: Security, privacy, and compatibility considerations
|
||||
description: Note any access-control, audit, data retention, network, platform, or backward-compatibility concerns.
|
||||
|
||||
- type: textarea
|
||||
id: implementation
|
||||
attributes:
|
||||
label: Implementation ideas
|
||||
description: Optional. If you are familiar with the codebase or API, share possible implementation notes.
|
||||
|
||||
- type: dropdown
|
||||
id: contribution
|
||||
attributes:
|
||||
label: Are you willing to help?
|
||||
options:
|
||||
- Yes, I can submit a PR if the approach is accepted.
|
||||
- Yes, I can test or validate a proposed implementation.
|
||||
- Yes, I can provide more use-case details.
|
||||
- Not at this time.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add screenshots, diagrams, links, or anything else that helps explain the request.
|
||||
237
.github/DISCUSSION_TEMPLATE/issue-triage.yml
vendored
@@ -1,237 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Issue Triage
|
||||
|
||||
Use this category for reproducible bugs and regressions in NetBird.
|
||||
|
||||
The more context you include, the faster we can validate and act on your report. If you're not sure whether something is a bug, **Q&A / Support** is a good starting point — we can always move the conversation here once we've confirmed it's a product issue.
|
||||
|
||||
Intermittent issues are useful too. Include the trigger, frequency, timing, and any logs or debug evidence you have, and we'll work from there.
|
||||
|
||||
Please don't include secrets, tokens, private keys, internal hostnames, or public IPs. Security vulnerabilities should be reported through the repository security policy rather than a public discussion.
|
||||
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before posting
|
||||
options:
|
||||
- label: I searched existing discussions and issues, including closed ones, and checked the relevant docs.
|
||||
required: true
|
||||
- label: I believe this is a product bug rather than a configuration or setup question.
|
||||
required: true
|
||||
- label: I can reproduce this issue, or for intermittent issues I've included trigger, frequency, and timing details below.
|
||||
required: true
|
||||
- label: I removed or anonymized sensitive data from logs, screenshots, and configuration.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Affected area
|
||||
description: Select every area this report touches.
|
||||
multiple: true
|
||||
options:
|
||||
- Client / Agent
|
||||
- Reverse Proxy
|
||||
- CLI
|
||||
- Desktop UI
|
||||
- Mobile app
|
||||
- Peer connectivity
|
||||
- DNS
|
||||
- Routes / Exit nodes
|
||||
- NetBird SSH
|
||||
- Relay / Signal / NAT traversal
|
||||
- Login / Authentication / IdP
|
||||
- Dashboard / Admin UI
|
||||
- Management service / API
|
||||
- Access control policies / Posture checks
|
||||
- Self-hosting / Deployment
|
||||
- Kubernetes / Operator
|
||||
- Documentation
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: deployment
|
||||
attributes:
|
||||
label: Deployment type
|
||||
options:
|
||||
- NetBird Cloud
|
||||
- Self-hosted - quickstart script
|
||||
- Self-hosted - advanced/custom deployment
|
||||
- Local development build
|
||||
- Not sure / environment I do not fully control
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Operating system or environment
|
||||
description: Select every environment involved in the reproduction.
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- Android
|
||||
- iOS
|
||||
- FreeBSD
|
||||
- OpenWRT
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- Synology
|
||||
- Browser
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: NetBird version and upgrade status
|
||||
description: Run `netbird version` where applicable. For self-hosted deployments, include management, signal, relay, and dashboard versions if available. If you cannot test on a current/supported version, explain why.
|
||||
placeholder: |
|
||||
Example:
|
||||
- Client: 0.30.2
|
||||
- Management: 0.30.2
|
||||
- Signal: 0.30.2
|
||||
- Relay: 0.30.2
|
||||
- Dashboard: 0.30.2
|
||||
- Upgrade status: reproduced on current version / cannot upgrade because ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: regression
|
||||
attributes:
|
||||
label: Did this work before?
|
||||
options:
|
||||
- Yes, this worked before
|
||||
- No, this never worked
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: regression-details
|
||||
attributes:
|
||||
label: Regression details
|
||||
description: If this worked before, include the last known working version, first known broken version, and any recent upgrade, configuration, network, or IdP changes.
|
||||
placeholder: |
|
||||
- Last known working version:
|
||||
- First known broken version:
|
||||
- Recent changes:
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Briefly describe the reproducible bug.
|
||||
placeholder: What is broken?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: current-behavior
|
||||
attributes:
|
||||
label: Current behavior
|
||||
description: What happens now? Include exact errors, timeouts, UI messages, or failed commands when possible.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What did you expect to happen instead?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide the smallest set of steps that reliably reproduces the bug. If the issue is intermittent, include the trigger, frequency, timing, and relevant timestamps.
|
||||
placeholder: |
|
||||
1. Configure ...
|
||||
2. Run ...
|
||||
3. Observe ...
|
||||
|
||||
For intermittent issues:
|
||||
- Trigger:
|
||||
- Frequency:
|
||||
- Timing/timestamps:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment and topology
|
||||
description: Include the relevant topology and software involved in the reproduction. For UI/docs-only reports, write `N/A` if this does not apply. Use `None`, `Unknown`, or `N/A` where appropriate.
|
||||
placeholder: |
|
||||
- Peer A:
|
||||
- Peer B:
|
||||
- Same LAN or different networks:
|
||||
- NAT/CGNAT/corporate firewall/mobile network:
|
||||
- Other VPN software:
|
||||
- Firewall, DNS, or endpoint security software:
|
||||
- Routes, DNS, policies, posture checks, or SSH rules involved:
|
||||
- IdP, reverse proxy, or browser involved:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: self-hosted-details
|
||||
attributes:
|
||||
label: Self-hosted details, if available
|
||||
description: Optional. If you use self-hosting and have access to these details, include them. If you do not administer the environment, provide what you know and say what you cannot access.
|
||||
placeholder: |
|
||||
- Deployment method: quickstart / Docker Compose / Helm / operator / custom
|
||||
- Management/signal/relay/dashboard versions:
|
||||
- Reverse proxy:
|
||||
- IdP/provider:
|
||||
- STUN/TURN/coturn/relay details:
|
||||
- Relevant component logs:
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs, status output, or debug evidence
|
||||
description: |
|
||||
For client, connectivity, DNS, route, relay/signal, or self-hosted reports, logs are essential — please include anonymized output from `netbird status -dA`, or a debug bundle via `netbird debug for 1m -AS -U`. Debug bundles are automatically deleted after 30 days.
|
||||
|
||||
For UI, dashboard, or documentation reports, leave the pre-filled `N/A`.
|
||||
value: "N/A"
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: related-reports
|
||||
attributes:
|
||||
label: Related issues or discussions
|
||||
description: Optional. Link similar reports you found while searching, if any.
|
||||
placeholder: |
|
||||
- Related issue/discussion:
|
||||
- Why this may be the same or different:
|
||||
|
||||
- type: textarea
|
||||
id: impact
|
||||
attributes:
|
||||
label: Impact
|
||||
description: Optional. Help us understand priority. How many users, peers, environments, or workflows are affected? Is there a workaround?
|
||||
placeholder: |
|
||||
- Affected users/peers:
|
||||
- Business or production impact:
|
||||
- Workaround available:
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add links to related discussions, issues, docs, screenshots, recordings, or anything else that may help validation.
|
||||
146
.github/DISCUSSION_TEMPLATE/q-a-support.yml
vendored
@@ -1,146 +0,0 @@
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Q&A / Support
|
||||
|
||||
Use this category for questions about configuration, setup, self-hosted deployments, troubleshooting, and general NetBird usage.
|
||||
|
||||
This is community support and does not provide an SLA. For NetBird Cloud support, use the official support channel linked from the issue creation page. Please do not post secrets, tokens, private keys, internal hostnames, or public IPs unless you intentionally want them public.
|
||||
|
||||
If your question turns into a reproducible product defect, DevRel or a maintainer may ask you to open or move the conversation to Issue Triage.
|
||||
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before posting
|
||||
options:
|
||||
- label: I searched existing discussions and issues for similar questions.
|
||||
required: true
|
||||
- label: I reviewed the relevant NetBird documentation or troubleshooting guide.
|
||||
required: true
|
||||
- label: I removed or anonymized sensitive data from logs, screenshots, and configuration.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: topic
|
||||
attributes:
|
||||
label: Topic
|
||||
multiple: true
|
||||
options:
|
||||
- Getting started
|
||||
- Self-hosting
|
||||
- Client / Agent
|
||||
- CLI
|
||||
- Desktop UI
|
||||
- Mobile app
|
||||
- Dashboard / Admin UI
|
||||
- DNS
|
||||
- Routes / Exit nodes
|
||||
- NetBird SSH
|
||||
- Relay
|
||||
- Access control policies
|
||||
- Posture checks
|
||||
- Identity provider / SSO
|
||||
- API
|
||||
- Kubernetes / Operator
|
||||
- Terraform / Automation
|
||||
- Documentation
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: deployment
|
||||
attributes:
|
||||
label: Deployment type
|
||||
options:
|
||||
- NetBird Cloud
|
||||
- Self-hosted - quickstart script
|
||||
- Self-hosted - advanced/custom deployment
|
||||
- Local development build
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Operating system or environment
|
||||
multiple: true
|
||||
options:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- Android
|
||||
- iOS
|
||||
- FreeBSD
|
||||
- OpenWRT
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- Synology
|
||||
- Browser
|
||||
- Other / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: NetBird version
|
||||
description: Run `netbird version` where applicable. For self-hosted deployments, include component versions if relevant.
|
||||
placeholder: "Example: client 0.30.2, management 0.30.2"
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: What are you trying to understand or accomplish?
|
||||
placeholder: Describe your question clearly.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: goal
|
||||
attributes:
|
||||
label: Desired outcome
|
||||
description: What would a successful answer help you do?
|
||||
placeholder: |
|
||||
I want to configure ...
|
||||
I expected ...
|
||||
I need help deciding ...
|
||||
|
||||
- type: textarea
|
||||
id: attempted
|
||||
attributes:
|
||||
label: What have you tried?
|
||||
description: Include commands, documentation links, configuration attempts, or troubleshooting steps already tried.
|
||||
placeholder: |
|
||||
- Read ...
|
||||
- Ran ...
|
||||
- Changed ...
|
||||
- Observed ...
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Relevant environment details
|
||||
description: Include redacted topology, IdP/provider, reverse proxy, firewall, DNS, route, policy, or self-hosted setup details that may affect the answer.
|
||||
placeholder: |
|
||||
- Deployment:
|
||||
- Components involved:
|
||||
- Network/topology:
|
||||
- Related config:
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs or output
|
||||
description: Optional. Include anonymized logs, command output, screenshots, or `netbird status -dA` if relevant.
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add links, diagrams, screenshots, or other details that may help the community answer.
|
||||
71
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: Bug/Issue report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ['triage-needed']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the problem**
|
||||
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Are you using NetBird Cloud?**
|
||||
|
||||
Please specify whether you use NetBird Cloud or self-host NetBird's control plane.
|
||||
|
||||
**NetBird version**
|
||||
|
||||
`netbird version`
|
||||
|
||||
**Is any other VPN software installed?**
|
||||
|
||||
If yes, which one?
|
||||
|
||||
**Debug output**
|
||||
|
||||
To help us resolve the problem, please attach the following anonymized status output
|
||||
|
||||
netbird status -dA
|
||||
|
||||
Create and upload a debug bundle, and share the returned file key:
|
||||
|
||||
netbird debug for 1m -AS -U
|
||||
|
||||
*Uploaded files are automatically deleted after 30 days.*
|
||||
|
||||
|
||||
Alternatively, create the file only and attach it here manually:
|
||||
|
||||
netbird debug for 1m -AS
|
||||
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Have you tried these troubleshooting steps?**
|
||||
- [ ] Reviewed [client troubleshooting](https://docs.netbird.io/how-to/troubleshooting-client) (if applicable)
|
||||
- [ ] Checked for newer NetBird versions
|
||||
- [ ] Searched for similar issues on GitHub (including closed ones)
|
||||
- [ ] Restarted the NetBird client
|
||||
- [ ] Disabled other VPN software
|
||||
- [ ] Checked firewall settings
|
||||
|
||||
26
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,26 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Start an Issue Triage discussion
|
||||
url: https://github.com/netbirdio/netbird/discussions/new?category=issue-triage
|
||||
about: Report a bug, regression, or unexpected behavior so DevRel can validate it before it becomes an issue.
|
||||
- name: Propose an idea or feature request
|
||||
url: https://github.com/netbirdio/netbird/discussions/new?category=ideas-feature-requests
|
||||
about: Share feature requests, enhancements, and integration ideas for community feedback and prioritization.
|
||||
- name: Ask a Q&A / Support question
|
||||
url: https://github.com/netbirdio/netbird/discussions/new?category=q-a-support
|
||||
about: Get help with setup, configuration, self-hosting, troubleshooting, and general usage.
|
||||
- name: Security vulnerability disclosure
|
||||
url: https://github.com/netbirdio/netbird/security/policy
|
||||
about: Please do not report security vulnerabilities in public issues or discussions.
|
||||
- name: Community Support Forum
|
||||
- name: Community Support
|
||||
url: https://forum.netbird.io/
|
||||
about: Community support forum.
|
||||
about: Community support forum
|
||||
- name: Cloud Support
|
||||
url: https://docs.netbird.io/help/report-bug-issues
|
||||
about: Contact NetBird for Cloud support.
|
||||
- name: Client / Connection Troubleshooting
|
||||
about: Contact us for support
|
||||
- name: Client/Connection Troubleshooting
|
||||
url: https://docs.netbird.io/help/troubleshooting-client
|
||||
about: See the client troubleshooting guide for common connectivity issues.
|
||||
about: See our client troubleshooting guide for help addressing common issues
|
||||
- name: Self-host Troubleshooting
|
||||
url: https://docs.netbird.io/selfhosted/troubleshooting
|
||||
about: See the self-host troubleshooting guide for common deployment issues.
|
||||
about: See our self-host troubleshooting guide for help addressing common issues
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ['feature-request']
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
128
.github/ISSUE_TEMPLATE/validated_issue.yml
vendored
@@ -1,128 +0,0 @@
|
||||
name: Validated issue
|
||||
description: Maintainer/DevRel only. Create an issue after a discussion has been validated or for internally validated work.
|
||||
title: "[Validated]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Discussion-first issue policy
|
||||
|
||||
Issues are maintainer-curated work items. Community reports and feature requests should start in [Discussions](https://github.com/netbirdio/netbird/discussions) so DevRel can validate, reproduce, and route them before engineering time is committed.
|
||||
|
||||
Use this form when:
|
||||
- A discussion has been validated and should become actionable work.
|
||||
- A maintainer is opening internally validated work that can bypass the discussion-first flow.
|
||||
|
||||
Issues opened without a relevant validated discussion or maintainer context may be closed and redirected to Discussions.
|
||||
|
||||
- type: checkboxes
|
||||
id: validation-checks
|
||||
attributes:
|
||||
label: Validation checklist
|
||||
options:
|
||||
- label: This issue is linked to a validated discussion, or it is being opened directly by a maintainer.
|
||||
required: true
|
||||
- label: The report has enough context for engineering to act on it without re-triaging from scratch.
|
||||
required: true
|
||||
- label: Sensitive data, secrets, private keys, internal hostnames, and public IPs have been removed or intentionally disclosed.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: issue-type
|
||||
attributes:
|
||||
label: Issue type
|
||||
options:
|
||||
- Bug / Regression
|
||||
- Feature / Enhancement
|
||||
- Documentation
|
||||
- Maintenance / Refactor
|
||||
- Cross-repository coordination
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: source-discussion
|
||||
attributes:
|
||||
label: Source discussion
|
||||
description: Link the GitHub Discussion that was validated. Maintainers bypassing the flow can write "Maintainer-created" and explain why below.
|
||||
placeholder: https://github.com/netbirdio/netbird/discussions/1234
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: validation-owner
|
||||
attributes:
|
||||
label: Validation owner
|
||||
description: GitHub handle of the DevRel team member or maintainer who validated this work.
|
||||
placeholder: "@username"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: target-repository
|
||||
attributes:
|
||||
label: Target repository
|
||||
description: Where should the implementation work happen?
|
||||
options:
|
||||
- netbirdio/netbird
|
||||
- netbirdio/dashboard
|
||||
- netbirdio/kubernetes-operator
|
||||
- netbirdio/docs
|
||||
- Multiple repositories
|
||||
- Unknown / needs routing
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Concise description of the validated work.
|
||||
placeholder: What needs to be fixed, changed, documented, or built?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Validation evidence
|
||||
description: For bugs, include reproduction status, affected versions, logs, and environment. For features, include community traction, affected users, and alignment notes.
|
||||
placeholder: |
|
||||
- Reproduced by:
|
||||
- Affected versions / platforms:
|
||||
- Community signal:
|
||||
- Related logs or screenshots:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: scope
|
||||
attributes:
|
||||
label: Proposed scope
|
||||
description: Describe what is in scope and, if helpful, what is explicitly out of scope.
|
||||
placeholder: |
|
||||
In scope:
|
||||
- ...
|
||||
|
||||
Out of scope:
|
||||
- ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: acceptance-criteria
|
||||
attributes:
|
||||
label: Acceptance criteria
|
||||
description: What must be true for this issue to be closed?
|
||||
placeholder: |
|
||||
- [ ] ...
|
||||
- [ ] ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Links to related PRs, docs, issues in other repositories, roadmap items, or implementation notes.
|
||||
305
.github/workflows/release.yml
vendored
@@ -114,13 +114,7 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-24.04-8-core
|
||||
outputs:
|
||||
release_artifact_url: ${{ steps.upload_release.outputs.artifact-url }}
|
||||
linux_packages_artifact_url: ${{ steps.upload_linux_packages.outputs.artifact-url }}
|
||||
windows_packages_artifact_url: ${{ steps.upload_windows_packages.outputs.artifact-url }}
|
||||
macos_packages_artifact_url: ${{ steps.upload_macos_packages.outputs.artifact-url }}
|
||||
ghcr_images: ${{ steps.tag_and_push_images.outputs.images_markdown }}
|
||||
runs-on: ubuntu-latest-m
|
||||
env:
|
||||
flags: ""
|
||||
steps:
|
||||
@@ -219,13 +213,10 @@ jobs:
|
||||
if: always()
|
||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||
- name: Tag and push images (amd64 only)
|
||||
id: tag_and_push_images
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
resolve_tags() {
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "pr-${{ github.event.pull_request.number }}"
|
||||
@@ -234,17 +225,6 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
ghcr_package_url() {
|
||||
local image="$1" package encoded_package
|
||||
package="${image#ghcr.io/}"
|
||||
package="${package#*/}"
|
||||
package="${package%%:*}"
|
||||
encoded_package="${package//\//%2F}"
|
||||
echo "https://github.com/orgs/netbirdio/packages/container/package/${encoded_package}"
|
||||
}
|
||||
|
||||
image_refs=()
|
||||
|
||||
tag_and_push() {
|
||||
local src="$1" img_name tag dst
|
||||
img_name="${src%%:*}"
|
||||
@@ -253,56 +233,35 @@ jobs:
|
||||
echo "Tagging ${src} -> ${dst}"
|
||||
docker tag "$src" "$dst"
|
||||
docker push "$dst"
|
||||
image_refs+=("$dst")
|
||||
done
|
||||
}
|
||||
|
||||
cat > /tmp/goreleaser-artifacts.json <<'JSON'
|
||||
${{ steps.goreleaser.outputs.artifacts }}
|
||||
JSON
|
||||
export -f tag_and_push resolve_tags
|
||||
|
||||
mapfile -t src_images < <(
|
||||
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name | select(startswith("ghcr.io/"))' /tmp/goreleaser-artifacts.json
|
||||
)
|
||||
|
||||
for src in "${src_images[@]}"; do
|
||||
tag_and_push "$src"
|
||||
done
|
||||
|
||||
{
|
||||
echo "images_markdown<<EOF"
|
||||
if [[ ${#image_refs[@]} -eq 0 ]]; then
|
||||
echo "_No GHCR images were pushed._"
|
||||
else
|
||||
printf '%s\n' "${image_refs[@]}" | sort -u | while read -r image; do
|
||||
printf -- '- [`%s`](%s)\n' "$image" "$(ghcr_package_url "$image")"
|
||||
done
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo '${{ steps.goreleaser.outputs.artifacts }}' | \
|
||||
jq -r '.[] | select(.type == "Docker Image") | select(.goarch == "amd64") | .name' | \
|
||||
grep '^ghcr.io/' | while read -r SRC; do
|
||||
tag_and_push "$SRC"
|
||||
done
|
||||
- name: upload non tags for debug purposes
|
||||
id: upload_release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
- name: upload linux packages
|
||||
id: upload_linux_packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-packages
|
||||
path: dist/netbird_linux**
|
||||
retention-days: 7
|
||||
- name: upload windows packages
|
||||
id: upload_windows_packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-packages
|
||||
path: dist/netbird_windows**
|
||||
retention-days: 7
|
||||
- name: upload macos packages
|
||||
id: upload_macos_packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-packages
|
||||
@@ -311,8 +270,6 @@ jobs:
|
||||
|
||||
release_ui:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_ui_artifact_url: ${{ steps.upload_release_ui.outputs.artifact-url }}
|
||||
steps:
|
||||
- name: Parse semver string
|
||||
id: semver_parser
|
||||
@@ -403,7 +360,6 @@ jobs:
|
||||
if: always()
|
||||
run: rm -f /tmp/gpg-rpm-signing-key.asc
|
||||
- name: upload non tags for debug purposes
|
||||
id: upload_release_ui
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-ui
|
||||
@@ -412,8 +368,6 @@ jobs:
|
||||
|
||||
release_ui_darwin:
|
||||
runs-on: macos-latest
|
||||
outputs:
|
||||
release_ui_darwin_artifact_url: ${{ steps.upload_release_ui_darwin.outputs.artifact-url }}
|
||||
steps:
|
||||
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||
@@ -448,258 +402,15 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: upload non tags for debug purposes
|
||||
id: upload_release_ui_darwin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-ui-darwin
|
||||
path: dist/
|
||||
retention-days: 3
|
||||
|
||||
test_windows_installer:
|
||||
name: "Windows Installer / Build Test"
|
||||
runs-on: windows-2022
|
||||
needs: [release, release_ui]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
wintun_arch: amd64
|
||||
- arch: arm64
|
||||
wintun_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
env:
|
||||
PackageWorkdir: netbird_windows_${{ matrix.arch }}
|
||||
downloadPath: '${{ github.workspace }}\temp'
|
||||
steps:
|
||||
- name: Parse semver string
|
||||
id: semver_parser
|
||||
uses: booxmedialtd/ws-action-parse-semver@v1
|
||||
with:
|
||||
input_string: ${{ (startsWith(github.ref, 'refs/tags/v') && github.ref) || 'refs/tags/v0.0.0' }}
|
||||
version_extractor_regex: '\/v(.*)$'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add 7-Zip to PATH
|
||||
run: echo "C:\Program Files\7-Zip" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release
|
||||
path: release
|
||||
|
||||
- name: Download UI release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release-ui
|
||||
path: release-ui
|
||||
|
||||
- name: Stage binaries into dist
|
||||
run: |
|
||||
$workdir = "dist\${{ env.PackageWorkdir }}"
|
||||
New-Item -ItemType Directory -Force -Path $workdir | Out-Null
|
||||
$client = Get-ChildItem -Recurse -Path release -Filter "netbird_*_windows_${{ matrix.arch }}.tar.gz" | Select-Object -First 1
|
||||
$ui = Get-ChildItem -Recurse -Path release-ui -Filter "netbird-ui-windows_*_windows_${{ matrix.arch }}.tar.gz" | Select-Object -First 1
|
||||
if (-not $client) { Write-Host "::error::client tarball not found for ${{ matrix.arch }}"; exit 1 }
|
||||
if (-not $ui) { Write-Host "::error::ui tarball not found for ${{ matrix.arch }}"; exit 1 }
|
||||
Write-Host "Client: $($client.FullName)"
|
||||
Write-Host "UI: $($ui.FullName)"
|
||||
tar -zvxf $client.FullName -C $workdir
|
||||
tar -zvxf $ui.FullName -C $workdir
|
||||
Get-ChildItem $workdir
|
||||
|
||||
- name: Download wintun
|
||||
uses: carlosperate/download-file-action@v2
|
||||
id: download-wintun
|
||||
with:
|
||||
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||
file-name: wintun.zip
|
||||
location: ${{ env.downloadPath }}
|
||||
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||
|
||||
- name: Decompress wintun files
|
||||
run: tar -zvxf "${{ steps.download-wintun.outputs.file-path }}" -C ${{ env.downloadPath }}
|
||||
|
||||
- name: Move wintun.dll into dist
|
||||
run: mv ${{ env.downloadPath }}\wintun\bin\${{ matrix.wintun_arch }}\wintun.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||
|
||||
- name: Download Mesa3D (amd64 only)
|
||||
uses: carlosperate/download-file-action@v2
|
||||
id: download-mesa3d
|
||||
if: matrix.arch == 'amd64'
|
||||
with:
|
||||
file-url: https://downloads.fdossena.com/Projects/Mesa3D/Builds/MesaForWindows-x64-20.1.8.7z
|
||||
file-name: mesa3d.7z
|
||||
location: ${{ env.downloadPath }}
|
||||
sha256: '71c7cb64ec229a1d6b8d62fa08e1889ed2bd17c0eeede8689daf0f25cb31d6b9'
|
||||
|
||||
- name: Extract Mesa3D driver (amd64 only)
|
||||
if: matrix.arch == 'amd64'
|
||||
run: 7z x -o"${{ env.downloadPath }}" "${{ env.downloadPath }}/mesa3d.7z"
|
||||
|
||||
- name: Move opengl32.dll into dist (amd64 only)
|
||||
if: matrix.arch == 'amd64'
|
||||
run: mv ${{ env.downloadPath }}\opengl32.dll ${{ github.workspace }}\dist\${{ env.PackageWorkdir }}\
|
||||
|
||||
- name: Download EnVar plugin for NSIS
|
||||
uses: carlosperate/download-file-action@v2
|
||||
with:
|
||||
file-url: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip
|
||||
file-name: envar_plugin.zip
|
||||
location: ${{ github.workspace }}
|
||||
|
||||
- name: Extract EnVar plugin
|
||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/envar_plugin.zip"
|
||||
|
||||
- name: Download ShellExecAsUser plugin for NSIS (amd64 only)
|
||||
uses: carlosperate/download-file-action@v2
|
||||
if: matrix.arch == 'amd64'
|
||||
with:
|
||||
file-url: https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z
|
||||
file-name: ShellExecAsUser_amd64-Unicode.7z
|
||||
location: ${{ github.workspace }}
|
||||
|
||||
- name: Extract ShellExecAsUser plugin (amd64 only)
|
||||
if: matrix.arch == 'amd64'
|
||||
run: 7z x -o"${{ github.workspace }}/NSIS_Plugins" "${{ github.workspace }}/ShellExecAsUser_amd64-Unicode.7z"
|
||||
|
||||
- name: Build NSIS installer
|
||||
uses: joncloud/makensis-action@v3.3
|
||||
with:
|
||||
additional-plugin-paths: ${{ github.workspace }}/NSIS_Plugins/Plugins
|
||||
script-file: client/installer.nsis
|
||||
arguments: "/V4 /DARCH=${{ matrix.arch }}"
|
||||
env:
|
||||
APPVER: ${{ steps.semver_parser.outputs.major }}.${{ steps.semver_parser.outputs.minor }}.${{ steps.semver_parser.outputs.patch }}.${{ github.run_id }}
|
||||
|
||||
- name: Rename NSIS installer
|
||||
run: mv netbird-installer.exe netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||
|
||||
- name: Install WiX
|
||||
run: |
|
||||
dotnet tool install --global wix --version 6.0.2
|
||||
wix extension add WixToolset.Util.wixext/6.0.2
|
||||
|
||||
- name: Build MSI installer
|
||||
env:
|
||||
NETBIRD_VERSION: "${{ steps.semver_parser.outputs.fullversion }}"
|
||||
run: wix build -arch ${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }} -ext WixToolset.Util.wixext -o netbird_installer_test_windows_${{ matrix.arch }}.msi .\client\netbird.wxs -d ProcessorArchitecture=${{ matrix.arch == 'amd64' && 'x64' || 'arm64' }} -d ArchSuffix=${{ matrix.arch }}
|
||||
|
||||
- name: Upload installer artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-installer-test-${{ matrix.arch }}
|
||||
path: |
|
||||
netbird_installer_test_windows_${{ matrix.arch }}.exe
|
||||
netbird_installer_test_windows_${{ matrix.arch }}.msi
|
||||
retention-days: 3
|
||||
|
||||
comment_release_artifacts:
|
||||
name: Comment release artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release_ui, release_ui_darwin]
|
||||
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Create or update PR comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_RESULT: ${{ needs.release.result }}
|
||||
RELEASE_UI_RESULT: ${{ needs.release_ui.result }}
|
||||
RELEASE_UI_DARWIN_RESULT: ${{ needs.release_ui_darwin.result }}
|
||||
RELEASE_ARTIFACT_URL: ${{ needs.release.outputs.release_artifact_url }}
|
||||
LINUX_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.linux_packages_artifact_url }}
|
||||
WINDOWS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.windows_packages_artifact_url }}
|
||||
MACOS_PACKAGES_ARTIFACT_URL: ${{ needs.release.outputs.macos_packages_artifact_url }}
|
||||
RELEASE_UI_ARTIFACT_URL: ${{ needs.release_ui.outputs.release_ui_artifact_url }}
|
||||
RELEASE_UI_DARWIN_ARTIFACT_URL: ${{ needs.release_ui_darwin.outputs.release_ui_darwin_artifact_url }}
|
||||
GHCR_IMAGES_MARKDOWN: ${{ needs.release.outputs.ghcr_images }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const marker = '<!-- netbird-release-artifacts -->';
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
const shortSha = context.payload.pull_request.head.sha.slice(0, 7);
|
||||
|
||||
const artifactCell = (url, result) => {
|
||||
if (url) return `[Download](${url})`;
|
||||
return result && result !== 'success' ? `_Not available (${result})_` : '_Not available_';
|
||||
};
|
||||
|
||||
const artifacts = [
|
||||
['All release artifacts', process.env.RELEASE_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['Linux packages', process.env.LINUX_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['Windows packages', process.env.WINDOWS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['macOS packages', process.env.MACOS_PACKAGES_ARTIFACT_URL, process.env.RELEASE_RESULT],
|
||||
['UI artifacts', process.env.RELEASE_UI_ARTIFACT_URL, process.env.RELEASE_UI_RESULT],
|
||||
['UI macOS artifacts', process.env.RELEASE_UI_DARWIN_ARTIFACT_URL, process.env.RELEASE_UI_DARWIN_RESULT],
|
||||
];
|
||||
|
||||
const artifactRows = artifacts
|
||||
.map(([name, url, result]) => `| ${name} | ${artifactCell(url, result)} |`)
|
||||
.join('\n');
|
||||
|
||||
const ghcrImages = (process.env.GHCR_IMAGES_MARKDOWN || '').trim() || '_No GHCR images were pushed._';
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
'## Release artifacts',
|
||||
'',
|
||||
`Built for PR head \`${shortSha}\` in [workflow run #${process.env.GITHUB_RUN_NUMBER}](${runUrl}).`,
|
||||
'',
|
||||
'| Artifact | Link |',
|
||||
'| --- | --- |',
|
||||
artifactRows,
|
||||
'',
|
||||
'### GHCR images (amd64)',
|
||||
ghcrImages,
|
||||
'',
|
||||
'_This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy._',
|
||||
].join('\n');
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const previous = comments.find(comment =>
|
||||
comment.user?.type === 'Bot' && comment.body?.includes(marker)
|
||||
);
|
||||
|
||||
if (previous) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: previous.id,
|
||||
body,
|
||||
});
|
||||
core.info(`Updated release artifacts comment ${previous.id}`);
|
||||
} else {
|
||||
const { data } = await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
core.info(`Created release artifacts comment ${data.id}`);
|
||||
}
|
||||
|
||||
trigger_signer:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, release_ui, release_ui_darwin, test_windows_installer]
|
||||
needs: [release, release_ui, release_ui_darwin]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Trigger binaries sign pipelines
|
||||
|
||||
28
.github/workflows/sync-tag.yml
vendored
@@ -9,8 +9,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Receiving workflows (cloud sync-tag, mobile bump-netbird) expect the short
|
||||
# tag form (e.g. v0.30.0), not refs/tags/v0.30.0 — github.ref_name, not github.ref.
|
||||
jobs:
|
||||
trigger_sync_tag:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -22,30 +20,4 @@ jobs:
|
||||
ref: main
|
||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||
|
||||
trigger_android_bump:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||
steps:
|
||||
- name: Trigger android-client submodule bump
|
||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||
with:
|
||||
workflow: bump-netbird.yml
|
||||
ref: main
|
||||
repo: netbirdio/android-client
|
||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||
|
||||
trigger_ios_bump:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.created && !github.event.deleted && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
|
||||
steps:
|
||||
- name: Trigger ios-client submodule bump
|
||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1.3.1
|
||||
with:
|
||||
workflow: bump-netbird.yml
|
||||
ref: main
|
||||
repo: netbirdio/ios-client
|
||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||
@@ -58,11 +58,6 @@ linters:
|
||||
govet:
|
||||
enable:
|
||||
- nilness
|
||||
disable:
|
||||
# The inline analyzer flags x/exp/maps Clone/Clear with //go:fix inline
|
||||
# directives but cannot perform the rewrite due to generic type
|
||||
# parameter inference limitations in the Go inliner.
|
||||
- inline
|
||||
enable-all: false
|
||||
revive:
|
||||
rules:
|
||||
@@ -92,9 +87,6 @@ linters:
|
||||
- linters:
|
||||
- unused
|
||||
path: client/firewall/iptables/rule\.go
|
||||
- linters:
|
||||
- unused
|
||||
path: client/internal/dns/dnsfw/(types|syscall|zsyscall)_windows.*\.go
|
||||
- linters:
|
||||
- gosec
|
||||
- mirror
|
||||
|
||||
@@ -17,7 +17,6 @@ ENV \
|
||||
NETBIRD_BIN="/usr/local/bin/netbird" \
|
||||
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
||||
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
||||
NB_ENABLE_CAPTURE="false" \
|
||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||
|
||||
@@ -23,7 +23,6 @@ ENV \
|
||||
NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
|
||||
NB_LOG_FILE="console,/var/lib/netbird/client.log" \
|
||||
NB_DISABLE_DNS="true" \
|
||||
NB_ENABLE_CAPTURE="false" \
|
||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/util/capture"
|
||||
)
|
||||
|
||||
var captureCmd = &cobra.Command{
|
||||
Use: "capture",
|
||||
Short: "Capture packets on the WireGuard interface",
|
||||
Long: `Captures decrypted packets flowing through the WireGuard interface.
|
||||
|
||||
Default output is human-readable text. Use --pcap or --output for pcap binary.
|
||||
Requires --enable-capture to be set at service install or reconfigure time.
|
||||
|
||||
Examples:
|
||||
netbird debug capture
|
||||
netbird debug capture host 100.64.0.1 and port 443
|
||||
netbird debug capture tcp
|
||||
netbird debug capture icmp
|
||||
netbird debug capture src host 10.0.0.1 and dst port 80
|
||||
netbird debug capture -o capture.pcap
|
||||
netbird debug capture --pcap | tshark -r -
|
||||
netbird debug capture --pcap | tcpdump -r - -n`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: runCapture,
|
||||
}
|
||||
|
||||
func init() {
|
||||
debugCmd.AddCommand(captureCmd)
|
||||
|
||||
captureCmd.Flags().Bool("pcap", false, "Force pcap binary output (default when --output is set)")
|
||||
captureCmd.Flags().BoolP("verbose", "v", false, "Show seq/ack, TTL, window, total length")
|
||||
captureCmd.Flags().Bool("ascii", false, "Print payload as ASCII after each packet (useful for HTTP)")
|
||||
captureCmd.Flags().Uint32("snap-len", 0, "Max bytes per packet (0 = full)")
|
||||
captureCmd.Flags().DurationP("duration", "d", 0, "Capture duration (0 = until interrupted)")
|
||||
captureCmd.Flags().StringP("output", "o", "", "Write pcap to file instead of stdout")
|
||||
}
|
||||
|
||||
func runCapture(cmd *cobra.Command, args []string) error {
|
||||
conn, err := getClient(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
cmd.PrintErrf(errCloseConnection, err)
|
||||
}
|
||||
}()
|
||||
|
||||
client := proto.NewDaemonServiceClient(conn)
|
||||
|
||||
req, err := buildCaptureRequest(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
stream, err := client.StartCapture(ctx, req)
|
||||
if err != nil {
|
||||
return handleCaptureError(err)
|
||||
}
|
||||
|
||||
// First Recv is the empty acceptance message from the server. If the
|
||||
// device is unavailable (kernel WG, not connected, capture disabled),
|
||||
// the server returns an error instead.
|
||||
if _, err := stream.Recv(); err != nil {
|
||||
return handleCaptureError(err)
|
||||
}
|
||||
|
||||
out, cleanup, err := captureOutput(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.TextOutput {
|
||||
cmd.PrintErrf("Capturing packets... Press Ctrl+C to stop.\n")
|
||||
} else {
|
||||
cmd.PrintErrf("Capturing packets (pcap)... Press Ctrl+C to stop.\n")
|
||||
}
|
||||
|
||||
streamErr := streamCapture(ctx, cmd, stream, out)
|
||||
cleanupErr := cleanup()
|
||||
if streamErr != nil {
|
||||
return streamErr
|
||||
}
|
||||
return cleanupErr
|
||||
}
|
||||
|
||||
func buildCaptureRequest(cmd *cobra.Command, args []string) (*proto.StartCaptureRequest, error) {
|
||||
req := &proto.StartCaptureRequest{}
|
||||
|
||||
if len(args) > 0 {
|
||||
expr := strings.Join(args, " ")
|
||||
if _, err := capture.ParseFilter(expr); err != nil {
|
||||
return nil, fmt.Errorf("invalid filter: %w", err)
|
||||
}
|
||||
req.FilterExpr = expr
|
||||
}
|
||||
|
||||
if snap, _ := cmd.Flags().GetUint32("snap-len"); snap > 0 {
|
||||
req.SnapLen = snap
|
||||
}
|
||||
if d, _ := cmd.Flags().GetDuration("duration"); d != 0 {
|
||||
if d < 0 {
|
||||
return nil, fmt.Errorf("duration must not be negative")
|
||||
}
|
||||
req.Duration = durationpb.New(d)
|
||||
}
|
||||
req.Verbose, _ = cmd.Flags().GetBool("verbose")
|
||||
req.Ascii, _ = cmd.Flags().GetBool("ascii")
|
||||
|
||||
outPath, _ := cmd.Flags().GetString("output")
|
||||
forcePcap, _ := cmd.Flags().GetBool("pcap")
|
||||
req.TextOutput = !forcePcap && outPath == ""
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func streamCapture(ctx context.Context, cmd *cobra.Command, stream proto.DaemonService_StartCaptureClient, out io.Writer) error {
|
||||
for {
|
||||
pkt, err := stream.Recv()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
cmd.PrintErrf("\nCapture stopped.\n")
|
||||
return nil //nolint:nilerr // user interrupted
|
||||
}
|
||||
if err == io.EOF {
|
||||
cmd.PrintErrf("\nCapture finished.\n")
|
||||
return nil
|
||||
}
|
||||
return handleCaptureError(err)
|
||||
}
|
||||
if _, err := out.Write(pkt.GetData()); err != nil {
|
||||
return fmt.Errorf("write output: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// captureOutput returns the writer for capture data and a cleanup function
|
||||
// that finalizes the file. Errors from the cleanup must be propagated.
|
||||
func captureOutput(cmd *cobra.Command) (io.Writer, func() error, error) {
|
||||
outPath, _ := cmd.Flags().GetString("output")
|
||||
if outPath == "" {
|
||||
return os.Stdout, func() error { return nil }, nil
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp(filepath.Dir(outPath), filepath.Base(outPath)+".*.tmp")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create output file: %w", err)
|
||||
}
|
||||
tmpPath := f.Name()
|
||||
return f, func() error {
|
||||
var merr *multierror.Error
|
||||
if err := f.Close(); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("close output file: %w", err))
|
||||
}
|
||||
fi, statErr := os.Stat(tmpPath)
|
||||
if statErr != nil || fi.Size() == 0 {
|
||||
if rmErr := os.Remove(tmpPath); rmErr != nil && !os.IsNotExist(rmErr) {
|
||||
merr = multierror.Append(merr, fmt.Errorf("remove empty output file: %w", rmErr))
|
||||
}
|
||||
return nberrors.FormatErrorOrNil(merr)
|
||||
}
|
||||
if err := os.Rename(tmpPath, outPath); err != nil {
|
||||
merr = multierror.Append(merr, fmt.Errorf("rename output file: %w", err))
|
||||
return nberrors.FormatErrorOrNil(merr)
|
||||
}
|
||||
cmd.PrintErrf("Wrote %s\n", outPath)
|
||||
return nberrors.FormatErrorOrNil(merr)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func handleCaptureError(err error) error {
|
||||
if s, ok := status.FromError(err); ok {
|
||||
return fmt.Errorf("%s", s.Message())
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/internal/debug"
|
||||
@@ -240,50 +239,11 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
||||
}()
|
||||
}
|
||||
|
||||
captureStarted := false
|
||||
if wantCapture, _ := cmd.Flags().GetBool("capture"); wantCapture {
|
||||
captureTimeout := duration + 30*time.Second
|
||||
const maxBundleCapture = 10 * time.Minute
|
||||
if captureTimeout > maxBundleCapture {
|
||||
captureTimeout = maxBundleCapture
|
||||
}
|
||||
_, err := client.StartBundleCapture(cmd.Context(), &proto.StartBundleCaptureRequest{
|
||||
Timeout: durationpb.New(captureTimeout),
|
||||
})
|
||||
if err != nil {
|
||||
cmd.PrintErrf("Failed to start packet capture: %v\n", status.Convert(err).Message())
|
||||
} else {
|
||||
captureStarted = true
|
||||
cmd.Println("Packet capture started.")
|
||||
// Safety: always stop on exit, even if the normal stop below runs too.
|
||||
defer func() {
|
||||
if captureStarted {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if _, err := client.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
||||
cmd.PrintErrf("Failed to stop packet capture: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
cmd.Println("\nDuration completed")
|
||||
|
||||
if captureStarted {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if _, err := client.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
||||
cmd.PrintErrf("Failed to stop packet capture: %v\n", err)
|
||||
} else {
|
||||
captureStarted = false
|
||||
cmd.Println("Packet capture stopped.")
|
||||
}
|
||||
}
|
||||
|
||||
if cpuProfilingStarted {
|
||||
if _, err := client.StopCPUProfile(cmd.Context(), &proto.StopCPUProfileRequest{}); err != nil {
|
||||
cmd.PrintErrf("Failed to stop CPU profiling: %v\n", err)
|
||||
@@ -456,5 +416,4 @@ func init() {
|
||||
forCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
|
||||
forCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server")
|
||||
forCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
|
||||
forCmd.Flags().Bool("capture", false, "Capture packets during the debug duration and include in bundle")
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc/codes"
|
||||
gstatus "google.golang.org/grpc/status"
|
||||
|
||||
@@ -24,7 +23,6 @@ import (
|
||||
|
||||
func init() {
|
||||
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
||||
loginCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
|
||||
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
||||
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
|
||||
}
|
||||
@@ -258,7 +256,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
||||
}
|
||||
|
||||
func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.LoginResponse, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager) error {
|
||||
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser, showQR)
|
||||
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
|
||||
|
||||
resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
||||
if err != nil {
|
||||
@@ -326,7 +324,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
|
||||
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
||||
}
|
||||
|
||||
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser, showQR)
|
||||
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode, noBrowser)
|
||||
|
||||
tokenInfo, err := oAuthFlow.WaitToken(context.TODO(), flowInfo)
|
||||
if err != nil {
|
||||
@@ -336,7 +334,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *pro
|
||||
return &tokenInfo, nil
|
||||
}
|
||||
|
||||
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser, showQR bool) {
|
||||
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBrowser bool) {
|
||||
var codeMsg string
|
||||
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
|
||||
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
||||
@@ -350,12 +348,6 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
|
||||
verificationURIComplete + " " + codeMsg)
|
||||
}
|
||||
|
||||
if showQR {
|
||||
if f, ok := cmd.OutOrStdout().(*os.File); ok && term.IsTerminal(int(f.Fd())) {
|
||||
printQRCode(f, verificationURIComplete)
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Println("")
|
||||
|
||||
if !noBrowser {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
)
|
||||
|
||||
// printQRCode prints a QR code for the given URL to the writer.
|
||||
// Called only when the user explicitly requests QR output via --qr.
|
||||
func printQRCode(w io.Writer, url string) {
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
qrterminal.GenerateWithConfig(url, qrterminal.Config{
|
||||
Level: qrterminal.M,
|
||||
Writer: w,
|
||||
HalfBlocks: true,
|
||||
BlackChar: qrterminal.BLACK_BLACK,
|
||||
WhiteChar: qrterminal.WHITE_WHITE,
|
||||
BlackWhiteChar: qrterminal.BLACK_WHITE,
|
||||
WhiteBlackChar: qrterminal.WHITE_BLACK,
|
||||
QuietZone: qrterminal.QUIET_ZONE,
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrintQRCode_EmptyURL(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
printQRCode(&buf, "")
|
||||
|
||||
if buf.Len() != 0 {
|
||||
t.Error("expected no output for empty URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintQRCode_WritesOutput(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
printQRCode(&buf, "https://example.com/auth")
|
||||
|
||||
if buf.Len() == 0 {
|
||||
t.Error("expected QR code output for non-empty URL")
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,6 @@ var (
|
||||
mtu uint16
|
||||
profilesDisabled bool
|
||||
updateSettingsDisabled bool
|
||||
captureEnabled bool
|
||||
networksDisabled bool
|
||||
|
||||
rootCmd = &cobra.Command{
|
||||
|
||||
@@ -44,7 +44,6 @@ func init() {
|
||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd)
|
||||
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile. To persist this setting, use: netbird service install --disable-profiles")
|
||||
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
|
||||
serviceCmd.PersistentFlags().BoolVar(&captureEnabled, "enable-capture", false, "Enables packet capture via 'netbird debug capture'. To persist, use: netbird service install --enable-capture")
|
||||
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||
|
||||
@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
|
||||
}
|
||||
}
|
||||
|
||||
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled)
|
||||
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, networksDisabled)
|
||||
if err := serverInstance.Start(); err != nil {
|
||||
log.Fatalf("failed to start daemon: %v", err)
|
||||
}
|
||||
|
||||
@@ -59,10 +59,6 @@ func buildServiceArguments() []string {
|
||||
args = append(args, "--disable-update-settings")
|
||||
}
|
||||
|
||||
if captureEnabled {
|
||||
args = append(args, "--enable-capture")
|
||||
}
|
||||
|
||||
if networksDisabled {
|
||||
args = append(args, "--disable-networks")
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ type serviceParams struct {
|
||||
LogFiles []string `json:"log_files,omitempty"`
|
||||
DisableProfiles bool `json:"disable_profiles,omitempty"`
|
||||
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
||||
EnableCapture bool `json:"enable_capture,omitempty"`
|
||||
DisableNetworks bool `json:"disable_networks,omitempty"`
|
||||
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
||||
}
|
||||
@@ -80,7 +79,6 @@ func currentServiceParams() *serviceParams {
|
||||
LogFiles: logFiles,
|
||||
DisableProfiles: profilesDisabled,
|
||||
DisableUpdateSettings: updateSettingsDisabled,
|
||||
EnableCapture: captureEnabled,
|
||||
DisableNetworks: networksDisabled,
|
||||
}
|
||||
|
||||
@@ -146,10 +144,6 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
|
||||
updateSettingsDisabled = params.DisableUpdateSettings
|
||||
}
|
||||
|
||||
if !serviceCmd.PersistentFlags().Changed("enable-capture") {
|
||||
captureEnabled = params.EnableCapture
|
||||
}
|
||||
|
||||
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
|
||||
networksDisabled = params.DisableNetworks
|
||||
}
|
||||
|
||||
@@ -535,7 +535,6 @@ func fieldToGlobalVar(field string) string {
|
||||
"LogFiles": "logFiles",
|
||||
"DisableProfiles": "profilesDisabled",
|
||||
"DisableUpdateSettings": "updateSettingsDisabled",
|
||||
"EnableCapture": "captureEnabled",
|
||||
"DisableNetworks": "networksDisabled",
|
||||
"ServiceEnvVars": "serviceEnvVars",
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil, nil)
|
||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, jobManager, secretsManager, nil, nil, &mgmt.MockIntegratedValidator{}, networkMapController, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func startClientDaemon(
|
||||
s := grpc.NewServer()
|
||||
|
||||
server := client.New(ctx,
|
||||
"", "", false, false, false, false)
|
||||
"", "", false, false, false)
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ const (
|
||||
noBrowserFlag = "no-browser"
|
||||
noBrowserDesc = "do not open the browser for SSO login"
|
||||
|
||||
showQRFlag = "qr"
|
||||
showQRDesc = "show QR code for the SSO login URL (useful for headless machines without browser access)"
|
||||
|
||||
profileNameFlag = "profile"
|
||||
profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used."
|
||||
)
|
||||
@@ -51,7 +48,6 @@ var (
|
||||
dnsLabels []string
|
||||
dnsLabelsValidated domain.List
|
||||
noBrowser bool
|
||||
showQR bool
|
||||
profileName string
|
||||
configPath string
|
||||
|
||||
@@ -84,7 +80,6 @@ func init() {
|
||||
)
|
||||
|
||||
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
|
||||
upCmd.PersistentFlags().BoolVar(&showQR, showQRFlag, false, showQRDesc)
|
||||
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
|
||||
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) NetBird config file location. ")
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/util/capture"
|
||||
)
|
||||
|
||||
// CaptureOptions configures a packet capture session.
|
||||
type CaptureOptions struct {
|
||||
// Output receives pcap-formatted data. Nil disables pcap output.
|
||||
Output io.Writer
|
||||
// TextOutput receives human-readable packet summaries. Nil disables text output.
|
||||
TextOutput io.Writer
|
||||
// Filter is a BPF-like filter expression (e.g. "host 10.0.0.1 and tcp port 443").
|
||||
// Empty captures all packets.
|
||||
Filter string
|
||||
// Verbose adds seq/ack, TTL, window, and total length to text output.
|
||||
Verbose bool
|
||||
// ASCII dumps transport payload as printable ASCII after each packet line.
|
||||
ASCII bool
|
||||
}
|
||||
|
||||
// CaptureStats reports capture session counters.
|
||||
type CaptureStats struct {
|
||||
Packets int64
|
||||
Bytes int64
|
||||
Dropped int64
|
||||
}
|
||||
|
||||
// CaptureSession represents an active packet capture. Call Stop to end the
|
||||
// capture and flush buffered packets.
|
||||
type CaptureSession struct {
|
||||
sess *capture.Session
|
||||
engine *internal.Engine
|
||||
}
|
||||
|
||||
// Stop ends the capture, flushes remaining packets, and detaches from the device.
|
||||
// Safe to call multiple times.
|
||||
func (cs *CaptureSession) Stop() {
|
||||
if cs.engine != nil {
|
||||
_ = cs.engine.SetCapture(nil)
|
||||
cs.engine = nil
|
||||
}
|
||||
if cs.sess != nil {
|
||||
cs.sess.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Stats returns current capture counters.
|
||||
func (cs *CaptureSession) Stats() CaptureStats {
|
||||
s := cs.sess.Stats()
|
||||
return CaptureStats{
|
||||
Packets: s.Packets,
|
||||
Bytes: s.Bytes,
|
||||
Dropped: s.Dropped,
|
||||
}
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the capture's writer goroutine
|
||||
// has fully exited and all buffered packets have been flushed.
|
||||
func (cs *CaptureSession) Done() <-chan struct{} {
|
||||
return cs.sess.Done()
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/util/capture"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -66,7 +65,7 @@ type Options struct {
|
||||
PrivateKey string
|
||||
// ManagementURL overrides the default management server URL
|
||||
ManagementURL string
|
||||
// PreSharedKey is the pre-shared key for the tunnel interface
|
||||
// PreSharedKey is the pre-shared key for the WireGuard interface
|
||||
PreSharedKey string
|
||||
// LogOutput is the output destination for logs (defaults to os.Stderr if nil)
|
||||
LogOutput io.Writer
|
||||
@@ -82,9 +81,9 @@ type Options struct {
|
||||
DisableClientRoutes bool
|
||||
// BlockInbound blocks all inbound connections from peers
|
||||
BlockInbound bool
|
||||
// WireguardPort is the port for the tunnel interface. Use 0 for a random port.
|
||||
// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
|
||||
WireguardPort *int
|
||||
// MTU is the MTU for the tunnel interface.
|
||||
// MTU is the MTU for the WireGuard interface.
|
||||
// Valid values are in the range 576..8192 bytes.
|
||||
// If non-nil, this value overrides any value stored in the config file.
|
||||
// If nil, the existing config MTU (if non-zero) is preserved; otherwise it defaults to 1280.
|
||||
@@ -470,52 +469,6 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
||||
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
||||
}
|
||||
|
||||
// StartCapture begins capturing packets on this client's tunnel device.
|
||||
// Only one capture can be active at a time; starting a new one stops the previous.
|
||||
// Call StopCapture (or CaptureSession.Stop) to end it.
|
||||
func (c *Client) StartCapture(opts CaptureOptions) (*CaptureSession, error) {
|
||||
engine, err := c.getEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matcher capture.Matcher
|
||||
if opts.Filter != "" {
|
||||
m, err := capture.ParseFilter(opts.Filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse filter: %w", err)
|
||||
}
|
||||
matcher = m
|
||||
}
|
||||
|
||||
sess, err := capture.NewSession(capture.Options{
|
||||
Output: opts.Output,
|
||||
TextOutput: opts.TextOutput,
|
||||
Matcher: matcher,
|
||||
Verbose: opts.Verbose,
|
||||
ASCII: opts.ASCII,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create capture session: %w", err)
|
||||
}
|
||||
|
||||
if err := engine.SetCapture(sess); err != nil {
|
||||
sess.Stop()
|
||||
return nil, fmt.Errorf("set capture: %w", err)
|
||||
}
|
||||
|
||||
return &CaptureSession{sess: sess, engine: engine}, nil
|
||||
}
|
||||
|
||||
// StopCapture stops the active capture session if one is running.
|
||||
func (c *Client) StopCapture() error {
|
||||
engine, err := c.getEngine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return engine.SetCapture(nil)
|
||||
}
|
||||
|
||||
// getEngine safely retrieves the engine from the client with proper locking.
|
||||
// Returns ErrClientNotStarted if the client is not started.
|
||||
// Returns ErrEngineNotStarted if the engine is not available.
|
||||
|
||||
@@ -115,13 +115,12 @@ type Manager struct {
|
||||
|
||||
localipmanager *localIPManager
|
||||
|
||||
udpTracker *conntrack.UDPTracker
|
||||
icmpTracker *conntrack.ICMPTracker
|
||||
tcpTracker *conntrack.TCPTracker
|
||||
forwarder atomic.Pointer[forwarder.Forwarder]
|
||||
pendingCapture atomic.Pointer[forwarder.PacketCapture]
|
||||
logger *nblog.Logger
|
||||
flowLogger nftypes.FlowLogger
|
||||
udpTracker *conntrack.UDPTracker
|
||||
icmpTracker *conntrack.ICMPTracker
|
||||
tcpTracker *conntrack.TCPTracker
|
||||
forwarder atomic.Pointer[forwarder.Forwarder]
|
||||
logger *nblog.Logger
|
||||
flowLogger nftypes.FlowLogger
|
||||
|
||||
blockRule firewall.Rule
|
||||
|
||||
@@ -352,19 +351,6 @@ func (m *Manager) determineRouting() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPacketCapture sets or clears packet capture on the forwarder endpoint.
|
||||
// This captures outbound response packets that bypass the FilteredDevice in netstack mode.
|
||||
func (m *Manager) SetPacketCapture(pc forwarder.PacketCapture) {
|
||||
if pc == nil {
|
||||
m.pendingCapture.Store(nil)
|
||||
} else {
|
||||
m.pendingCapture.Store(&pc)
|
||||
}
|
||||
if fwder := m.forwarder.Load(); fwder != nil {
|
||||
fwder.SetCapture(pc)
|
||||
}
|
||||
}
|
||||
|
||||
// initForwarder initializes the forwarder, it disables routing on errors
|
||||
func (m *Manager) initForwarder() error {
|
||||
if m.forwarder.Load() != nil {
|
||||
@@ -386,11 +372,6 @@ func (m *Manager) initForwarder() error {
|
||||
|
||||
m.forwarder.Store(forwarder)
|
||||
|
||||
// Re-load after store: a concurrent SetPacketCapture may have seen forwarder as nil and only updated pendingCapture.
|
||||
if pc := m.pendingCapture.Load(); pc != nil {
|
||||
forwarder.SetCapture(*pc)
|
||||
}
|
||||
|
||||
log.Debug("forwarder initialized")
|
||||
|
||||
return nil
|
||||
@@ -633,7 +614,6 @@ func (m *Manager) resetState() {
|
||||
}
|
||||
|
||||
if fwder := m.forwarder.Load(); fwder != nil {
|
||||
fwder.SetCapture(nil)
|
||||
fwder.Stop()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,19 +12,12 @@ import (
|
||||
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||
)
|
||||
|
||||
// PacketCapture captures raw packets for debugging. Implementations must be
|
||||
// safe for concurrent use and must not block.
|
||||
type PacketCapture interface {
|
||||
Offer(data []byte, outbound bool)
|
||||
}
|
||||
|
||||
// endpoint implements stack.LinkEndpoint and handles integration with the wireguard device
|
||||
type endpoint struct {
|
||||
logger *nblog.Logger
|
||||
dispatcher stack.NetworkDispatcher
|
||||
device *wgdevice.Device
|
||||
mtu atomic.Uint32
|
||||
capture atomic.Pointer[PacketCapture]
|
||||
}
|
||||
|
||||
func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||
@@ -61,17 +54,13 @@ func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
pktBytes := data.AsSlice()
|
||||
|
||||
// Send the packet through WireGuard
|
||||
address := netHeader.DestinationAddress()
|
||||
if err := e.device.CreateOutboundPacket(pktBytes, address.AsSlice()); err != nil {
|
||||
err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice())
|
||||
if err != nil {
|
||||
e.logger.Error1("CreateOutboundPacket: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if pc := e.capture.Load(); pc != nil {
|
||||
(*pc).Offer(pktBytes, true)
|
||||
}
|
||||
written++
|
||||
}
|
||||
|
||||
|
||||
@@ -139,16 +139,6 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// SetCapture sets or clears the packet capture on the forwarder endpoint.
|
||||
// This captures outbound packets that bypass the FilteredDevice (netstack forwarding).
|
||||
func (f *Forwarder) SetCapture(pc PacketCapture) {
|
||||
if pc == nil {
|
||||
f.endpoint.capture.Store(nil)
|
||||
return
|
||||
}
|
||||
f.endpoint.capture.Store(&pc)
|
||||
}
|
||||
|
||||
func (f *Forwarder) InjectIncomingPacket(payload []byte) error {
|
||||
if len(payload) < header.IPv4MinimumSize {
|
||||
return fmt.Errorf("packet too small: %d bytes", len(payload))
|
||||
|
||||
@@ -270,9 +270,5 @@ func (f *Forwarder) injectICMPReply(id stack.TransportEndpointID, icmpPayload []
|
||||
return 0
|
||||
}
|
||||
|
||||
if pc := f.endpoint.capture.Load(); pc != nil {
|
||||
(*pc).Offer(fullPacket, true)
|
||||
}
|
||||
|
||||
return len(fullPacket)
|
||||
}
|
||||
|
||||
6
client/flutter_ui/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
build/
|
||||
coverage/
|
||||
36
client/flutter_ui/.metadata
Normal file
@@ -0,0 +1,36 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "02085feb3f5d8a8156e5e28512b9d99351d510c0"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: linux
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: macos
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: windows
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
115
client/flutter_ui/MIGRATION.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Flutter UI Migration
|
||||
|
||||
## Current Boundary
|
||||
|
||||
Keep the daemon as-is and replace only the desktop UI process. The Flutter app
|
||||
should continue to talk to `DaemonService` from `client/proto/daemon.proto`.
|
||||
|
||||
The current UI is not a simple settings window. It owns:
|
||||
|
||||
- tray/menu-bar state and nested menu actions
|
||||
- gRPC connection management and event subscription
|
||||
- connect, disconnect, login, and session-expired flows
|
||||
- profile switching, deregistration, and profile windows
|
||||
- network route and exit-node selection
|
||||
- advanced settings
|
||||
- debug bundle creation and upload status dialogs
|
||||
- enforced update notifications and progress windows
|
||||
- OS sleep/wake notification to the daemon
|
||||
- single-instance signaling and quick-actions windows
|
||||
|
||||
## Phases
|
||||
|
||||
1. Scaffold and generated gRPC client
|
||||
- Done: generated Dart stubs from `client/proto/daemon.proto`.
|
||||
- Done: app defaults to a gRPC-backed implementation and keeps
|
||||
`--fake-daemon` for UI-only work.
|
||||
- Remaining: replace the development user agent suffix with the release
|
||||
version at build time.
|
||||
|
||||
2. Core connection parity
|
||||
- Done: status polling and `SubscribeEvents` refresh hooks.
|
||||
- Done: `connect()` runs `Login` → optional SSO browser handoff via
|
||||
`openExternalUrl` → `WaitSSOLogin` → `Up`, with an `awaitingLogin` snapshot
|
||||
state and a banner that exposes the verification URI and user code.
|
||||
- Done: `disconnect()` calls `Down`.
|
||||
- Match current daemon address defaults:
|
||||
- Windows: `tcp://127.0.0.1:41731`
|
||||
- Unix-like desktop: `unix:///var/run/netbird.sock`
|
||||
|
||||
3. Settings, profiles, and networks
|
||||
- Done: `GetConfig`/`SetConfig` for the toggleable settings (auto-connect,
|
||||
allow SSH, quantum resistance, lazy connections, block inbound,
|
||||
notifications). Read-only fields (management URL, interface, port, MTU)
|
||||
still need editable forms.
|
||||
- Done: profile add/switch/remove/logout via `AddProfile`,
|
||||
`SwitchProfile`, `RemoveProfile`, `Logout`.
|
||||
- Done: network list with overlap filtering, per-route
|
||||
`SelectNetworks`/`DeselectNetworks`, and exit-node single-selection.
|
||||
|
||||
4. Desktop integration
|
||||
- Done: tray icon and menu via `tray_manager` (status header, profile,
|
||||
Connect/Disconnect, Show window, Quit) with status-aware icons that fall
|
||||
back to template variants on macOS.
|
||||
- Done: window lifecycle via `window_manager` — close hides instead of
|
||||
exiting; tray "Quit" actually destroys the window.
|
||||
- Done: native notifications via `local_notifier`, fed by the daemon's
|
||||
`SubscribeEvents` stream and gated by the `notifications` setting (with
|
||||
CRITICAL severity always firing).
|
||||
- Done: browser launch and clipboard via `Process.run` and
|
||||
`flutter/services` Clipboard.
|
||||
- Remaining: file/folder reveal for debug bundles, single-instance
|
||||
signaling, quick-actions invocation, and sleep/wake forwarding through
|
||||
`NotifyOSLifecycle`. Settings/Networks submenus on the tray are deferred
|
||||
until the window-side flows are stable.
|
||||
- Note: `local_notifier` uses macOS's deprecated `NSUserNotificationCenter`
|
||||
(warns at build time). Plan to swap to `flutter_local_notifications`
|
||||
before release.
|
||||
|
||||
5. Debug and update flows
|
||||
- Done: rich debug bundle screen with anonymize, system-info, upload (URL),
|
||||
and run-with-trace + duration. State machine drives `GetLogLevel` →
|
||||
`SetLogLevel(TRACE)` → `Down` → `SetSyncResponsePersistence` → `Up` →
|
||||
progress over duration → `StopCPUProfile` → `DebugBundle`, with restore
|
||||
of original log level and persistence in a finally. Result dialog covers
|
||||
uploaded, upload-failed, and local-only outcomes with copy/open actions.
|
||||
- Done: enforced-update modal triggered by daemon `progress_window=show`
|
||||
metadata. Polls `GetInstallerResult` with a 15-min timeout, blocks close
|
||||
for 10 s, then surfaces success (auto-close) or failure (error message).
|
||||
- Remaining: hook a "Check for updates" / "Install now" button into the
|
||||
About surface that calls `TriggerUpdate` directly.
|
||||
|
||||
6. Release pipeline
|
||||
- Update `.github/workflows/release.yml` UI build steps.
|
||||
- Update `client/netbird.wxs`, `release_files/install.sh`, and
|
||||
`release_files/ui-post-install.sh` where they assume the Go UI artifact.
|
||||
- Update updater restart behavior in `client/internal/updater/installer`.
|
||||
- Preserve public artifact names until installers and updater logic are
|
||||
intentionally migrated.
|
||||
|
||||
## RPCs Used By The Current UI
|
||||
|
||||
The first production implementation should cover:
|
||||
|
||||
- `Status`, `Up`, `Down`
|
||||
- `Login`, `WaitSSOLogin`, `Logout`
|
||||
- `GetConfig`, `SetConfig`, `GetFeatures`
|
||||
- `SubscribeEvents`
|
||||
- `ListNetworks`, `SelectNetworks`, `DeselectNetworks`
|
||||
- `ListProfiles`, `AddProfile`, `SwitchProfile`, `RemoveProfile`,
|
||||
`GetActiveProfile`
|
||||
- `DebugBundle`, `GetLogLevel`, `SetLogLevel`, `SetSyncResponsePersistence`,
|
||||
`StartCPUProfile`, `StopCPUProfile`
|
||||
- `TriggerUpdate`, `GetInstallerResult`
|
||||
- `NotifyOSLifecycle`
|
||||
|
||||
## Risk Register
|
||||
|
||||
- Desktop tray support differs sharply across Windows, macOS, and Linux.
|
||||
- Linux app indicators and desktop-session startup need distro-level testing.
|
||||
- The updater currently restarts `netbird-ui` by process/app name on Windows and
|
||||
macOS, so artifact naming changes must be coordinated.
|
||||
- Dart gRPC over Unix domain sockets must be validated against the daemon's
|
||||
existing `unix://` address behavior.
|
||||
- Flutter desktop packaging is separate from Go builds, so release CI needs a
|
||||
new toolchain and cache strategy.
|
||||
54
client/flutter_ui/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# NetBird Flutter UI
|
||||
|
||||
This is the migration workspace for a Flutter-based replacement for `client/ui`.
|
||||
The existing Go/Fyne UI remains the production UI until this package reaches
|
||||
feature and release-pipeline parity.
|
||||
|
||||
## Scope
|
||||
|
||||
The first target is the desktop UI only. The NetBird daemon, service lifecycle,
|
||||
network engine, and daemon gRPC API stay in Go.
|
||||
|
||||
Initial parity target:
|
||||
|
||||
- tray/menu-bar entry with connection status and connect/disconnect actions
|
||||
- settings and feature flags backed by `DaemonService.GetConfig` and `SetConfig`
|
||||
- profile management
|
||||
- network and exit-node selection
|
||||
- daemon event subscription and desktop notifications
|
||||
- login/session-expired flow
|
||||
- debug bundle flow
|
||||
- enforced-update progress window
|
||||
- Windows, macOS, and Linux packaging integration
|
||||
|
||||
## Bootstrap
|
||||
|
||||
Flutter and Dart are not committed into this repository. After installing the
|
||||
Flutter SDK, run:
|
||||
|
||||
```sh
|
||||
cd client/flutter_ui
|
||||
bash tool/bootstrap.sh
|
||||
bash tool/generate_proto.sh
|
||||
flutter run -d macos -- --daemon-addr=unix:///var/run/netbird.sock
|
||||
```
|
||||
|
||||
Use `-d windows` or `-d linux` on those platforms. The Windows daemon address is
|
||||
currently `tcp://127.0.0.1:41731`.
|
||||
|
||||
For UI-only development without a daemon, run:
|
||||
|
||||
```sh
|
||||
flutter run -d macos -- --fake-daemon
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
- `lib/main.dart`: app entry point and command-line flag parsing
|
||||
- `lib/src/app_shell.dart`: first-pass desktop shell
|
||||
- `lib/src/daemon_client.dart`: daemon boundary with fake and gRPC-backed clients
|
||||
- `lib/src/models.dart`: UI-facing models independent from generated protobufs
|
||||
- `lib/src/generated/`: generated Dart protobuf and gRPC files
|
||||
- `tool/bootstrap.sh`: creates Flutter desktop platform folders once Flutter is installed
|
||||
- `tool/generate_proto.sh`: generates Dart gRPC bindings from `client/proto/daemon.proto`
|
||||
- `MIGRATION.md`: parity plan and release integration checklist
|
||||
10
client/flutter_ui/analysis_options.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- lib/src/generated/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
avoid_print: true
|
||||
|
||||
BIN
client/flutter_ui/assets/tray/connected-macos.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
client/flutter_ui/assets/tray/connected.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/flutter_ui/assets/tray/connecting-macos.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
client/flutter_ui/assets/tray/connecting.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/flutter_ui/assets/tray/disconnected-macos.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
client/flutter_ui/assets/tray/disconnected.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
client/flutter_ui/assets/tray/error-macos.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
client/flutter_ui/assets/tray/error.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
53
client/flutter_ui/lib/main.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'src/app_shell.dart';
|
||||
import 'src/daemon_client.dart';
|
||||
import 'src/desktop_integration.dart';
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final daemonAddr = _readFlag(args, 'daemon-addr') ?? _defaultDaemonAddr();
|
||||
final fakeDaemon = args.contains('--fake-daemon');
|
||||
|
||||
await windowManager.ensureInitialized();
|
||||
const windowOptions = WindowOptions(
|
||||
size: Size(900, 640),
|
||||
minimumSize: Size(720, 520),
|
||||
center: true,
|
||||
title: 'NetBird',
|
||||
);
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
});
|
||||
|
||||
final client = fakeDaemon
|
||||
? FakeDaemonClient(daemonAddr: daemonAddr)
|
||||
: GrpcDaemonClient(daemonAddr: daemonAddr);
|
||||
|
||||
final integration = DesktopIntegration(client: client);
|
||||
await integration.initialize();
|
||||
|
||||
runApp(NetBirdFlutterApp(client: client, integration: integration));
|
||||
}
|
||||
|
||||
String? _readFlag(List<String> args, String name) {
|
||||
final prefix = '--$name=';
|
||||
for (final arg in args) {
|
||||
if (arg.startsWith(prefix)) {
|
||||
return arg.substring(prefix.length);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _defaultDaemonAddr() {
|
||||
if (Platform.isWindows) {
|
||||
return 'tcp://127.0.0.1:41731';
|
||||
}
|
||||
return 'unix:///var/run/netbird.sock';
|
||||
}
|
||||
889
client/flutter_ui/lib/src/app_shell.dart
Normal file
@@ -0,0 +1,889 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'daemon_client.dart';
|
||||
import 'debug_screen.dart';
|
||||
import 'desktop_integration.dart';
|
||||
import 'models.dart';
|
||||
import 'platform.dart';
|
||||
import 'update_progress.dart';
|
||||
|
||||
class NetBirdFlutterApp extends StatelessWidget {
|
||||
const NetBirdFlutterApp({required this.client, this.integration, super.key});
|
||||
|
||||
final DaemonClient client;
|
||||
final DesktopIntegration? integration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'NetBird',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: const Color(0xFF008C95),
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorSchemeSeed: const Color(0xFF008C95),
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
home: AppShell(client: client, integration: integration),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppShell extends StatefulWidget {
|
||||
const AppShell({required this.client, this.integration, super.key});
|
||||
|
||||
final DaemonClient client;
|
||||
final DesktopIntegration? integration;
|
||||
|
||||
@override
|
||||
State<AppShell> createState() => _AppShellState();
|
||||
}
|
||||
|
||||
class _AppShellState extends State<AppShell> {
|
||||
late ClientSnapshot _snapshot;
|
||||
StreamSubscription<ClientSnapshot>? _subscription;
|
||||
StreamSubscription<UpdateProgressEvent>? _updateSubscription;
|
||||
StreamSubscription<int>? _tabSubscription;
|
||||
int _selectedIndex = 0;
|
||||
bool _busy = false;
|
||||
bool _updateDialogOpen = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_snapshot = ClientSnapshot.initial(widget.client.daemonAddr);
|
||||
_subscription = widget.client.watchSnapshot().listen((snapshot) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _snapshot = snapshot);
|
||||
});
|
||||
_updateSubscription = widget.client.watchUpdateRequests().listen(
|
||||
_showUpdateDialog,
|
||||
);
|
||||
_tabSubscription = widget.integration?.tabRequests.listen((index) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _selectedIndex = index);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_updateSubscription?.cancel();
|
||||
_tabSubscription?.cancel();
|
||||
widget.client.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _showUpdateDialog(UpdateProgressEvent event) async {
|
||||
if (!mounted || _updateDialogOpen) {
|
||||
return;
|
||||
}
|
||||
_updateDialogOpen = true;
|
||||
try {
|
||||
await showUpdateProgressDialog(
|
||||
context: context,
|
||||
client: widget.client,
|
||||
event: event,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
_updateDialogOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRail(
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() => _selectedIndex = index);
|
||||
},
|
||||
labelType: NavigationRailLabelType.all,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: _StatusGlyph(status: _snapshot.status),
|
||||
),
|
||||
destinations: const [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.hub_outlined),
|
||||
selectedIcon: Icon(Icons.hub),
|
||||
label: Text('Status'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.route_outlined),
|
||||
selectedIcon: Icon(Icons.route),
|
||||
label: Text('Networks'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.account_circle_outlined),
|
||||
selectedIcon: Icon(Icons.account_circle),
|
||||
label: Text('Profiles'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.tune_outlined),
|
||||
selectedIcon: Icon(Icons.tune),
|
||||
label: Text('Settings'),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.bug_report_outlined),
|
||||
selectedIcon: Icon(Icons.bug_report),
|
||||
label: Text('Debug'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
Expanded(child: SafeArea(child: _buildPage(context))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(BuildContext context) {
|
||||
return switch (_selectedIndex) {
|
||||
0 => _StatusPane(
|
||||
snapshot: _snapshot,
|
||||
busy: _busy,
|
||||
onConnect: () => _run(widget.client.connect),
|
||||
onDisconnect: () => _run(widget.client.disconnect),
|
||||
),
|
||||
1 => _NetworksPane(snapshot: _snapshot, client: widget.client),
|
||||
2 => _ProfilesPane(snapshot: _snapshot, client: widget.client),
|
||||
3 => _SettingsPane(snapshot: _snapshot, client: widget.client),
|
||||
_ => DebugScreen(client: widget.client),
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _run(Future<void> Function() action) async {
|
||||
if (_busy) {
|
||||
return;
|
||||
}
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await action();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Page extends StatelessWidget {
|
||||
const _Page({required this.title, required this.child, this.actions});
|
||||
|
||||
final String title;
|
||||
final Widget child;
|
||||
final List<Widget>? actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
if (actions != null) ...actions!,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusPane extends StatelessWidget {
|
||||
const _StatusPane({
|
||||
required this.snapshot,
|
||||
required this.busy,
|
||||
required this.onConnect,
|
||||
required this.onDisconnect,
|
||||
});
|
||||
|
||||
final ClientSnapshot snapshot;
|
||||
final bool busy;
|
||||
final VoidCallback onConnect;
|
||||
final VoidCallback onDisconnect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connected = snapshot.status == ConnectionStatus.connected;
|
||||
final connecting =
|
||||
snapshot.status == ConnectionStatus.connecting ||
|
||||
snapshot.status == ConnectionStatus.awaitingLogin;
|
||||
|
||||
return _Page(
|
||||
title: 'Status',
|
||||
child: ListView(
|
||||
children: [
|
||||
_InfoRow(label: 'Connection', value: snapshot.status.label),
|
||||
_InfoRow(label: 'Daemon', value: snapshot.daemonAddr),
|
||||
_InfoRow(label: 'Daemon version', value: snapshot.daemonVersion),
|
||||
if (snapshot.pendingLogin != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_LoginBanner(pending: snapshot.pendingLogin!),
|
||||
],
|
||||
if (snapshot.errorMessage != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
_ErrorBanner(message: snapshot.errorMessage!),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: busy || connected || connecting ? null : onConnect,
|
||||
icon: const Icon(Icons.power_settings_new),
|
||||
label: const Text('Connect'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: busy || !connected ? null : onDisconnect,
|
||||
icon: const Icon(Icons.power_off),
|
||||
label: const Text('Disconnect'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_SectionLabel('Active profile'),
|
||||
_ProfileTile(profile: snapshot.activeProfile),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworksPane extends StatefulWidget {
|
||||
const _NetworksPane({required this.snapshot, required this.client});
|
||||
|
||||
final ClientSnapshot snapshot;
|
||||
final DaemonClient client;
|
||||
|
||||
@override
|
||||
State<_NetworksPane> createState() => _NetworksPaneState();
|
||||
}
|
||||
|
||||
class _NetworksPaneState extends State<_NetworksPane> {
|
||||
NetworkFilter _filter = NetworkFilter.all;
|
||||
final Set<String> _busyRoutes = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final networks = widget.snapshot.networks
|
||||
.where(_filter.matches)
|
||||
.toList();
|
||||
|
||||
return _Page(
|
||||
title: 'Networks',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SegmentedButton<NetworkFilter>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: NetworkFilter.all,
|
||||
icon: Icon(Icons.all_inclusive),
|
||||
label: Text('All'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: NetworkFilter.overlapping,
|
||||
icon: Icon(Icons.compare_arrows),
|
||||
label: Text('Overlapping'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: NetworkFilter.exitNode,
|
||||
icon: Icon(Icons.public),
|
||||
label: Text('Exit nodes'),
|
||||
),
|
||||
],
|
||||
selected: {_filter},
|
||||
onSelectionChanged: (selected) {
|
||||
setState(() => _filter = selected.single);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (networks.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Text('No networks to show.'),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: networks.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final route = networks[index];
|
||||
final exitNodeMode = _filter == NetworkFilter.exitNode;
|
||||
return _NetworkTile(
|
||||
route: route,
|
||||
exitNodeMode: exitNodeMode,
|
||||
busy: _busyRoutes.contains(route.id),
|
||||
onChanged: (selected) =>
|
||||
_toggle(route, selected, exitNodeMode),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggle(
|
||||
NetworkRoute route,
|
||||
bool selected,
|
||||
bool exitNodeMode,
|
||||
) async {
|
||||
if (_busyRoutes.contains(route.id)) {
|
||||
return;
|
||||
}
|
||||
setState(() => _busyRoutes.add(route.id));
|
||||
try {
|
||||
if (exitNodeMode) {
|
||||
await widget.client.setExitNode(selected ? route.id : null);
|
||||
} else {
|
||||
await widget.client.setNetworkSelection(route.id, selected);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _busyRoutes.remove(route.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfilesPane extends StatefulWidget {
|
||||
const _ProfilesPane({required this.snapshot, required this.client});
|
||||
|
||||
final ClientSnapshot snapshot;
|
||||
final DaemonClient client;
|
||||
|
||||
@override
|
||||
State<_ProfilesPane> createState() => _ProfilesPaneState();
|
||||
}
|
||||
|
||||
class _ProfilesPaneState extends State<_ProfilesPane> {
|
||||
bool _busy = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _Page(
|
||||
title: 'Profiles',
|
||||
actions: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: _busy ? null : _showAddDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add profile'),
|
||||
),
|
||||
],
|
||||
child: ListView.separated(
|
||||
itemCount: widget.snapshot.profiles.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final profile = widget.snapshot.profiles[index];
|
||||
return _ProfileTile(
|
||||
profile: profile,
|
||||
onTap: profile.active || _busy ? null : () => _confirmSwitch(profile),
|
||||
trailing: _profileMenu(profile),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _profileMenu(ProfileInfo profile) {
|
||||
return PopupMenuButton<_ProfileAction>(
|
||||
enabled: !_busy,
|
||||
onSelected: (action) => _handleAction(action, profile),
|
||||
itemBuilder: (context) => [
|
||||
if (profile.active)
|
||||
const PopupMenuItem(
|
||||
value: _ProfileAction.logout,
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.logout),
|
||||
title: Text('Logout'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _ProfileAction.remove,
|
||||
enabled: !profile.active,
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.delete_outline),
|
||||
title: Text('Remove'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleAction(
|
||||
_ProfileAction action,
|
||||
ProfileInfo profile,
|
||||
) async {
|
||||
switch (action) {
|
||||
case _ProfileAction.logout:
|
||||
await _confirmAndRun(
|
||||
title: 'Logout from ${profile.name}?',
|
||||
message:
|
||||
'This disconnects the active profile and clears its session.',
|
||||
run: widget.client.logoutActive,
|
||||
);
|
||||
case _ProfileAction.remove:
|
||||
await _confirmAndRun(
|
||||
title: 'Remove profile ${profile.name}?',
|
||||
message: 'This deletes the profile from this device.',
|
||||
run: () => widget.client.removeProfile(profile.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmSwitch(ProfileInfo profile) async {
|
||||
await _confirmAndRun(
|
||||
title: 'Switch to ${profile.name}?',
|
||||
message: 'The connection will restart with the new profile.',
|
||||
run: () => widget.client.switchProfile(profile.name),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showAddDialog() async {
|
||||
final controller = TextEditingController();
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Add profile'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
onSubmitted: (value) => Navigator.of(context).pop(value.trim()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(controller.text.trim()),
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (name == null || name.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await _runBusy(() => widget.client.addProfile(name));
|
||||
}
|
||||
|
||||
Future<void> _confirmAndRun({
|
||||
required String title,
|
||||
required String message,
|
||||
required Future<void> Function() run,
|
||||
}) async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Confirm'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
if (confirm != true) {
|
||||
return;
|
||||
}
|
||||
await _runBusy(run);
|
||||
}
|
||||
|
||||
Future<void> _runBusy(Future<void> Function() action) async {
|
||||
if (_busy) {
|
||||
return;
|
||||
}
|
||||
setState(() => _busy = true);
|
||||
try {
|
||||
await action();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _ProfileAction { logout, remove }
|
||||
|
||||
class _SettingsPane extends StatefulWidget {
|
||||
const _SettingsPane({required this.snapshot, required this.client});
|
||||
|
||||
final ClientSnapshot snapshot;
|
||||
final DaemonClient client;
|
||||
|
||||
@override
|
||||
State<_SettingsPane> createState() => _SettingsPaneState();
|
||||
}
|
||||
|
||||
class _SettingsPaneState extends State<_SettingsPane> {
|
||||
bool _writing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settings = widget.snapshot.settings;
|
||||
final disabled = _writing;
|
||||
|
||||
return _Page(
|
||||
title: 'Settings',
|
||||
child: ListView(
|
||||
children: [
|
||||
_InfoRow(label: 'Management URL', value: settings.managementUrl),
|
||||
_InfoRow(label: 'Interface', value: settings.interfaceName),
|
||||
_InfoRow(label: 'WireGuard port', value: '${settings.wireguardPort}'),
|
||||
_InfoRow(label: 'MTU', value: '${settings.mtu}'),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
value: settings.autoConnect,
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) =>
|
||||
_apply(settings.copyWith(autoConnect: value)),
|
||||
title: const Text('Connect on startup'),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.allowSsh,
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) => _apply(settings.copyWith(allowSsh: value)),
|
||||
title: const Text('Allow SSH'),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.quantumResistance,
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) =>
|
||||
_apply(settings.copyWith(quantumResistance: value)),
|
||||
title: const Text('Quantum resistance'),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.lazyConnection,
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) =>
|
||||
_apply(settings.copyWith(lazyConnection: value)),
|
||||
title: const Text('Lazy connections'),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.blockInbound,
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) =>
|
||||
_apply(settings.copyWith(blockInbound: value)),
|
||||
title: const Text('Block inbound'),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.notifications,
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) =>
|
||||
_apply(settings.copyWith(notifications: value)),
|
||||
title: const Text('Notifications'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _apply(ClientSettings updated) async {
|
||||
setState(() => _writing = true);
|
||||
try {
|
||||
await widget.client.updateSettings(updated);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _writing = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusGlyph extends StatelessWidget {
|
||||
const _StatusGlyph({required this.status});
|
||||
|
||||
final ConnectionStatus status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = switch (status) {
|
||||
ConnectionStatus.connected => Colors.green,
|
||||
ConnectionStatus.connecting => Colors.amber,
|
||||
ConnectionStatus.awaitingLogin => Colors.lightBlue,
|
||||
ConnectionStatus.error => Colors.red,
|
||||
ConnectionStatus.disconnected => Colors.grey,
|
||||
};
|
||||
|
||||
return Tooltip(
|
||||
message: status.label,
|
||||
child: Icon(Icons.circle, color: color, size: 18),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
const _InfoRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: Text(label, style: Theme.of(context).textTheme.labelLarge),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionLabel extends StatelessWidget {
|
||||
const _SectionLabel(this.text);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(text, style: Theme.of(context).textTheme.titleMedium),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorBanner extends StatelessWidget {
|
||||
const _ErrorBanner({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: colors.onErrorContainer),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(color: colors.onErrorContainer),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoginBanner extends StatelessWidget {
|
||||
const _LoginBanner({required this.pending});
|
||||
|
||||
final PendingLogin pending;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: colors.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Sign in to continue',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colors.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'A browser window opened to complete sign-in. '
|
||||
'If it did not, open the URL below.',
|
||||
style: TextStyle(color: colors.onTertiaryContainer),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText(
|
||||
pending.verificationUri,
|
||||
style: TextStyle(color: colors.onTertiaryContainer),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Code: ${pending.userCode}',
|
||||
style: TextStyle(color: colors.onTertiaryContainer),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => _openUrl(pending.verificationUri),
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('Open in browser'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _copy(context, pending.verificationUri),
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text('Copy URL'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openUrl(String url) async {
|
||||
await openExternalUrl(url);
|
||||
}
|
||||
|
||||
Future<void> _copy(BuildContext context, String url) async {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('URL copied')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkTile extends StatelessWidget {
|
||||
const _NetworkTile({
|
||||
required this.route,
|
||||
required this.exitNodeMode,
|
||||
required this.busy,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final NetworkRoute route;
|
||||
final bool exitNodeMode;
|
||||
final bool busy;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitle = [
|
||||
route.range,
|
||||
if (route.domains.isNotEmpty) route.domains.join(', '),
|
||||
].join(' ');
|
||||
|
||||
Widget leading;
|
||||
if (busy) {
|
||||
leading = const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
);
|
||||
} else if (exitNodeMode) {
|
||||
leading = IconButton(
|
||||
icon: Icon(
|
||||
route.selected
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_unchecked,
|
||||
),
|
||||
onPressed: () => onChanged(!route.selected),
|
||||
);
|
||||
} else {
|
||||
leading = Checkbox(
|
||||
value: route.selected,
|
||||
onChanged: (value) => onChanged(value ?? false),
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: leading,
|
||||
title: Text(route.id),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: route.isExitNode ? const Icon(Icons.public) : null,
|
||||
onTap: busy ? null : () => onChanged(!route.selected),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileTile extends StatelessWidget {
|
||||
const _ProfileTile({required this.profile, this.onTap, this.trailing});
|
||||
|
||||
final ProfileInfo profile;
|
||||
final VoidCallback? onTap;
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(
|
||||
profile.active ? Icons.check_circle : Icons.circle_outlined,
|
||||
),
|
||||
title: Text(profile.name),
|
||||
subtitle: profile.email == null ? null : Text(profile.email!),
|
||||
onTap: onTap,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
916
client/flutter_ui/lib/src/daemon_client.dart
Normal file
@@ -0,0 +1,916 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:grpc/grpc.dart';
|
||||
|
||||
import 'generated/daemon.pbgrpc.dart' as daemon;
|
||||
import 'models.dart';
|
||||
import 'platform.dart';
|
||||
|
||||
const _userAgent = 'netbird-desktop-ui/development';
|
||||
|
||||
abstract class DaemonClient {
|
||||
String get daemonAddr;
|
||||
|
||||
Stream<ClientSnapshot> watchSnapshot();
|
||||
|
||||
Stream<SystemNotification> watchEvents();
|
||||
|
||||
Stream<UpdateProgressEvent> watchUpdateRequests();
|
||||
|
||||
Future<void> connect();
|
||||
|
||||
Future<void> disconnect();
|
||||
|
||||
Future<void> bringUp();
|
||||
|
||||
Future<void> bringDown();
|
||||
|
||||
Future<DebugBundleResult> debugBundle({
|
||||
required bool anonymize,
|
||||
required bool systemInfo,
|
||||
String? uploadUrl,
|
||||
});
|
||||
|
||||
Future<DaemonLogLevel> getLogLevel();
|
||||
|
||||
Future<void> setLogLevel(DaemonLogLevel level);
|
||||
|
||||
Future<void> setSyncResponsePersistence(bool enabled);
|
||||
|
||||
Future<void> startCpuProfile();
|
||||
|
||||
Future<void> stopCpuProfile();
|
||||
|
||||
Future<TriggerUpdateResult> triggerUpdate();
|
||||
|
||||
Future<InstallerResult> getInstallerResult();
|
||||
|
||||
Future<void> updateSettings(ClientSettings updated);
|
||||
|
||||
Future<void> setNetworkSelection(String routeId, bool selected);
|
||||
|
||||
Future<void> setExitNode(String? routeId);
|
||||
|
||||
Future<void> switchProfile(String name);
|
||||
|
||||
Future<void> addProfile(String name);
|
||||
|
||||
Future<void> removeProfile(String name);
|
||||
|
||||
Future<void> logoutActive();
|
||||
|
||||
void dispose();
|
||||
}
|
||||
|
||||
class GrpcDaemonClient implements DaemonClient {
|
||||
GrpcDaemonClient({required this.daemonAddr}) {
|
||||
_snapshot = ClientSnapshot.initial(daemonAddr);
|
||||
_channel = _createChannel(daemonAddr);
|
||||
_client = daemon.DaemonServiceClient(_channel);
|
||||
}
|
||||
|
||||
@override
|
||||
final String daemonAddr;
|
||||
|
||||
final _snapshots = StreamController<ClientSnapshot>.broadcast();
|
||||
final _events = StreamController<SystemNotification>.broadcast();
|
||||
final _updateRequests = StreamController<UpdateProgressEvent>.broadcast();
|
||||
final _refreshInterval = const Duration(seconds: 2);
|
||||
final _callTimeout = const Duration(seconds: 5);
|
||||
final _ssoLoginTimeout = const Duration(minutes: 5);
|
||||
final _installerPollTimeout = const Duration(minutes: 15);
|
||||
|
||||
late final ClientChannel _channel;
|
||||
late final daemon.DaemonServiceClient _client;
|
||||
late ClientSnapshot _snapshot;
|
||||
|
||||
Timer? _poller;
|
||||
StreamSubscription<daemon.SystemEvent>? _eventSubscription;
|
||||
var _started = false;
|
||||
|
||||
@override
|
||||
Stream<ClientSnapshot> watchSnapshot() {
|
||||
_start();
|
||||
scheduleMicrotask(_emit);
|
||||
return _snapshots.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<SystemNotification> watchEvents() {
|
||||
_start();
|
||||
return _events.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<UpdateProgressEvent> watchUpdateRequests() {
|
||||
_start();
|
||||
return _updateRequests.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> connect() async {
|
||||
_setStatus(ConnectionStatus.connecting, clearError: true);
|
||||
try {
|
||||
await _runLoginFlow();
|
||||
await _client.up(
|
||||
daemon.UpRequest(username: _username()),
|
||||
options: _options(timeout: const Duration(seconds: 30)),
|
||||
);
|
||||
} catch (error) {
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: ConnectionStatus.error,
|
||||
errorMessage: _formatError(error),
|
||||
clearPendingLogin: true,
|
||||
);
|
||||
_emit();
|
||||
return;
|
||||
} finally {
|
||||
await _refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
await _runRpc(() async {
|
||||
await _client.down(daemon.DownRequest(), options: _options());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> bringUp() async {
|
||||
await _client.up(
|
||||
daemon.UpRequest(username: _username()),
|
||||
options: _options(timeout: const Duration(seconds: 30)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> bringDown() async {
|
||||
await _client.down(
|
||||
daemon.DownRequest(),
|
||||
options: _options(timeout: const Duration(seconds: 15)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DebugBundleResult> debugBundle({
|
||||
required bool anonymize,
|
||||
required bool systemInfo,
|
||||
String? uploadUrl,
|
||||
}) async {
|
||||
final request = daemon.DebugBundleRequest(
|
||||
anonymize: anonymize,
|
||||
systemInfo: systemInfo,
|
||||
uploadURL: uploadUrl ?? '',
|
||||
);
|
||||
final response = await _client.debugBundle(
|
||||
request,
|
||||
options: _options(timeout: const Duration(minutes: 2)),
|
||||
);
|
||||
return DebugBundleResult(
|
||||
path: response.path,
|
||||
uploadedKey: response.uploadedKey,
|
||||
uploadFailureReason: response.uploadFailureReason,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DaemonLogLevel> getLogLevel() async {
|
||||
final response = await _client.getLogLevel(
|
||||
daemon.GetLogLevelRequest(),
|
||||
options: _options(),
|
||||
);
|
||||
return _mapLogLevelFromProto(response.level);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setLogLevel(DaemonLogLevel level) async {
|
||||
await _client.setLogLevel(
|
||||
daemon.SetLogLevelRequest(level: _mapLogLevelToProto(level)),
|
||||
options: _options(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setSyncResponsePersistence(bool enabled) async {
|
||||
await _client.setSyncResponsePersistence(
|
||||
daemon.SetSyncResponsePersistenceRequest(enabled: enabled),
|
||||
options: _options(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startCpuProfile() async {
|
||||
await _client.startCPUProfile(
|
||||
daemon.StartCPUProfileRequest(),
|
||||
options: _options(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopCpuProfile() async {
|
||||
await _client.stopCPUProfile(
|
||||
daemon.StopCPUProfileRequest(),
|
||||
options: _options(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TriggerUpdateResult> triggerUpdate() async {
|
||||
final response = await _client.triggerUpdate(
|
||||
daemon.TriggerUpdateRequest(),
|
||||
options: _options(timeout: const Duration(seconds: 30)),
|
||||
);
|
||||
return TriggerUpdateResult(
|
||||
success: response.success,
|
||||
errorMessage: response.errorMsg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InstallerResult> getInstallerResult() async {
|
||||
final response = await _client.getInstallerResult(
|
||||
daemon.InstallerResultRequest(),
|
||||
options: _options(timeout: _installerPollTimeout),
|
||||
);
|
||||
return InstallerResult(
|
||||
success: response.success,
|
||||
errorMessage: response.errorMsg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateSettings(ClientSettings updated) async {
|
||||
await _runRpc(() async {
|
||||
final activeProfile = _snapshot.activeProfile.name;
|
||||
await _client.setConfig(
|
||||
daemon.SetConfigRequest(
|
||||
username: _username(),
|
||||
profileName: activeProfile,
|
||||
managementUrl: updated.managementUrl,
|
||||
rosenpassEnabled: updated.quantumResistance,
|
||||
serverSSHAllowed: updated.allowSsh,
|
||||
disableAutoConnect: !updated.autoConnect,
|
||||
disableNotifications: !updated.notifications,
|
||||
lazyConnectionEnabled: updated.lazyConnection,
|
||||
blockInbound: updated.blockInbound,
|
||||
),
|
||||
options: _options(timeout: const Duration(seconds: 10)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNetworkSelection(String routeId, bool selected) async {
|
||||
await _runRpc(() async {
|
||||
final request = daemon.SelectNetworksRequest(networkIDs: [routeId]);
|
||||
if (selected) {
|
||||
await _client.selectNetworks(request, options: _options());
|
||||
} else {
|
||||
await _client.deselectNetworks(request, options: _options());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setExitNode(String? routeId) async {
|
||||
await _runRpc(() async {
|
||||
final exitNodeIds = _snapshot.networks
|
||||
.where((route) => route.isExitNode)
|
||||
.map((route) => route.id)
|
||||
.toList();
|
||||
if (exitNodeIds.isNotEmpty) {
|
||||
await _client.deselectNetworks(
|
||||
daemon.SelectNetworksRequest(networkIDs: exitNodeIds),
|
||||
options: _options(),
|
||||
);
|
||||
}
|
||||
if (routeId != null) {
|
||||
await _client.selectNetworks(
|
||||
daemon.SelectNetworksRequest(networkIDs: [routeId]),
|
||||
options: _options(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> switchProfile(String name) async {
|
||||
await _runRpc(() async {
|
||||
await _client.switchProfile(
|
||||
daemon.SwitchProfileRequest(profileName: name, username: _username()),
|
||||
options: _options(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addProfile(String name) async {
|
||||
await _runRpc(() async {
|
||||
await _client.addProfile(
|
||||
daemon.AddProfileRequest(profileName: name, username: _username()),
|
||||
options: _options(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeProfile(String name) async {
|
||||
await _runRpc(() async {
|
||||
await _client.removeProfile(
|
||||
daemon.RemoveProfileRequest(profileName: name, username: _username()),
|
||||
options: _options(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logoutActive() async {
|
||||
await _runRpc(() async {
|
||||
final active = _snapshot.activeProfile.name;
|
||||
await _client.logout(
|
||||
daemon.LogoutRequest(profileName: active, username: _username()),
|
||||
options: _options(timeout: const Duration(seconds: 15)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_poller?.cancel();
|
||||
unawaited(_eventSubscription?.cancel() ?? Future<void>.value());
|
||||
_events.close();
|
||||
_updateRequests.close();
|
||||
_snapshots.close();
|
||||
unawaited(_channel.shutdown());
|
||||
}
|
||||
|
||||
void _start() {
|
||||
if (_started) {
|
||||
return;
|
||||
}
|
||||
_started = true;
|
||||
unawaited(_refresh());
|
||||
_poller = Timer.periodic(_refreshInterval, (_) {
|
||||
unawaited(_refresh());
|
||||
});
|
||||
_eventSubscription = _client
|
||||
.subscribeEvents(daemon.SubscribeRequest(), options: _options())
|
||||
.listen(
|
||||
(event) {
|
||||
_checkUpdateMetadata(event);
|
||||
final notification = _mapSystemEvent(event);
|
||||
if (notification != null && !_events.isClosed) {
|
||||
_events.add(notification);
|
||||
}
|
||||
unawaited(_refresh());
|
||||
},
|
||||
onError: (_) {},
|
||||
);
|
||||
}
|
||||
|
||||
DaemonLogLevel _mapLogLevelFromProto(daemon.LogLevel level) {
|
||||
return switch (level) {
|
||||
daemon.LogLevel.PANIC => DaemonLogLevel.panic,
|
||||
daemon.LogLevel.FATAL => DaemonLogLevel.fatal,
|
||||
daemon.LogLevel.ERROR => DaemonLogLevel.error,
|
||||
daemon.LogLevel.WARN => DaemonLogLevel.warn,
|
||||
daemon.LogLevel.INFO => DaemonLogLevel.info,
|
||||
daemon.LogLevel.DEBUG => DaemonLogLevel.debug,
|
||||
daemon.LogLevel.TRACE => DaemonLogLevel.trace,
|
||||
_ => DaemonLogLevel.unknown,
|
||||
};
|
||||
}
|
||||
|
||||
daemon.LogLevel _mapLogLevelToProto(DaemonLogLevel level) {
|
||||
return switch (level) {
|
||||
DaemonLogLevel.panic => daemon.LogLevel.PANIC,
|
||||
DaemonLogLevel.fatal => daemon.LogLevel.FATAL,
|
||||
DaemonLogLevel.error => daemon.LogLevel.ERROR,
|
||||
DaemonLogLevel.warn => daemon.LogLevel.WARN,
|
||||
DaemonLogLevel.info => daemon.LogLevel.INFO,
|
||||
DaemonLogLevel.debug => daemon.LogLevel.DEBUG,
|
||||
DaemonLogLevel.trace => daemon.LogLevel.TRACE,
|
||||
DaemonLogLevel.unknown => daemon.LogLevel.UNKNOWN,
|
||||
};
|
||||
}
|
||||
|
||||
void _checkUpdateMetadata(daemon.SystemEvent event) {
|
||||
final action = event.metadata['progress_window'];
|
||||
if (action != 'show') {
|
||||
return;
|
||||
}
|
||||
final version = event.metadata['version'] ?? 'unknown';
|
||||
if (!_updateRequests.isClosed) {
|
||||
_updateRequests.add(UpdateProgressEvent(version: version));
|
||||
}
|
||||
}
|
||||
|
||||
SystemNotification? _mapSystemEvent(daemon.SystemEvent event) {
|
||||
final severity = switch (event.severity) {
|
||||
daemon.SystemEvent_Severity.WARNING => NotificationSeverity.warning,
|
||||
daemon.SystemEvent_Severity.ERROR => NotificationSeverity.error,
|
||||
daemon.SystemEvent_Severity.CRITICAL => NotificationSeverity.critical,
|
||||
_ => NotificationSeverity.info,
|
||||
};
|
||||
final category = switch (event.category) {
|
||||
daemon.SystemEvent_Category.NETWORK => NotificationCategory.network,
|
||||
daemon.SystemEvent_Category.DNS => NotificationCategory.dns,
|
||||
daemon.SystemEvent_Category.AUTHENTICATION =>
|
||||
NotificationCategory.authentication,
|
||||
daemon.SystemEvent_Category.CONNECTIVITY =>
|
||||
NotificationCategory.connectivity,
|
||||
daemon.SystemEvent_Category.SYSTEM => NotificationCategory.system,
|
||||
_ => NotificationCategory.system,
|
||||
};
|
||||
return SystemNotification(
|
||||
severity: severity,
|
||||
category: category,
|
||||
message: event.message,
|
||||
userMessage: event.userMessage,
|
||||
id: event.metadata['id'],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runLoginFlow() async {
|
||||
final loginResponse = await _client.login(
|
||||
daemon.LoginRequest(
|
||||
isUnixDesktopClient: Platform.isLinux,
|
||||
profileName: _snapshot.activeProfile.name,
|
||||
username: _username(),
|
||||
hint: _snapshot.activeProfile.email,
|
||||
),
|
||||
options: _options(timeout: const Duration(seconds: 30)),
|
||||
);
|
||||
|
||||
if (!loginResponse.needsSSOLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: ConnectionStatus.awaitingLogin,
|
||||
pendingLogin: PendingLogin(
|
||||
verificationUri: loginResponse.verificationURIComplete,
|
||||
userCode: loginResponse.userCode,
|
||||
),
|
||||
);
|
||||
_emit();
|
||||
|
||||
if (loginResponse.verificationURIComplete.isNotEmpty) {
|
||||
await openExternalUrl(loginResponse.verificationURIComplete);
|
||||
}
|
||||
|
||||
await _client.waitSSOLogin(
|
||||
daemon.WaitSSOLoginRequest(userCode: loginResponse.userCode),
|
||||
options: _options(timeout: _ssoLoginTimeout),
|
||||
);
|
||||
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: ConnectionStatus.connecting,
|
||||
clearPendingLogin: true,
|
||||
);
|
||||
_emit();
|
||||
}
|
||||
|
||||
Future<void> _runRpc(Future<void> Function() action) async {
|
||||
try {
|
||||
_snapshot = _snapshot.copyWith(clearError: true);
|
||||
_emit();
|
||||
await action();
|
||||
} catch (error) {
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: ConnectionStatus.error,
|
||||
errorMessage: _formatError(error),
|
||||
);
|
||||
_emit();
|
||||
} finally {
|
||||
await _refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
try {
|
||||
final status = await _client.status(
|
||||
daemon.StatusRequest(),
|
||||
options: _options(),
|
||||
);
|
||||
|
||||
final activeProfile = await _loadActiveProfile();
|
||||
final profiles = await _loadProfiles(activeProfile);
|
||||
final networks = await _loadNetworks();
|
||||
final settings = await _loadSettings(activeProfile);
|
||||
|
||||
final mappedStatus = _mapStatus(status.status);
|
||||
final preserveAwaiting =
|
||||
_snapshot.status == ConnectionStatus.awaitingLogin &&
|
||||
mappedStatus != ConnectionStatus.connected;
|
||||
|
||||
_snapshot = ClientSnapshot(
|
||||
daemonAddr: daemonAddr,
|
||||
daemonVersion: status.daemonVersion.isEmpty
|
||||
? 'unknown'
|
||||
: status.daemonVersion,
|
||||
status: preserveAwaiting ? ConnectionStatus.awaitingLogin : mappedStatus,
|
||||
activeProfile: activeProfile,
|
||||
profiles: profiles,
|
||||
networks: networks,
|
||||
settings: settings,
|
||||
pendingLogin: preserveAwaiting ? _snapshot.pendingLogin : null,
|
||||
);
|
||||
} catch (error) {
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: ConnectionStatus.error,
|
||||
errorMessage: _formatError(error),
|
||||
);
|
||||
}
|
||||
_emit();
|
||||
}
|
||||
|
||||
Future<ProfileInfo> _loadActiveProfile() async {
|
||||
try {
|
||||
final active = await _client.getActiveProfile(
|
||||
daemon.GetActiveProfileRequest(),
|
||||
options: _options(),
|
||||
);
|
||||
if (active.profileName.isNotEmpty) {
|
||||
return ProfileInfo(
|
||||
name: active.profileName,
|
||||
email: _snapshot.activeProfile.email,
|
||||
active: true,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// Keep the status pane usable even when optional profile RPCs fail.
|
||||
}
|
||||
return _snapshot.activeProfile;
|
||||
}
|
||||
|
||||
Future<List<ProfileInfo>> _loadProfiles(ProfileInfo activeProfile) async {
|
||||
try {
|
||||
final response = await _client.listProfiles(
|
||||
daemon.ListProfilesRequest(username: _username()),
|
||||
options: _options(),
|
||||
);
|
||||
final profiles = response.profiles.map((profile) {
|
||||
return ProfileInfo(name: profile.name, active: profile.isActive);
|
||||
}).toList();
|
||||
if (profiles.isNotEmpty) {
|
||||
return profiles;
|
||||
}
|
||||
} catch (_) {
|
||||
// Profile listing is not required for core connection status.
|
||||
}
|
||||
return [activeProfile];
|
||||
}
|
||||
|
||||
Future<List<NetworkRoute>> _loadNetworks() async {
|
||||
try {
|
||||
final response = await _client.listNetworks(
|
||||
daemon.ListNetworksRequest(),
|
||||
options: _options(),
|
||||
);
|
||||
return _mapNetworks(response.routes);
|
||||
} catch (_) {
|
||||
return _snapshot.networks;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ClientSettings> _loadSettings(ProfileInfo activeProfile) async {
|
||||
try {
|
||||
final config = await _client.getConfig(
|
||||
daemon.GetConfigRequest(
|
||||
profileName: activeProfile.name,
|
||||
username: _username(),
|
||||
),
|
||||
options: _options(),
|
||||
);
|
||||
return ClientSettings(
|
||||
managementUrl: config.managementUrl.isEmpty
|
||||
? 'https://api.netbird.io'
|
||||
: config.managementUrl,
|
||||
interfaceName: config.interfaceName.isEmpty
|
||||
? 'wt0'
|
||||
: config.interfaceName,
|
||||
wireguardPort: config.hasWireguardPort()
|
||||
? config.wireguardPort.toInt()
|
||||
: 51820,
|
||||
mtu: config.hasMtu() ? config.mtu.toInt() : 1280,
|
||||
autoConnect: !config.disableAutoConnect,
|
||||
allowSsh: config.serverSSHAllowed,
|
||||
quantumResistance: config.rosenpassEnabled,
|
||||
notifications: !config.disableNotifications,
|
||||
lazyConnection: config.lazyConnectionEnabled,
|
||||
blockInbound: config.blockInbound,
|
||||
);
|
||||
} catch (_) {
|
||||
return _snapshot.settings;
|
||||
}
|
||||
}
|
||||
|
||||
List<NetworkRoute> _mapNetworks(Iterable<daemon.Network> routes) {
|
||||
final rangeCounts = <String, int>{};
|
||||
for (final route in routes) {
|
||||
if (route.domains.isEmpty) {
|
||||
rangeCounts.update(
|
||||
route.range,
|
||||
(count) => count + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return routes.map((route) {
|
||||
final resolvedIps = route.resolvedIPs.map((domain, ipList) {
|
||||
return MapEntry(domain, ipList.ips.toList());
|
||||
});
|
||||
|
||||
return NetworkRoute(
|
||||
id: route.iD,
|
||||
range: route.range,
|
||||
selected: route.selected,
|
||||
domains: route.domains.toList(),
|
||||
resolvedIps: resolvedIps,
|
||||
overlapping:
|
||||
route.domains.isEmpty && (rangeCounts[route.range] ?? 0) > 1,
|
||||
);
|
||||
}).toList()
|
||||
..sort((a, b) => a.id.toLowerCase().compareTo(b.id.toLowerCase()));
|
||||
}
|
||||
|
||||
CallOptions _options({Duration? timeout}) {
|
||||
return CallOptions(timeout: timeout ?? _callTimeout);
|
||||
}
|
||||
|
||||
void _setStatus(
|
||||
ConnectionStatus status, {
|
||||
bool clearError = false,
|
||||
bool clearPendingLogin = false,
|
||||
}) {
|
||||
_snapshot = _snapshot.copyWith(
|
||||
status: status,
|
||||
clearError: clearError,
|
||||
clearPendingLogin: clearPendingLogin,
|
||||
);
|
||||
_emit();
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
if (!_snapshots.isClosed) {
|
||||
_snapshots.add(_snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDaemonClient implements DaemonClient {
|
||||
FakeDaemonClient({required this.daemonAddr}) {
|
||||
scheduleMicrotask(_emit);
|
||||
}
|
||||
|
||||
@override
|
||||
final String daemonAddr;
|
||||
|
||||
final _snapshots = StreamController<ClientSnapshot>.broadcast();
|
||||
|
||||
late ClientSnapshot _snapshot = ClientSnapshot.initial(daemonAddr).copyWith(
|
||||
daemonVersion: 'development',
|
||||
profiles: const [
|
||||
ProfileInfo(name: 'default', email: 'user@example.com', active: true),
|
||||
ProfileInfo(name: 'staging', active: false),
|
||||
],
|
||||
networks: const [
|
||||
NetworkRoute(id: 'office', range: '10.10.0.0/16', selected: true),
|
||||
NetworkRoute(id: 'prod', range: '10.20.0.0/16'),
|
||||
NetworkRoute(id: 'exit-us', range: '0.0.0.0/0'),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Stream<ClientSnapshot> watchSnapshot() {
|
||||
scheduleMicrotask(_emit);
|
||||
return _snapshots.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<SystemNotification> watchEvents() =>
|
||||
const Stream<SystemNotification>.empty();
|
||||
|
||||
@override
|
||||
Stream<UpdateProgressEvent> watchUpdateRequests() =>
|
||||
const Stream<UpdateProgressEvent>.empty();
|
||||
|
||||
@override
|
||||
Future<void> connect() async {
|
||||
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connecting);
|
||||
_emit();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 450));
|
||||
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connected);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> bringUp() async {
|
||||
_snapshot = _snapshot.copyWith(status: ConnectionStatus.connected);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> bringDown() async {
|
||||
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DebugBundleResult> debugBundle({
|
||||
required bool anonymize,
|
||||
required bool systemInfo,
|
||||
String? uploadUrl,
|
||||
}) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
||||
return DebugBundleResult(
|
||||
path: '/tmp/netbird-debug.tar.gz',
|
||||
uploadedKey: uploadUrl == null ? '' : 'fake-upload-key',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DaemonLogLevel> getLogLevel() async => DaemonLogLevel.info;
|
||||
|
||||
@override
|
||||
Future<void> setLogLevel(DaemonLogLevel level) async {}
|
||||
|
||||
@override
|
||||
Future<void> setSyncResponsePersistence(bool enabled) async {}
|
||||
|
||||
@override
|
||||
Future<void> startCpuProfile() async {}
|
||||
|
||||
@override
|
||||
Future<void> stopCpuProfile() async {}
|
||||
|
||||
@override
|
||||
Future<TriggerUpdateResult> triggerUpdate() async {
|
||||
return const TriggerUpdateResult(success: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InstallerResult> getInstallerResult() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
return const InstallerResult(success: true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateSettings(ClientSettings updated) async {
|
||||
_snapshot = _snapshot.copyWith(settings: updated);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNetworkSelection(String routeId, bool selected) async {
|
||||
final next = _snapshot.networks.map((route) {
|
||||
if (route.id != routeId) {
|
||||
return route;
|
||||
}
|
||||
return NetworkRoute(
|
||||
id: route.id,
|
||||
range: route.range,
|
||||
domains: route.domains,
|
||||
resolvedIps: route.resolvedIps,
|
||||
overlapping: route.overlapping,
|
||||
selected: selected,
|
||||
);
|
||||
}).toList();
|
||||
_snapshot = _snapshot.copyWith(networks: next);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setExitNode(String? routeId) async {
|
||||
final next = _snapshot.networks.map((route) {
|
||||
if (!route.isExitNode) {
|
||||
return route;
|
||||
}
|
||||
return NetworkRoute(
|
||||
id: route.id,
|
||||
range: route.range,
|
||||
domains: route.domains,
|
||||
resolvedIps: route.resolvedIps,
|
||||
overlapping: route.overlapping,
|
||||
selected: route.id == routeId,
|
||||
);
|
||||
}).toList();
|
||||
_snapshot = _snapshot.copyWith(networks: next);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> switchProfile(String name) async {
|
||||
final profiles = _snapshot.profiles.map((profile) {
|
||||
return ProfileInfo(
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
active: profile.name == name,
|
||||
);
|
||||
}).toList();
|
||||
final active = profiles.firstWhere(
|
||||
(profile) => profile.active,
|
||||
orElse: () => _snapshot.activeProfile,
|
||||
);
|
||||
_snapshot = _snapshot.copyWith(profiles: profiles, activeProfile: active);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addProfile(String name) async {
|
||||
final profiles = [
|
||||
..._snapshot.profiles,
|
||||
ProfileInfo(name: name, active: false),
|
||||
];
|
||||
_snapshot = _snapshot.copyWith(profiles: profiles);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeProfile(String name) async {
|
||||
final profiles = _snapshot.profiles
|
||||
.where((profile) => profile.name != name)
|
||||
.toList();
|
||||
_snapshot = _snapshot.copyWith(profiles: profiles);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logoutActive() async {
|
||||
_snapshot = _snapshot.copyWith(status: ConnectionStatus.disconnected);
|
||||
_emit();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_snapshots.close();
|
||||
}
|
||||
|
||||
void _emit() {
|
||||
if (!_snapshots.isClosed) {
|
||||
_snapshots.add(_snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClientChannel _createChannel(String daemonAddr) {
|
||||
final options = ChannelOptions(
|
||||
credentials: const ChannelCredentials.insecure(),
|
||||
userAgent: _userAgent,
|
||||
connectTimeout: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
if (daemonAddr.startsWith('unix://')) {
|
||||
final path = daemonAddr.substring('unix://'.length);
|
||||
return ClientChannel(
|
||||
InternetAddress(path, type: InternetAddressType.unix),
|
||||
port: 0,
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
|
||||
final uri = daemonAddr.contains('://')
|
||||
? Uri.parse(daemonAddr)
|
||||
: Uri.parse('tcp://$daemonAddr');
|
||||
final host = uri.host.isEmpty ? '127.0.0.1' : uri.host;
|
||||
final port = uri.hasPort ? uri.port : 41731;
|
||||
return ClientChannel(host, port: port, options: options);
|
||||
}
|
||||
|
||||
ConnectionStatus _mapStatus(String status) {
|
||||
return switch (status) {
|
||||
'Connected' => ConnectionStatus.connected,
|
||||
'Connecting' => ConnectionStatus.connecting,
|
||||
'Idle' || 'SessionExpired' => ConnectionStatus.disconnected,
|
||||
_ => ConnectionStatus.error,
|
||||
};
|
||||
}
|
||||
|
||||
String _username() {
|
||||
if (Platform.isWindows) {
|
||||
final username = Platform.environment['USERNAME'] ?? '';
|
||||
final domain = Platform.environment['USERDOMAIN'] ?? '';
|
||||
if (domain.isNotEmpty && username.isNotEmpty) {
|
||||
return '$domain\\$username';
|
||||
}
|
||||
return username;
|
||||
}
|
||||
return Platform.environment['USER'] ?? Platform.environment['LOGNAME'] ?? '';
|
||||
}
|
||||
|
||||
String _formatError(Object error) {
|
||||
if (error is GrpcError) {
|
||||
return error.message ?? error.toString();
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
460
client/flutter_ui/lib/src/debug_screen.dart
Normal file
@@ -0,0 +1,460 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'daemon_client.dart';
|
||||
import 'models.dart';
|
||||
import 'platform.dart';
|
||||
|
||||
const _defaultUploadUrl = 'https://upload.netbird.io/';
|
||||
|
||||
class DebugScreen extends StatefulWidget {
|
||||
const DebugScreen({required this.client, super.key});
|
||||
|
||||
final DaemonClient client;
|
||||
|
||||
@override
|
||||
State<DebugScreen> createState() => _DebugScreenState();
|
||||
}
|
||||
|
||||
class _DebugScreenState extends State<DebugScreen> {
|
||||
final _uploadUrlController =
|
||||
TextEditingController(text: _defaultUploadUrl);
|
||||
final _durationController = TextEditingController(text: '1');
|
||||
|
||||
bool _anonymize = false;
|
||||
bool _systemInfo = true;
|
||||
bool _upload = true;
|
||||
bool _runWithTrace = true;
|
||||
bool _busy = false;
|
||||
|
||||
String _status = '';
|
||||
double? _progress;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uploadUrlController.dispose();
|
||||
_durationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Debug', style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Create a debug bundle to help troubleshoot issues with NetBird.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _anonymize,
|
||||
onChanged: _busy
|
||||
? null
|
||||
: (value) => setState(() => _anonymize = value ?? false),
|
||||
title: const Text(
|
||||
'Anonymize sensitive information (public IPs, domains, ...)',
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _systemInfo,
|
||||
onChanged: _busy
|
||||
? null
|
||||
: (value) => setState(() => _systemInfo = value ?? false),
|
||||
title: const Text(
|
||||
'Include system information (routes, interfaces, ...)',
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _upload,
|
||||
onChanged: _busy
|
||||
? null
|
||||
: (value) => setState(() => _upload = value ?? false),
|
||||
title: const Text('Upload bundle automatically after creation'),
|
||||
),
|
||||
if (_upload)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32, bottom: 8, top: 4),
|
||||
child: TextField(
|
||||
controller: _uploadUrlController,
|
||||
enabled: !_busy,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Debug upload URL',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
value: _runWithTrace,
|
||||
onChanged: _busy
|
||||
? null
|
||||
: (value) =>
|
||||
setState(() => _runWithTrace = value ?? false),
|
||||
title: const Text(
|
||||
'Run with trace logs before creating bundle',
|
||||
),
|
||||
),
|
||||
if (_runWithTrace)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('Run for'),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: TextField(
|
||||
controller: _durationController,
|
||||
enabled: !_busy,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(_durationLabel()),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_runWithTrace)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 32, top: 8),
|
||||
child: Text(
|
||||
'Note: NetBird will be brought up and down during collection.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_status.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(_status),
|
||||
),
|
||||
if (_progress != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: LinearProgressIndicator(value: _progress),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _busy ? null : _onCreate,
|
||||
icon: _busy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.archive_outlined),
|
||||
label: const Text('Create Debug Bundle'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _durationLabel() {
|
||||
final value = int.tryParse(_durationController.text) ?? 0;
|
||||
return value == 1 ? 'minute' : 'minutes';
|
||||
}
|
||||
|
||||
Future<void> _onCreate() async {
|
||||
final uploadUrl = _upload ? _uploadUrlController.text.trim() : null;
|
||||
if (_upload && (uploadUrl == null || uploadUrl.isEmpty)) {
|
||||
setState(() => _status = 'Upload URL is required when upload is enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
Duration? traceDuration;
|
||||
if (_runWithTrace) {
|
||||
final minutes = int.tryParse(_durationController.text);
|
||||
if (minutes == null || minutes < 1) {
|
||||
setState(() => _status = 'Duration must be a number ≥ 1');
|
||||
return;
|
||||
}
|
||||
traceDuration = Duration(minutes: minutes);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_busy = true;
|
||||
_status = '';
|
||||
_progress = null;
|
||||
});
|
||||
|
||||
try {
|
||||
DebugBundleResult result;
|
||||
if (traceDuration != null) {
|
||||
result = await _runWithTraceLogs(
|
||||
duration: traceDuration,
|
||||
uploadUrl: uploadUrl,
|
||||
);
|
||||
} else {
|
||||
setState(() => _status = 'Creating debug bundle...');
|
||||
result = await widget.client.debugBundle(
|
||||
anonymize: _anonymize,
|
||||
systemInfo: _systemInfo,
|
||||
uploadUrl: uploadUrl,
|
||||
);
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _status = 'Bundle created successfully');
|
||||
await _showResultDialog(result);
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_status = 'Error: $error';
|
||||
_progress = null;
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _busy = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<DebugBundleResult> _runWithTraceLogs({
|
||||
required Duration duration,
|
||||
required String? uploadUrl,
|
||||
}) async {
|
||||
final initialLevel = await widget.client.getLogLevel();
|
||||
final wasTrace = initialLevel == DaemonLogLevel.trace;
|
||||
|
||||
var levelChanged = false;
|
||||
var persistenceEnabled = false;
|
||||
var cpuProfileStarted = false;
|
||||
|
||||
try {
|
||||
if (!wasTrace) {
|
||||
await widget.client.setLogLevel(DaemonLogLevel.trace);
|
||||
levelChanged = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await widget.client.bringDown();
|
||||
} catch (_) {
|
||||
// Already down is fine; daemon returns OK either way.
|
||||
}
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
|
||||
try {
|
||||
await widget.client.setSyncResponsePersistence(true);
|
||||
persistenceEnabled = true;
|
||||
} catch (_) {
|
||||
// Persistence is best-effort — the bundle still works without it.
|
||||
}
|
||||
|
||||
await widget.client.bringUp();
|
||||
await Future<void>.delayed(const Duration(seconds: 3));
|
||||
|
||||
try {
|
||||
await widget.client.startCpuProfile();
|
||||
cpuProfileStarted = true;
|
||||
} catch (_) {
|
||||
// CPU profiling is optional.
|
||||
}
|
||||
|
||||
await _trackProgress(duration);
|
||||
|
||||
if (cpuProfileStarted) {
|
||||
try {
|
||||
await widget.client.stopCpuProfile();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return const DebugBundleResult(path: '');
|
||||
}
|
||||
setState(() {
|
||||
_status = 'Creating debug bundle with collected logs...';
|
||||
_progress = null;
|
||||
});
|
||||
|
||||
return await widget.client.debugBundle(
|
||||
anonymize: _anonymize,
|
||||
systemInfo: _systemInfo,
|
||||
uploadUrl: uploadUrl,
|
||||
);
|
||||
} finally {
|
||||
if (levelChanged) {
|
||||
try {
|
||||
await widget.client.setLogLevel(initialLevel);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (persistenceEnabled) {
|
||||
try {
|
||||
await widget.client.setSyncResponsePersistence(false);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _trackProgress(Duration total) async {
|
||||
final start = DateTime.now();
|
||||
final end = start.add(total);
|
||||
setState(() {
|
||||
_progress = 0;
|
||||
_status = 'Running with trace logs... ${_formatRemaining(total)} remaining';
|
||||
});
|
||||
|
||||
while (DateTime.now().isBefore(end)) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final elapsed = DateTime.now().difference(start);
|
||||
final fraction = (elapsed.inMilliseconds / total.inMilliseconds).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
final remaining = end.difference(DateTime.now());
|
||||
setState(() {
|
||||
_progress = fraction;
|
||||
_status =
|
||||
'Running with trace logs... ${_formatRemaining(remaining < Duration.zero ? Duration.zero : remaining)} remaining';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _formatRemaining(Duration d) {
|
||||
final hours = d.inHours.toString().padLeft(2, '0');
|
||||
final minutes = (d.inMinutes % 60).toString().padLeft(2, '0');
|
||||
final seconds = (d.inSeconds % 60).toString().padLeft(2, '0');
|
||||
return '$hours:$minutes:$seconds';
|
||||
}
|
||||
|
||||
Future<void> _showResultDialog(DebugBundleResult result) async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => _DebugResultDialog(result: result),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DebugResultDialog extends StatelessWidget {
|
||||
const _DebugResultDialog({required this.result});
|
||||
|
||||
final DebugBundleResult result;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final folder = _parentFolder(result.path);
|
||||
|
||||
String title;
|
||||
Widget body;
|
||||
if (result.uploadFailed) {
|
||||
title = 'Upload Failed';
|
||||
body = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Bundle upload failed:\n${result.uploadFailureReason}'),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText('Local copy: ${result.path}'),
|
||||
],
|
||||
);
|
||||
} else if (result.uploaded) {
|
||||
title = 'Upload Successful';
|
||||
body = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Bundle uploaded successfully.'),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Upload key:'),
|
||||
SelectableText(result.uploadedKey),
|
||||
const SizedBox(height: 12),
|
||||
SelectableText('Local copy: ${result.path}'),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
title = 'Debug Bundle Created';
|
||||
body = Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Bundle created locally at:\n${result.path}'),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Administrator privileges may be required to access the file.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: SingleChildScrollView(child: body),
|
||||
actions: [
|
||||
if (result.uploaded)
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(text: result.uploadedKey),
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Upload key copied')),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text('Copy key'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: result.path.isEmpty
|
||||
? null
|
||||
: () => openExternalUrl(result.path),
|
||||
icon: const Icon(Icons.description_outlined),
|
||||
label: const Text('Open file'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: folder.isEmpty ? null : () => openExternalUrl(folder),
|
||||
icon: const Icon(Icons.folder_open),
|
||||
label: const Text('Open folder'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _parentFolder(String path) {
|
||||
if (path.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final lastSlash = path.lastIndexOf(RegExp(r'[/\\]'));
|
||||
return lastSlash <= 0 ? '' : path.substring(0, lastSlash);
|
||||
}
|
||||
}
|
||||
434
client/flutter_ui/lib/src/desktop_integration.dart
Normal file
@@ -0,0 +1,434 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'daemon_client.dart';
|
||||
import 'models.dart';
|
||||
import 'platform.dart';
|
||||
|
||||
const uiVersion = '0.1.0';
|
||||
const _githubUrl = 'https://github.com/netbirdio/netbird';
|
||||
const _downloadUrl = 'https://netbird.io/download/';
|
||||
|
||||
class TabIndex {
|
||||
static const status = 0;
|
||||
static const networks = 1;
|
||||
static const profiles = 2;
|
||||
static const settings = 3;
|
||||
static const debug = 4;
|
||||
}
|
||||
|
||||
/// Owns native desktop integration: window lifecycle (hide on close), system
|
||||
/// tray icon and menu, and OS-level notifications driven by daemon events.
|
||||
class DesktopIntegration with TrayListener, WindowListener {
|
||||
DesktopIntegration({required this.client});
|
||||
|
||||
final DaemonClient client;
|
||||
final _tabRequests = StreamController<int>.broadcast();
|
||||
|
||||
StreamSubscription<ClientSnapshot>? _snapshotSub;
|
||||
StreamSubscription<SystemNotification>? _eventSub;
|
||||
ClientSnapshot? _lastSnapshot;
|
||||
String? _lastMenuKey;
|
||||
bool _disposed = false;
|
||||
bool _settingsBusy = false;
|
||||
|
||||
Stream<int> get tabRequests => _tabRequests.stream;
|
||||
|
||||
static const _trayMenuConnect = 'connect';
|
||||
static const _trayMenuDisconnect = 'disconnect';
|
||||
static const _trayMenuShow = 'show';
|
||||
static const _trayMenuQuit = 'quit';
|
||||
static const _trayMenuAllowSSH = 'settings.allow_ssh';
|
||||
static const _trayMenuAutoConnect = 'settings.auto_connect';
|
||||
static const _trayMenuQuantum = 'settings.quantum';
|
||||
static const _trayMenuLazy = 'settings.lazy';
|
||||
static const _trayMenuBlockInbound = 'settings.block_inbound';
|
||||
static const _trayMenuNotifications = 'settings.notifications';
|
||||
static const _trayMenuAdvancedSettings = 'open.settings';
|
||||
static const _trayMenuDebugBundle = 'open.debug';
|
||||
static const _trayMenuNetworks = 'open.networks';
|
||||
static const _trayMenuManageProfiles = 'open.profiles';
|
||||
static const _trayMenuLogout = 'profile.logout';
|
||||
static const _trayMenuGithub = 'about.github';
|
||||
static const _trayMenuDownload = 'about.download';
|
||||
static const _profileSwitchPrefix = 'profile.switch:';
|
||||
|
||||
Future<void> initialize() async {
|
||||
await localNotifier.setup(appName: 'NetBird');
|
||||
await windowManager.setPreventClose(true);
|
||||
windowManager.addListener(this);
|
||||
trayManager.addListener(this);
|
||||
|
||||
await _applyTrayIcon(ConnectionStatus.disconnected);
|
||||
await trayManager.setToolTip('NetBird');
|
||||
await _refreshTrayMenu(null);
|
||||
|
||||
_snapshotSub = client.watchSnapshot().listen(_onSnapshot);
|
||||
_eventSub = client.watchEvents().listen(_onEvent);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
await _snapshotSub?.cancel();
|
||||
await _eventSub?.cancel();
|
||||
await _tabRequests.close();
|
||||
windowManager.removeListener(this);
|
||||
trayManager.removeListener(this);
|
||||
await trayManager.destroy();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
unawaited(_handleWindowClose());
|
||||
}
|
||||
|
||||
Future<void> _handleWindowClose() async {
|
||||
final prevent = await windowManager.isPreventClose();
|
||||
if (prevent) {
|
||||
await windowManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconMouseDown() {
|
||||
if (Platform.isMacOS) {
|
||||
unawaited(trayManager.popUpContextMenu());
|
||||
} else {
|
||||
unawaited(_showWindow());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseDown() {
|
||||
unawaited(trayManager.popUpContextMenu());
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
final key = menuItem.key;
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if (key.startsWith(_profileSwitchPrefix)) {
|
||||
final name = key.substring(_profileSwitchPrefix.length);
|
||||
unawaited(_switchProfile(name));
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case _trayMenuConnect:
|
||||
unawaited(client.connect());
|
||||
case _trayMenuDisconnect:
|
||||
unawaited(client.disconnect());
|
||||
case _trayMenuShow:
|
||||
unawaited(_showWindow());
|
||||
case _trayMenuQuit:
|
||||
unawaited(_quit());
|
||||
case _trayMenuAllowSSH:
|
||||
unawaited(_toggleSetting((s) => s.copyWith(allowSsh: !s.allowSsh)));
|
||||
case _trayMenuAutoConnect:
|
||||
unawaited(
|
||||
_toggleSetting((s) => s.copyWith(autoConnect: !s.autoConnect)),
|
||||
);
|
||||
case _trayMenuQuantum:
|
||||
unawaited(
|
||||
_toggleSetting(
|
||||
(s) => s.copyWith(quantumResistance: !s.quantumResistance),
|
||||
),
|
||||
);
|
||||
case _trayMenuLazy:
|
||||
unawaited(
|
||||
_toggleSetting(
|
||||
(s) => s.copyWith(lazyConnection: !s.lazyConnection),
|
||||
),
|
||||
);
|
||||
case _trayMenuBlockInbound:
|
||||
unawaited(
|
||||
_toggleSetting(
|
||||
(s) => s.copyWith(blockInbound: !s.blockInbound),
|
||||
),
|
||||
);
|
||||
case _trayMenuNotifications:
|
||||
unawaited(
|
||||
_toggleSetting(
|
||||
(s) => s.copyWith(notifications: !s.notifications),
|
||||
),
|
||||
);
|
||||
case _trayMenuAdvancedSettings:
|
||||
unawaited(_openTab(TabIndex.settings));
|
||||
case _trayMenuDebugBundle:
|
||||
unawaited(_openTab(TabIndex.debug));
|
||||
case _trayMenuNetworks:
|
||||
unawaited(_openTab(TabIndex.networks));
|
||||
case _trayMenuManageProfiles:
|
||||
unawaited(_openTab(TabIndex.profiles));
|
||||
case _trayMenuLogout:
|
||||
unawaited(client.logoutActive());
|
||||
case _trayMenuGithub:
|
||||
unawaited(openExternalUrl(_githubUrl));
|
||||
case _trayMenuDownload:
|
||||
unawaited(openExternalUrl(_downloadUrl));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openTab(int index) async {
|
||||
if (!_tabRequests.isClosed) {
|
||||
_tabRequests.add(index);
|
||||
}
|
||||
await _showWindow();
|
||||
}
|
||||
|
||||
Future<void> _toggleSetting(
|
||||
ClientSettings Function(ClientSettings) mutate,
|
||||
) async {
|
||||
if (_settingsBusy) {
|
||||
return;
|
||||
}
|
||||
final snapshot = _lastSnapshot;
|
||||
if (snapshot == null) {
|
||||
return;
|
||||
}
|
||||
_settingsBusy = true;
|
||||
try {
|
||||
await client.updateSettings(mutate(snapshot.settings));
|
||||
} finally {
|
||||
_settingsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _switchProfile(String name) async {
|
||||
final snapshot = _lastSnapshot;
|
||||
if (snapshot == null || snapshot.activeProfile.name == name) {
|
||||
return;
|
||||
}
|
||||
await client.switchProfile(name);
|
||||
}
|
||||
|
||||
Future<void> _showWindow() async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
}
|
||||
|
||||
Future<void> _quit() async {
|
||||
await dispose();
|
||||
await windowManager.setPreventClose(false);
|
||||
await windowManager.destroy();
|
||||
}
|
||||
|
||||
void _onSnapshot(ClientSnapshot snapshot) {
|
||||
final previous = _lastSnapshot;
|
||||
_lastSnapshot = snapshot;
|
||||
if (previous == null || previous.status != snapshot.status) {
|
||||
unawaited(_applyTrayIcon(snapshot.status));
|
||||
unawaited(trayManager.setToolTip('NetBird — ${snapshot.status.label}'));
|
||||
}
|
||||
unawaited(_refreshTrayMenu(snapshot));
|
||||
}
|
||||
|
||||
void _onEvent(SystemNotification event) {
|
||||
if (event.userMessage.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final notificationsEnabled =
|
||||
_lastSnapshot?.settings.notifications ?? true;
|
||||
final critical = event.severity == NotificationSeverity.critical;
|
||||
if (!notificationsEnabled && !critical) {
|
||||
return;
|
||||
}
|
||||
|
||||
final title = '${_severityPrefix(event.severity)} [${event.category.label}]';
|
||||
final body = event.id == null
|
||||
? event.userMessage
|
||||
: '${event.userMessage} (id: ${event.id})';
|
||||
|
||||
unawaited(
|
||||
LocalNotification(title: title, body: body).show(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _applyTrayIcon(ConnectionStatus status) async {
|
||||
final basename = switch (status) {
|
||||
ConnectionStatus.connected => 'connected',
|
||||
ConnectionStatus.connecting ||
|
||||
ConnectionStatus.awaitingLogin => 'connecting',
|
||||
ConnectionStatus.error => 'error',
|
||||
ConnectionStatus.disconnected => 'disconnected',
|
||||
};
|
||||
final asset = Platform.isMacOS
|
||||
? 'assets/tray/$basename-macos.png'
|
||||
: 'assets/tray/$basename.png';
|
||||
await trayManager.setIcon(asset, isTemplate: Platform.isMacOS);
|
||||
}
|
||||
|
||||
Future<void> _refreshTrayMenu(ClientSnapshot? snapshot) async {
|
||||
final key = _menuKey(snapshot);
|
||||
if (key == _lastMenuKey) {
|
||||
return;
|
||||
}
|
||||
_lastMenuKey = key;
|
||||
|
||||
final connected = snapshot?.status == ConnectionStatus.connected;
|
||||
final connecting = snapshot?.status == ConnectionStatus.connecting ||
|
||||
snapshot?.status == ConnectionStatus.awaitingLogin;
|
||||
|
||||
final statusLabel =
|
||||
snapshot?.status.label ?? ConnectionStatus.disconnected.label;
|
||||
final settings = snapshot?.settings ?? const ClientSettings();
|
||||
final activeName = snapshot?.activeProfile.name ?? 'unknown';
|
||||
final email = snapshot?.activeProfile.email;
|
||||
final daemonVersion = snapshot?.daemonVersion ?? 'unknown';
|
||||
|
||||
final profileItems = <MenuItem>[];
|
||||
final profiles = snapshot?.profiles ?? const <ProfileInfo>[];
|
||||
for (final profile in profiles) {
|
||||
profileItems.add(
|
||||
MenuItem.checkbox(
|
||||
key: '$_profileSwitchPrefix${profile.name}',
|
||||
label: profile.name,
|
||||
checked: profile.active,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (profileItems.isNotEmpty) {
|
||||
profileItems.add(MenuItem.separator());
|
||||
}
|
||||
profileItems
|
||||
..add(MenuItem(key: _trayMenuManageProfiles, label: 'Manage Profiles'))
|
||||
..add(
|
||||
MenuItem(
|
||||
key: _trayMenuLogout,
|
||||
label: 'Deregister',
|
||||
disabled: !connected,
|
||||
),
|
||||
);
|
||||
|
||||
await trayManager.setContextMenu(
|
||||
Menu(
|
||||
items: [
|
||||
MenuItem(label: statusLabel, disabled: true),
|
||||
MenuItem.submenu(
|
||||
label: 'Profile: $activeName',
|
||||
submenu: Menu(items: profileItems),
|
||||
),
|
||||
if (email != null && email.isNotEmpty)
|
||||
MenuItem(label: '($email)', disabled: true),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
key: _trayMenuConnect,
|
||||
label: 'Connect',
|
||||
disabled: connected || connecting,
|
||||
),
|
||||
MenuItem(
|
||||
key: _trayMenuDisconnect,
|
||||
label: 'Disconnect',
|
||||
disabled: !connected,
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem.submenu(
|
||||
label: 'Settings',
|
||||
submenu: Menu(
|
||||
items: [
|
||||
MenuItem.checkbox(
|
||||
key: _trayMenuAllowSSH,
|
||||
label: 'Allow SSH',
|
||||
checked: settings.allowSsh,
|
||||
),
|
||||
MenuItem.checkbox(
|
||||
key: _trayMenuAutoConnect,
|
||||
label: 'Connect on Startup',
|
||||
checked: settings.autoConnect,
|
||||
),
|
||||
MenuItem.checkbox(
|
||||
key: _trayMenuQuantum,
|
||||
label: 'Enable Quantum-Resistance',
|
||||
checked: settings.quantumResistance,
|
||||
),
|
||||
MenuItem.checkbox(
|
||||
key: _trayMenuLazy,
|
||||
label: 'Enable Lazy Connections',
|
||||
checked: settings.lazyConnection,
|
||||
),
|
||||
MenuItem.checkbox(
|
||||
key: _trayMenuBlockInbound,
|
||||
label: 'Block Inbound Connections',
|
||||
checked: settings.blockInbound,
|
||||
),
|
||||
MenuItem.checkbox(
|
||||
key: _trayMenuNotifications,
|
||||
label: 'Notifications',
|
||||
checked: settings.notifications,
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
key: _trayMenuAdvancedSettings,
|
||||
label: 'Advanced Settings',
|
||||
),
|
||||
MenuItem(
|
||||
key: _trayMenuDebugBundle,
|
||||
label: 'Create Debug Bundle',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MenuItem(key: _trayMenuNetworks, label: 'Networks'),
|
||||
MenuItem.separator(),
|
||||
MenuItem.submenu(
|
||||
label: 'About',
|
||||
submenu: Menu(
|
||||
items: [
|
||||
MenuItem(key: _trayMenuGithub, label: 'GitHub'),
|
||||
MenuItem(label: 'GUI: $uiVersion', disabled: true),
|
||||
MenuItem(label: 'Daemon: $daemonVersion', disabled: true),
|
||||
MenuItem(
|
||||
key: _trayMenuDownload,
|
||||
label: 'Download latest version',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(key: _trayMenuShow, label: 'Show window'),
|
||||
MenuItem(key: _trayMenuQuit, label: 'Quit'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _menuKey(ClientSnapshot? snapshot) {
|
||||
if (snapshot == null) {
|
||||
return 'null';
|
||||
}
|
||||
final s = snapshot.settings;
|
||||
final profiles = snapshot.profiles
|
||||
.map((p) => '${p.name}:${p.active}:${p.email ?? ''}')
|
||||
.join(',');
|
||||
return [
|
||||
snapshot.status.name,
|
||||
snapshot.activeProfile.name,
|
||||
snapshot.activeProfile.email ?? '',
|
||||
snapshot.daemonVersion,
|
||||
profiles,
|
||||
s.allowSsh,
|
||||
s.autoConnect,
|
||||
s.quantumResistance,
|
||||
s.lazyConnection,
|
||||
s.blockInbound,
|
||||
s.notifications,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
String _severityPrefix(NotificationSeverity severity) {
|
||||
return switch (severity) {
|
||||
NotificationSeverity.critical => 'Critical',
|
||||
NotificationSeverity.error => 'Error',
|
||||
NotificationSeverity.warning => 'Warning',
|
||||
NotificationSeverity.info => 'Info',
|
||||
};
|
||||
}
|
||||
}
|
||||
7393
client/flutter_ui/lib/src/generated/daemon.pb.dart
Normal file
153
client/flutter_ui/lib/src/generated/daemon.pbenum.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
// This is a generated file - do not edit.
|
||||
//
|
||||
// Generated from daemon.proto.
|
||||
|
||||
// @dart = 3.3
|
||||
|
||||
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
|
||||
|
||||
import 'dart:core' as $core;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class LogLevel extends $pb.ProtobufEnum {
|
||||
static const LogLevel UNKNOWN =
|
||||
LogLevel._(0, _omitEnumNames ? '' : 'UNKNOWN');
|
||||
static const LogLevel PANIC = LogLevel._(1, _omitEnumNames ? '' : 'PANIC');
|
||||
static const LogLevel FATAL = LogLevel._(2, _omitEnumNames ? '' : 'FATAL');
|
||||
static const LogLevel ERROR = LogLevel._(3, _omitEnumNames ? '' : 'ERROR');
|
||||
static const LogLevel WARN = LogLevel._(4, _omitEnumNames ? '' : 'WARN');
|
||||
static const LogLevel INFO = LogLevel._(5, _omitEnumNames ? '' : 'INFO');
|
||||
static const LogLevel DEBUG = LogLevel._(6, _omitEnumNames ? '' : 'DEBUG');
|
||||
static const LogLevel TRACE = LogLevel._(7, _omitEnumNames ? '' : 'TRACE');
|
||||
|
||||
static const $core.List<LogLevel> values = <LogLevel>[
|
||||
UNKNOWN,
|
||||
PANIC,
|
||||
FATAL,
|
||||
ERROR,
|
||||
WARN,
|
||||
INFO,
|
||||
DEBUG,
|
||||
TRACE,
|
||||
];
|
||||
|
||||
static final $core.List<LogLevel?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 7);
|
||||
static LogLevel? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const LogLevel._(super.value, super.name);
|
||||
}
|
||||
|
||||
class ExposeProtocol extends $pb.ProtobufEnum {
|
||||
static const ExposeProtocol EXPOSE_HTTP =
|
||||
ExposeProtocol._(0, _omitEnumNames ? '' : 'EXPOSE_HTTP');
|
||||
static const ExposeProtocol EXPOSE_HTTPS =
|
||||
ExposeProtocol._(1, _omitEnumNames ? '' : 'EXPOSE_HTTPS');
|
||||
static const ExposeProtocol EXPOSE_TCP =
|
||||
ExposeProtocol._(2, _omitEnumNames ? '' : 'EXPOSE_TCP');
|
||||
static const ExposeProtocol EXPOSE_UDP =
|
||||
ExposeProtocol._(3, _omitEnumNames ? '' : 'EXPOSE_UDP');
|
||||
static const ExposeProtocol EXPOSE_TLS =
|
||||
ExposeProtocol._(4, _omitEnumNames ? '' : 'EXPOSE_TLS');
|
||||
|
||||
static const $core.List<ExposeProtocol> values = <ExposeProtocol>[
|
||||
EXPOSE_HTTP,
|
||||
EXPOSE_HTTPS,
|
||||
EXPOSE_TCP,
|
||||
EXPOSE_UDP,
|
||||
EXPOSE_TLS,
|
||||
];
|
||||
|
||||
static final $core.List<ExposeProtocol?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 4);
|
||||
static ExposeProtocol? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const ExposeProtocol._(super.value, super.name);
|
||||
}
|
||||
|
||||
/// avoid collision with loglevel enum
|
||||
class OSLifecycleRequest_CycleType extends $pb.ProtobufEnum {
|
||||
static const OSLifecycleRequest_CycleType UNKNOWN =
|
||||
OSLifecycleRequest_CycleType._(0, _omitEnumNames ? '' : 'UNKNOWN');
|
||||
static const OSLifecycleRequest_CycleType SLEEP =
|
||||
OSLifecycleRequest_CycleType._(1, _omitEnumNames ? '' : 'SLEEP');
|
||||
static const OSLifecycleRequest_CycleType WAKEUP =
|
||||
OSLifecycleRequest_CycleType._(2, _omitEnumNames ? '' : 'WAKEUP');
|
||||
|
||||
static const $core.List<OSLifecycleRequest_CycleType> values =
|
||||
<OSLifecycleRequest_CycleType>[
|
||||
UNKNOWN,
|
||||
SLEEP,
|
||||
WAKEUP,
|
||||
];
|
||||
|
||||
static final $core.List<OSLifecycleRequest_CycleType?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 2);
|
||||
static OSLifecycleRequest_CycleType? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const OSLifecycleRequest_CycleType._(super.value, super.name);
|
||||
}
|
||||
|
||||
class SystemEvent_Severity extends $pb.ProtobufEnum {
|
||||
static const SystemEvent_Severity INFO =
|
||||
SystemEvent_Severity._(0, _omitEnumNames ? '' : 'INFO');
|
||||
static const SystemEvent_Severity WARNING =
|
||||
SystemEvent_Severity._(1, _omitEnumNames ? '' : 'WARNING');
|
||||
static const SystemEvent_Severity ERROR =
|
||||
SystemEvent_Severity._(2, _omitEnumNames ? '' : 'ERROR');
|
||||
static const SystemEvent_Severity CRITICAL =
|
||||
SystemEvent_Severity._(3, _omitEnumNames ? '' : 'CRITICAL');
|
||||
|
||||
static const $core.List<SystemEvent_Severity> values = <SystemEvent_Severity>[
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
CRITICAL,
|
||||
];
|
||||
|
||||
static final $core.List<SystemEvent_Severity?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 3);
|
||||
static SystemEvent_Severity? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const SystemEvent_Severity._(super.value, super.name);
|
||||
}
|
||||
|
||||
class SystemEvent_Category extends $pb.ProtobufEnum {
|
||||
static const SystemEvent_Category NETWORK =
|
||||
SystemEvent_Category._(0, _omitEnumNames ? '' : 'NETWORK');
|
||||
static const SystemEvent_Category DNS =
|
||||
SystemEvent_Category._(1, _omitEnumNames ? '' : 'DNS');
|
||||
static const SystemEvent_Category AUTHENTICATION =
|
||||
SystemEvent_Category._(2, _omitEnumNames ? '' : 'AUTHENTICATION');
|
||||
static const SystemEvent_Category CONNECTIVITY =
|
||||
SystemEvent_Category._(3, _omitEnumNames ? '' : 'CONNECTIVITY');
|
||||
static const SystemEvent_Category SYSTEM =
|
||||
SystemEvent_Category._(4, _omitEnumNames ? '' : 'SYSTEM');
|
||||
|
||||
static const $core.List<SystemEvent_Category> values = <SystemEvent_Category>[
|
||||
NETWORK,
|
||||
DNS,
|
||||
AUTHENTICATION,
|
||||
CONNECTIVITY,
|
||||
SYSTEM,
|
||||
];
|
||||
|
||||
static final $core.List<SystemEvent_Category?> _byValue =
|
||||
$pb.ProtobufEnum.$_initByValueList(values, 4);
|
||||
static SystemEvent_Category? valueOf($core.int value) =>
|
||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||
|
||||
const SystemEvent_Category._(super.value, super.name);
|
||||
}
|
||||
|
||||
const $core.bool _omitEnumNames =
|
||||
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
1141
client/flutter_ui/lib/src/generated/daemon.pbgrpc.dart
Normal file
2589
client/flutter_ui/lib/src/generated/daemon.pbjson.dart
Normal file
257
client/flutter_ui/lib/src/models.dart
Normal file
@@ -0,0 +1,257 @@
|
||||
enum ConnectionStatus {
|
||||
disconnected,
|
||||
connecting,
|
||||
awaitingLogin,
|
||||
connected,
|
||||
error;
|
||||
|
||||
String get label {
|
||||
return switch (this) {
|
||||
ConnectionStatus.disconnected => 'Disconnected',
|
||||
ConnectionStatus.connecting => 'Connecting',
|
||||
ConnectionStatus.awaitingLogin => 'Awaiting login',
|
||||
ConnectionStatus.connected => 'Connected',
|
||||
ConnectionStatus.error => 'Error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkFilter {
|
||||
all,
|
||||
overlapping,
|
||||
exitNode;
|
||||
|
||||
bool matches(NetworkRoute route) {
|
||||
return switch (this) {
|
||||
NetworkFilter.all => true,
|
||||
NetworkFilter.overlapping => route.overlapping,
|
||||
NetworkFilter.exitNode => route.isExitNode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ClientSnapshot {
|
||||
const ClientSnapshot({
|
||||
required this.daemonAddr,
|
||||
required this.daemonVersion,
|
||||
required this.status,
|
||||
required this.activeProfile,
|
||||
required this.profiles,
|
||||
required this.networks,
|
||||
required this.settings,
|
||||
this.errorMessage,
|
||||
this.pendingLogin,
|
||||
});
|
||||
|
||||
factory ClientSnapshot.initial(String daemonAddr) {
|
||||
return ClientSnapshot(
|
||||
daemonAddr: daemonAddr,
|
||||
daemonVersion: 'unknown',
|
||||
status: ConnectionStatus.disconnected,
|
||||
activeProfile: const ProfileInfo(name: 'default', active: true),
|
||||
profiles: const [ProfileInfo(name: 'default', active: true)],
|
||||
networks: const [],
|
||||
settings: const ClientSettings(),
|
||||
);
|
||||
}
|
||||
|
||||
final String daemonAddr;
|
||||
final String daemonVersion;
|
||||
final ConnectionStatus status;
|
||||
final ProfileInfo activeProfile;
|
||||
final List<ProfileInfo> profiles;
|
||||
final List<NetworkRoute> networks;
|
||||
final ClientSettings settings;
|
||||
final String? errorMessage;
|
||||
final PendingLogin? pendingLogin;
|
||||
|
||||
ClientSnapshot copyWith({
|
||||
String? daemonAddr,
|
||||
String? daemonVersion,
|
||||
ConnectionStatus? status,
|
||||
ProfileInfo? activeProfile,
|
||||
List<ProfileInfo>? profiles,
|
||||
List<NetworkRoute>? networks,
|
||||
ClientSettings? settings,
|
||||
String? errorMessage,
|
||||
PendingLogin? pendingLogin,
|
||||
bool clearError = false,
|
||||
bool clearPendingLogin = false,
|
||||
}) {
|
||||
return ClientSnapshot(
|
||||
daemonAddr: daemonAddr ?? this.daemonAddr,
|
||||
daemonVersion: daemonVersion ?? this.daemonVersion,
|
||||
status: status ?? this.status,
|
||||
activeProfile: activeProfile ?? this.activeProfile,
|
||||
profiles: profiles ?? this.profiles,
|
||||
networks: networks ?? this.networks,
|
||||
settings: settings ?? this.settings,
|
||||
errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
|
||||
pendingLogin: clearPendingLogin
|
||||
? null
|
||||
: pendingLogin ?? this.pendingLogin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PendingLogin {
|
||||
const PendingLogin({
|
||||
required this.verificationUri,
|
||||
required this.userCode,
|
||||
});
|
||||
|
||||
final String verificationUri;
|
||||
final String userCode;
|
||||
}
|
||||
|
||||
class ProfileInfo {
|
||||
const ProfileInfo({required this.name, required this.active, this.email});
|
||||
|
||||
final String name;
|
||||
final String? email;
|
||||
final bool active;
|
||||
}
|
||||
|
||||
class NetworkRoute {
|
||||
const NetworkRoute({
|
||||
required this.id,
|
||||
required this.range,
|
||||
this.domains = const [],
|
||||
this.resolvedIps = const {},
|
||||
this.selected = false,
|
||||
this.overlapping = false,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String range;
|
||||
final List<String> domains;
|
||||
final Map<String, List<String>> resolvedIps;
|
||||
final bool selected;
|
||||
final bool overlapping;
|
||||
|
||||
bool get isExitNode => range == '0.0.0.0/0';
|
||||
}
|
||||
|
||||
enum DaemonLogLevel { unknown, panic, fatal, error, warn, info, debug, trace }
|
||||
|
||||
class DebugBundleResult {
|
||||
const DebugBundleResult({
|
||||
required this.path,
|
||||
this.uploadedKey = '',
|
||||
this.uploadFailureReason = '',
|
||||
});
|
||||
|
||||
final String path;
|
||||
final String uploadedKey;
|
||||
final String uploadFailureReason;
|
||||
|
||||
bool get uploaded => uploadedKey.isNotEmpty && uploadFailureReason.isEmpty;
|
||||
bool get uploadFailed => uploadFailureReason.isNotEmpty;
|
||||
}
|
||||
|
||||
class TriggerUpdateResult {
|
||||
const TriggerUpdateResult({required this.success, this.errorMessage = ''});
|
||||
|
||||
final bool success;
|
||||
final String errorMessage;
|
||||
}
|
||||
|
||||
class InstallerResult {
|
||||
const InstallerResult({required this.success, this.errorMessage = ''});
|
||||
|
||||
final bool success;
|
||||
final String errorMessage;
|
||||
}
|
||||
|
||||
class UpdateProgressEvent {
|
||||
const UpdateProgressEvent({required this.version});
|
||||
final String version;
|
||||
}
|
||||
|
||||
enum NotificationSeverity { info, warning, error, critical }
|
||||
|
||||
enum NotificationCategory {
|
||||
network,
|
||||
dns,
|
||||
authentication,
|
||||
connectivity,
|
||||
system;
|
||||
|
||||
String get label {
|
||||
return switch (this) {
|
||||
NotificationCategory.network => 'Network',
|
||||
NotificationCategory.dns => 'DNS',
|
||||
NotificationCategory.authentication => 'Authentication',
|
||||
NotificationCategory.connectivity => 'Connectivity',
|
||||
NotificationCategory.system => 'System',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SystemNotification {
|
||||
const SystemNotification({
|
||||
required this.severity,
|
||||
required this.category,
|
||||
required this.message,
|
||||
required this.userMessage,
|
||||
this.id,
|
||||
});
|
||||
|
||||
final NotificationSeverity severity;
|
||||
final NotificationCategory category;
|
||||
final String message;
|
||||
final String userMessage;
|
||||
final String? id;
|
||||
}
|
||||
|
||||
class ClientSettings {
|
||||
const ClientSettings({
|
||||
this.managementUrl = 'https://api.netbird.io',
|
||||
this.interfaceName = 'wt0',
|
||||
this.wireguardPort = 51820,
|
||||
this.mtu = 1280,
|
||||
this.autoConnect = true,
|
||||
this.allowSsh = false,
|
||||
this.quantumResistance = false,
|
||||
this.notifications = true,
|
||||
this.lazyConnection = false,
|
||||
this.blockInbound = false,
|
||||
});
|
||||
|
||||
final String managementUrl;
|
||||
final String interfaceName;
|
||||
final int wireguardPort;
|
||||
final int mtu;
|
||||
final bool autoConnect;
|
||||
final bool allowSsh;
|
||||
final bool quantumResistance;
|
||||
final bool notifications;
|
||||
final bool lazyConnection;
|
||||
final bool blockInbound;
|
||||
|
||||
ClientSettings copyWith({
|
||||
String? managementUrl,
|
||||
String? interfaceName,
|
||||
int? wireguardPort,
|
||||
int? mtu,
|
||||
bool? autoConnect,
|
||||
bool? allowSsh,
|
||||
bool? quantumResistance,
|
||||
bool? notifications,
|
||||
bool? lazyConnection,
|
||||
bool? blockInbound,
|
||||
}) {
|
||||
return ClientSettings(
|
||||
managementUrl: managementUrl ?? this.managementUrl,
|
||||
interfaceName: interfaceName ?? this.interfaceName,
|
||||
wireguardPort: wireguardPort ?? this.wireguardPort,
|
||||
mtu: mtu ?? this.mtu,
|
||||
autoConnect: autoConnect ?? this.autoConnect,
|
||||
allowSsh: allowSsh ?? this.allowSsh,
|
||||
quantumResistance: quantumResistance ?? this.quantumResistance,
|
||||
notifications: notifications ?? this.notifications,
|
||||
lazyConnection: lazyConnection ?? this.lazyConnection,
|
||||
blockInbound: blockInbound ?? this.blockInbound,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
client/flutter_ui/lib/src/platform.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// Opens a URL in the user's default browser. Returns false if the platform
|
||||
/// helper exits non-zero or is missing. Mirrors the Go UI's `openURL` logic.
|
||||
Future<bool> openExternalUrl(String url) async {
|
||||
try {
|
||||
final ProcessResult result;
|
||||
if (Platform.isMacOS) {
|
||||
result = await Process.run('open', [url]);
|
||||
} else if (Platform.isWindows) {
|
||||
result = await Process.run('rundll32', [
|
||||
'url.dll,FileProtocolHandler',
|
||||
url,
|
||||
]);
|
||||
} else {
|
||||
result = await Process.run('xdg-open', [url]);
|
||||
}
|
||||
return result.exitCode == 0;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
140
client/flutter_ui/lib/src/update_progress.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'daemon_client.dart';
|
||||
import 'models.dart';
|
||||
|
||||
const _allowCloseAfter = Duration(seconds: 10);
|
||||
const _dotInterval = Duration(seconds: 1);
|
||||
|
||||
/// Shows a modal dialog while the daemon installs an update. Polls
|
||||
/// `GetInstallerResult` and resolves when the daemon finishes or fails.
|
||||
Future<void> showUpdateProgressDialog({
|
||||
required BuildContext context,
|
||||
required DaemonClient client,
|
||||
required UpdateProgressEvent event,
|
||||
}) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => _UpdateProgressDialog(client: client, event: event),
|
||||
);
|
||||
}
|
||||
|
||||
class _UpdateProgressDialog extends StatefulWidget {
|
||||
const _UpdateProgressDialog({required this.client, required this.event});
|
||||
|
||||
final DaemonClient client;
|
||||
final UpdateProgressEvent event;
|
||||
|
||||
@override
|
||||
State<_UpdateProgressDialog> createState() => _UpdateProgressDialogState();
|
||||
}
|
||||
|
||||
class _UpdateProgressDialogState extends State<_UpdateProgressDialog> {
|
||||
Timer? _dotTimer;
|
||||
Timer? _allowCloseTimer;
|
||||
int _dots = 0;
|
||||
bool _canClose = false;
|
||||
String _status = 'Updating';
|
||||
String? _error;
|
||||
bool _resolved = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_dotTimer = Timer.periodic(_dotInterval, (_) => _tick());
|
||||
_allowCloseTimer = Timer(_allowCloseAfter, () {
|
||||
if (mounted) {
|
||||
setState(() => _canClose = true);
|
||||
}
|
||||
});
|
||||
unawaited(_pollInstaller());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dotTimer?.cancel();
|
||||
_allowCloseTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _tick() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_dots = (_dots + 1) % 4;
|
||||
_status = 'Updating${'.' * _dots}';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pollInstaller() async {
|
||||
try {
|
||||
final result = await widget.client.getInstallerResult();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (result.success) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_resolved = true;
|
||||
_canClose = true;
|
||||
_status = 'Update failed';
|
||||
_error = result.errorMessage.isEmpty
|
||||
? 'Unknown error'
|
||||
: result.errorMessage;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_resolved = true;
|
||||
_canClose = true;
|
||||
_status = 'Update timed out';
|
||||
_error = error.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: _canClose,
|
||||
child: AlertDialog(
|
||||
title: const Text('Updating client'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your client version is older than the auto-update version set in '
|
||||
'Management.\nUpdating client to ${widget.event.version}.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (!_resolved) const LinearProgressIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
Text(_status),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_error!,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _canClose ? () => Navigator.of(context).pop() : null,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
client/flutter_ui/linux/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
128
client/flutter_ui/linux/CMakeLists.txt
Normal file
@@ -0,0 +1,128 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "netbird_flutter_ui")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "io.netbird.netbird_flutter_ui")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
88
client/flutter_ui/linux/flutter/CMakeLists.txt
Normal file
@@ -0,0 +1,88 @@
|
||||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <local_notifier/local_notifier_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) local_notifier_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
|
||||
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
|
||||
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) window_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
|
||||
window_manager_plugin_register_with_registrar(window_manager_registrar);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
27
client/flutter_ui/linux/flutter/generated_plugins.cmake
Normal file
@@ -0,0 +1,27 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
local_notifier
|
||||
screen_retriever_linux
|
||||
tray_manager
|
||||
window_manager
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
26
client/flutter_ui/linux/runner/CMakeLists.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
6
client/flutter_ui/linux/runner/main.cc
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
148
client/flutter_ui/linux/runner/my_application.cc
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "netbird_flutter_ui");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "netbird_flutter_ui");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||
self);
|
||||
gtk_widget_realize(GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
// the application to be recognized beyond its binary name.
|
||||
g_set_prgname(APPLICATION_ID);
|
||||
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID, "flags",
|
||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||
}
|
||||
21
client/flutter_ui/linux/runner/my_application.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||
my_application,
|
||||
MY,
|
||||
APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
7
client/flutter_ui/macos/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||
2
client/flutter_ui/macos/Flutter/Flutter-Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
2
client/flutter_ui/macos/Flutter/Flutter-Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import local_notifier
|
||||
import screen_retriever_macos
|
||||
import tray_manager
|
||||
import window_manager
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
|
||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
42
client/flutter_ui/macos/Podfile
Normal file
@@ -0,0 +1,42 @@
|
||||
platform :osx, '10.15'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_macos_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
|
||||
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||
target 'RunnerTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_macos_build_settings(target)
|
||||
end
|
||||
end
|
||||
40
client/flutter_ui/macos/Podfile.lock
Normal file
@@ -0,0 +1,40 @@
|
||||
PODS:
|
||||
- FlutterMacOS (1.0.0)
|
||||
- local_notifier (0.1.0):
|
||||
- FlutterMacOS
|
||||
- screen_retriever_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- tray_manager (0.0.1):
|
||||
- FlutterMacOS
|
||||
- window_manager (0.5.0):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
|
||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
local_notifier:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
|
||||
screen_retriever_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos
|
||||
tray_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
window_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
|
||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||
window_manager: b729e31d38fb04905235df9ea896128991cad99e
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
801
client/flutter_ui/macos/Runner.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,801 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXAggregateTarget section */
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||
buildPhases = (
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Flutter Assemble";
|
||||
productName = FLX;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
5F10F38F17483368E6B26C16 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */; };
|
||||
6E2193E107D1C306C0B38295 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA24562430C7E3798566E220 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||
remoteInfo = FLX;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Bundle Framework";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = netbird_flutter_ui.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
5344037698CB477EF6AE75A3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AA24562430C7E3798566E220 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E69F59E3113C82C71F7A2757 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EB350F2E61DA77DD3D20E0EB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5F10F38F17483368E6B26C16 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6E2193E107D1C306C0B38295 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
16123F31EB7196617B509F9C /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5344037698CB477EF6AE75A3 /* Pods-Runner.debug.xcconfig */,
|
||||
E69F59E3113C82C71F7A2757 /* Pods-Runner.release.xcconfig */,
|
||||
EB350F2E61DA77DD3D20E0EB /* Pods-Runner.profile.xcconfig */,
|
||||
97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */,
|
||||
14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
16123F31EB7196617B509F9C /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||
);
|
||||
path = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AA24562430C7E3798566E220 /* Pods_Runner.framework */,
|
||||
2D1C698E330CDD6D9457E84F /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
13F875F4B0174355038870C8 /* [CP] Check Pods Manifest.lock */,
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
6D776BDDAB33DFA32528CFE2 /* [CP] Check Pods Manifest.lock */,
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
DF9F03510A6543FA652C823E /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* netbird_flutter_ui.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C80D4294CF70F00263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 33CC10EC2044A3C60003C045;
|
||||
};
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
33CC111A2044C6BA0003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
33CC10EC2044A3C60003C045 /* Runner */,
|
||||
331C80D4294CF70F00263BE5 /* RunnerTests */,
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C80D3294CF70F00263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
13F875F4B0174355038870C8 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
|
||||
};
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||
);
|
||||
inputPaths = (
|
||||
Flutter/ephemeral/tripwire,
|
||||
);
|
||||
outputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
6D776BDDAB33DFA32528CFE2 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DF9F03510A6543FA652C823E /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C80D1294CF70F00263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC10EC2044A3C60003C045 /* Runner */;
|
||||
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
33CC10F52044A3C60003C045 /* Base */,
|
||||
);
|
||||
name = MainMenu.xib;
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 97BFF106FF1D50C0EF3C4AF6 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3B081925C026B73446CD514F /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 14CA49126DC810A7FD8021C0 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/netbird_flutter_ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/netbird_flutter_ui";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C80DB294CF71000263BE5 /* Debug */,
|
||||
331C80DC294CF71000263BE5 /* Release */,
|
||||
331C80DD294CF71000263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10F92044A3C60003C045 /* Debug */,
|
||||
33CC10FA2044A3C60003C045 /* Release */,
|
||||
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10FC2044A3C60003C045 /* Debug */,
|
||||
33CC10FD2044A3C60003C045 /* Release */,
|
||||
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC111C2044C6BA0003C045 /* Debug */,
|
||||
33CC111D2044C6BA0003C045 /* Release */,
|
||||
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "netbird_flutter_ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "netbird_flutter_ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "netbird_flutter_ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "netbird_flutter_ui.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
client/flutter_ui/macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
client/flutter_ui/macos/Runner/AppDelegate.swift
Normal file
@@ -0,0 +1,13 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
@main
|
||||
class AppDelegate: FlutterAppDelegate {
|
||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_64.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_128.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 520 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
343
client/flutter_ui/macos/Runner/Base.lproj/MainMenu.xib
Normal file
@@ -0,0 +1,343 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="EPT-qC-fAb">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
<point key="canvasLocation" x="142" y="-258"/>
|
||||
</menu>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
14
client/flutter_ui/macos/Runner/Configs/AppInfo.xcconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
// Application-level settings for the Runner target.
|
||||
//
|
||||
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
|
||||
// future. If not, the values below would default to using the project name when this becomes a
|
||||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = netbird_flutter_ui
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.netbird.netbirdFlutterUi
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 io.netbird. All rights reserved.
|
||||
2
client/flutter_ui/macos/Runner/Configs/Debug.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include "../../Flutter/Flutter-Debug.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
||||
2
client/flutter_ui/macos/Runner/Configs/Release.xcconfig
Normal file
@@ -0,0 +1,2 @@
|
||||
#include "../../Flutter/Flutter-Release.xcconfig"
|
||||
#include "Warnings.xcconfig"
|
||||
13
client/flutter_ui/macos/Runner/Configs/Warnings.xcconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES
|
||||
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
|
||||
CLANG_WARN_PRAGMA_PACK = YES
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES
|
||||
CLANG_WARN_COMMA = YES
|
||||
GCC_WARN_STRICT_SELECTOR_MATCH = YES
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
|
||||
GCC_WARN_SHADOW = YES
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES
|
||||
12
client/flutter_ui/macos/Runner/DebugProfile.entitlements
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
32
client/flutter_ui/macos/Runner/Info.plist
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
15
client/flutter_ui/macos/Runner/MainFlutterWindow.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController()
|
||||
let windowFrame = self.frame
|
||||
self.contentViewController = flutterViewController
|
||||
self.setFrame(windowFrame, display: true)
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
||||
10
client/flutter_ui/macos/Runner/Release.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
12
client/flutter_ui/macos/RunnerTests/RunnerTests.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
413
client/flutter_ui/pubspec.lock
Normal file
@@ -0,0 +1,413 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
fixnum:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
google_cloud:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_cloud
|
||||
sha256: fbcde933b2d8600c3cdb2328f8f4c47628ec29a39e9cef85dee535c7868993c4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_identity_services_web
|
||||
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+1"
|
||||
googleapis_auth:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: googleapis_auth
|
||||
sha256: "661738b763d3e524de69df53bf4e03943e4e01e98265cebcc6684871b06a5379"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
grpc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: grpc
|
||||
sha256: "86be3a7d39ad865b214a7370021ac80e68939238b507730de6d97fc662cb2723"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http2:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http2
|
||||
sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
local_notifier:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_notifier
|
||||
sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: menu_base
|
||||
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
protobuf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: protobuf
|
||||
sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever
|
||||
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_linux
|
||||
sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_macos
|
||||
sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_platform_interface
|
||||
sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
screen_retriever_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever_windows
|
||||
sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shortid
|
||||
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.3"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.1.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
28
client/flutter_ui/pubspec.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: netbird_flutter_ui
|
||||
description: Experimental Flutter desktop UI for NetBird.
|
||||
publish_to: none
|
||||
version: 0.1.0
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
fixnum: ^1.1.1
|
||||
grpc: ^5.1.0
|
||||
protobuf: ^6.0.0
|
||||
tray_manager: ^0.5.0
|
||||
window_manager: ^0.5.1
|
||||
local_notifier: ^0.1.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- assets/tray/
|
||||
|
||||
19
client/flutter_ui/test/app_shell_test.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:netbird_flutter_ui/src/app_shell.dart';
|
||||
import 'package:netbird_flutter_ui/src/daemon_client.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders the status shell', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
NetBirdFlutterApp(
|
||||
client: FakeDaemonClient(daemonAddr: 'tcp://127.0.0.1:41731'),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Status'), findsWidgets);
|
||||
expect(find.text('Connect'), findsOneWidget);
|
||||
expect(find.text('Disconnect'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
36
client/flutter_ui/tool/bootstrap.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
command -v flutter >/dev/null 2>&1 || {
|
||||
echo "flutter is not installed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
cp "$project_dir/pubspec.yaml" "$tmp_dir/pubspec.yaml"
|
||||
cp "$project_dir/analysis_options.yaml" "$tmp_dir/analysis_options.yaml"
|
||||
cp -R "$project_dir/lib" "$tmp_dir/lib"
|
||||
cp -R "$project_dir/test" "$tmp_dir/test"
|
||||
|
||||
flutter create \
|
||||
--platforms=windows,macos,linux \
|
||||
--project-name=netbird_flutter_ui \
|
||||
--org=io.netbird \
|
||||
"$project_dir"
|
||||
|
||||
cp "$tmp_dir/pubspec.yaml" "$project_dir/pubspec.yaml"
|
||||
cp "$tmp_dir/analysis_options.yaml" "$project_dir/analysis_options.yaml"
|
||||
rm -rf "$project_dir/lib"
|
||||
cp -R "$tmp_dir/lib" "$project_dir/lib"
|
||||
rm -rf "$project_dir/test"
|
||||
cp -R "$tmp_dir/test" "$project_dir/test"
|
||||
|
||||
cd "$project_dir"
|
||||
flutter pub get
|
||||