chore: upgrade StudioBridge v2.1.2 -> v2.1.3, add persistent-folder creation, update runbook

- Rebuild StudioBridge-2.1.3.jar from latest upstream (v2.1.3) and deploy to portable-dist
- Add ensurePersistentDirectories() so the app creates ~/.studio-bridge and Profiles at startup (GUI + --sendonly)
- Bundle full StudioBridge source under StudioBridge-src/ (with rebuild-jar.bat)
- Update launchers (Launch-StudioBridge.bat, portable-dist/StudioBridge.bat) to v2.1.3
- Add check-update.ps1 and UPDATE.md for the step-by-step update runbook
- Update README (v2.1.3, persistent data + updating sections)
- Ignore StudioBridge-src/target/ build output
This commit is contained in:
ske087
2026-08-06 20:05:21 +03:00
parent 42293e0d5a
commit a50dd3b34b
52 changed files with 5875 additions and 13 deletions
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: Rdiger36
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required for JVM JIT compilation under hardened runtime -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Required for JVM memory management under hardened runtime -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
main-jar=StudioBridge.jar
java-options=-Xmx512m
+5
View File
@@ -0,0 +1,5 @@
main-jar=StudioBridge.jar
java-options=-Xmx512m
win-console=true
win-shortcut=false
win-menu=false
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+29
View File
@@ -0,0 +1,29 @@
changelog:
exclude:
labels:
- duplicate
- invalid
- wontfix
- question
categories:
- title: "💥 Breaking Changes"
labels:
- breaking change
- title: "✨ New Features & Improvements"
labels:
- enhancement
- title: "🐛 Bug Fixes"
labels:
- bug
- title: "📦 Dependency Updates"
labels:
- dependencies
- title: "🔧 Build & CI"
labels:
- build
- title: "📝 Documentation"
labels:
- documentation
- title: "🔍 Other Changes"
labels:
- "*"
+517
View File
@@ -0,0 +1,517 @@
name: Build and Release
on:
push:
branches: [ "main" ]
tags: [ "v*" ]
env:
APP_NAME: StudioBridge
MAIN_JAR: StudioBridge.jar
MODULES: java.base,java.desktop,java.net.http,java.security.jgss,java.security.sasl,java.naming,jdk.crypto.ec
JAVA_OPTS: "-Xmx512m"
COPYRIGHT: Rdiger-36
VENDOR: Rdiger-36
DESCRIPTION: "With StudioBridge it is possible to make Bambu Lab 3D Printers visible in Bambu Studio or Orca Slicer, that can not be automatically added by them."
ABOUT_URL: https://github.com/Rdiger-36/StudioBridge
WIN_UUID: b062e5bc-0c30-48e7-8f48-9067789d244b
jobs:
# ─────────────────────────────────────────────────────────────────────────
# JAR — cross-platform fat JAR
# Runs on every push to main (CI check) and on tag pushes (release)
# ─────────────────────────────────────────────────────────────────────────
build-jar:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Get project version
id: version
run: echo "value=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT
- name: Build fat JAR
run: mvn clean package -q
- name: Prepare JAR artifact
run: |
mkdir input
cp target/*-jar-with-dependencies.jar input/${{ env.MAIN_JAR }}
cp target/*-jar-with-dependencies.jar "target/StudioBridge-${{ steps.version.outputs.value }}.jar"
- uses: actions/upload-artifact@v7
with:
name: jar-release
path: target/StudioBridge-*.jar
- uses: actions/upload-artifact@v7
with:
name: jar-input
path: input/StudioBridge.jar
# ─────────────────────────────────────────────────────────────────────────
# Windows — MSI installer + portable ZIP (x86 and arm64)
# ─────────────────────────────────────────────────────────────────────────
build-windows:
needs: build-jar
if: startsWith(github.ref, 'refs/tags/v')
strategy:
fail-fast: false
matrix:
include:
- runner: windows-latest
arch: x86
- runner: windows-11-arm
arch: arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
# icon.ico is pre-converted and stored in .github/packaging/icons/
- name: Install WiX toolset (ARM64 only — not pre-installed on windows-11-arm)
if: matrix.arch == 'arm64'
shell: pwsh
run: |
choco install wixtoolset --no-progress -y
$wixDir = Get-ChildItem "C:\Program Files (x86)\WiX Toolset*\bin" |
Select-Object -First 1 -ExpandProperty FullName
echo "$wixDir" >> $env:GITHUB_PATH
- name: Download JAR
uses: actions/download-artifact@v7
with:
name: jar-input
path: input
- name: Build minimal runtime via jlink
shell: pwsh
run: |
jlink `
--add-modules "${{ env.MODULES }}" `
--strip-debug `
--no-header-files `
--no-man-pages `
--compress=zip-6 `
--output runtime
- name: Build MSI installer
shell: pwsh
run: |
# Add icon to CLI properties
Copy-Item .github/packaging/cli-win.properties cli-win.properties
Add-Content cli-win.properties "icon=.github/packaging/icons/icon.ico"
jpackage `
--type msi `
--name "${{ env.APP_NAME }}" `
--app-version "${{ needs.build-jar.outputs.version }}" `
--input input `
--main-jar "${{ env.MAIN_JAR }}" `
--icon .github/packaging/icons/icon.ico `
--runtime-image runtime `
--java-options "${{ env.JAVA_OPTS }}" `
--copyright "${{ env.COPYRIGHT }}" `
--vendor "${{ env.VENDOR }}" `
--description "${{ env.DESCRIPTION }}" `
--about-url "${{ env.ABOUT_URL }}" `
--win-menu --win-shortcut `
--win-dir-chooser --win-shortcut-prompt `
--win-upgrade-uuid "${{ env.WIN_UUID }}" `
--add-launcher "StudioBridgeCLI=cli-win.properties" `
--dest msi-output
- name: Build portable app-image
shell: pwsh
run: |
Copy-Item .github/packaging/cli-win.properties cli-win.properties
Add-Content cli-win.properties "icon=.github/packaging/icons/icon.ico"
jpackage `
--type app-image `
--name "${{ env.APP_NAME }}" `
--app-version "${{ needs.build-jar.outputs.version }}" `
--input input `
--main-jar "${{ env.MAIN_JAR }}" `
--icon .github/packaging/icons/icon.ico `
--runtime-image runtime `
--java-options "${{ env.JAVA_OPTS }}" `
--add-launcher "StudioBridgeCLI=cli-win.properties" `
--dest app-image
- name: Create portable ZIP
shell: pwsh
run: |
$v = "${{ needs.build-jar.outputs.version }}"
Compress-Archive `
-Path "app-image\${{ env.APP_NAME }}\*" `
-DestinationPath "StudioBridge-${v}_win_${{ matrix.arch }}_NO-INSTALL.zip"
- name: Rename and move MSI
shell: pwsh
run: |
$v = "${{ needs.build-jar.outputs.version }}"
Get-ChildItem msi-output\*.msi |
Move-Item -Destination "StudioBridge-${v}_win_${{ matrix.arch }}.msi"
- uses: actions/upload-artifact@v7
with:
name: windows-${{ matrix.arch }}
path: |
*.msi
*.zip
# ─────────────────────────────────────────────────────────────────────────
# Linux — AppImage (x86_64 and aarch64)
# ─────────────────────────────────────────────────────────────────────────
build-linux:
needs: build-jar
if: startsWith(github.ref, 'refs/tags/v')
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: x86_64
- runner: ubuntu-24.04-arm
arch: aarch64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Install dependencies
run: sudo apt-get install -y libfuse2
- name: Download appimagetool
run: |
wget -q \
"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${{ matrix.arch }}.AppImage" \
-O appimagetool
chmod +x appimagetool
- name: Download JAR
uses: actions/download-artifact@v7
with:
name: jar-input
path: input
- name: Build minimal runtime via jlink
run: |
jlink \
--add-modules "${{ env.MODULES }}" \
--strip-debug \
--no-header-files \
--no-man-pages \
--compress=zip-6 \
--output runtime
- name: Build app-image via jpackage
run: |
jpackage \
--type app-image \
--name "${{ env.APP_NAME }}" \
--app-version "${{ needs.build-jar.outputs.version }}" \
--input input \
--main-jar "${{ env.MAIN_JAR }}" \
--icon src/main/resources/icon.png \
--runtime-image runtime \
--java-options "${{ env.JAVA_OPTS }}"
- name: Package as AppImage
run: |
VERSION="${{ needs.build-jar.outputs.version }}"
APPDIR="build/${{ env.APP_NAME }}.AppDir"
# FHS-compliant AppDir structure
mkdir -p \
"${APPDIR}/usr/bin" \
"${APPDIR}/usr/lib/${{ env.APP_NAME }}" \
"${APPDIR}/usr/share/applications" \
"${APPDIR}/usr/share/icons/hicolor/256x256/apps"
# Copy jpackage app-image into usr/lib
cp -a "${{ env.APP_NAME }}/." "${APPDIR}/usr/lib/${{ env.APP_NAME }}/"
# Symlink launcher into PATH
ln -sfn "../lib/${{ env.APP_NAME }}/bin/${{ env.APP_NAME }}" \
"${APPDIR}/usr/bin/${{ env.APP_NAME }}"
# AppRun
cat > "${APPDIR}/AppRun" << 'APPRUN'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/usr/bin/StudioBridge" "$@"
APPRUN
chmod +x "${APPDIR}/AppRun"
# .desktop entry
cat > "${APPDIR}/${{ env.APP_NAME }}.desktop" << 'DESKTOP'
[Desktop Entry]
Type=Application
Name=StudioBridge
Exec=StudioBridge
Icon=StudioBridge
Categories=Utility;
Terminal=false
DESKTOP
# Icons
install -m 0644 src/main/resources/icon.png "${APPDIR}/${{ env.APP_NAME }}.png"
install -m 0644 src/main/resources/icon.png \
"${APPDIR}/usr/share/icons/hicolor/256x256/apps/${{ env.APP_NAME }}.png"
ARCH=${{ matrix.arch }} APPIMAGE_EXTRACT_AND_RUN=1 \
./appimagetool "${APPDIR}" \
"${{ env.APP_NAME }}-${VERSION}-${{ matrix.arch }}.AppImage"
- uses: actions/upload-artifact@v7
with:
name: linux-${{ matrix.arch }}
path: "*.AppImage"
# ─────────────────────────────────────────────────────────────────────────
# macOS — signed + notarized DMG (arm64 / Apple Silicon only)
#
# Intel (x86) support dropped: macOS 27 (September 2026) removes Rosetta 2,
# making x86 app execution on Apple Silicon impossible. Intel Macs cannot
# upgrade to macOS 27, so the x86 user base will effectively be zero.
#
# Required secrets:
# MACOS_CERTIFICATE — base64-encoded Developer ID Application .p12
# MACOS_CERTIFICATE_PWD — export password for the .p12
# MACOS_KEYCHAIN_PWD — temporary keychain password (any value)
# APPLE_TEAM_NAME — name part of "Developer ID Application: Name (ID)"
# APPLE_TEAM_ID — 10-character Apple Team ID
# APPLE_ID — Apple ID e-mail for notarytool
# APPLE_APP_PASSWORD — app-specific password (appleid.apple.com)
# ─────────────────────────────────────────────────────────────────────────
build-macos:
needs: build-jar
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-latest # arm64 — Apple Silicon (M-Series)
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
- name: Import Developer ID certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }}
run: |
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_KEYCHAIN_PWD" build.keychain
security import certificate.p12 \
-k build.keychain \
-P "$MACOS_CERTIFICATE_PWD" \
-T /usr/bin/codesign \
-T /usr/bin/jpackage
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$MACOS_KEYCHAIN_PWD" build.keychain
# icon.icns is pre-converted and stored in .github/packaging/icons/
- name: Download JAR
uses: actions/download-artifact@v7
with:
name: jar-input
path: input
- name: Build minimal runtime via jlink
run: |
jlink \
--add-modules "${{ env.MODULES }}" \
--strip-debug \
--no-header-files \
--no-man-pages \
--compress=zip-6 \
--output runtime
- name: Sign all native libraries inside JAR
# Apple notarization scans inside JARs for .dylib/.jnilib and rejects unsigned ones.
# Multiple dependencies (JNA, FlatLaf, ...) bundle macOS native code — sign them all.
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
run: |
mkdir -p jar-natives
# Collect paths of all native libs inside the JAR
NATIVE_LIBS=$(jar tf input/StudioBridge.jar | grep -E '\.(jnilib|dylib)$')
if [ -z "$NATIVE_LIBS" ]; then
echo "No native libs found in JAR — skipping."
else
# Extract only those files
(cd jar-natives && echo "$NATIVE_LIBS" | xargs jar xf ../input/StudioBridge.jar)
# Sign each one with hardened runtime + secure timestamp
find jar-natives -name "*.jnilib" -o -name "*.dylib" | \
while read lib; do
echo "Signing: $lib"
codesign --force \
--sign "Developer ID Application: ${APPLE_TEAM_NAME} (${APPLE_TEAM_ID})" \
--options runtime \
--timestamp \
--entitlements .github/entitlements.plist \
"$lib"
done
# Repack signed natives back into the JAR
(cd jar-natives && find . -name "*.jnilib" -o -name "*.dylib" | \
sed 's|^\./||' | xargs jar uf ../input/StudioBridge.jar)
fi
rm -rf jar-natives
- name: Build signed DMG
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
run: |
# Add icon to CLI properties
cp .github/packaging/cli-mac.properties cli-mac.properties
echo "icon=.github/packaging/icons/icon.icns" >> cli-mac.properties
jpackage \
--type dmg \
--name "${{ env.APP_NAME }}" \
--app-version "${{ needs.build-jar.outputs.version }}" \
--input input \
--main-jar "${{ env.MAIN_JAR }}" \
--icon .github/packaging/icons/icon.icns \
--runtime-image runtime \
--java-options "${{ env.JAVA_OPTS }}" \
--copyright "${{ env.COPYRIGHT }}" \
--vendor "${{ env.VENDOR }}" \
--description "${{ env.DESCRIPTION }}" \
--mac-sign \
--mac-signing-key-user-name "Developer ID Application: ${APPLE_TEAM_NAME} (${APPLE_TEAM_ID})" \
--mac-entitlements .github/entitlements.plist \
--add-launcher "StudioBridgeCLI=cli-mac.properties" \
--dest dmg-output
- name: Rename DMG
run: |
V="${{ needs.build-jar.outputs.version }}"
mv dmg-output/*.dmg "StudioBridge-${V}_macOS_arm64.dmg"
- name: Notarize and staple DMG
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
DMG=$(ls *.dmg | head -1)
# Submit and capture result as JSON for error logging
RESULT=$(xcrun notarytool submit "$DMG" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait --output-format json)
echo "$RESULT"
STATUS=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
ID=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# On failure print the detailed log from Apple before exiting
if [ "$STATUS" != "Accepted" ]; then
echo "Notarization failed (status: $STATUS) — fetching log..."
xcrun notarytool log "$ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD"
exit 1
fi
xcrun stapler staple "$DMG"
- uses: actions/upload-artifact@v7
with:
name: macos-arm64
path: "*.dmg"
# ─────────────────────────────────────────────────────────────────────────
# Release — collects all artifacts, creates GitHub Release on tag push
# ─────────────────────────────────────────────────────────────────────────
release:
needs: [ build-jar, build-windows, build-linux, build-macos ]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Download all artifacts
uses: actions/download-artifact@v7
with:
path: artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/jar-release/*
artifacts/windows-x86/*
artifacts/windows-arm64/*
artifacts/linux-x86_64/*
artifacts/linux-aarch64/*
artifacts/macos-arm64/*
name: "Version ${{ needs.build-jar.outputs.version }}"
generate_release_notes: true
append_body: true
body: |
---
## Downloads
### Windows
| | Installer (MSI) | Portable (ZIP) |
|---|---|---|
| **x86 (Intel/AMD)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}_win_x86.msi](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_x86.msi) | [⬇ NO-INSTALL x86](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_x86_NO-INSTALL.zip) |
| **ARM64** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}_win_arm64.msi](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_arm64.msi) | [⬇ NO-INSTALL arm64](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_win_arm64_NO-INSTALL.zip) |
### Linux
| | AppImage |
|---|---|
| **x86_64 (Intel/AMD)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}-x86_64.AppImage](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}-x86_64.AppImage) |
| **aarch64 (ARM)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}-aarch64.AppImage](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}-aarch64.AppImage) |
### macOS
| | DMG |
|---|---|
| **Apple Silicon (M-Series)** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}_macOS_arm64.dmg](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}_macOS_arm64.dmg) |
### JAR (alle Plattformen, erfordert Java 21+)
| | |
|---|---|
| **Standalone JAR** | [⬇ StudioBridge-${{ needs.build-jar.outputs.version }}.jar](https://github.com/Rdiger-36/StudioBridge/releases/download/${{ github.ref_name }}/StudioBridge-${{ needs.build-jar.outputs.version }}.jar) |
prerelease: ${{ contains(github.ref_name, '-') }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
.project
.settings
target/
.idea/
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+237
View File
@@ -0,0 +1,237 @@
<p align="center">
<img src=".github/packaging/icons/icon.png" width="120" alt="StudioBridge icon" />
</p>
<h1 align="center">StudioBridge</h1>
<p align="center">
Make your Bambu Lab 3D printers visible in Bambu Studio or Orca Slicer — across any network.<br/>
Connect printers from other subnets, VPNs, or your home network with a simple GUI.
</p>
<p align="center">
<img src="https://img.shields.io/github/v/release/Rdiger-36/StudioBridge?style=flat-square&label=version&color=blue" alt="version" />
<img src="https://img.shields.io/badge/Java-%E2%89%A521-ED8B00?style=flat-square&logo=openjdk&logoColor=white" alt="Java" />
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=flat-square&color=orange" alt="platform" />
<img src="https://img.shields.io/badge/license-GPL--3.0-green?style=flat-square" alt="license" />
<img src="https://img.shields.io/github/actions/workflow/status/Rdiger-36/StudioBridge/build-and-release.yml?style=flat-square&label=build" alt="build" />
<img src="https://img.shields.io/badge/maintained-yes-brightgreen?style=flat-square" alt="maintained" />
</p>
<p align="center">
<img src="https://img.shields.io/github/stars/Rdiger-36/StudioBridge?style=flat-square&color=yellow" alt="stars" />
<img src="https://img.shields.io/github/forks/Rdiger-36/StudioBridge?style=flat-square&color=orange" alt="forks" />
<img src="https://img.shields.io/github/issues/Rdiger-36/StudioBridge?style=flat-square" alt="open issues" />
<img src="https://img.shields.io/github/downloads/Rdiger-36/StudioBridge/total?style=flat-square&label=downloads&color=blue" alt="total downloads" />
<img src="https://img.shields.io/github/last-commit/Rdiger-36/StudioBridge?style=flat-square&label=last%20commit" alt="last commit" />
</p>
---
With StudioBridge it is possible to make Bambu Lab 3D Printers visible in Bambu Studio or Orca Slicer, that can not be automatically added by them.
It is also possible to connect printers from other subnets or from accessible external networks (e.g. via VPN) or in your normal home network.
There are many scripts for all kinds of systems, but many users feel more comfortable with a GUI. Therefore I developed StudioBridge, which is basically based on the scripts i found in countless forums.
# How it works
StudioBridge sends a UDP request to your Slicer (Bambu Studio or Orca Slicer) with the printer data (IP address, serial number, printer type, printer name).
Once all the data has been entered, it can be sent to your Slicer, making the printer visible. Now the printer can be accessed via the LAN access code. This means that all the usual functions are available.
This Programm has nothing to do with Bambu Connect and it also doesn't replace it. It's only for the connection in your Slicer.
With StudioBridge all printers of the Bambu Lab assortment can be added. It is also possible to create and save a profile for each printer.
You have the option to send the package via broadcast or directly to Bambu Studio.
You can check if the printer would connect correctly in Bambu Studio with the function "Check Printer".
Successfully tested on:
- Windows 10/11 ARM and x86
- Linux Ubuntu 25.10 ARM and x86
- macOS M-Series (Apple Silicon)
> **Note for Intel Mac users:** macOS 27 (September 2026) removes Rosetta 2 support. Native Intel (x86) builds are no longer provided. Intel Mac users can use the `.jar` release with Java 21+.
# Requirements
- min. Java 21 (only for .jar compiled release)
# Preview
Lightmode
![image](https://github.com/user-attachments/assets/d7f51a53-35d4-4e5d-b77d-9bf5a77eaf36) ![image](https://github.com/user-attachments/assets/8a1b0062-bc4d-4b25-8247-d3349b8b6b74)
Darkmode
![image](https://github.com/user-attachments/assets/216cb8b1-8913-4d71-87af-b4356de9c614) ![image](https://github.com/user-attachments/assets/a41a6a8b-a3ae-4baf-92a9-1069df042c4c)
Multiple Printer Setup
![Bildschirmfoto 2025-02-05 um 00 04 00](https://github.com/user-attachments/assets/3a295a9c-2ca2-4d1d-ad1e-8e68909f9f84)
![Bildschirmfoto 2025-02-05 um 00 03 04](https://github.com/user-attachments/assets/62ed0dd8-000d-4682-8399-c5b6229204ed)
Printer Check
![Printer_Check](https://github.com/user-attachments/assets/ccb139b0-e737-49a5-9b8c-0faf00316892)
If TCP Handshake is not working, then it helps to restart your printer.
# Demo
![StudioBridgeDemo](https://github.com/user-attachments/assets/e5e197ab-54bf-4a6d-bc40-dd46607597f8)
# Arguments
To use StudioBridge via Terminal/CMD/PowerShell you have to run the StudioBridgeCLI on MacOS and Windows
#### MacOS
```bash
/Applications/StudioBridge.app/Contents/MacOS/StudioBridgeCLI --help
*** StudioBridge by Rdiger-36 v.2.1.3 ***
Usage:
/Applications/StudioBridge.app/Contents/MacOS/StudioBridgeCLI [OPTIONS]
Options:
You can combine every option with each other
Starting options:
--noupdate Skip search for latest updates on startup
--sendonly Start with no GUI, only send data from all printers to Bambu Studio
--direct Send UPD package directly to Bambu Studio, not over broadcast
--help Show help for StudioBridge
Profile options:
--customProfilesDir='/path/to/profiles/'
Set custom profiles directory which will be loaded from this for StudioBridgeCLI
Default: `~/.studio-bridge/Profiles`
--selectedPrinters='/path/to/profile.sbp/','/path/to/other/profile2.sbp/'
Set single profiles which will be used for StudioBridgeCLI (single and multi profiles comma-separated)
Example:
/Applications/StudioBridge.app/Contents/MacOS/StudioBridgeCLI --help
/Applications/StudioBridge.app/Contents/MacOS/StudioBridgeCLI --sendonly --noupdate --direct
/Applications/StudioBridge.app/Contents/MacOS/StudioBridgeCLI --sendonly --customProfilesDir='/Users/rdiger-36/Documents/StudioBridgeProfiles'
/Applications/StudioBridge.app/Contents/MacOS/StudioBridgeCLI --sendonly --selectedPrinters='/Users/rdiger-36/Documents/My P1S.sbp','/Users/rdiger-36/Desktop/Profiles/H2D-Pro Work.sbp.sbp'
```
#### Windows
You can find the StudioBridgeCLI.exe in your installation directory, standard: "C:\Program Files\StudioBridge"
```bash
.\StudioBridgeCLI.exe --help
*** StudioBridge by Rdiger-36 v.2.1.3 ***
You are currently running this application in raw mode (via .jar)!
For the official version for your OS and CPU architecture, please visit the GitHub repository.
https://github.com/Rdiger-36/StudioBridge/
Usage:
./StudioBridgeCLI.exe [OPTIONS]
Options:
You can combine every option with each other
Starting options:
--noupdate Skip search for latest updates on startup
--sendonly Start with no GUI, only send data from all printers to Bambu Studio
--direct Send UPD package directly to Bambu Studio, not over broadcast
--help Show help for StudioBridge
Profile options:
--customProfilesDir='/path/to/profiles/'
Set custom profiles directory which will be loaded from this for StudioBridgeCLI
Default: `~/.studio-bridge/Profiles`
--selectedPrinters='/path/to/profile.sbp/','/path/to/other/profile2.sbp/'
Set single profiles which will be used for StudioBridgeCLI (single and multi profiles comma-separated)
Example:
./StudioBridgeCLI.exe --help
./StudioBridgeCLI.exe --sendonly --noupdate --direct
./StudioBridgeCLI.exe --sendonly --customProfilesDir='C:\Users\rdiger-36\Documents\StudioBridgeProfiles'
./StudioBridgeCLI.exe --sendonly --selectedPrinters='C:\Users\rdiger-36\Documents\My P1S.sbp','C:\Users\rdiger-36\Desktop\H2D-Pro Work.sbp'
```
#### Linux
```bash
.\StudioBridge.AppImage --help
*** StudioBridge by Rdiger-36 v.2.1.3 ***
You are currently running this application in raw mode (via .jar)!
For the official version for your OS and CPU architecture, please visit the GitHub repository.
https://github.com/Rdiger-36/StudioBridge/
Usage:
./StudioBridge.AppImage [OPTIONS]
Options:
You can combine every option with each other
Starting options:
--noupdate Skip search for latest updates on startup
--sendonly Start with no GUI, only send data from all printers to Bambu Studio
--direct Send UPD package directly to Bambu Studio, not over broadcast
--help Show help for StudioBridge
Profile options:
--customProfilesDir='/path/to/profiles/'
Set custom profiles directory which will be loaded from this for StudioBridgeCLI
Default: `~/.studio-bridge/Profiles`
--selectedPrinters='/path/to/profile.sbp/','/path/to/other/profile2.sbp/'
Set single profiles which will be used for StudioBridgeCLI (single and multi profiles comma-separated)
Example:
./StudioBridge.AppImage --help
./StudioBridge.AppImage --sendonly --noupdate --direct
./StudioBridge.AppImage --sendonly --customProfilesDir='/home/rdiger-36/Documents/StudioBridgeProfiles'
./StudioBridge.AppImage --sendonly --selectedPrinters='/home/rdiger-36/Documents/My P1S.sbp','/home/rdiger-36/Desktop/H2D-Pro Work.sbp'
```
If you use --sendonly the output will look like this:
```bash
*** StudioBridge by Rdiger-36 ***
Found 2 profiles!
Try to send all of them to Bambu Studio
Successfully sended Test Bambu X1E - 192.168.XXX.XXX - X1E to Bambu Studio
Successfully sended Bambu Lab P1S - 192.168.XXX.XXX - P1S to Bambu Studio
All Packages successfully sent to Bambu Studio
```
# Installing Java and Running the JAR File (Only for using .jar!)
StudioBridge.jar is built in Java, so a Java Runtime Environment (JRE) is required to run the program. Below are simple instructions for installing Java on Windows, macOS, and Linux, as well as for launching the StudioBridge GUI.
Java Installation
Windows:
1. Visit the official Java website and download the latest Java Runtime Environment (JRE) for Windows.
2. Run the downloaded installer file and follow the on-screen instructions to complete the installation.
3. To confirm that Java is installed, you can search for “Java” in the Start menu. You should see options like “Java Control Panel” if installation was successful.
macOS:
1. Visit the official Java website and download the latest Java Runtime Environment (JRE) for macOS.
2. Open the downloaded .dmg file and follow the instructions to install Java by dragging it into the Applications folder.
3. After installation, open “System Preferences,” then “Java” to verify Java is installed and configured correctly.
Linux:
1. Most Linux distributions offer Java in their software repositories. Open the Software Center or Package Manager for your distribution and search for “OpenJDK” or “Java Runtime Environment.”
2. Select the version you need (Java 8 or later) and install it.
3. You can check if Java is installed by searching for it in the applications menu or checking for “Java” in system settings.
Running the StudioBridge GUI
1. Download the StudioBridge.jar file from the GitHub releases page.
2. Locate the StudioBridge.jar file in your Downloads or specified folder.
3. Double-click the .jar file to launch StudioBridge. This should open the GUI, allowing you to configure and use your 3D printers with Bambu Studio over LAN.
If the .jar file does not open directly, ensure Java is properly installed, and check your systems default settings for opening .jar files.
# Support Me
[![Buy Me a Coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://www.buymeacoffee.com/Rdiger36)
+143
View File
@@ -0,0 +1,143 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>rdiger36</groupId>
<artifactId>StudioBridge</artifactId>
<version>2.1.3</version>
<packaging>jar</packaging>
<name>StudioBridge</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>rdiger36.StudioBridge.gui.MainMenu</main.class>
<!-- Dependency Versions -->
<flatlaf.version>3.7.1</flatlaf.version>
<commons-io.version>2.22.0</commons-io.version>
<jna.version>5.19.0</jna.version>
<json.version>20260522</json.version>
<junit.version>4.13.2</junit.version>
<jnafilechooser.version>1.1.2</jnafilechooser.version>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.18.0</version>
<configuration>
<!-- JitPack-Abhängigkeiten vom Auto-Update ausschließen -->
<excludes>
<exclude>com.github.steos:jnafilechooser</exclude>
</excludes>
<ignoredVersions>.*-SNAPSHOT</ignoredVersions>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>${flatlaf.version}</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf-extras</artifactId>
<version>${flatlaf.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
<dependency>
<groupId>com.github.steos</groupId>
<artifactId>jnafilechooser</artifactId>
<version>${jnafilechooser.version}</version>
</dependency>
</dependencies>
</project>
+65
View File
@@ -0,0 +1,65 @@
@echo off
REM ===========================================================================
REM rebuild-jar.bat - Rebuild StudioBridge.jar from source and deploy it to
REM the portable-dist folder.
REM
REM Requirements:
REM - Apache Maven (auto-located from %LOCALAPPDATA%\Programs\apache-maven-*)
REM - A JDK/JRE with javac (uses the bundled one in ..\portable-dist\jre)
REM ===========================================================================
setlocal
cd /d "%~dp0"
REM --- Locate Maven ----------------------------------------------------------
set "MAVEN_CMD="
for /d %%D in ("%LOCALAPPDATA%\Programs\apache-maven-*") do (
if exist "%%D\bin\mvn.cmd" set "MAVEN_CMD=%%D\bin\mvn.cmd"
)
if not defined MAVEN_CMD (
echo ERROR: Maven not found. Install it or set MAVEN_CMD manually.
pause
exit /b 1
)
REM --- Use the bundled JDK/JRE for building ---------------------------------
set "JAVA_HOME=%~dp0..\portable-dist\jre"
if not exist "%JAVA_HOME%\bin\javac.exe" (
echo ERROR: No compiler found in "%JAVA_HOME%".
pause
exit /b 1
)
echo [BUILD] Using Maven : %MAVEN_CMD%
echo [BUILD] Using JAVA_HOME : %JAVA_HOME%
echo.
REM --- Build ----------------------------------------------------------------
call "%MAVEN_CMD%" -f pom.xml clean package
if errorlevel 1 (
echo.
echo ERROR: Maven build failed.
pause
exit /b 1
)
REM --- Deploy the fat jar to portable-dist ----------------------------------
set "JAR_VERSION=2.1.3"
set "BUILT=target\StudioBridge-%JAR_VERSION%-jar-with-dependencies.jar"
set "DEST=..\portable-dist\StudioBridge-%JAR_VERSION%.jar"
if not exist "%BUILT%" (
echo ERROR: Build output not found: %BUILT%
pause
exit /b 1
)
copy /Y "%BUILT%" "%DEST%" >nul
if errorlevel 1 (
echo ERROR: Could not copy jar to %DEST%
pause
exit /b 1
)
echo.
echo [OK] Rebuilt and deployed: %DEST%
endlocal
@@ -0,0 +1,430 @@
package rdiger36.StudioBridge.functions;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import com.formdev.flatlaf.FlatLaf;
import rdiger36.StudioBridge.gui.DialogOneButton;
import rdiger36.StudioBridge.gui.DialogTwoButtons;
import rdiger36.StudioBridge.gui.MainMenu;
import rdiger36.StudioBridge.gui.SaveDialog;
import rdiger36.StudioBridge.objects.Printer;
/**
* The {@code Config} class provides utility methods for managing application settings
* and printer profiles. It includes functionality for saving and loading user
* preferences (e.g., theme, profile directories, last used profile) as well as
* reading and writing printer profile configuration files.
*
* <p>This class serves as a central configuration handler for the application and
* ensures consistent access to settings and profile data.</p>
*/
public class Config {
/**
* Displays a warning dialog if the current printer profile is new and contains
* unsaved data. The user may choose to save the profile or continue without saving.
*
* <p>If the user chooses to save, a dialog is shown to enter a profile name.
* The profile is then written to disk and the given {@link Printer} object is
* updated accordingly.</p>
*
* @param mainFrame the window used for dialog positioning.
* @param printer the printer object containing the unsaved profile data.
* @return the name of the saved profile, or an empty string if the user chose
* not to save.
*/
public static String saveUnsavedNewProfile(JFrame mainFrame, Printer printer) {
// Display a dialog that warns the user about unsaved profile changes.
// The dialog has two buttons: "Save new profile" and "Continue without saving".
// The user's choice is stored in the variable 'response'.
int response = new DialogTwoButtons(mainFrame, null,
new ImageIcon(MainMenu.class.getResource("/achtung.png")),
"<html>Warning! The new profile has not been saved yet!</html>",
"Save new profile",
"Continue without saving").showDialog();
// If the user chooses to save the profile (response == 0):
if (response == 0) {
// Get the profile name by showing a save dialog.
// The text from the 'txtName' field is used as the initial suggestion for the profile name.
String profileName = new SaveDialog(mainFrame, printer.getProfileName(), "Save").saveProfile().trim();
// Save the profile with the entered name and associated details (model, IP, serial number), when the profile name is set.
if (!profileName.equals("")) {
printer.setProfileName(profileName);
saveProfile(mainFrame, printer);
}
// Return the name of the saved profile.
return profileName;
}
// If the user chooses to continue without saving, return an empty string.
return "";
}
/**
* Displays a dialog when an existing profile contains unsaved changes. The user
* may choose to save or continue without saving.
*
* <p>If the user chooses to save, the changes are written to the profile file.</p>
*
* @param mainFrame the window used for dialog positioning.
* @param printer the printer object representing the modified profile.
* @return {@code true} if the profile was saved, {@code false} if the user chose
* not to save.
*/
public static boolean saveUnsavedProfile(JFrame mainFrame, Printer printer) {
// Display a dialog warning the user that the current profile contains unsaved changes.
// The dialog has two buttons: "Save profile" and "Continue without saving".
int response = new DialogTwoButtons(mainFrame, null,
new ImageIcon(MainMenu.class.getResource("/achtung.png")),
"<html>Warning! The selected profile contains changes!</html>",
"Save profile",
"Continue without saving").showDialog();
// If the user chooses to save the profile (response == 0):
if (response == 0) {
// Save the profile using the selected name and the provided profile details.
saveProfile(mainFrame, printer);
// Return true indicating that the profile has been saved.
return true;
}
// If the user chooses to continue without saving, return false.
return false;
}
/**
* Writes the contents of a {@link Printer} profile to a configuration file.
* The file is stored inside the application's profile directory and saved
* using the profile name with ".sbp" as extension.
*
* @param mainFrame the window used for displaying error dialogs.
* @param printer the printer object whose data should be stored.
* @return {@code true} if the profile was saved successfully,
* {@code false} if an error occurred.
*/
public static boolean saveProfile(JFrame mainFrame, Printer printer) {
// Create a StringBuilder to build the profile configuration line by line.
StringBuilder config = new StringBuilder();
// Add the profile name from the comboBox to the configuration.
config.append("ProfileName=" + printer.getProfileName());
config.append(System.getProperty("line.separator")); // Add a new line.
// Add the IP address from the txtIP field to the configuration.
config.append("IP-Address=" + printer.getIpAdress());
config.append(System.getProperty("line.separator")); // Add a new line.
// Add the printer serial number from the txtSerial field to the configuration.
config.append("PrinterSN=" + printer.getSerialNumber());
config.append(System.getProperty("line.separator")); // Add a new line.
// Add the printer model type (as index) from the cbxModel comboBox to the configuration.
config.append("PrinterType=" + printer.getPrinterModel());
config.append(System.getProperty("line.separator")); // Add a new line.
// Add the printer name from the txtName field to the configuration.
config.append("PrinterName=" + printer.getPrinterName());
/**
* Create a FileOutputStream to write the content of the config StringBuilder into a file.
* The file is saved in the profiles directory defined by MainMenu.ProfilesDir, with the profile
* name and ".sbp" file extension.
*/
FileOutputStream fos;
try {
// Create and open a file at the specified path with the profile name.
fos = new FileOutputStream(MainMenu.ProfilesDir + System.getProperty("file.separator") + printer.getProfileName() + ".sbp");
// Write the content of the config to the file as bytes.
fos.write(config.toString().getBytes());
// Close the file stream after writing.
fos.close();
} catch (Exception e1) {
// Show error if saving the file has failed
new DialogOneButton(mainFrame, null, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Warning! Failed to save the profile!</html>", "Ok").showDialog();
return false;
}
// Return true if the profile was successfully saved.
return true;
}
/**
* Loads the contents of a printer profile file and constructs a {@link Printer}
* object based on its data.
*
* @param mainFrame the frame used for error dialog display.
* @param profile the absolute file path of the profile to load.
* @return a {@link Printer} object with the loaded data,
* or {@code null} if the file could not be read.
*/
public static Printer loadProfile(JFrame mainFrame, String profile) {
// Create a File object representing the configuration file.
File configFile = new File(profile);
// Variable to hold each line read from the configuration file.
String line = "";
// BufferedReader to read the contents of the configuration file.
BufferedReader inputStream;
// Check if the configuration file exists.
if (configFile.exists()) {
Printer printer = new Printer(configFile.getName().replace(".sbp", ""), "", "", "", "");
try {
// Open the configuration file for reading.
inputStream = new BufferedReader(new FileReader(configFile));
// Read each line of the file until the end.
while((line = inputStream.readLine()) != null) {
/**
* Use the get_Conf() method to assign the values from the configuration file
* to the appropriate variables (combo box and text fields).
* Each line is split into a key-value pair, where the key is on the left side of '='
* and the value on the right.
*/
String[] parts = line.split("=", 2);
getPrinterConf(parts[0], parts.length > 1 ? parts[1] : "", printer);
}
// Close the BufferedReader after reading all lines.
inputStream.close();
} catch (Exception e) {
// Show error if reading the file has failed
new DialogOneButton(mainFrame, null, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Warning! Failed to read the profile file!</html>", "Ok").showDialog();
return null;
}
return printer;
} else {
return null;
}
}
/**
* Parses a configuration key/value pair and assigns the corresponding value
* to the given {@link Printer} object's fields.
*
* @param configType the configuration key (e.g., "IP-Address").
* @param wert the configuration value.
* @param printer the printer object that receives the configuration data.
*/
private static void getPrinterConf(String configType, String wert, Printer printer) {
// Switch statement to assign the value to the correct field based on the configuration type.
switch(configType) {
// Set the IP address in the txtIP field, limiting the length to 15 characters.
case "IP-Address":
printer.setIpAdress(checkLength(wert, 15));
break;
// Set the printer serial number in the txtSerial field, limiting the length to 25 characters.
case "PrinterSN":
printer.setSerialNumber(checkLength(wert, 25));
break;
// Set the selected index of the printer model in the cbxModel combo box based on the value.
case "PrinterType":
printer.setPrinterModel(checkLength(wert, 25));
break;
// Set the printer name in the txtName field, limiting the length to 25 characters.
case "PrinterName":
printer.setPrinterName(checkLength(wert, 25));
break;
// Default case if no known configuration type is found (does nothing).
default:
break;
}
}
/**
* Ensures that a string does not exceed a maximum length. If the string is
* longer than the allowed value, it is truncated.
*
* @param wert the string to check.
* @param length the maximum allowed length.
* @return the original string if within limits, otherwise a truncated version.
*/
private static String checkLength(String wert, int length) {
// If the length of the string exceeds the specified maximum, truncate it.
if (wert.length() > length) {
wert = wert.substring(0, length);
}
// Return the original or truncated string.
return wert;
}
/**
* Writes the user's application settings to the "settings" file in the
* application configuration directory.
*
* <p>The following settings are stored:</p>
* <ul>
* <li>Dark mode state</li>
* <li>Custom profile directory</li>
* <li>Remember last used profile</li>
* <li>Last used profile name (optional)</li>
* <li>Direct mode flag</li>
* <li>Show JAR info flag</li>
* </ul>
*
* @param mainFrame the frame used to display possible error dialogs.
*/
public static void saveUserSettings(JFrame mainFrame) {
// Create a StringBuilder to construct the configuration settings line by line.
StringBuilder config = new StringBuilder();
// Append the dark mode setting to the configuration.
config.append("enableDarkmode=" + FlatLaf.isLafDark());
config.append(System.getProperty("line.separator")); // Add a new line.
// Append the custom profiles path to the configuration.
config.append("customProfilesPath=" + MainMenu.ProfilesDir);
config.append(System.getProperty("line.separator")); // Add a new line.
// Append the usage of the last used profile to the configuration.
config.append("rememberLastUsedProfile=" + MainMenu.rememberLastUsedProfile);
config.append(System.getProperty("line.separator")); // Add a new line.
// If the option to remember the last used profile is enabled, store the name of the profile to the configuration
if (MainMenu.rememberLastUsedProfile) {
// Append the last used profile to the configuration.
config.append("lastUsedProfile=" + MainMenu.lastUsedProfile);
config.append(System.getProperty("line.separator")); // Add a new line.
}
// Append the usage of the mode for sending UDP packages to the configuration.
config.append("directMode=" + MainMenu.directMode);
config.append(System.getProperty("line.separator")); // Add a new line.
// Append the usage of the mode to show the jar info to the configuration.
config.append("showJarInfo=" + MainMenu.jarInfo);
config.append(System.getProperty("line.separator")); // Add a new line.
/**
* FileOutputStream to create and write the contents of the StringBuilder to
* a file at the specified save path.
*/
FileOutputStream fos;
try {
// Create a FileOutputStream for the settings file.
fos = new FileOutputStream(MainMenu.savePath + System.getProperty("file.separator") + "settings");
// Write the configuration settings to the file.
fos.write(config.toString().getBytes());
// Close the output stream.
fos.close();
} catch (Exception e1) {
// Show error if saving the file has failed
e1.printStackTrace();
new DialogOneButton(mainFrame, null, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Warning! Failed to write the config to the config file!</html>", "Ok").showDialog();
}
}
/**
* Loads the application settings from the "settings" file and assigns the
* values to the corresponding fields in {@link MainMenu}.
*
* <p>If the settings file does not exist, no values are loaded.</p>
*/
public static void loadAppSettings() {
// Define the path to the user settings file.
String userSettings = MainMenu.savePath + System.getProperty("file.separator") + "settings";
// Check if the settings file exists.
if (new File(userSettings).exists()) {
try {
String line;
// Open the settings file for reading using a BufferedReader.
try (BufferedReader inputStream = new BufferedReader(new FileReader(userSettings))) {
// Read each line of the file.
while((line = inputStream.readLine()) != null) {
String[] parts = line.split("=", 2);
getAppConf(parts[0], parts.length > 1 ? parts[1] : "");
}
// The BufferedReader is automatically closed here due to the try-with-resources statement.
}
} catch (Exception e) {
// Show error if reading the file has failed
e.printStackTrace();
new DialogOneButton(null, null, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Warning! Failed to read the config file!</html>", "Ok").showDialog();
}
}
}
/**
* Assigns a setting key/value pair from the settings file to the appropriate
* application field in {@link MainMenu}.
*
* @param configType the settings key.
* @param wert the settings value.
*/
private static void getAppConf(String configType, String wert) {
// Switch statement to assign the value to the correct field based on the configuration type.
switch(configType) {
// Set the enableDarkmode setting
case "enableDarkmode":
MainMenu.darkmode = Boolean.valueOf(wert);
break;
// Set the customProfilesPath setting
case "customProfilesPath":
MainMenu.ProfilesDir = wert;
break;
// Set the rememberLastUsedProfile setting
case "rememberLastUsedProfile":
MainMenu.rememberLastUsedProfile = Boolean.valueOf(wert);
break;
// Set the lastUsedProfile setting
case "lastUsedProfile":
MainMenu.lastUsedProfile = wert;
break;
// Set the directMode setting
case "directMode":
MainMenu.directMode = Boolean.valueOf(wert);
break;
// Set the showJarInfo setting
case "showJarInfo":
MainMenu.jarInfo = Boolean.valueOf(wert);
break;
// Default case if no known configuration type is found (does nothing).
default:
break;
}
}
}
@@ -0,0 +1,110 @@
package rdiger36.StudioBridge.functions;
import javax.net.ssl.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
/**
* Utility class that provides methods to check network connectivity,
* including TLS handshake validation and reachability tests.
*/
public class Connectivity {
/**
* Attempts to establish a TLS connection to the specified host on port 6000.
* <p>
* This method:
* <ul>
* <li>Creates a custom SSLContext that trusts all certificates (insecure, for testing purposes).</li>
* <li>Opens a TLS socket to the host and performs a handshake.</li>
* <li>Sends a basic HTTP GET request to test communication.</li>
* </ul>
*
* @param host The hostname or IP address of the target server.
* @return {@code true} if the TLS handshake was successful (or if data exchange
* succeeded after handshake failure); {@code false} otherwise.
*/
public static boolean checkTLSHandshake(String host) {
int port = 6000;
SSLSocket socket = null;
boolean handshakeDone = false;
try {
// Create an SSL context that trusts all certificates (not recommended for production use)
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[]{new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) {}
public void checkServerTrusted(X509Certificate[] c, String a) {}
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}}, new SecureRandom());
// Create an SSL socket
SSLSocketFactory factory = sc.getSocketFactory();
socket = (SSLSocket) factory.createSocket();
// Set connection timeout
socket.setSoTimeout(5000);
// Connect to host and start TLS handshake
socket.connect(new InetSocketAddress(host, port), 5000);
socket.startHandshake();
handshakeDone = true;
// Send a simple HTTP GET request to check data transmission
String http = "GET / HTTP/1.1\r\n" +
"Host: " + host + ":" + port + "\r\n" +
"User-Agent: java-ssl-probe\r\n" +
"Accept: */*\r\n" +
"Connection: close\r\n\r\n";
OutputStream out = socket.getOutputStream();
out.write(http.getBytes(StandardCharsets.US_ASCII));
out.flush();
// Read one byte to verify response from server
InputStream in = socket.getInputStream();
byte[] one = in.readNBytes(1);
boolean gotResponse = one.length > 0;
return handshakeDone && gotResponse;
} catch (SSLHandshakeException e) {
// TLS handshake failed (likely invalid certificate or protocol mismatch)
return false;
} catch (Exception e) {
// If handshake was already done but a later error occurred, still return true
if (handshakeDone) {
return true;
}
return false;
} finally {
// Clean up the socket
if (socket != null) {
try {
socket.close();
} catch (Exception ignore) {}
}
}
}
/**
* Attempts to checkPort 6000 of the printer to check network reachability.
*
* @param host The hostname or IP address to ping.
* @return {@code true} if the host is reachable within 5 seconds,
* {@code false} otherwise.
*/
public static boolean pingPrinter(String host) {
try (java.net.Socket s = new java.net.Socket()) {
s.connect(new java.net.InetSocketAddress(host, 8883), 5000);
return true;
} catch (Exception e) {
return false;
}
}
}
@@ -0,0 +1,130 @@
package rdiger36.StudioBridge.functions;
import java.io.File;
import java.util.ArrayList;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import rdiger36.StudioBridge.gui.DialogTwoButtons;
import rdiger36.StudioBridge.gui.MainMenu;
import rdiger36.StudioBridge.objects.Printer;
/**
* The {@code Exit} class provides functionality for performing a clean shutdown
* of the application. Before termination, it ensures that unsaved profiles and
* user settings are properly handled to prevent data loss.
*
* <p>The class checks whether the currently edited or imported profiles contain
* unsaved changes and triggers the necessary save dialogs if required before
* the application closes.</p>
*/
public class Exit {
/**
* Initiates the application shutdown sequence while ensuring that all
* unsaved profile data and user settings are correctly saved.
*
* <p>The method performs the following checks:</p>
* <ul>
* <li>If the currently selected profile is a new, unsaved profile
* containing data, the user is prompted to save it.</li>
* <li>If the loaded profile has been modified, the user is prompted
* to save the changes.</li>
* <li>If imported profiles exist that have not yet been saved to the
* filesystem, the user is warned accordingly.</li>
* <li>Finally, user settings are saved and the application terminates.</li>
* </ul>
*
* @param mainFrame the main application window used to display dialogs.
* @param profiles a list of all profile names currently loaded in the UI.
* @param printer the currently edited {@link Printer} object whose values
* may need to be saved before exiting.
*/
public static void closeApp(JFrame mainFrame, ArrayList<String> profiles, Printer printer) {
String importedProfileSaved = checkImportedProfiles(profiles);
// Check if the selected profile is "New profile" and has unsaved data
if (printer.getPrinterModel().equals("New profile") &&
(printer.getIpAdress().length() > 0 || printer.getSerialNumber().length() > 0 || printer.getPrinterName().length() > 0)) {
Config.saveUnsavedNewProfile(mainFrame, printer);
}
// Check if there are changes in the existing profile
else if (MainMenu.mainPrinter != null) {
if (!printer.getIpAdress().equals(MainMenu.mainPrinter.getIpAdress()) ||
!printer.getSerialNumber().equals(MainMenu.mainPrinter.getSerialNumber()) ||
!printer.getPrinterName().equals(MainMenu.mainPrinter.getPrinterName()) ||
!printer.getPrinterModel().equals(MainMenu.mainPrinter.getPrinterModel())) {
Config.saveUnsavedNewProfile(mainFrame, printer);
}
}
// Check if there is exactly one unsaved imported profile
else if (importedProfileSaved.equals("oneUnsaved")) {
Config.saveUnsavedNewProfile(mainFrame, printer);
}
// Check if several imported profiles are unsaved
else if (importedProfileSaved.equals("multipleUnsaved")) {
int result = new DialogTwoButtons(
mainFrame,
null,
new ImageIcon(MainMenu.class.getResource("/achtung.png")),
"<html>Warning! There are several unsaved imported profiles!<br>Do you want to go back to save them?</html>",
"Yes",
"No"
).showDialog();
if (result == 0) {
return; // Abort exit
}
}
// Save user settings before exiting
Config.saveUserSettings(mainFrame);
System.exit(0);
}
/**
* Checks whether imported profiles listed in the profile overview
* exist on disk as .sbp files. Profiles that do not have a corresponding
* file are considered unsaved.
*
* <p>The result indicates how many profiles are unsaved:</p>
* <ul>
* <li>{@code "multipleUnsaved"} more than one unsaved profile</li>
* <li>{@code "oneUnsaved"} exactly one unsaved profile</li>
* <li>empty string all profiles are saved</li>
* </ul>
*
* @param profiles a list of profile names displayed in the UI.
* @return a status string describing the number of unsaved profiles.
*/
private static String checkImportedProfiles(ArrayList<String> profiles) {
// Directory where profiles are stored
String profDir = MainMenu.ProfilesDir + System.getProperty("file.separator");
int unsavedCounter = 0;
for (String profile : profiles) {
File profileFile = new File(profDir + profile + ".sbp");
if (!profileFile.exists()) {
unsavedCounter++;
}
}
if (unsavedCounter > 1) {
return "multipleUnsaved";
} else if (unsavedCounter == 1) {
return "oneUnsaved";
}
return "";
}
}
@@ -0,0 +1,83 @@
package rdiger36.StudioBridge.functions;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
/**
* A DocumentFilter that restricts the length of text in a document.
* <p>
* This filter ensures that the total length of the document does not exceed
* a specified maximum length when text is inserted or replaced.
* </p>
*/
public class LengthRestrictedDocumentFilter extends DocumentFilter {
private int maxLength;
/**
* Constructs a LengthRestrictedDocumentFilter with the specified maximum length.
*
* @param maxLength The maximum allowed length of the document.
*/
public LengthRestrictedDocumentFilter(int maxLength) {
this.maxLength = maxLength;
}
/**
* Inserts the specified string at the given offset if it does not
* exceed the maximum length of the document.
*
* @param fb The FilterBypass to use for the insert operation.
* @param offset The offset at which to insert the string.
* @param string The string to insert.
* @param attr The attributes to apply to the inserted string.
* @throws BadLocationException If the insert position is invalid.
*/
@Override
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException {
if (string == null) {
return;
}
// Ensure that the total length does not exceed the maximum length
if ((fb.getDocument().getLength() + string.length()) <= maxLength) {
super.insertString(fb, offset, string, attr);
}
}
/**
* Replaces text in the document at the specified offset if the result
* does not exceed the maximum length.
*
* @param fb The FilterBypass to use for the replace operation.
* @param offset The offset at which to start replacing text.
* @param length The length of the text to be replaced.
* @param string The string to insert in place of the replaced text.
* @param attrs The attributes to apply to the replaced string.
* @throws BadLocationException If the replace position is invalid.
*/
@Override
public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attrs) throws BadLocationException {
if (string == null) {
return;
}
// Check if the new total length exceeds the maximum length
if ((fb.getDocument().getLength() + string.length() - length) <= maxLength) {
super.replace(fb, offset, length, string, attrs);
}
}
/**
* Removes text from the document at the specified offset.
*
* @param fb The FilterBypass to use for the remove operation.
* @param offset The offset at which to start removing text.
* @param length The length of the text to remove.
* @throws BadLocationException If the remove position is invalid.
*/
@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
super.remove(fb, offset, length);
}
}
@@ -0,0 +1,61 @@
package rdiger36.StudioBridge.functions;
public class Models {
/**
* Converts model names to a specific format for sending to the printer.
*
* @param model The model name as selected by the user.
* @return The formatted model name.
*/
public static String getModelFromName(String model) {
switch (model) {
case "A1": model = "N2S"; break;
case "A1 Mini": model = "N1"; break;
case "A2L": model = "N9"; break;
case "P1P": model = "C11"; break;
case "P1S": model = "C12"; break;
case "P2S": model = "N7"; break;
case "X1": model = "3DPrinter-X1"; break;
case "X1C": model = "3DPrinter-X1-Carbon"; break;
case "X1E": model = "C13"; break;
case "X2D": model = "N6"; break;
case "H2C": model = "O1C"; break;
case "H2C (new revision)": model = "O1C2"; break;
case "H2D": model = "O1D"; break;
case "H2D Pro": model = "O1E"; break;
case "H2S": model = "O1S"; break;
default: break;
}
return model;
}
/**
* Maps model codes to model names.
*
* @param model The model code as a string.
* @return The corresponding model name.
*/
public static String getNameFromModel(String model) {
switch (model) {
case "N2S": model = "A1"; break;
case "N1": model = "A1 Mini"; break;
case "N9": model = "A2L"; break;
case "C11": model = "P1P"; break;
case "C12": model = "P1S"; break;
case "N7": model = "P2S"; break;
case "3DPrinter-X1": model = "X1"; break;
case "3DPrinter-X1-Carbon": model = "X1C"; break;
case "C13": model = "X1E"; break;
case "N6": model = "X2D"; break;
case "O1C": model = "H2C"; break;
case "O1C2": model = "H2C (new revision)"; break;
case "O1D": model = "H2D"; break;
case "O1E": model = "H2D Pro"; break;
case "O1S": model = "H2S"; break;
default: break;
}
return model;
}
}
@@ -0,0 +1,122 @@
package rdiger36.StudioBridge.functions;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import jnafilechooser.api.JnaFileChooser;
import jnafilechooser.api.JnaFileChooser.Mode;
import net.miginfocom.swing.MigLayout;
import rdiger36.StudioBridge.gui.MainMenu;
import rdiger36.StudioBridge.gui.SaveDialog;
/**
* A dialog for selecting and setting the path for saving profiles.
* <p>
* This dialog allows the user to choose a directory for storing profiles
* and reset the path to a default value.
* </p>
*/
public class ProfilesPath {
private JDialog dial;
private JTextField textField;
private String response;
private JButton btnReset;
private JButton btnChooseDir;
/**
* Constructs a ProfilesPath dialog.
*
* @param mainFrame The parent frame to center the dialog.
*/
public ProfilesPath(JFrame mainFrame) {
dial = new JDialog();
dial.setResizable(false);
dial.setTitle("StudioBridge");
dial.setIconImage(Toolkit.getDefaultToolkit().getImage(SaveDialog.class.getResource("/icon.png")));
dial.setModal(true);
dial.setAlwaysOnTop(true);
dial.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
response = "";
dial.dispose();
}
});
dial.setSize(369, 103);
if (mainFrame != null) {
dial.setLocationRelativeTo(mainFrame);
} else {
dial.setLocationRelativeTo(null);
}
JLabel lblPath = new JLabel("Path:");
lblPath.setHorizontalAlignment(SwingConstants.CENTER);
dial.getContentPane().setLayout(new MigLayout("", "[grow][grow][200px,grow][200px,grow][grow]", "[14px,grow][23px,grow]"));
dial.getContentPane().add(lblPath, "cell 0 0 2 1,alignx left,aligny center");
JButton btnSave = new JButton("Save");
btnSave.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
response = textField.getText().toString();
dial.dispose();
}
});
textField = new JTextField(MainMenu.ProfilesDir);
dial.getContentPane().add(textField, "cell 2 0 2 1,grow");
textField.setColumns(10);
btnChooseDir = new JButton("...");
dial.getContentPane().add(btnChooseDir, "cell 4 0,alignx right");
dial.getContentPane().add(btnSave, "cell 2 1,alignx right,aligny center");
btnReset = new JButton("Reset to default");
dial.getContentPane().add(btnReset, "cell 3 1");
btnReset.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
textField.setText(MainMenu.defaultSavePath + System.getProperty("file.separator") + "Profiles");
}
});
btnChooseDir.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JnaFileChooser fc = new JnaFileChooser();
fc.setMode(Mode.Directories);
fc.setTitle("Choose profile save directory");
boolean returnValue = fc.showOpenDialog(dial);
if (returnValue) {
textField.setText(fc.getSelectedFile().getAbsolutePath());
}
}
});
btnSave.setMinimumSize(btnReset.getPreferredSize());
dial.pack();
dial.setLocationRelativeTo(mainFrame);
}
/**
* Displays the dialog and returns the selected path.
*
* @return The selected path as a string.
*/
public String changePath() {
dial.setVisible(true); // Shows the dialog (blocking)
return response; // Returns the state of the dialog
}
}
@@ -0,0 +1,31 @@
package rdiger36.StudioBridge.functions;
import java.awt.Image;
import javax.swing.ImageIcon;
/**
* A utility class for resizing image icons.
* <p>
* This class provides methods to scale image icons to specified dimensions.
* </p>
*/
public class Resize {
/**
* Scales an ImageIcon to a new width and height.
*
* @param originalIcon The original ImageIcon to be scaled.
* @param width The new width of the scaled icon.
* @param height The new height of the scaled icon.
* @return The scaled ImageIcon.
*/
public static ImageIcon setNewImageIconSize(ImageIcon originalIcon, int width, int height) {
Image originalImage = originalIcon.getImage(); // Transform the ImageIcon to an Image
Image resizedImage = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH); // Scale the image
ImageIcon resizedIcon = new ImageIcon(resizedImage); // Transform back to an ImageIcon
return resizedIcon;
}
}
@@ -0,0 +1,189 @@
package rdiger36.StudioBridge.functions;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import rdiger36.StudioBridge.gui.DialogOneButton;
import rdiger36.StudioBridge.gui.MainMenu;
import rdiger36.StudioBridge.objects.Printer;
/**
* The UDPPackage class provides functionality to send UDP packets
* to a local Bambu Studio instance for 3D printers. It includes methods
* to create and send a specific UDP message containing printer details.
*
* <p>This class facilitates communication between the application
* and Bambu Studio, enabling the transmission of essential information
* about the connected 3D printer.</p>
*
* <h2>Methods:</h2>
* <ul>
* <li><code>send(JFrame, Printer, int, boolean)</code> - Sends a specific UDP data packet to the local Bambu Studio instance with the printer's details.</li>
* <li><code>getAvailableUDPPort()</code> - Retrieves the first available UDP port from the specified list of ports.</li>
* </ul>
*/
public class UDPPackage {
/**
* Sends a specific UDP data packet to the local Bambu Studio instance.
*
* @param mainFrame The main frame of the application, used to show dialogs.
* @param printer The Printer object with all necessary data.
* @param remoteUdpPort The correct port for BambuStudio.
* @param multiMode The option for single or multi mode.
*/
public static boolean send(JFrame mainFrame, Printer printer, int remoteUdpPort, boolean multiMode) {
Date date = new Date();
try {
// Create the message to be sent
String message =
"NOTIFY * HTTP/1.1\r\n" +
"Server: Buildroot/2018.02-rc3 UPnP/1.0 ssdpd/1.8\r\n" +
"Date: " + date.toString() + "\r\n" +
"Location: " + printer.getIpAdress() + "\r\n" +
"ST: urn:bambulab-com:device:3dprinter:1\r\n" +
"EXT:\r\n" +
"USN: " + printer.getSerialNumber() + "\r\n" +
"Cache-Control: max-age=1800\r\n" +
"DevModel.bambu.com: " + Models.getModelFromName(printer.getPrinterModel()) + "\r\n" +
"DevName.bambu.com: " + printer.getPrinterName() + "\r\n" +
"DevSignal.bambu.com: -44\r\n" +
"DevConnect.bambu.com: lan\r\n" +
"DevBind.bambu.com: free\r\n" +
"\r\n";
UDPPackage.startBroadcast(message);
if (!multiMode && mainFrame != null) new DialogOneButton(mainFrame, null, new ImageIcon(MainMenu.class.getResource("/success.png")), "<html>Package sent successfully.<br>The Printer will appear in the next 60 seconds in Bambu Studio.<br>You can close this Programm when your printer(s) appear in Bambu Studio</html>", "Ok").showDialog();
} catch (Exception e) {
// Show a warning that the package could not be sent to Bambu Studio
if (!multiMode && mainFrame != null) new DialogOneButton(mainFrame, null, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Warning! Failed to send the package!</html>", "Ok").showDialog();
return false;
}
return true;
}
/**
* Retrieves the first available UDP port from a list of specified ports (2021 or 1990).
* Uses `lsof` on macOS, and Java socket binding on other operating systems.
*
* @return the port number if available, or 0 if none are available.
*/
public static int getAvailableUDPPort() {
int[] portsToCheck = {2021, 1990};
String os = System.getProperty("os.name").toLowerCase();
for (int port : portsToCheck) {
boolean isInUse;
if (os.contains("mac")) {
// On macOS, check port using lsof
isInUse = isPortInUseWithLsof(port);
} else {
// On other systems, use socket-based port check
isInUse = isUDPPortInUse(port);
}
if (isInUse) {
return port; // Return the first available port
}
}
return 0; // Return 0 if no port is available
}
/**
* Checks if a specific UDP port is in use on macOS using the lsof command.
*
* @param port The UDP port number to check.
* @return true if the port is in use, false otherwise.
*/
private static boolean isPortInUseWithLsof(int port) {
try {
// Run lsof command to check if the port is in use
Process process = new ProcessBuilder("lsof", "-i", "udp:" + port).start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
return reader.readLine() != null; // If output is not null, port is in use
} catch (Exception e) {
return false;
}
}
/**
* Checks if a specific UDP port is in use using Java socket binding.
*
* @param port The UDP port number to check.
* @return true if the port is already in use, false otherwise.
*/
private static boolean isUDPPortInUse(int port) {
DatagramSocket socket = null;
try {
// Attempt to create a socket on the specified port
socket = new DatagramSocket(port);
return false; // Port is available
} catch (SocketException e) {
return true; // Port is already in use
} finally {
// Close the socket if it was successfully opened
if (socket != null && !socket.isClosed()) {
socket.close();
}
}
}
/**
* Starts a background thread that sends the given message as a UDP broadcast
* or to a specific destination address. The message is sent to multiple ports
* in 10 intervals.
*
* @param message the text message to send via UDP
*/
public static void startBroadcast(String message) {
new Thread(() -> {
try (DatagramSocket socket = new DatagramSocket()) {
// Default destination (configured in settings)
String destionation = MainMenu.destionation4UDP;
// Enable true broadcast mode when directMode is active
if (MainMenu.directMode) {
socket.setBroadcast(true);
destionation = "255.255.255.255";
}
// Ports to which the packet will be sent
int[] targetPorts = {2021, 1990};
// Send message 10 times in 5-second intervals
for (int i = 0; i < 10; i++) {
byte[] data = message.getBytes(StandardCharsets.US_ASCII);
InetAddress bcast = InetAddress.getByName(destionation);
// Send packet to each target port
for (int port : targetPorts) {
DatagramPacket p = new DatagramPacket(data, data.length, bcast, port);
socket.send(p);
}
Thread.sleep(5_000);
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
@@ -0,0 +1,96 @@
package rdiger36.StudioBridge.functions;
import java.awt.Desktop;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import org.json.JSONObject;
import rdiger36.StudioBridge.gui.DialogTwoButtons;
import rdiger36.StudioBridge.gui.MainMenu;
/**
* A utility class for checking and managing software updates for StudioBridge.
* This class retrieves the latest version of the application from GitHub
* and prompts the user to update if a new version is available.
*/
public class Update {
private static final String RELEASES_URL = "https://github.com/Rdiger-36/StudioBridge/releases/latest";
/**
* Checks if a new version of StudioBridge is available online.
*
* @param mainFrame The parent JFrame used for displaying dialogs.
*/
public static void checkVersion(JFrame mainFrame) {
// Retrieve only the release version info
String onlineVersion = getOnlineVersion();
if (onlineVersion != null) {
// Strip everything except digits — handles tag formats like
// "v.2.1.2", "Version 2.1.1", "2.1.0-BETA", etc.
String onlineVersionString = onlineVersion.replaceAll("[^0-9]", "");
// Convert version strings to integers for comparison
int online = Integer.parseInt(onlineVersionString);
int local = Integer.parseInt(MainMenu.version);
if (local < online) {
if (mainFrame != null) {
// Prompt user with update dialog
int response = new DialogTwoButtons(mainFrame, null,
new ImageIcon(MainMenu.class.getResource("/achtung.png")),
"An update for StudioBridge is available!",
"Open releases page",
"Continue without update").showDialog();
if (response == 0) {
try {
Desktop.getDesktop().browse(new URI(RELEASES_URL));
} catch (Exception e) {
e.printStackTrace();
}
}
} else {
System.out.println("Attention! A new update for StudioBridge is available:\r\n");
System.out.println(RELEASES_URL);
System.out.println();
}
}
}
}
/**
* Retrieves the latest release version from the GitHub API.
*
* @return The version name of the latest release, or null if an error occurs.
*/
private static String getOnlineVersion() {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/Rdiger-36/StudioBridge/releases/latest"))
.header("Accept", "application/vnd.github.v3+json")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JSONObject jsonResponse = new JSONObject(response.body());
return jsonResponse.getString("name");
}
return null;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
@@ -0,0 +1,105 @@
package rdiger36.StudioBridge.gui;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;
import net.miginfocom.swing.MigLayout;
import rdiger36.StudioBridge.functions.Resize;
/**
* The DialogOneButton class creates a modal dialog with a single button
* for user confirmation or information display. It can be used to present
* messages to the user and capture their response.
*
* <p>This class is designed to simplify the creation of dialogs in the
* application, providing a consistent look and feel.</p>
*/
public class DialogOneButton {
private boolean response; // Indicates whether the dialog was confirmed or canceled
private JDialog dial; // The dialog window
/**
* Constructs a new DialogOneButton instance.
*
* @param mainFrame The main frame of the application, used to center the dialog.
* @param mainDial An optional main dialog to center the new dialog relative to.
* @param icon An optional icon to display in the dialog.
* @param message The message to display in the dialog.
* @param buttonText The text for the button that closes the dialog.
*/
public DialogOneButton(JFrame mainFrame, JDialog mainDial, ImageIcon icon, String message, String buttonText) {
dial = new JDialog();
dial.setResizable(false);
dial.setTitle("StudioBridge");
dial.setIconImage(Toolkit.getDefaultToolkit().getImage(DialogOneButton.class.getResource("/icon.png")));
dial.setModal(true);
dial.setAlwaysOnTop(true);
// Close action for the dialog
dial.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
response = false;
dial.dispose();
}
});
dial.setSize(276, 167);
if (mainFrame != null) {
dial.setLocationRelativeTo(mainFrame);
} else if (mainDial != null) {
dial.setLocationRelativeTo(mainDial);
} else {
dial.setLocationRelativeTo(null);
}
// Create and configure the message label
JLabel label = new JLabel(message);
label.setHorizontalAlignment(SwingConstants.CENTER);
if (icon != null) {
label.setIconTextGap(15);
if (!icon.toString().endsWith(".gif")) {
label.setIcon(Resize.setNewImageIconSize(icon, 24, 24));
} else {
label.setIcon(icon);
}
}
// Layout configuration using MigLayout
dial.getContentPane().setLayout(new MigLayout("", "[grow][87px][][87px][grow]", "[14px,grow][10.00][23px,grow]"));
dial.getContentPane().add(label, "cell 0 0 5 1,alignx center,aligny center");
// Button to close the dialog
JButton btn1 = new JButton(buttonText);
btn1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
response = true; // Set response to true when button is clicked
dial.dispose(); // Close the dialog
}
});
dial.getContentPane().add(btn1, "cell 2 2,alignx center,aligny center");
if (buttonText.contains("NoButton")) btn1.setVisible(false); // Optionally hide button
dial.pack(); // Adjust dialog size to fit components
dial.setLocationRelativeTo(mainFrame); // Center dialog relative to the main frame
}
/**
* Displays the dialog and waits for the user to respond.
*
* @return true if the dialog was confirmed, false otherwise.
*/
public boolean showDialog() {
dial.setVisible(true); // Show the dialog (blocking)
return response; // Return the state of the dialog
}
}
@@ -0,0 +1,121 @@
package rdiger36.StudioBridge.gui;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingConstants;
import net.miginfocom.swing.MigLayout;
import rdiger36.StudioBridge.functions.Resize;
/**
* The DialogTwoButtons class creates a modal dialog with two buttons for user confirmation.
* It allows users to make a choice between two options.
*
* <p>This class provides a simple way to present decisions to users, enhancing interaction
* with the application.</p>
*/
public class DialogTwoButtons {
private int response; // Indicates which button was pressed
private JDialog dial; // The dialog window
/**
* Constructs a new DialogTwoButtons instance.
*
* @param mainFrame The main frame of the application, used to center the dialog.
* @param mainDial An optional main dialog to center the new dialog relative to.
* @param icon An optional icon to display in the dialog.
* @param message The message to display in the dialog.
* @param button1Text The text for the first button.
* @param button2Text The text for the second button.
*/
public DialogTwoButtons(JFrame mainFrame, JDialog mainDial, ImageIcon icon, String message, String button1Text, String button2Text) {
dial = new JDialog();
dial.setResizable(false);
dial.setTitle("StudioBridge");
dial.setIconImage(Toolkit.getDefaultToolkit().getImage(DialogTwoButtons.class.getResource("/icon.png")));
dial.setModal(true);
dial.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dial.setAlwaysOnTop(true);
// Close action for the dialog
dial.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
response = 99; // Set response to a default value when closed
dial.dispose();
}
});
dial.setSize(276, 167);
// Create and configure the message label
JLabel label = new JLabel(message);
label.setHorizontalAlignment(SwingConstants.CENTER);
if (icon != null) {
label.setIconTextGap(15);
label.setIcon(Resize.setNewImageIconSize(icon, 24, 24));
}
// Button 1 configuration
JButton btn1 = new JButton(button1Text);
btn1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
response = 0; // Set response to 0 for the first button
dial.dispose(); // Close the dialog
}
});
// Button 2 configuration
JButton btn2 = new JButton(button2Text);
btn2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
response = 1; // Set response to 1 for the second button
dial.dispose(); // Close the dialog
}
});
// Layout configuration using MigLayout
dial.getContentPane().setLayout(new MigLayout("", "[grow][87px][87px][grow]", "[14px,grow][10.00][23px,grow]"));
dial.getContentPane().add(label, "cell 0 0 4 1,alignx center,aligny center");
dial.getContentPane().add(btn1, "cell 1 2,alignx center,aligny center");
dial.getContentPane().add(btn2, "cell 2 2,alignx center,aligny center");
// Ensure both buttons have the same minimum size
if (btn1.getText().length() > btn2.getText().length()) {
btn2.setMinimumSize(btn1.getPreferredSize());
} else {
btn1.setMinimumSize(btn2.getPreferredSize());
}
dial.pack(); // Adjust dialog size to fit components
// Center dialog relative to the main frame or another dialog
if (mainFrame != null) {
dial.setLocationRelativeTo(mainFrame);
} else if (mainDial != null) {
dial.setLocationRelativeTo(mainDial);
} else {
dial.setLocationRelativeTo(null);
}
}
/**
* Displays the dialog and waits for the user to respond.
*
* @return An integer indicating which button was pressed:
* 0 for the first button, 1 for the second button,
* or 99 if the dialog was closed without selection.
*/
public int showDialog() {
dial.setVisible(true); // Show the dialog (blocking)
return response; // Return the state of the dialog
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,242 @@
package rdiger36.StudioBridge.gui;
import java.awt.Dialog.ModalExclusionType;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import net.miginfocom.swing.MigLayout;
import rdiger36.StudioBridge.functions.*;
import rdiger36.StudioBridge.objects.Printer;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JProgressBar;
/**
* Class responsible for managing the setup of multiple printers within a GUI.
* Provides functionality to import printers, select/deselect them, and send configuration packages.
*/
public class MultiPrinterSetup {
private JTable table;
private volatile boolean stopThread = false;
private volatile boolean printerSelected = false;
private static ArrayList<String> errors = new ArrayList<>();
/**
* Constructor that initializes the MultiPrinterSetup dialog.
*
* @param frmStudioBridge The parent JFrame for positioning the dialog.
*/
public MultiPrinterSetup(JFrame frmStudioBridge) {
JDialog dialInfo = new JDialog();
dialInfo.setModalExclusionType(ModalExclusionType.APPLICATION_EXCLUDE);
dialInfo.setModal(true);
dialInfo.setAlwaysOnTop(true);
dialInfo.setTitle("Multiple Printer Setup");
dialInfo.setIconImage(Toolkit.getDefaultToolkit().getImage(MultiPrinterSetup.class.getResource("/icon.png")));
dialInfo.setBounds(100, 100, 602, 482);
dialInfo.toFront();
dialInfo.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
stopThread = true;
dialInfo.dispose();
}
});
JScrollPane scrollPane = new JScrollPane();
table = new JTable();
table.setFocusTraversalKeysEnabled(false);
table.setFocusable(false);
table.setModel(new DefaultTableModel(
new Object[][] {},
new String[] { "Profil", "Name", "IP-Address", "Serial", "Model", "Send Package" }
) {
Class<?>[] columnTypes = new Class[] {
Object.class, Object.class, Object.class, Object.class, Object.class, Boolean.class
};
public Class<?> getColumnClass(int columnIndex) {
return columnTypes[columnIndex];
}
});
// Set preferred column widths
table.getColumnModel().getColumn(0).setPreferredWidth(110);
table.getColumnModel().getColumn(1).setPreferredWidth(120);
table.getColumnModel().getColumn(2).setPreferredWidth(164);
table.getColumnModel().getColumn(3).setPreferredWidth(159);
table.getColumnModel().getColumn(4).setPreferredWidth(108);
table.getColumnModel().getColumn(5).setPreferredWidth(93);
scrollPane.setViewportView(table);
dialInfo.getContentPane().setLayout(new MigLayout("", "[188.00,grow][223.00,grow][390.00][]", "[grow][bottom][]"));
dialInfo.getContentPane().add(scrollPane, "cell 0 0 4 1,grow");
importPrinters(table);
// Adjust scroll pane size based on content
int maxVisibleRows = 10;
int rowHeight = table.getRowHeight();
int tableHeaderHeight = table.getTableHeader().getPreferredSize().height;
int visibleRows = Math.min(table.getRowCount(), maxVisibleRows);
int preferredHeight = (rowHeight * visibleRows) + tableHeaderHeight + 5;
int totalColumnWidth = 0;
for (int col = 0; col < table.getColumnCount(); col++) {
totalColumnWidth += table.getColumnModel().getColumn(col).getPreferredWidth();
}
int padding = 30;
scrollPane.setPreferredSize(new Dimension(totalColumnWidth + padding, preferredHeight));
JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
progressBar.setVisible(false);
progressBar.setStringPainted(true);
dialInfo.getContentPane().add(progressBar, "cell 1 1,grow");
JButton btnSendPrinters = new JButton("Send selected Printers");
btnSendPrinters.addActionListener(e -> sendSelectedPrinters(frmStudioBridge, dialInfo, progressBar));
dialInfo.getContentPane().add(btnSendPrinters, "cell 0 1,grow");
JButton btnSelAll = new JButton("Select All");
btnSelAll.addActionListener(e -> setAllBooleans(true));
dialInfo.getContentPane().add(btnSelAll, "cell 2 1,alignx right");
JButton btnDeSelAll = new JButton("Deselect All");
btnDeSelAll.addActionListener(e -> setAllBooleans(false));
dialInfo.getContentPane().add(btnDeSelAll, "cell 3 1,alignx left");
dialInfo.requestFocus();
dialInfo.pack();
dialInfo.setLocationRelativeTo(frmStudioBridge);
dialInfo.setVisible(true);
}
/**
* Imports printer configurations from files and populates the JTable.
*
* @param table The JTable to populate with printer data.
*/
private void importPrinters(JTable table) {
DefaultTableModel model = (DefaultTableModel) table.getModel();
for (Printer printer : MainMenu.printerMap.values()) {
model.addRow(new Object[]{printer.getProfileName(), printer.getPrinterName(), printer.getIpAdress(), printer.getSerialNumber(), printer.getPrinterModel(), false});
}
}
/**
* Selects or deselects all printers in the table.
*
* @param value true to select all, false to deselect all.
*/
private void setAllBooleans(boolean value) {
DefaultTableModel model = (DefaultTableModel) table.getModel();
for (int i = 0; i < model.getRowCount(); i++) {
model.setValueAt(value, i, 5);
}
}
/**
* Handles the process of sending selected printers' data.
*
* @param frmStudioBridge The parent JFrame.
* @param dialInfo The dialog interface.
* @param progressBar The progress bar to indicate sending status.
*/
private void sendSelectedPrinters(JFrame frmStudioBridge, JDialog dialInfo, JProgressBar progressBar) {
// This method handles the sending logic for selected printers.
stopThread = false;
errors.clear();
Thread sendingThread = new Thread(() -> {
progressBar.setString("Check Printers...");
progressBar.setVisible(true);
printerSelected = false;
// Determine which UDP port (2021 or 1990) is in use, or set to 0 if none is available
int remoteUdpPort = UDPPackage.getAvailableUDPPort();
// Check if a valid port was assigned to remoteUdpPort
if (remoteUdpPort > 0) {
DefaultTableModel model = (DefaultTableModel) table.getModel();
int rowCount = model.getRowCount();
for (int i = 0; i < rowCount && !stopThread; i++) {
Boolean shouldSend = (Boolean) model.getValueAt(i, 5);
if (shouldSend != null && shouldSend) {
printerSelected = true;
Printer printer = new Printer((String) model.getValueAt(i, 0), (String) model.getValueAt(i, 1), (String) model.getValueAt(i, 2), (String) model.getValueAt(i, 3), (String) model.getValueAt(i, 4));
SwingUtilities.invokeLater(() -> progressBar.setString("Send Package for " + printer.getPrinterName()));
if (!UDPPackage.send(frmStudioBridge, printer, remoteUdpPort, true)) {
errors.add(printer.getPrinterName() + " - " + printer.getIpAdress() + " - " + printer.getPrinterModel());
}
try {
Thread.sleep(1000); // Simulierte Verzögerung
} catch (InterruptedException e) {
// Thread wurde unterbrochen, daher beenden wir den Prozess
Thread.currentThread().interrupt();
break;
}
}
}
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
if (!stopThread) {
dialInfo.setModal(false);
if (printerSelected) {
if (!errors.isEmpty()) {
String errorsOn = "";
for (String error : errors) {
errorsOn = errorsOn + error + "<br>";
}
new DialogOneButton(null, dialInfo, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Attention! Following printers could not sent :<br>" + errorsOn + "</html>", "Ok").showDialog();
} else {
new DialogOneButton(null, dialInfo, new ImageIcon(MainMenu.class.getResource("/success.png")), "<html>Packages sent successfully.<br>The Printer(s) will appear in the next 60 seconds in Bambu Studio.<br>You can close this Programm when your printer(s) appear in Bambu Studio</html>", "Ok").showDialog();
}
} else {
new DialogOneButton(null, dialInfo, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Attention! Please select at least one printer!</html>", "Back").showDialog();
}
dialInfo.setModal(true);
}
});
} else {
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
if (!stopThread) {
dialInfo.setModal(false);
new DialogOneButton(null, dialInfo, new ImageIcon(MainMenu.class.getResource("/achtung.png")), "<html>Warning! Bambu Studio is not running!</html>", "Ok").showDialog();
dialInfo.setModal(true);
}
});
}
});
sendingThread.start();
}
}
@@ -0,0 +1,192 @@
package rdiger36.StudioBridge.gui;
import java.awt.Toolkit;
import java.awt.Dialog.ModalExclusionType;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JFrame;
import net.miginfocom.swing.MigLayout;
import rdiger36.StudioBridge.functions.*;
import javax.swing.JLabel;
import javax.swing.JSeparator;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.JButton;
/**
* A dialog window that checks the connectivity of a printer using ping and
* a TCP/TLS handshake test. Both checks are executed in background threads,
* and the results are displayed in the dialog.
*/
public class PrinterCheck {
// Prevents multiple scans from running at the same time
boolean isRunning = false;
/**
* Creates and displays a modal dialog that performs connectivity checks
* for a printer at the given IP address.
*
* @param frmStudioBridge the parent frame used for positioning the dialog
* @param IPAddress the IP address of the printer to test
*/
public PrinterCheck(JFrame frmStudioBridge, String IPAddress) {
// Create dialog
JDialog dialPrinter = new JDialog();
dialPrinter.setResizable(false);
dialPrinter.setModalExclusionType(ModalExclusionType.APPLICATION_EXCLUDE);
dialPrinter.setModal(true);
dialPrinter.setAlwaysOnTop(true);
dialPrinter.setTitle("Check Printer Connectivity");
dialPrinter.setIconImage(Toolkit.getDefaultToolkit().getImage(MultiPrinterSetup.class.getResource("/icon.png")));
dialPrinter.setBounds(100, 100, 602, 482);
dialPrinter.toFront();
// Close dialog on window close
dialPrinter.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dialPrinter.dispose();
}
});
dialPrinter.getContentPane().setLayout(new MigLayout("", "[][]", "[][10px:10px:10px][][10px:10px:10px][][][][]"));
// Intro text
JLabel lblInfo = new JLabel("<html>Here you can verify whether your printer is reachable from your machine and whether"
+ "<br>it accepts TCP connections on port 6000. If both tests succeed, everything is working correctly."
+ "<br>If either test fails, you should review your network settings. If the TCP handshake fails specifically,"
+ "<br>Bambu Studio often displays a \"Connection Failed Error 1\" message.</html>");
dialPrinter.getContentPane().add(lblInfo, "cell 0 0 2 1");
// Separator line
JSeparator separator = new JSeparator();
dialPrinter.getContentPane().add(separator, "cell 0 1 2 1,growx,aligny center");
// IP information
JLabel lblInfoIP = new JLabel("Connectivity check for the printer witc the IP-Address: " + IPAddress);
dialPrinter.getContentPane().add(lblInfoIP, "cell 0 2 2 1,alignx center,aligny top");
// Ping test label
JLabel lblPing = new JLabel("Is printer reachable?");
dialPrinter.getContentPane().add(lblPing, "cell 0 4");
// Ping test status
JLabel lblPingState = new JLabel("Please wait...");
lblPingState.setIcon(new ImageIcon(Toolkit.getDefaultToolkit().getImage(MultiPrinterSetup.class.getResource("/load.gif"))));
lblPingState.setHorizontalTextPosition(SwingConstants.LEFT);
lblPingState.setVerticalTextPosition(SwingConstants.CENTER);
dialPrinter.getContentPane().add(lblPingState, "cell 1 4");
// Handshake test label
JLabel lblHandshake = new JLabel("TCP handshake ");
dialPrinter.getContentPane().add(lblHandshake, "cell 0 5");
// Handshake test status
JLabel lblHandshakeState = new JLabel("Please wait...");
lblHandshakeState.setIcon(new ImageIcon(Toolkit.getDefaultToolkit().getImage(MultiPrinterSetup.class.getResource("/load.gif"))));
lblHandshakeState.setHorizontalTextPosition(SwingConstants.LEFT);
lblHandshakeState.setVerticalTextPosition(SwingConstants.CENTER);
dialPrinter.getContentPane().add(lblHandshakeState, "cell 1 5");
// Retry button
JButton btnRetry = new JButton("Retry");
dialPrinter.getContentPane().add(btnRetry, "cell 0 7 2 1,alignx center");
// Start scan automatically when dialog opens
dialPrinter.addWindowListener(new WindowAdapter() {
@Override public void windowOpened(WindowEvent e) {
new SwingWorker<Void, Void>() {
@Override protected Void doInBackground() {
startScan(IPAddress, lblPingState, lblHandshakeState);
return null;
}
}.execute();
}
});
// Retry button action
btnRetry.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
// Make sure no scan is running
if (!isRunning)
new Thread(() -> startScan(IPAddress, lblPingState, lblHandshakeState)).start();
}
});
dialPrinter.requestFocus();
dialPrinter.pack();
dialPrinter.setLocationRelativeTo(frmStudioBridge);
dialPrinter.setVisible(true);
}
/**
* Executes a ping check and a TLS handshake against the given printer IP
* and updates the provided label components with the results.
*
* @param IPAddress the printer's IP address
* @param lblPingState label that displays ping test results
* @param lblHandshakeState label that displays handshake results
*/
private void startScan(String IPAddress, JLabel lblPingState, JLabel lblHandshakeState) {
isRunning = true;
// Set both indicators to "loading"
SwingUtilities.invokeLater(() -> {
lblPingState.setText("Please wait...");
lblPingState.setIcon(new ImageIcon(Toolkit.getDefaultToolkit().getImage(MultiPrinterSetup.class.getResource("/load.gif"))));
lblHandshakeState.setText("Please wait...");
lblHandshakeState.setIcon(new ImageIcon(Toolkit.getDefaultToolkit().getImage(MultiPrinterSetup.class.getResource("/load.gif"))));
});
// Execute ping test
if (Connectivity.pingPrinter(IPAddress)) {
// Ping successful
SwingUtilities.invokeLater(() -> {
lblPingState.setText("Printer is reachable from your computer!");
lblPingState.setIcon(Resize.setNewImageIconSize(new ImageIcon(ToolInfo.class.getResource("/success.png")), 16, 16));
});
// Execute handshake test
if (Connectivity.checkTLSHandshake(IPAddress)) {
// Handshake successful
SwingUtilities.invokeLater(() -> {
lblHandshakeState.setText("Handshake was sucessfully between computer and printer!");
lblHandshakeState.setIcon(Resize.setNewImageIconSize(new ImageIcon(ToolInfo.class.getResource("/success.png")), 16, 16));
});
} else {
// Handshake failed
SwingUtilities.invokeLater(() -> {
lblHandshakeState.setText("Error on Handshake between computer and printer!");
lblHandshakeState.setIcon(Resize.setNewImageIconSize(new ImageIcon(ToolInfo.class.getResource("/delete.png")), 16, 16));
});
}
} else {
// Printer not reachable
SwingUtilities.invokeLater(() -> {
lblPingState.setText("Printer is not reachable from your computer!");
lblPingState.setIcon(Resize.setNewImageIconSize(new ImageIcon(ToolInfo.class.getResource("/delete.png")), 16, 16));
lblHandshakeState.setText("Printer is not reachable from your computer!");
lblHandshakeState.setIcon(Resize.setNewImageIconSize(new ImageIcon(ToolInfo.class.getResource("/delete.png")), 16, 16));
});
}
isRunning = false;
}
}
@@ -0,0 +1,93 @@
package rdiger36.StudioBridge.gui;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.text.AbstractDocument;
import net.miginfocom.swing.MigLayout;
import rdiger36.StudioBridge.functions.LengthRestrictedDocumentFilter;
/**
* A dialog for saving a profile name.
* <p>
* This class creates a modal dialog that prompts the user to enter a profile name
* and provides a save button to confirm the action.
* </p>
*/
public class SaveDialog {
private JDialog dial;
private JTextField textField;
private String response;
/**
* Constructs a SaveDialog instance.
*
* @param mainFrame The parent JFrame for positioning the dialog.
* @param profileName The default profile name to display in the text field.
* @param btnText The String that is set for the Button.
*/
public SaveDialog(JFrame mainFrame, String profileName, String btnText) {
dial = new JDialog();
dial.setResizable(false);
dial.setTitle("StudioBridge");
dial.setIconImage(Toolkit.getDefaultToolkit().getImage(SaveDialog.class.getResource("/icon.png")));
dial.setModal(true);
dial.setAlwaysOnTop(true);
dial.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
response = null;
dial.dispose();
}
});
dial.setSize(276, 167);
if (mainFrame != null) {
dial.setLocationRelativeTo(mainFrame);
} else {
dial.setLocationRelativeTo(null);
}
JLabel lblProfilename = new JLabel("Profilename:");
lblProfilename.setHorizontalAlignment(SwingConstants.CENTER);
dial.getContentPane().setLayout(new MigLayout("", "[grow][87px][grow][87px][grow]", "[14px,grow][23px,grow]"));
dial.getContentPane().add(lblProfilename, "cell 0 0 2 1,alignx center,aligny center");
JButton btn1 = new JButton(btnText);
btn1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
response = textField.getText();
dial.dispose();
}
});
textField = new JTextField(profileName);
dial.getContentPane().add(textField, "cell 2 0 2 1,growx");
textField.setColumns(10);
((AbstractDocument) textField.getDocument()).setDocumentFilter(new LengthRestrictedDocumentFilter(25));
dial.getContentPane().add(btn1, "cell 2 1,alignx center,aligny center");
dial.pack();
dial.setLocationRelativeTo(mainFrame);
}
/**
* Displays the dialog and waits for the user to enter a profile name.
*
* @return The profile name entered by the user.
*/
public String saveProfile() {
dial.setVisible(true); // Show the dialog (blocking)
return response; // Return the dialog's response
}
}
@@ -0,0 +1,141 @@
package rdiger36.StudioBridge.gui;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.Dialog.ModalExclusionType;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.net.URI;
import javax.swing.GroupLayout;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSeparator;
import javax.swing.LayoutStyle;
import javax.swing.SwingConstants;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import rdiger36.StudioBridge.functions.Resize;
/**
* A dialog displaying information about the StudioBridge application.
* <p>
* This class creates a modal dialog that presents the developer's information,
* GitHub link, and icon source.
* </p>
*/
public class ToolInfo {
/**
* Constructs a ToolInfo dialog.
*
* @param frmBambuConnect The parent JFrame for positioning the dialog.
*/
public ToolInfo(JFrame frmBambuConnect) {
JDialog dialInfo = new JDialog();
dialInfo.setModalExclusionType(ModalExclusionType.APPLICATION_EXCLUDE);
dialInfo.setModal(true);
dialInfo.setAlwaysOnTop(true);
dialInfo.setTitle("StudioBridge Info");
dialInfo.setResizable(false);
dialInfo.setIconImage(Toolkit.getDefaultToolkit().getImage(ToolInfo.class.getResource("/icon.png")));
dialInfo.setBounds(100, 100, 386, 250);
dialInfo.toFront();
dialInfo.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dialInfo.dispose();
}
});
JEditorPane dtrpnDeveloper = new JEditorPane("text/html", "<html><body>\r\n" +
"Developer: Niklas (Rdiger-36)<br>\r\n" +
"GitHub: <a href=\"https://github.com/Rdiger-36/StudioBridge\">StudioBridge Repo</a><br>\r\n" +
"<br>\r\n" +
"Icon Source: <a href=\"https://www.flaticon.com/de/kostenlose-icons/3d-drucker\">Freepik - Flaticon</a>\r\n" +
"<br>\r\n" +
"<br>\r\n" +
"</body></html>");
dtrpnDeveloper.setEditable(false);
dtrpnDeveloper.setOpaque(false);
dtrpnDeveloper.setBorder(null);
dtrpnDeveloper.setFont(new Font("Segoe UI", Font.PLAIN, 14));
dtrpnDeveloper.setCaretColor(dtrpnDeveloper.getBackground());
// Hyperlink listener for handling clicks on links in the JEditorPane
dtrpnDeveloper.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (e.getEventType() == HyperlinkEvent.EventType.ENTERED) {
dtrpnDeveloper.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else if (e.getEventType() == HyperlinkEvent.EventType.EXITED) {
dtrpnDeveloper.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
} else if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
try {
String clickedText = dtrpnDeveloper.getDocument().getText(
e.getSourceElement().getStartOffset(),
e.getSourceElement().getEndOffset() - e.getSourceElement().getStartOffset());
if (clickedText.equals("StudioBridge Repo")) {
clickedText = "https://github.com/Rdiger-36/StudioBridge";
} else if (clickedText.equals("Freepik - Flaticon")) {
clickedText = "https://www.flaticon.com/de/kostenlose-icons/3d-drucker";
}
Desktop.getDesktop().browse(new URI(clickedText));
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
});
JLabel lbueberschrift = new JLabel("StudioBridge v" + MainMenu.version.substring(0, 1) + "." +
MainMenu.version.substring(1, 2) + "." + MainMenu.version.substring(2, 3));
lbueberschrift.setHorizontalAlignment(SwingConstants.CENTER);
lbueberschrift.setIconTextGap(25);
lbueberschrift.setForeground(new Color(229, 152, 67));
lbueberschrift.setFont(new Font("Segoe UI", Font.BOLD, 16));
ImageIcon icon = Resize.setNewImageIconSize(new ImageIcon(ToolInfo.class.getResource("/icon.png")), 64, 64);
lbueberschrift.setIcon(icon);
JSeparator separator = new JSeparator();
GroupLayout groupLayout = new GroupLayout(dialInfo.getContentPane());
groupLayout.setHorizontalGroup(
groupLayout.createParallelGroup(GroupLayout.Alignment.TRAILING)
.addGroup(groupLayout.createSequentialGroup()
.addContainerGap()
.addGroup(groupLayout.createParallelGroup(GroupLayout.Alignment.TRAILING)
.addComponent(lbueberschrift, GroupLayout.Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 243, Short.MAX_VALUE)
.addComponent(separator, GroupLayout.Alignment.LEADING, GroupLayout.DEFAULT_SIZE, 243, Short.MAX_VALUE)
.addGroup(groupLayout.createSequentialGroup()
.addGap(10)
.addComponent(dtrpnDeveloper, GroupLayout.DEFAULT_SIZE, 243, Short.MAX_VALUE)))
.addContainerGap())
);
groupLayout.setVerticalGroup(
groupLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(groupLayout.createSequentialGroup()
.addContainerGap()
.addComponent(lbueberschrift, GroupLayout.PREFERRED_SIZE, 91, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(separator, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
.addComponent(dtrpnDeveloper)
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED))
);
dialInfo.requestFocus();
dialInfo.getContentPane().setLayout(groupLayout);
dialInfo.pack();
dialInfo.setLocationRelativeTo(frmBambuConnect);
dialInfo.setVisible(true);
}
}
@@ -0,0 +1,55 @@
package rdiger36.StudioBridge.gui;
import javax.swing.UIManager;
import com.formdev.flatlaf.FlatLaf;
/**
* A utility class for managing the Look and Feel (LAF) of the user interface.
* This class provides methods to change the appearance of the application
* to a dark mode or revert to the system default.
*/
public class UI {
/**
* Changes the Look and Feel of the application based on the specified mode.
*
* @param mode A boolean value indicating whether to switch to dark mode (true)
* or to revert to the system's default Look and Feel (false).
*/
public static void changeLAF(boolean mode) {
try {
FlatLaf.setUseNativeWindowDecorations(true); // Use native decorations for windows
if (mode) {
// Set the Look and Feel to a dark theme using FlatLaf
if (MainMenu.localMachine.getOs().equals("macos")) {
UIManager.setLookAndFeel("com.formdev.flatlaf.themes.FlatMacDarkLaf");
} else {
UIManager.setLookAndFeel("com.formdev.flatlaf.FlatDarkLaf");
}
} else {
// Revert to the system's default Look and Feel
if (MainMenu.localMachine.getOs().equals("macos")) {
UIManager.setLookAndFeel("com.formdev.flatlaf.themes.FlatMacLightLaf");
} else {
UIManager.setLookAndFeel("com.formdev.flatlaf.FlatLightLaf");
}
}
// Configure additional UI properties for the dark theme
// UIManager.put("TitlePane.menuBarEmbedded", true);
// UIManager.put("TitlePane.unifiedBackground", true);
UIManager.put("MenuItem.selectionType", "underline");
FlatLaf.updateUI(); // Refresh the UI to apply changes
} catch (Exception e1) {
// Print stack trace if an exception occurs during Look and Feel change
e1.printStackTrace();
}
}
}
@@ -0,0 +1,109 @@
package rdiger36.StudioBridge.objects;
/**
* Represents information about the local machine, such as operating system,
* architecture, OS version, and data model (32/64 bit). The values are normalized
* for consistent use across the application.
*/
public class LocalMachine {
private final String os;
private final String osArch;
private final String osVersion;
private final String dataModel;
/**
* Creates a new {@code LocalMachine} instance by gathering system properties
* and normalizing them for unified representation.
*/
public LocalMachine() {
// Normalize OS name
this.os = simplifyOS(System.getProperty("os.name"));
// Normalize OS architecture
this.osArch = normalizeArch(System.getProperty("os.arch"));
// Raw OS version
this.osVersion = System.getProperty("os.version");
// JVM data model (e.g., 32 or 64)
this.dataModel = System.getProperty("sun.arch.data.model", "?");
}
/**
* Converts raw architecture strings into a consistent format.
*
* @param raw the architecture string from system properties
* @return normalized architecture identifier
*/
private String normalizeArch(String raw) {
if (raw == null) return "unknown";
String lower = raw.toLowerCase();
switch (lower) {
case "amd64":
case "x86_64":
return "x86_64";
case "aarch64":
case "arm64":
return "arm64";
case "x86":
case "i386":
case "i686":
return "x86";
default:
return lower; // fallback for uncommon architectures
}
}
/**
* Maps OS names to simplified identifiers (windows, macos, linux).
*
* @param raw the OS name from system properties
* @return normalized OS identifier
*/
private String simplifyOS(String raw) {
if (raw == null) return "unknown";
String lower = raw.toLowerCase();
if (lower.startsWith("win")) return "windows";
else if (lower.startsWith("mac")) return "macos";
else if (lower.startsWith("linux")) return "linux";
else return lower; // fallback
}
/**
* @return simplified OS name (windows, macos, linux, …)
*/
public String getOs() {
return os;
}
/**
* @return normalized CPU architecture (x86, x86_64, arm64, …)
*/
public String getOsArch() {
return osArch;
}
/**
* @return raw OS version string
*/
public String getOsVersion() {
return osVersion;
}
/**
* @return data model (typically 32 or 64)
*/
public String getDataModel() {
return dataModel;
}
/**
* Returns a formatted summary of the machine information.
*/
@Override
public String toString() {
return String.format("OS=%s %s, ARCH=%s, Model=%s-bit",
os, osVersion, osArch, dataModel);
}
}
@@ -0,0 +1,88 @@
package rdiger36.StudioBridge.objects;
/**
* Represents a printer profile containing the printer's identifying information
* such as profile name, printer name, IP address, serial number and model.
*
* <p>This class is used throughout the application to store profile data and
* synchronize UI fields with configuration files.</p>
*/
public class Printer {
// Basic profile properties
private String profileName;
private String printerName;
private String ipAdress;
private String serialNumber;
private String printerModel;
/**
* Creates a new {@code Printer} instance with the provided profile data.
*
* @param profileName the name of the printer profile
* @param printerName the human-readable name of the printer
* @param ipAdress the printer's network IP address
* @param serialNumber the unique printer serial number
* @param printerModel the printer model identifier
*/
public Printer(String profileName, String printerName,
String ipAdress, String serialNumber, String printerModel) {
this.profileName = profileName;
this.printerName = printerName;
this.ipAdress = ipAdress;
this.serialNumber = serialNumber;
this.printerModel = printerModel;
}
// ------------------------ Getter / Setter ------------------------
/** @return the name of the printer profile */
public String getProfileName() { return profileName; }
/** Sets the profile name. */
public void setProfileName(String profileName) {
this.profileName = profileName;
}
/** @return the human-readable name of the printer */
public String getPrinterName() { return printerName; }
/** Sets the printer's display name. */
public void setPrinterName(String printerName) {
this.printerName = printerName;
}
/** @return the printer's IP address */
public String getIpAdress() { return ipAdress; }
/** Sets the printer's IP address. */
public void setIpAdress(String ipAdress) {
this.ipAdress = ipAdress;
}
/** @return the printer's serial number */
public String getSerialNumber() { return serialNumber; }
/** Sets the printer's serial number. */
public void setSerialNumber(String serialNumber) {
this.serialNumber = serialNumber;
}
/** @return the model identifier of the printer */
public String getPrinterModel() { return printerModel; }
/** Sets the model identifier of the printer. */
public void setPrinterModel(String printerModel) {
this.printerModel = printerModel;
}
/**
* Returns a formatted string representing the printer profile.
*/
@Override
public String toString() {
return String.format("Profile=%s, Name=%s, IP-Address=%s, Serialnumber=%s, Model=%s",
profileName, printerName, ipAdress, serialNumber, printerModel);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,2 @@
greeting=Hallo Welt
exit=Beenden
@@ -0,0 +1,2 @@
greeting=Hello World
exit=Exit
Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

@@ -0,0 +1,38 @@
package rdiger36.StudioBridge;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}
@@ -0,0 +1,84 @@
package rdiger36.StudioBridge.gui;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
/**
* Tests for the remapping of profile paths that were stored in the settings file
* before the data directory was renamed from "StudioBridge" to ".studio-bridge".
*/
public class MainMenuTest {
private static final String SEP = System.getProperty("file.separator");
private static final String LEGACY_DIR = SEP + "home" + SEP + "user" + SEP + "StudioBridge";
private static final String CURRENT_DIR = SEP + "home" + SEP + "user" + SEP + ".studio-bridge";
/**
* The default profiles directory of an old installation has to end up in the new
* data directory. This is the case reported in issue #25.
*/
@Test
public void remapsDefaultProfilesDirectoryOfOldInstallations() {
assertEquals(CURRENT_DIR + SEP + "Profiles",
MainMenu.remapLegacyProfilesPath(LEGACY_DIR + SEP + "Profiles", LEGACY_DIR, CURRENT_DIR));
}
/**
* A path pointing at the old data directory itself is remapped to the new one.
*/
@Test
public void remapsLegacyDirectoryItself() {
assertEquals(CURRENT_DIR,
MainMenu.remapLegacyProfilesPath(LEGACY_DIR, LEGACY_DIR, CURRENT_DIR));
}
/**
* Sub directories keep their relative position, because the directory migration
* merges them recursively.
*/
@Test
public void keepsRelativePositionOfNestedDirectories() {
assertEquals(CURRENT_DIR + SEP + "My" + SEP + "Profiles",
MainMenu.remapLegacyProfilesPath(LEGACY_DIR + SEP + "My" + SEP + "Profiles", LEGACY_DIR, CURRENT_DIR));
}
/**
* A directory the user selected somewhere else must not be touched.
*/
@Test
public void keepsCustomDirectoriesOutsideTheLegacyDirectory() {
String custom = SEP + "home" + SEP + "user" + SEP + "Documents" + SEP + "Profiles";
assertEquals(custom, MainMenu.remapLegacyProfilesPath(custom, LEGACY_DIR, CURRENT_DIR));
}
/**
* A directory that only shares its prefix with the old data directory is not a match.
*/
@Test
public void doesNotMatchSiblingDirectoriesSharingThePrefix() {
String sibling = LEGACY_DIR + "Backup" + SEP + "Profiles";
assertEquals(sibling, MainMenu.remapLegacyProfilesPath(sibling, LEGACY_DIR, CURRENT_DIR));
}
/**
* Running the remapping again on an already migrated path changes nothing, so it is
* safe to execute it on every start.
*/
@Test
public void isIdempotentForAlreadyMigratedPaths() {
String migrated = CURRENT_DIR + SEP + "Profiles";
assertEquals(migrated, MainMenu.remapLegacyProfilesPath(migrated, LEGACY_DIR, CURRENT_DIR));
}
/**
* A missing path must not cause a failure during startup.
*/
@Test
public void keepsNullPathUntouched() {
assertEquals(null, MainMenu.remapLegacyProfilesPath(null, LEGACY_DIR, CURRENT_DIR));
}
}