Compare commits

..

30 Commits

Author SHA1 Message Date
Ashley Mensah
ca4a31c0be fix(ci): improve system prompt for partial fixes and stale issues
- Add rules for partial fixes: PRs that only address some items in a
  multi-item request or explicitly say "partially addresses" do not
  count as resolution
- Clarify that maintainer-offered alternatives without reporter
  confirmation are not fixes
- Add stale issue guidance: 12+ months inactive with unanswered
  maintainer response should be MANUAL_REVIEW not KEEP_OPEN
2026-04-30 16:26:20 +02:00
Ashley Mensah
5bf17023fb feat(ci): sort by oldest updated and bump MAX_ISSUES to 100 2026-04-30 11:53:40 +02:00
Ashley Mensah
42e1f19689 fix(ci): remove unwritable fields, add Status single-select support
- Remove Linked PRs and Repository field writes (built-in, auto-populated)
- Add setSingleSelectField for Status field
- Set items to "Needs Review" when added to project board
- Clean up workflow env vars to only include writable fields
2026-04-30 11:34:14 +02:00
Ashley Mensah
8195aa43d5 fix(ci): skip linked_pull_requests field (not writable via API) 2026-04-29 18:36:36 +02:00
Ashley Mensah
6c4ce8df87 fix(ci): use number mutation for Confidence project field 2026-04-29 18:29:12 +02:00
Ashley Mensah
ca481b0ffc feat(ci): use PROJECT_PAT for project board, populate board in dry-run
- Add PROJECT_PAT secret for project board GraphQL calls, falling
  back to GITHUB_TOKEN if not set
- Dry-run now populates the project board with decisions (confidence,
  reason, evidence fields) while still skipping labels/comments/closing
- Extract addToProjectWithFields helper to reduce duplication
2026-04-29 17:45:46 +02:00
Ashley Mensah
6d118d9c99 feat(ci): trust LLM decisions and feed it PR merge status
- Remove pre_score override from enforcePolicy — policy now only gates
  AUTO_CLOSE, otherwise trusts the model's decision
- Pass pre_score evidence (hard signals, contradictions) to LLM as
  context instead of using it as a decision override
- Fetch linked PR merge status (MERGED/OPEN/CLOSED) in fetch step
  and include in LLM prompt so it can distinguish merged fixes from
  open proposals
2026-04-29 15:49:09 +02:00
Ashley Mensah
222d498bb6 fix(ci): distinguish workarounds from actual fixes in system prompt 2026-04-28 17:48:43 +02:00
Ashley Mensah
52cd104f1e chore(ci): switch back to gpt-4o-mini for higher quota 2026-04-28 17:44:05 +02:00
Ashley Mensah
92f666f652 fix(ci): cap retry-after and handle quota exhaustion gracefully 2026-04-28 17:42:43 +02:00
Ashley Mensah
4fc0cb7ec4 fix(ci): pace API requests to avoid rate limit thrashing 2026-04-28 17:30:18 +02:00
Ashley Mensah
695614834e fix(ci): fix policy logic and add message truncation
- enforcePolicy: respect KEEP_OPEN when model is confident and
  pre_score is low. Only promote to MANUAL_REVIEW when model suggests
  resolution or pre_score has hard signals
- Truncate user messages to 24k chars (issue body capped at 4k) to
  stay within GitHub Models 8000 token input limit
2026-04-28 16:38:36 +02:00
Ashley Mensah
d75fa6ad45 feat(ci): switch to gpt-4o and add rate limit retry
- Upgrade model from gpt-4o-mini to gpt-4o for better classification
- Add retry loop with backoff on 429 responses (up to 5 retries)
- Respect Retry-After header from GitHub Models API
2026-04-28 16:23:31 +02:00
Ashley Mensah
7ce7f322eb increase max number of dry run issues to 100 2026-04-28 16:13:38 +02:00
Ashley Mensah
22bcf70b6e fix(ci): add additionalProperties to nested schema objects 2026-04-28 16:11:57 +02:00
Ashley Mensah
fe8aa21245 feat(ci): wire up gpt-4o-mini for issue classification
- Replace stub callGitHubModel() with real GitHub Models API call
  using gpt-4o-mini with structured JSON output
- Build detailed user messages from issue body, comments, and timeline
- Add per-issue decision logging to classify step
- Upload candidates.json and decisions.json as workflow artifacts
2026-04-28 16:11:01 +02:00
Ashley Mensah
29f211e51c fix(ci): guard all decisions behind dry-run check
Move the dry-run check to the top of the loop so it applies to all
decision types, not just AUTO_CLOSE. In dry-run mode the workflow now
only logs what it would do without touching any issues.
2026-04-28 16:08:16 +02:00
Ashley Mensah
6df3580bd3 fix(ci): handle project API permission errors gracefully
GITHUB_TOKEN cannot access org-level Projects V2. Make addToProject
return null on failure instead of crashing, and skip setTextField
calls when project access is unavailable. A PAT with project scope
is needed for full project board integration.
2026-04-28 15:55:12 +02:00
Ashley Mensah
9ff735dd52 fix(ci): fix workflow by adding working-directory, fetch script, and removing npm ci
- Set defaults.run.working-directory to .github/issue-resolution so
  scripts resolve from the correct path
