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
@@ -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>
|
||||
@@ -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']
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
main-jar=StudioBridge.jar
|
||||
java-options=-Xmx512m
|
||||
@@ -0,0 +1,5 @@
|
||||
main-jar=StudioBridge.jar
|
||||
java-options=-Xmx512m
|
||||
win-console=true
|
||||
win-shortcut=false
|
||||
win-menu=false
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -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:
|
||||
- "*"
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.project
|
||||
.settings
|
||||
target/
|
||||
.idea/
|
||||
@@ -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>.
|
||||
@@ -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
|
||||
|
||||
 
|
||||
|
||||
|
||||
Darkmode
|
||||
|
||||
 
|
||||
|
||||
Multiple Printer Setup
|
||||
|
||||

|
||||

|
||||
|
||||
Printer Check
|
||||
|
||||

|
||||
|
||||
If TCP Handshake is not working, then it helps to restart your printer.
|
||||
|
||||
# Demo
|
||||
|
||||

|
||||
|
||||
|
||||
# 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 system’s default settings for opening .jar files.
|
||||
|
||||
# Support Me
|
||||
[](https://www.buymeacoffee.com/Rdiger36)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 340 B |
|
After Width: | Height: | Size: 20 KiB |
|
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
|
||||
|
After Width: | Height: | Size: 1021 B |
|
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));
|
||||
}
|
||||
}
|
||||