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() +}