- Remove npm ci step (no npm dependencies needed)
- Add fetch-candidates.mjs to gather open issues with comments and
  timeline events via GitHub REST API
- Add minimal package.json with type: module
2026-04-28 15:53:17 +02:00
Ashley Mensah
7c43973bc9 fix typo in workflow 2026-04-28 15:48:35 +02:00
Ashley Mensah
01e53d07b9 fix typo in workflow 2026-04-28 15:46:57 +02:00
Ashley Mensah
797dce1631 dummy commit to test workflow 2026-04-28 15:43:37 +02:00
Ashley Mensah
2877fcbbf6 add push trigger to workflow 2026-04-28 15:42:50 +02:00
Ashley Mensah
5d8201fcd0 Merge branch 'main' into github-issue-resolver 2026-04-28 15:36:54 +02:00
Ashley Mensah
09595bd0c2 enable dry run, add project field id values 2026-04-28 15:36:21 +02:00
Zoltan Papp
8fc4265995 [relay] evict foreign client cache on disconnect (#6015)
* [relay] evict foreign client cache on disconnect

When a foreign relay's TCP connection drops, the manager's
onServerDisconnected handler only triggered reconnect logic for the
home server; the disconnected foreign entry stayed in the relayClients
cache. Subsequent OpenConn calls reused the closed client until the
60-second cleanup tick evicted it, breaking peer connectivity through
that relay for up to a minute.

Evict the foreign entry from the cache on disconnect so the next
OpenConn dials a fresh client.

Also:
- Make the reconnect backoff cap configurable via WithMaxBackoffInterval
  ManagerOption; the previous hard-coded 60s constant forced
  TestAutoReconnect to sleep ~61s. Test now polls Ready() and finishes
  in ~2s.
- Add NB_HOME_RELAY_SERVERS env var that overrides the relay URL list
  received from management, so a peer can be pinned to a specific home
  relay (used by the netbird-conn-lab Edge 4 reproducer).

* [client] treat empty NB_HOME_RELAY_SERVERS as unset

Returning (urls=[], ok=true) when the env var contained only separators or
whitespace caused callers to wipe the mgmt-provided relay list, leaving the
peer with no relays. Treat a parsed-empty result the same as an unset env.
2026-04-28 15:04:48 +02:00
Zoltan Papp
9c50819f20 Don't mark management disconnected on transient job stream errors (#6005)
The JOB stream is a separate channel from the SYNC stream. Server-side
EOF or other transient errors on the JOB stream do not indicate that
the management connection is unhealthy — the SYNC stream remains the
authoritative state signal.

Previously, a JOB stream EOF would call notifyDisconnected and the
client would emit OnConnecting to the UI. The backoff retry would
reconnect the JOB stream, but handleJobStream never calls notifyConnected
on success, so the UI was stuck on "Connecting" until the next SYNC
event or health check.

Keep notifyDisconnected for codes.PermissionDenied since IsLoginRequired
relies on managementError to detect expired auth.
2026-04-28 15:04:41 +02:00
Bethuel Mmbaga
6f0eff3ba0 [management] Handle single-string JWT group claim from IdPs (#6014) 2026-04-28 14:48:28 +03:00
Bethuel Mmbaga
f8745723fc [management] Add Microsoft AD FS support for embedded Dex identity providers (#6008) 2026-04-28 12:42:19 +03:00
Ashley Mensah
6b8e40f78d initial commit - workflow yaml, prompts and schemas 2026-04-23 18:48:38 +02:00
110 changed files with 933 additions and 18397 deletions

5
.github/issue-resolution/package.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "issue-resolution",
"private": true,
"type": "module"
}

View File

@@ -0,0 +1,41 @@
You are a GitHub issue resolution classifier.
Your job is to decide whether an open GitHub issue is:
- AUTO_CLOSE
- MANUAL_REVIEW
- KEEP_OPEN
Rules:
1. AUTO_CLOSE is only allowed if there is objective, hard evidence:
- a merged linked PR that clearly resolves the issue, or
- an explicit maintainer/member/owner/collaborator comment saying the issue is fixed, resolved, duplicate, or superseded
2. If there is any contradictory later evidence, do NOT AUTO_CLOSE.
3. If evidence is promising but not airtight, choose MANUAL_REVIEW.
4. If the issue still appears active or unresolved, choose KEEP_OPEN.
5. Do not invent evidence.
6. Output valid JSON only.
Maintainer-authoritative roles:
- MEMBER
- OWNER
- COLLABORATOR
Partial fixes and multi-item issues:
- A merged PR that only addresses SOME items in a multi-item request does NOT resolve the issue. If the issue lists 5 feature requests and a PR fixes 1, the issue is still open.
- If a PR description or comment says "partially addresses", "partial fix", or similar, the issue is NOT resolved. Classify as KEEP_OPEN.
- If a merged PR addresses the core ask but a later comment objects or reports a regression, classify as MANUAL_REVIEW (not resolved).
Workarounds vs. actual fixes:
- A WORKAROUND is when a user changes their own setup to avoid the problem (editing configs, using a different setting, manual SQL fixes, switching tools, scripts). Workarounds do NOT count as resolution — the underlying issue is still present in the product.
- An ACTUAL FIX is when a user reports the problem went away after upgrading to a specific version (e.g., "fixed after updating to v0.65.1") or after a specific PR was merged. This suggests the fix was shipped in the product itself.
- A maintainer pointing to an existing alternative feature is NOT the same as fixing the issue. If the reporter never confirmed the alternative works for them, classify as KEEP_OPEN.
- If only workarounds exist and no maintainer has confirmed a fix, classify as KEEP_OPEN.
- If a user reports an actual fix via a version upgrade but no maintainer confirmed it, classify as MANUAL_REVIEW (not AUTO_CLOSE).
Stale issues:
- An issue with no activity for over 12 months, where a maintainer offered an alternative or asked for more info and the reporter never responded, is a candidate for MANUAL_REVIEW — not necessarily KEEP_OPEN.
Important:
- Later comments outweigh earlier ones.
- A non-maintainer saying "fixed for me" is not enough for AUTO_CLOSE.
- If uncertain, prefer MANUAL_REVIEW or KEEP_OPEN.

View File

@@ -0,0 +1,80 @@
{
"type": "object",
"additionalProperties": false,
"required": [
"decision",
"reason_code",
"confidence",
"hard_signals",
"contradictions",
"summary",
"close_comment",
"manual_review_note"
],
"properties": {
"decision": {
"type": "string",
"enum": ["AUTO_CLOSE", "MANUAL_REVIEW", "KEEP_OPEN"]
},
"reason_code": {
"type": "string",
"enum": [
"resolved_by_merged_pr",
"maintainer_confirmed_resolved",
"duplicate_confirmed",
"superseded_confirmed",
"likely_fixed_but_unconfirmed",
"still_open",
"unclear"
]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"hard_signals": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["type", "url"],
"properties": {
"type": {
"type": "string",
"enum": [
"merged_pr",
"maintainer_comment",
"duplicate_reference",
"superseded_reference"
]
},
"url": { "type": "string" }
}
}
},
"contradictions": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["type", "url"],
"properties": {
"type": {
"type": "string",
"enum": [
"reporter_still_broken",
"later_unresolved_comment",
"ambiguous_pr_link",
"other"
]
},
"url": { "type": "string" }
}
}
},
"summary": { "type": "string" },
"close_comment": { "type": "string" },
"manual_review_note": { "type": "string" }
}
}

