diff --git a/.gitignore b/.gitignore
index 03a3a24..736c170 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.DS_Store
cmd/update-release/venv*
-*.xcodeproj/xcuserdata
-*.xcodeproj/project.xcworkspace
+*.xcodeproj/xcuserdata*
+*.xcworkspace/xcuserdata
+*.xcodeproj/*.xcworkspace*
diff --git a/AUTHORS.md b/AUTHORS.md
index 222af70..1ab0979 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,3 +1,4 @@
* Jerry Jacobs ([@xor-gate](https://github.com/xor-gate))
* Victor Babenko ([@virusman](https://github.com/virusman))
* Jakob Borg ([@calmh](https://github.com/calmh))
+* Tommy van der Vorst ([@pixelspark](https://github.com/pixelspark))
diff --git a/Pods/Pods.xcodeproj/xcuserdata/jerry.xcuserdatad/xcschemes/xcschememanagement.plist b/Pods/Pods.xcodeproj/xcuserdata/jerry.xcuserdatad/xcschemes/xcschememanagement.plist
index 09d188d..a76d68e 100644
--- a/Pods/Pods.xcodeproj/xcuserdata/jerry.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Pods/Pods.xcodeproj/xcuserdata/jerry.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -9,14 +9,14 @@
isShown
orderHint
- 1
+ 0
Sparkle.xcscheme
isShown
orderHint
- 2
+ 1
SuppressBuildableAutocreation
diff --git a/cmd/update-release/update-release.py b/cmd/update-release/update-release.py
index cdd89b7..46ee0de 100755
--- a/cmd/update-release/update-release.py
+++ b/cmd/update-release/update-release.py
@@ -31,12 +31,73 @@
if 'tag_name' not in data:
raise ValueError("tag_name not present in latest_url")
+import urllib.request
+import json
+import semver
+
+def get_latest_v1_tag_name(repo_owner, repo_name, allow_prerelease: bool = False):
+ """
+ Fetches the latest v1 release tag_name from a GitHub repository's releases.
+
+ Args:
+ repo_owner (str): The owner of the GitHub repository (e.g., 'syncthing').
+ repo_name (str): The name of the GitHub repository (e.g., 'syncthing').
+
+ Returns:
+ str or None: The tag_name of the latest v1 release, or None if not found.
+ """
+ url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases"
+ try:
+ with urllib.request.urlopen(url) as response:
+ if response.getcode() == 200:
+ data = json.loads(response.read().decode('utf-8'))
+ else:
+ print(f"Error fetching data: HTTP {response.getcode()}")
+ return None
+ except urllib.error.URLError as e:
+ print(f"Error connecting to GitHub API: {e.reason}")
+ return None
+ except json.JSONDecodeError:
+ print("Error decoding JSON response.")
+ return None
+
+ v1_releases = []
+ for release in data:
+ tag_name = release.get('tag_name')
+ prerelease = release.get('prerelease')
+
+ if tag_name:
+ try:
+ version = semver.Version.parse(tag_name.lstrip('v')) # Remove 'v' prefix if present
+ if allow_prerelease and version.major == 1 and version.prerelease:
+ v1_releases.append(version)
+ elif version.major == 1:
+ v1_releases.append(version)
+ except ValueError:
+ # Not a valid semver string, skip
+ continue
+
+ if not v1_releases:
+ return None
+
+ # Sort the prereleases to find the latest
+ latest_v1_release = max(v1_releases)
+ return f"v{latest_v1_release}" # Re-add the 'v' prefix for consistency
+
###
# Parse the tag version and generate CFBundleShortVersionString and CFBundleVersion
###
+owner = "syncthing"
+repo = "syncthing"
+latest_tag = get_latest_v1_tag_name(owner, repo)
+
+if latest_tag:
+ print(f"The latest v1 release tag_name for {owner}/{repo} is: {latest_tag}")
+else:
+ print(f"No v1 release found for {owner}/{repo}.")
# Ugly hack because of https://github.com/python-semver/python-semver/issues/137
-tag_name = data['tag_name'].replace('v', '')
+tag_name = latest_tag.replace('v', '')
version = semver.VersionInfo.parse(tag_name)
CFBundleShortVersionString = "{}-{:d}".format(
diff --git a/syncthing.xcodeproj/project.pbxproj b/syncthing.xcodeproj/project.pbxproj
index 578ae51..4a2f904 100644
--- a/syncthing.xcodeproj/project.pbxproj
+++ b/syncthing.xcodeproj/project.pbxproj
@@ -35,6 +35,8 @@
C4A415681D0D579D00DC6018 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C4A415671D0D579D00DC6018 /* main.m */; };
C4A4156A1D0D579D00DC6018 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4A415691D0D579D00DC6018 /* Assets.xcassets */; };
C4A4156D1D0D579D00DC6018 /* STApplication.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4A4156B1D0D579D00DC6018 /* STApplication.xib */; };
+ C4D434FE2E5CFF3600B91C7F /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D434FD2E5CFF3600B91C7F /* OnboardingView.swift */; };
+ C4D435022E5D067C00B91C7F /* OnboardingViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D435012E5D067600B91C7F /* OnboardingViewFactory.swift */; };
C4F0E82E1DA1B9CF00435310 /* STPreferencesWindowGeneralViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C4F0E82C1DA1B9CF00435310 /* STPreferencesWindowGeneralViewController.m */; };
C4F0E82F1DA1B9CF00435310 /* STPreferencesWindowGeneralView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4F0E82D1DA1B9CF00435310 /* STPreferencesWindowGeneralView.xib */; };
C4FFB0661D0D7F870015D14A /* XGSyncthing.m in Sources */ = {isa = PBXBuildFile; fileRef = C4FFB0641D0D7E4C0015D14A /* XGSyncthing.m */; };
@@ -92,6 +94,8 @@
C4A415691D0D579D00DC6018 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
C4A4156C1D0D579D00DC6018 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/STApplication.xib; sourceTree = ""; };
C4A4156E1D0D579D00DC6018 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ C4D434FD2E5CFF3600B91C7F /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = OnboardingView.swift; path = UI/OnboardingView.swift; sourceTree = ""; };
+ C4D435012E5D067600B91C7F /* OnboardingViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewFactory.swift; sourceTree = ""; };
C4D6DD581D0D93D80024D20A /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
C4D896121D0DF90900D42F73 /* syncthing-resource.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = "syncthing-resource.sh"; path = "syncthing/Scripts/syncthing-resource.sh"; sourceTree = ""; };
C4F0E82B1DA1B9CF00435310 /* STPreferencesWindowGeneralViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPreferencesWindowGeneralViewController.h; sourceTree = ""; };
@@ -228,6 +232,8 @@
C4D6DD601D0DB18A0024D20A /* UI */ = {
isa = PBXGroup;
children = (
+ C4D435012E5D067600B91C7F /* OnboardingViewFactory.swift */,
+ C4D434FD2E5CFF3600B91C7F /* OnboardingView.swift */,
C4A4156B1D0D579D00DC6018 /* STApplication.xib */,
C4946B001D5877F2008447A2 /* STAboutWindow.xib */,
C4460A821D0DD38F00200C21 /* STPreferencesWindow.xib */,
@@ -395,6 +401,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ C4D435022E5D067C00B91C7F /* OnboardingViewFactory.swift in Sources */,
29AF1BA3210F11BF004212DE /* DaemonProcess.swift in Sources */,
C4FFB0661D0D7F870015D14A /* XGSyncthing.m in Sources */,
298A5C45210DA6C40034B89F /* LocalhostTLSDelegate.m in Sources */,
@@ -405,6 +412,7 @@
C4460A801D0DD2D500200C21 /* STPreferencesWindowController.m in Sources */,
C4F0E82E1DA1B9CF00435310 /* STPreferencesWindowGeneralViewController.m in Sources */,
C4A415651D0D579D00DC6018 /* STApplication.m in Sources */,
+ C4D434FE2E5CFF3600B91C7F /* OnboardingView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -574,7 +582,7 @@
);
INFOPLIST_FILE = syncthing/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 10.13;
+ MACOSX_DEPLOYMENT_TARGET = 12.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.github.xor-gate.syncthing-macosx";
PRODUCT_NAME = Syncthing;
PROVISIONING_PROFILE = "";
@@ -605,7 +613,7 @@
);
INFOPLIST_FILE = syncthing/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 10.13;
+ MACOSX_DEPLOYMENT_TARGET = 12.4;
PRODUCT_BUNDLE_IDENTIFIER = "com.github.xor-gate.syncthing-macosx";
PRODUCT_NAME = Syncthing;
PROVISIONING_PROFILE = "";
diff --git a/syncthing.xcworkspace/xcuserdata/jerry.xcuserdatad/UserInterfaceState.xcuserstate b/syncthing.xcworkspace/xcuserdata/jerry.xcuserdatad/UserInterfaceState.xcuserstate
index 949181c..4dea90c 100644
Binary files a/syncthing.xcworkspace/xcuserdata/jerry.xcuserdatad/UserInterfaceState.xcuserstate and b/syncthing.xcworkspace/xcuserdata/jerry.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/syncthing/Base.lproj/STApplication.xib b/syncthing/Base.lproj/STApplication.xib
index c7c87a3..92c39c3 100644
--- a/syncthing/Base.lproj/STApplication.xib
+++ b/syncthing/Base.lproj/STApplication.xib
@@ -1,8 +1,8 @@
-
+
-
+
diff --git a/syncthing/Info.plist b/syncthing/Info.plist
index f854fd7..9baaf24 100644
--- a/syncthing/Info.plist
+++ b/syncthing/Info.plist
@@ -19,9 +19,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.29.7-1
+ 1.30.0-1
CFBundleVersion
- 102900701
+ 103000001
LSApplicationCategoryType
public.app-category.utilities
LSMinimumSystemVersion
diff --git a/syncthing/OnboardingViewFactory.swift b/syncthing/OnboardingViewFactory.swift
new file mode 100644
index 0000000..1b83580
--- /dev/null
+++ b/syncthing/OnboardingViewFactory.swift
@@ -0,0 +1,26 @@
+//
+// OnboardingViewFactory.swift
+// syncthing
+//
+// Created by Jerry Jacobs on 25/08/2025.
+// Copyright © 2025 syncthing-macos authors. All rights reserved.
+//
+import SwiftUI
+import AppKit
+
+// Use @objcMembers to expose this class and its methods to Objective-C.
+@objcMembers
+public final class OnboardingViewFactory: NSObject {
+
+ // This factory method returns an NSViewController that can be used in AppKit.
+ public static func makeOnboardingViewController() -> NSViewController {
+ // 1. Instantiate your SwiftUI view.
+ let onboardingView = OnboardingView()
+
+ // 2. Wrap it in an NSHostingController.
+ let hostingController = NSHostingController(rootView: onboardingView)
+
+ // 3. Return it as a standard NSViewController.
+ return hostingController
+ }
+}
diff --git a/syncthing/STApplication.m b/syncthing/STApplication.m
index ef22088..84095ff 100644
--- a/syncthing/STApplication.m
+++ b/syncthing/STApplication.m
@@ -19,6 +19,7 @@ @interface STAppDelegate ()
@property (weak) IBOutlet NSMenuItem *daemonRestartMenuItem;
@property (strong) STPreferencesWindowController *preferencesWindow;
@property (strong) STAboutWindowController *aboutWindow;
+@property (strong) NSWindowController *myWindowController;
@property (nonatomic, assign) BOOL devicesPaused;
@property (nonatomic, assign) BOOL daemonOK;
@property (nonatomic, assign) BOOL connectionOK;
@@ -29,9 +30,11 @@ @implementation STAppDelegate
- (void) applicationDidFinishLaunching:(NSNotification *)aNotification {
_syncthing = [[XGSyncthing alloc] init];
-
+
[self applicationLoadConfiguration];
+ [self showOnboardingView];
+
_process = [[DaemonProcess alloc] initWithPath:_executable arguments: _arguments delegate:self];
[_process launch];
@@ -46,6 +49,34 @@ - (void) clickedFolder:(id)sender {
[[NSWorkspace sharedWorkspace] selectFile:path inFileViewerRootedAtPath:@""];
}
+- (void)showOnboardingView {
+ // 1. Call the Swift factory method to get the NSViewController.
+ NSViewController *onboardingViewController = [OnboardingViewFactory makeOnboardingViewController];
+
+ // 2. Create a new window instance.
+ NSRect frame = NSMakeRect(0, 0, 500, 500);
+ NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
+ NSWindow *newWindow = [[NSWindow alloc] initWithContentRect:frame
+ styleMask:style
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ [newWindow center];
+ [newWindow setLevel:NSFloatingWindowLevel];
+
+ // 3. Set your view controller as the window's content view controller.
+ // This is the key step that connects them.
+ newWindow.contentViewController = onboardingViewController;
+
+ // 4. Create a window controller to manage the window.
+ self.myWindowController = [[NSWindowController alloc] initWithWindow:newWindow];
+
+ // 5. Show the window.
+ [self.myWindowController showWindow:nil];
+
+ NSLog(@"Frame %@", NSStringFromRect(frame));
+ NSLog(@"Frame %@", NSStringFromRect(newWindow.frame));
+}
+
- (void) applicationWillTerminate:(NSNotification *)aNotification {
[_process terminate];
}
diff --git a/syncthing/Scripts/syncthing-resource.sh b/syncthing/Scripts/syncthing-resource.sh
index b7bccb5..733d657 100755
--- a/syncthing/Scripts/syncthing-resource.sh
+++ b/syncthing/Scripts/syncthing-resource.sh
@@ -2,7 +2,7 @@
set -euo pipefail
# Download and unpack syncthing into ${PRODUCT_NAME}.app/Contents/Resources
-SYNCTHING_VERSION="1.29.7"
+SYNCTHING_VERSION="1.30.0"
SYNCTHING_DIST_URL="https://github.com/syncthing/syncthing/releases/download"
SYNCTHING_TARBALL_URL="${SYNCTHING_DIST_URL}/v${SYNCTHING_VERSION}/syncthing-macos-universal-v${SYNCTHING_VERSION}.zip"
diff --git a/syncthing/UI/OnboardingView.swift b/syncthing/UI/OnboardingView.swift
new file mode 100644
index 0000000..323a5d7
--- /dev/null
+++ b/syncthing/UI/OnboardingView.swift
@@ -0,0 +1,123 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2024-2025 Tommy van der Vorst
+// Copyright (C) 2026 The syncthing-macos Authors. All rights reserved.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+import Foundation
+import SwiftUI
+
+struct FeatureView: View {
+ var image: String
+ var title: String
+ var description: String
+
+ var body: some View {
+ HStack(alignment: .top, spacing: 15) {
+ Image(systemName: image).foregroundColor(.accentColor)
+ .font(.system(size: 38, weight: .light))
+ VStack(alignment: .leading, spacing: 5) {
+ Text(self.title).bold()
+ Text(self.description)
+ Spacer()
+ }
+ }
+ }
+}
+
+struct OnboardingView: View {
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ ScrollView(.vertical) {
+ VStack(alignment: .leading, spacing: 20) {
+ self.title
+
+ HStack(alignment: .center) {
+ Text("Synchronize your files securely with your other devices.")
+ .bold()
+ .multilineTextAlignment(.leading)
+ .fixedSize(horizontal: false, vertical: true)
+ }.frame(
+ minWidth: 0,
+ minHeight: 0,
+ maxHeight: .infinity,
+ alignment: .topLeading
+ )
+
+ Text("Before we start, we need to go over a few things:").multilineTextAlignment(
+ .leading)
+
+ FeatureView(
+ image: "bolt.horizontal.circle",
+ title: String(localized: "Synchronization is not back-up"),
+ description: String(
+ localized:
+ "When you synchronize files, all changes, including deleting files, also happen on your other devices. Do not use Syncthing for back-up purposes, and always keep a back-up of your data."
+ ))
+
+ FeatureView(
+ image: "hand.raised.circle",
+ title: String(localized: "Your devices, your data, your responsibility"),
+ description: String(
+ localized:
+ "You decide with which devices you share your data with. Syncthing is a selfhosted secure Peer-to-peer app without a central server or cloud service. This also means the app makers cannot help you access or recover any lost files."
+ )
+ )
+
+ FeatureView(
+ image: "gear.circle",
+ title: String(localized: "Powered by Syncthing"),
+ description: String(
+ localized:
+ "This app is powered by the official Open source Syncthing."
+ )
+ )
+
+ self.footer.padding(.bottom).padding(10)
+
+ }.padding(.all).padding(20)
+ }
+ }
+
+ var title: some View {
+ Text(
+ "Welcome to Syncthing for macOS!"
+ )
+ .font(.largeTitle.bold())
+ .multilineTextAlignment(.center)
+ }
+
+ var footer: some View {
+ Color.blue
+ .frame(
+ minHeight: 48, maxHeight: .infinity
+ )
+ .cornerRadius(9.0)
+ .overlay(alignment: .center) {
+ Text("I understand, let's get started!").bold().foregroundColor(.white)
+ }.onTapGesture {
+ self.dismiss()
+ }
+ }
+}
+
+#Preview {
+ OnboardingView()
+}