View File

@@ -0,0 +1,213 @@
import fs from "node:fs/promises";
const decisions = JSON.parse(await fs.readFile("decisions.json", "utf8"));
const dryRun = String(process.env.DRY_RUN).toLowerCase() === "true";
const ghHeaders = {
Authorization: `Bearer ${process.env.GH_TOKEN}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
// Use PROJECT_PAT for project board operations, fall back to GH_TOKEN
const projectHeaders = {
Authorization: `Bearer ${process.env.PROJECT_PAT || process.env.GH_TOKEN}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
async function rest(url, method = "GET", body) {
const res = await fetch(url, {
method,
headers: ghHeaders,
body: body ? JSON.stringify(body) : undefined
});
if (!res.ok) throw new Error(`${res.status} ${url}: ${await res.text()}`);
return res.status === 204 ? null : res.json();
}
async function graphql(query, variables) {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: projectHeaders,
body: JSON.stringify({ query, variables })
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const json = await res.json();
if (json.errors) throw new Error(JSON.stringify(json.errors));
return json.data;
}
async function addLabel(owner, repo, issueNumber, labels) {
return rest(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
"POST",
{ labels }
);
}
async function addComment(owner, repo, issueNumber, body) {
return rest(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
"POST",
{ body }
);
}
async function closeIssue(owner, repo, issueNumber) {
return rest(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
"PATCH",
{ state: "closed", state_reason: "completed" }
);
}
async function getIssueNodeId(owner, repo, issueNumber) {
const issue = await rest(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`);
return issue.node_id;
}
async function addToProject(issueNodeId) {
const mutation = `
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item { id }
}
}
`;
try {
const data = await graphql(mutation, {
projectId: process.env.PROJECT_ID,
contentId: issueNodeId
});
return data.addProjectV2ItemById.item.id;
} catch (err) {
console.warn(`[WARN] Could not add to project (needs PAT with project scope): ${err.message}`);
return null;
}
}
async function setTextField(itemId, fieldId, value) {
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: { text: $value }
}) {
projectV2Item { id }
}
}
`;
return graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId,
fieldId,
value
});
}
async function setNumberField(itemId, fieldId, value) {
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: Float!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: { number: $value }
}) {
projectV2Item { id }
}
}
`;
return graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId,
fieldId,
value
});
}
async function setSingleSelectField(itemId, fieldId, optionId) {
const mutation = `
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`;
return graphql(mutation, {
projectId: process.env.PROJECT_ID,
itemId,
fieldId,
optionId
});
}
async function addToProjectWithFields(owner, repo, d) {
const issueNodeId = await getIssueNodeId(owner, repo, d.issue_number);
const itemId = await addToProject(issueNodeId);
if (itemId) {
if (process.env.PROJECT_STATUS_FIELD_ID && process.env.PROJECT_STATUS_OPTION_NEEDS_REVIEW_ID) {
await setSingleSelectField(itemId, process.env.PROJECT_STATUS_FIELD_ID, process.env.PROJECT_STATUS_OPTION_NEEDS_REVIEW_ID);
}
if (process.env.PROJECT_CONFIDENCE_FIELD_ID) {
await setNumberField(itemId, process.env.PROJECT_CONFIDENCE_FIELD_ID, d.model.confidence);
}
if (process.env.PROJECT_REASON_FIELD_ID) {
await setTextField(itemId, process.env.PROJECT_REASON_FIELD_ID, d.model.reason_code);
}
if (process.env.PROJECT_EVIDENCE_FIELD_ID) {
await setTextField(itemId, process.env.PROJECT_EVIDENCE_FIELD_ID, d.issue_url);
}
console.log(` → Added to project board (Status: Needs Review)`);
}
}
for (const d of decisions) {
const [owner, repo] = d.repository.split("/");
if (d.final_decision === "KEEP_OPEN") {
console.log(`#${d.issue_number} → KEEP_OPEN (confidence: ${d.model.confidence}, reason: ${d.model.reason_code})`);
continue;
}
if (dryRun) {
console.log(`[DRY RUN] #${d.issue_number}${d.final_decision} (confidence: ${d.model.confidence}, reason: ${d.model.reason_code})`);
// In dry-run: populate project board but don't touch issues
if (d.final_decision === "MANUAL_REVIEW" || d.final_decision === "AUTO_CLOSE") {
await addToProjectWithFields(owner, repo, d);
}
continue;
}
if (d.final_decision === "AUTO_CLOSE") {
await addLabel(owner, repo, d.issue_number, ["auto-closed-resolved"]);
await addComment(owner, repo, d.issue_number, d.model.close_comment);
await closeIssue(owner, repo, d.issue_number);
await addToProjectWithFields(owner, repo, d);
}
if (d.final_decision === "MANUAL_REVIEW") {
await addLabel(owner, repo, d.issue_number, ["resolution-candidate"]);
await addToProjectWithFields(owner, repo, d);
await addComment(
owner,
repo,
d.issue_number,
d.model.manual_review_note ||
"This issue looks like a possible resolution candidate, but not with enough certainty for automatic closure. Added to the review queue."
);
}
}

View File

@@ -0,0 +1,259 @@
import fs from "node:fs/promises";
const candidates = JSON.parse(await fs.readFile("candidates.json", "utf8"));
const systemPrompt = await fs.readFile("prompts/issue-resolution-system.txt", "utf8");
const outputSchema = JSON.parse(await fs.readFile("schemas/issue-resolution-output.json", "utf8"));
function isMaintainerRole(role) {
return ["MEMBER", "OWNER", "COLLABORATOR"].includes(role || "");
}
function preScore(candidate) {
let score = 0;
const hardSignals = [];
const contradictions = [];
for (const t of candidate.timeline) {
const sourceIssue = t.source?.issue;
if (t.event === "cross-referenced" && sourceIssue?.pull_request?.html_url) {
hardSignals.push({
type: "merged_pr",
url: sourceIssue.html_url
});
score += 40; // provisional until PR merged state is verified
}
if (["referenced", "connected"].includes(t.event)) {
score += 10;
}
}
for (const c of candidate.comments) {
const body = c.body.toLowerCase();
if (
isMaintainerRole(c.author_association) &&
/\b(fixed|resolved|duplicate|superseded|closing)\b/.test(body)
) {
score += 25;
hardSignals.push({
type: "maintainer_comment",
url: c.html_url
});
}
if (/\b(still broken|still happening|not fixed|reproducible)\b/.test(body)) {
score -= 50;
contradictions.push({
type: "later_unresolved_comment",
url: c.html_url
});
}
}
return { score, hardSignals, contradictions };
}
// GitHub Models gpt-4o has an 8000 token input limit.
// Reserve ~2000 tokens for system prompt + response overhead.
// 1 token ~= 4 chars, so cap user message at ~24000 chars.
const MAX_USER_MESSAGE_CHARS = 24000;
function truncate(text, maxChars) {
if (text.length <= maxChars) return text;
return text.slice(0, maxChars) + "\n\n[... truncated due to length]";
}
function buildUserMessage(candidate, pre) {
const { issue, comments, timeline } = candidate;
const commentBlock = comments
.map((c) => `[${c.author_association}] ${c.user} (${c.created_at}):\n${c.body}`)
.join("\n---\n");
const timelineBlock = timeline
.filter((t) => ["cross-referenced", "referenced", "connected", "closed", "reopened"].includes(t.event))
.map((t) => {
let line = `${t.event} (${t.created_at})`;
if (t.source?.issue?.html_url) line += `${t.source.issue.html_url}`;
if (t.source?.issue?.pull_request?.html_url) line += ` (PR: ${t.source.issue.pull_request.html_url})`;
return line;
})
.join("\n");
const sections = [
`## Issue #${issue.number}: ${issue.title}`,
`URL: ${issue.html_url}`,
`Created: ${issue.created_at} | Updated: ${issue.updated_at}`,
`Labels: ${issue.labels.join(", ") || "none"}`,
"",
"### Body",
truncate(issue.body || "(empty)", 4000),
"",
"### Comments",
commentBlock || "(none)",
"",
"### Timeline events",
timelineBlock || "(none)",
];
if (candidate.linked_prs?.length) {
sections.push("");
sections.push("### Linked PRs (verified state)");
for (const pr of candidate.linked_prs) {
const status = pr.merged ? `MERGED (${pr.merged_at})` : pr.state.toUpperCase();
sections.push(`- PR #${pr.number}: ${pr.title}${status}${pr.url}`);
}
}
if (pre.hardSignals.length || pre.contradictions.length) {
sections.push("");
sections.push("### Automated evidence scan");
for (const s of pre.hardSignals) {
sections.push(`- SIGNAL: ${s.type}${s.url}`);
}
for (const c of pre.contradictions) {
sections.push(`- CONTRADICTION: ${c.type}${c.url}`);
}
}
return truncate(sections.join("\n"), MAX_USER_MESSAGE_CHARS);
}
const MODEL = "gpt-4o-mini";
const MAX_RETRIES = 5;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function callGitHubModel(candidate, pre) {
const body = JSON.stringify({
model: MODEL,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: buildUserMessage(candidate, pre) },
],
response_format: {
type: "json_schema",
json_schema: {
name: "issue_resolution",
strict: true,
schema: outputSchema,
},
},
temperature: 0.1,
});
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
const res = await fetch("https://models.inference.ai.azure.com/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GH_TOKEN}`,
"Content-Type": "application/json",
},
body,
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("retry-after")) || 30;
if (retryAfter > 120) {
console.warn(` [QUOTA EXHAUSTED] API wants ${retryAfter}s wait — skipping remaining issues.`);
return null;
}
console.warn(` [RATE LIMITED] Waiting ${retryAfter}s (attempt ${attempt + 1}/${MAX_RETRIES})...`);
await sleep(retryAfter * 1000);
continue;
}
if (!res.ok) {
const text = await res.text();
throw new Error(`GitHub Models ${res.status}: ${text}`);
}
const data = await res.json();
return JSON.parse(data.choices[0].message.content);
}
throw new Error(`GitHub Models: exceeded ${MAX_RETRIES} retries due to rate limiting`);
}
function enforcePolicy(modelOut, pre) {
const approvedReasons = new Set([
"resolved_by_merged_pr",
"maintainer_confirmed_resolved",
"duplicate_confirmed",
"superseded_confirmed"
]);
const hasHardSignal =
(modelOut.hard_signals || []).some(s =>
["merged_pr", "maintainer_comment", "duplicate_reference", "superseded_reference"].includes(s.type)
) || pre.hardSignals.length > 0;
const hasContradiction =
(modelOut.contradictions || []).length > 0 || pre.contradictions.length > 0;
// Only auto-close with very strict criteria
if (
modelOut.decision === "AUTO_CLOSE" &&
modelOut.confidence >= 0.97 &&
approvedReasons.has(modelOut.reason_code) &&
hasHardSignal &&
!hasContradiction
) {
return "AUTO_CLOSE";
}
// Downgrade AUTO_CLOSE that didn't pass the gate
if (modelOut.decision === "AUTO_CLOSE") {
return "MANUAL_REVIEW";
}
// Otherwise trust the model
return modelOut.decision;
}
console.log(`Classifying ${candidates.length} candidates with ${MODEL}...\n`);
// 15 req/min limit → 1 request every 4s. Use 4.5s for safety margin.
const PACE_MS = 4500;
let lastRequestTime = 0;
async function paced(fn) {
const elapsed = Date.now() - lastRequestTime;
if (elapsed < PACE_MS) await sleep(PACE_MS - elapsed);
lastRequestTime = Date.now();
return fn();
}
const decisions = [];
for (const candidate of candidates) {
const pre = preScore(candidate);
const modelOut = await paced(() => callGitHubModel(candidate, pre));
if (modelOut === null) {
console.warn(`\nQuota exhausted after ${decisions.length} issues. Writing partial results.`);
break;
}
const finalDecision = enforcePolicy(modelOut, pre);
decisions.push({
repository: candidate.repository,
issue_number: candidate.issue.number,
issue_url: candidate.issue.html_url,
title: candidate.issue.title,
pre_score: pre.score,
final_decision: finalDecision,
model: modelOut
});
console.log(
`#${candidate.issue.number} | pre_score: ${pre.score} | model: ${modelOut.decision} @ ${modelOut.confidence} | final: ${finalDecision} | ${modelOut.reason_code}`
);
}
await fs.writeFile("decisions.json", JSON.stringify(decisions, null, 2));
console.log(`\nWrote ${decisions.length} decisions to decisions.json`);

View File

@@ -0,0 +1,123 @@
import fs from "node:fs/promises";
const token = process.env.GH_TOKEN;
const repo = process.env.REPO; // "owner/repo"
const maxIssues = Number(process.env.MAX_ISSUES) || 100;
const headers = {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
async function rest(url) {
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`${res.status} ${url}: ${await res.text()}`);
return res.json();
}
async function restSafe(url) {
const res = await fetch(url, { headers });
if (!res.ok) return null;
return res.json();
}
async function paginate(url, max) {
const items = [];
let page = 1;
while (items.length < max) {
const perPage = Math.min(100, max - items.length);
const sep = url.includes("?") ? "&" : "?";
const batch = await rest(`${url}${sep}per_page=${perPage}&page=${page}`);
if (!batch.length) break;
items.push(...batch);
page++;
}
return items.slice(0, max);
}
console.log(`Fetching up to ${maxIssues} open issues from ${repo}...`);
const issues = await paginate(
`https://api.github.com/repos/${repo}/issues?state=open&sort=updated&direction=asc`,
maxIssues
);
// Filter out pull requests (GitHub API returns PRs as issues too)
const realIssues = issues.filter((i) => !i.pull_request);
console.log(`Found ${realIssues.length} open issues (excluded PRs).`);
const candidates = [];
for (const issue of realIssues) {
const [comments, timeline] = await Promise.all([
rest(`https://api.github.com/repos/${repo}/issues/${issue.number}/comments?per_page=100`),
rest(`https://api.github.com/repos/${repo}/issues/${issue.number}/timeline?per_page=100`),
]);
candidates.push({
repository: repo,
issue: {
number: issue.number,
html_url: issue.html_url,
title: issue.title,
body: issue.body,
created_at: issue.created_at,
updated_at: issue.updated_at,
labels: issue.labels.map((l) => l.name),
},
comments: comments.map((c) => ({
body: c.body,
author_association: c.author_association,
html_url: c.html_url,
created_at: c.created_at,
user: c.user?.login,
})),
timeline: timeline.map((t) => ({
event: t.event,
created_at: t.created_at,
source: t.source
? {
issue: {
html_url: t.source.issue?.html_url,
pull_request: t.source.issue?.pull_request
? { html_url: t.source.issue.pull_request.html_url }
: undefined,
},
}
: undefined,
})),
linked_prs: [],
});
// Fetch merge status for cross-referenced PRs
const prUrls = new Set();
for (const t of timeline) {
const prHtml = t.source?.issue?.pull_request?.html_url;
if (t.event === "cross-referenced" && prHtml) {
prUrls.add(prHtml);
}
}
const candidate = candidates[candidates.length - 1];
for (const prHtml of prUrls) {
// Extract owner/repo and PR number from URL like https://github.com/owner/repo/pull/123
const match = prHtml.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
if (!match) continue;
const [, prRepo, prNum] = match;
const pr = await restSafe(`https://api.github.com/repos/${prRepo}/pulls/${prNum}`);
if (!pr) continue;
candidate.linked_prs.push({
number: pr.number,
title: pr.title,
url: prHtml,
state: pr.state,
merged: pr.merged || false,
merged_at: pr.merged_at,
});
}
console.log(` #${issue.number}${comments.length} comments, ${timeline.length} timeline events, ${candidate.linked_prs.length} linked PRs`);
}
await fs.writeFile("candidates.json", JSON.stringify(candidates, null, 2));
console.log(`Wrote ${candidates.length} candidates to candidates.json`);

View File

@@ -0,0 +1,63 @@
name: issue-resolution-triage
on:
push:
branches: [github-issue-resolver]
workflow_dispatch:
inputs:
dry_run:
description: "If true, do not close issues"
required: false
default: "true"
max_issues:
description: "How many issues to process"
required: false
default: "100"
schedule:
- cron: "17 2 * * *"
permissions:
contents: read
issues: write
pull-requests: read
models: read
# todo: remove hardcoded values
jobs:
triage:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
DRY_RUN: "true"
MAX_ISSUES: "100"
REPO: ${{ github.repository }}
PROJECT_ID: "PVT_kwDOBfz4Jc4BVeWR"
PROJECT_STATUS_FIELD_ID: "PVTSSF_lADOBfz4Jc4BVeWRzhQ56sU"
PROJECT_STATUS_OPTION_NEEDS_REVIEW_ID: "a55a2be9"
PROJECT_CONFIDENCE_FIELD_ID: "PVTF_lADOBfz4Jc4BVeWRzhQ57x4"
PROJECT_REASON_FIELD_ID: "PVTF_lADOBfz4Jc4BVeWRzhQ5-Lg"
PROJECT_EVIDENCE_FIELD_ID: "PVTF_lADOBfz4Jc4BVeWRzhQ5-Pw"
defaults:
run:
working-directory: .github/issue-resolution
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: node scripts/fetch-candidates.mjs
- run: node scripts/classify-candidates.mjs
- run: node scripts/apply-decisions.mjs
- uses: actions/upload-artifact@v4
if: always()
with:
name: triage-results
path: |
.github/issue-resolution/candidates.json
.github/issue-resolution/decisions.json

View File

@@ -1,6 +0,0 @@
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
build/
coverage/

View File

@@ -1,36 +0,0 @@
# 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'

View File

@@ -1,115 +0,0 @@
# 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.

View File

@@ -1,54 +0,0 @@
# 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

View File

@@ -1,10 +0,0 @@
include: package:lints/recommended.yaml
analyzer:
exclude:
- lib/src/generated/**
linter:
rules:
avoid_print: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,53 +0,0 @@
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';
}

View File

@@ -1,889 +0,0 @@
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,
);
}
}

View File

@@ -1,916 +0,0 @@
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();
}

View File

@@ -1,460 +0,0 @@
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);
}
}

View File

@@ -1,434 +0,0 @@
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',
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +0,0 @@
// 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');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +0,0 @@
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,
);
}
}

View File

@@ -1,22 +0,0 @@
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;
}
}

View File

@@ -1,140 +0,0 @@
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'),
),
],
),
);
}
}

View File

@@ -1 +0,0 @@
flutter/ephemeral

View File

@@ -1,128 +0,0 @@
# 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()

View File

@@ -1,88 +0,0 @@
# 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}
)

View File

@@ -1,27 +0,0 @@
//
// 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);
}

View File

@@ -1,15 +0,0 @@
//
// 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_

View File

@@ -1,27 +0,0 @@
#
# 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)

View File

@@ -1,26 +0,0 @@
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}")

View File

@@ -1,6 +0,0 @@
#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);
}

View File

@@ -1,148 +0,0 @@
#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));
}

View File

@@ -1,21 +0,0 @@
#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_

View File

@@ -1,7 +0,0 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -1,2 +0,0 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1,2 +0,0 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -1,18 +0,0 @@
//
// 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"))
}

View File

@@ -1,42 +0,0 @@
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

View File

@@ -1,40 +0,0 @@
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

View File

@@ -1,801 +0,0 @@
// !$*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 */;
}

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,99 +0,0 @@
<?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>

View File

@@ -1,10 +0,0 @@
<?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>

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,13 +0,0 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View File

@@ -1,68 +0,0 @@
{
"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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,343 +0,0 @@
<?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>

View File

@@ -1,14 +0,0 @@
// 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.

View File

@@ -1,2 +0,0 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -1,2 +0,0 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -1,13 +0,0 @@
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

View File

@@ -1,12 +0,0 @@
<?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>

View File

@@ -1,32 +0,0 @@
<?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>

View File

@@ -1,15 +0,0 @@
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()
}
}

View File

@@ -1,10 +0,0 @@
<?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>

View File

@@ -1,12 +0,0 @@
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.
}
}

View File

@@ -1,413 +0,0 @@
# 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"

View File

@@ -1,28 +0,0 @@
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/

View File

@@ -1,19 +0,0 @@
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);
});
}

View File

@@ -1,36 +0,0 @@
#!/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

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
repo_dir="$(cd "$project_dir/../.." && pwd)"
command -v protoc >/dev/null 2>&1 || {
echo "protoc is not installed"
exit 1
}
command -v dart >/dev/null 2>&1 || {
echo "dart is not installed"
exit 1
}
export PATH="$PATH:$HOME/.pub-cache/bin"
if ! command -v protoc-gen-dart >/dev/null 2>&1; then
dart pub global activate protoc_plugin
fi
mkdir -p "$project_dir/lib/src/generated"
protoc \
-I "$repo_dir/client/proto" \
--dart_out=grpc:"$project_dir/lib/src/generated" \
"$repo_dir/client/proto/daemon.proto"

View File

@@ -1,17 +0,0 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

View File

@@ -1,108 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.14)
project(netbird_flutter_ui 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")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(VERSION 3.14...3.25)
# Define build configuration option.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
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()
endif()
# Define settings for the Profile build mode.
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# 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_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
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)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
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.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)

View File

@@ -1,109 +0,0 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.14)
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.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# 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/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app 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.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View File

@@ -1,23 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <tray_manager/tray_manager_plugin.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
LocalNotifierPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalNotifierPlugin"));
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View File

@@ -1,15 +0,0 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@@ -1,27 +0,0 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
local_notifier
screen_retriever_windows
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}/windows 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}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View File

@@ -1,40 +0,0 @@
cmake_minimum_required(VERSION 3.14)
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} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
# 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 build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific
# dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)

View File

@@ -1,121 +0,0 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else
#define VERSION_AS_NUMBER 1,0,0,0
#endif
#if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING FLUTTER_VERSION
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "io.netbird" "\0"
VALUE "FileDescription", "netbird_flutter_ui" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "netbird_flutter_ui" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 io.netbird. All rights reserved." "\0"
VALUE "OriginalFilename", "netbird_flutter_ui.exe" "\0"
VALUE "ProductName", "netbird_flutter_ui" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -1,71 +0,0 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}

View File

@@ -1,33 +0,0 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View File

@@ -1,43 +0,0 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"netbird_flutter_ui", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}

View File

@@ -1,16 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@@ -1,65 +0,0 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
unsigned int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr)
-1; // remove the trailing null character
int input_length = (int)wcslen(utf16_string);
std::string utf8_string;
if (target_length == 0 || target_length > utf8_string.max_size()) {
return utf8_string;
}
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
input_length, utf8_string.data(), target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}

View File

@@ -1,19 +0,0 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View File

@@ -1,288 +0,0 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#include "resource.h"
namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
}
FreeLibrary(user32_module);
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registrar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::Create(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
UpdateTheme(window);
return OnCreate();
}
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}

View File

@@ -1,102 +0,0 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size this function will scale the inputted width and height as
// as appropriate for the default monitor. The window is invisible until
// |Show| is called. Returns true if the window was created successfully.
bool Create(const std::wstring& title, const Point& origin, const Size& size);
// Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_

View File

@@ -333,6 +333,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
c.statusRecorder.MarkSignalConnected()
relayURLs, token := parseRelayInfo(loginResp)
if override, ok := peer.OverrideRelayURLs(); ok {
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
relayURLs = override
}
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)

View File

@@ -944,7 +944,12 @@ func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
return fmt.Errorf("update relay token: %w", err)
}
e.relayManager.UpdateServerURLs(update.Urls)
urls := update.Urls
if override, ok := peer.OverrideRelayURLs(); ok {
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
urls = override
}
e.relayManager.UpdateServerURLs(urls)
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
// We can ignore all errors because the guard will manage the reconnection retries.

View File

@@ -7,7 +7,8 @@ import (
)
const (
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
EnvKeyNBHomeRelayServers = "NB_HOME_RELAY_SERVERS"
)
func IsForceRelayed() bool {
@@ -16,3 +17,28 @@ func IsForceRelayed() bool {
}
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
}
// OverrideRelayURLs returns the relay server URL list set in
// NB_HOME_RELAY_SERVERS (comma-separated) and a boolean indicating whether
// the override is active. When the env var is unset, the boolean is false
// and the caller should keep the list received from the management server.
// Intended for lab/debug scenarios where a peer must pin to a specific home
// relay regardless of what management offers.
func OverrideRelayURLs() ([]string, bool) {
raw := os.Getenv(EnvKeyNBHomeRelayServers)
if raw == "" {
return nil, false
}
parts := strings.Split(raw, ",")
urls := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
urls = append(urls, p)
}
}
if len(urls) == 0 {
return nil, false
}
return urls, true
}

View File

@@ -193,7 +193,7 @@ func (c *Connector) ToStorageConnector() (storage.Connector, error) {
// are stored with types that Dex can open.
func mapConnectorToDex(connType string, config map[string]interface{}) (string, map[string]interface{}) {
switch connType {
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak", "adfs":
return "oidc", applyOIDCDefaults(connType, config)
default:
return connType, config
@@ -218,6 +218,8 @@ func applyOIDCDefaults(connType string, config map[string]interface{}) map[strin
setDefault(augmented, "claimMapping", map[string]string{"email": "preferred_username"})
case "okta", "pocketid":
augmented["scopes"] = []string{"openid", "profile", "email", "groups"}
case "adfs":
augmented["scopes"] = []string{"openid", "profile", "email", "allatclaims"}
}
return augmented

View File

@@ -168,7 +168,7 @@ func (p *Provider) buildStorageConnector(cfg *ConnectorConfig) (storage.Connecto
var err error
switch cfg.Type {
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak":
case "oidc", "zitadel", "entra", "okta", "pocketid", "authentik", "keycloak", "adfs":
dexType = "oidc"
configData, err = buildOIDCConnectorConfig(cfg, redirectURI)
case "google":
@@ -220,6 +220,8 @@ func buildOIDCConnectorConfig(cfg *ConnectorConfig, redirectURI string) ([]byte,
oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
case "pocketid":
oidcConfig["scopes"] = []string{"openid", "profile", "email", "groups"}
case "adfs":
oidcConfig["scopes"] = []string{"openid", "profile", "email", "allatclaims"}
}
return encodeConnectorConfig(oidcConfig)
}
@@ -283,7 +285,7 @@ func inferIdentityProviderType(dexType, connectorID string, _ map[string]interfa
// inferOIDCProviderType infers the specific OIDC provider from connector ID
func inferOIDCProviderType(connectorID string) string {
connectorIDLower := strings.ToLower(connectorID)
for _, provider := range []string{"pocketid", "zitadel", "entra", "okta", "authentik", "keycloak"} {
for _, provider := range []string{"pocketid", "zitadel", "entra", "okta", "authentik", "keycloak", "adfs"} {
if strings.Contains(connectorIDLower, provider) {
return provider
}

Some files were not shown because too many files have changed in this diff Show More