Jelajahi Sumber

feat(settings): split API slicer + Open-in-Slicer preferences (#1329)

  Reporter wanted to slice via the Bambu Studio sidecar but open files
  locally in OrcaSlicer. preferred_slicer drove both the in-app
  SliceModal sidecar selection AND the desktop "Open in Slicer" URI
  handoff, so picking one forced the other.

  New open_in_slicer setting (str | None) drives only the desktop URI;
  null inherits from preferred_slicer so existing installs behave
  identically. Storage in the existing app_settings key/value table;
  GET normalises the "None" string back to null mirroring the
  default_printer_id convention.

  Frontend: Settings -> Slicer card adds a second dropdown ("Open in
  Slicer" with "Same as API slicer" / Bambu Studio / OrcaSlicer);
  ArchivesPage, MakerworldPage, ModelViewerModal switch desktop-URI
  call sites to open_in_slicer ?? preferred_slicer. MakerworldPage's
  "Slice in {{slicer}}" label additionally branches on useSlicerApi
  so the label matches what the button actually dispatches.
maziggy 3 bulan lalu
induk
melakukan
68877c639e

File diff ditekan karena terlalu besar
+ 0 - 0
CHANGELOG.md


+ 5 - 0
backend/app/api/routes/settings.py

@@ -163,6 +163,11 @@ async def _build_settings_response(db: AsyncSession, is_api_key: bool = False) -
             settings_dict[setting.key] = int(setting.value)
         elif setting.key == "default_printer_id":
             settings_dict[setting.key] = int(setting.value) if setting.value and setting.value != "None" else None
+        elif setting.key == "open_in_slicer":
+            # None means "inherit from preferred_slicer" (#1329). The PUT path
+            # serializes None as the literal string "None"; strip it back so
+            # the frontend sees a true null and falls back as intended.
+            settings_dict[setting.key] = setting.value if setting.value and setting.value != "None" else None
         else:
             settings_dict[setting.key] = setting.value
 

+ 14 - 2
backend/app/schemas/settings.py

@@ -187,10 +187,21 @@ class AppSettings(BaseModel):
         description="Camera view mode: 'window' opens in new browser window, 'embedded' shows overlay on main screen",
     )
 
-    # Preferred slicer application
+    # Preferred slicer application (server-side / API sidecar slicer)
     preferred_slicer: str = Field(
         default="bambu_studio",
-        description="Preferred slicer: 'bambu_studio' or 'orcaslicer'",
+        description="Slicer used for the server-side API / sidecar: 'bambu_studio' or 'orcaslicer'",
+    )
+    # "Open in Slicer" desktop URI handler — independent of the API slicer so
+    # a user can slice via the Bambu Studio sidecar but open files locally in
+    # OrcaSlicer, or vice versa (#1329). None falls back to ``preferred_slicer``
+    # so existing installs behave identically until someone changes it.
+    open_in_slicer: str | None = Field(
+        default=None,
+        description=(
+            "Desktop slicer for the 'Open in Slicer' button: 'bambu_studio' or "
+            "'orcaslicer'. None inherits from preferred_slicer."
+        ),
     )
 
     # Slicer dispatch mode: when True, "Slice" actions open the in-app
@@ -396,6 +407,7 @@ class AppSettingsUpdate(BaseModel):
     library_disk_warning_gb: float | None = None
     camera_view_mode: str | None = None
     preferred_slicer: str | None = None
+    open_in_slicer: str | None = None
     use_slicer_api: bool | None = None
     orcaslicer_api_url: str | None = None
     bambu_studio_api_url: str | None = None

+ 59 - 0
backend/tests/integration/test_settings_api.py

@@ -802,6 +802,65 @@ class TestSettingsAPI:
         assert result["ha_token"] == "my-long-lived-token"
 
 
+class TestOpenInSlicerOverride:
+    """Per #1329, the desktop 'Open in Slicer' target can diverge from the API
+    sidecar slicer. The new `open_in_slicer` setting is None by default (frontend
+    inherits from `preferred_slicer`); setting it to 'orcaslicer' or 'bambu_studio'
+    overrides only the desktop URI handoff, not the in-app SliceModal."""
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_open_in_slicer_default_is_null(self, async_client: AsyncClient):
+        response = await async_client.get("/api/v1/settings/")
+        assert response.status_code == 200
+        # Default null so existing installs behave identically — the frontend
+        # then falls back to preferred_slicer.
+        assert response.json()["open_in_slicer"] is None
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_open_in_slicer_override_persists(self, async_client: AsyncClient):
+        # Set preferred_slicer=bambu_studio (API sidecar) but
+        # open_in_slicer=orcaslicer (desktop). Exactly the reporter's case:
+        # slice via Bambu Studio sidecar, open files locally in OrcaSlicer.
+        response = await async_client.put(
+            "/api/v1/settings/",
+            json={"preferred_slicer": "bambu_studio", "open_in_slicer": "orcaslicer"},
+        )
+        assert response.status_code == 200
+        body = response.json()
+        assert body["preferred_slicer"] == "bambu_studio"
+        assert body["open_in_slicer"] == "orcaslicer"
+
+        # Persisted across a fresh GET.
+        get_resp = await async_client.get("/api/v1/settings/")
+        assert get_resp.json()["preferred_slicer"] == "bambu_studio"
+        assert get_resp.json()["open_in_slicer"] == "orcaslicer"
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_open_in_slicer_can_be_cleared_to_null(self, async_client: AsyncClient):
+        # Reset path: user picks an override, then later goes back to "Same as
+        # API slicer". The literal string "None" the PUT path writes for a
+        # None value must be normalized back to a real null on GET — otherwise
+        # the frontend can't distinguish "explicit override absent" from
+        # "explicit override set to a bogus value".
+        await async_client.put(
+            "/api/v1/settings/",
+            json={"open_in_slicer": "orcaslicer"},
+        )
+        response = await async_client.put(
+            "/api/v1/settings/",
+            json={"open_in_slicer": None},
+        )
+        assert response.status_code == 200
+        assert response.json()["open_in_slicer"] is None
+
+        # And a fresh GET also sees it as null, not the literal string "None".
+        get_resp = await async_client.get("/api/v1/settings/")
+        assert get_resp.json()["open_in_slicer"] is None
+
+
 class TestSimplifiedBackupRestore:
     """Integration tests for the simplified backup/restore endpoints (ZIP-based).
 

+ 4 - 1
frontend/src/api/client.ts

@@ -1131,8 +1131,11 @@ export interface AppSettings {
   library_disk_warning_gb: number;
   // Camera view settings
   camera_view_mode: 'window' | 'embedded';
-  // Preferred slicer
+  // Preferred slicer (server-side API / sidecar)
   preferred_slicer: 'bambu_studio' | 'orcaslicer';
+  // Desktop "Open in Slicer" override (#1329). Null inherits from
+  // preferred_slicer so existing installs behave identically.
+  open_in_slicer: 'bambu_studio' | 'orcaslicer' | null;
   // Use the slicer-API sidecar for slicing (in-app modal) vs desktop URI scheme
   use_slicer_api: boolean;
   // Per-install sidecar URLs. Empty string falls back to the env defaults.

+ 5 - 1
frontend/src/components/ModelViewerModal.tsx

@@ -35,7 +35,11 @@ interface Capabilities {
 export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, onClose, onSliceWithBambuddy }: ModelViewerModalProps) {
   const { t } = useTranslation();
   const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings });
-  const preferredSlicer: SlicerType = settings?.preferred_slicer || 'bambu_studio';
+  // Desktop "Open in Slicer" target — falls back to preferred_slicer when the
+  // user hasn't explicitly chosen a different desktop slicer (#1329). This
+  // variable is only used for URI-handoff; sidecar slicing keeps using
+  // preferred_slicer directly.
+  const preferredSlicer: SlicerType = settings?.open_in_slicer || settings?.preferred_slicer || 'bambu_studio';
   const isLibrary = libraryFileId != null;
   const [activeTab, setActiveTab] = useState<ViewTab | null>(null);
   const [capabilities, setCapabilities] = useState<Capabilities | null>(null);

+ 4 - 1
frontend/src/i18n/locales/de.ts

@@ -1931,7 +1931,10 @@ export default {
     newWindow: 'Neues Fenster',
     embeddedOverlay: 'Eingebettetes Overlay',
     preferredSlicer: 'Bevorzugter Slicer',
-    preferredSlicerDescription: 'Wähle die Slicer-Anwendung zum Öffnen von Dateien',
+    preferredSlicerDescription: 'Slicer für das In-App-Slicen über das API-Sidecar',
+    openInSlicerLabel: 'Im Slicer öffnen',
+    openInSlicerInherit: 'Wie API-Slicer',
+    openInSlicerDescription: 'Desktop-Slicer, der vom „Im Slicer öffnen"-Button verwendet wird. Auf „Wie API-Slicer" belassen, um zu erben, oder einen anderen Slicer für die lokale Nutzung wählen.',
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev haben bekannte CLI-Bugs, die das Slicen vieler von BambuStudio erstellter 3MF-Dateien blockieren — siehe Upstream-Issues #12426 (Segfault bei bemalten Multi-Extruder-Dateien) und #13386 (zu strikte Parameter-Range-Validierung). Bis die Upstream-Fixes verfügbar sind, wird Bambu Studio empfohlen.',
     useSlicerApi: 'Slicer-API verwenden',
     useSlicerApiDescription: 'Wenn aktiv, öffnen "Slice"-Aktionen das in-App Slicer-Modal und rufen den Slicer-API-Sidecar. Aus (Standard): Übergabe an den Desktop-Slicer per URI-Schema.',

+ 4 - 1
frontend/src/i18n/locales/en.ts

@@ -1934,7 +1934,10 @@ export default {
     newWindow: 'New Window',
     embeddedOverlay: 'Embedded Overlay',
     preferredSlicer: 'Preferred Slicer',
-    preferredSlicerDescription: 'Choose which slicer application to open files with',
+    preferredSlicerDescription: 'Slicer used for in-app slicing via the API sidecar',
+    openInSlicerLabel: 'Open in Slicer',
+    openInSlicerInherit: 'Same as API slicer',
+    openInSlicerDescription: "Desktop slicer used by the 'Open in Slicer' button. Leave on 'Same as API slicer' to inherit, or pick a different slicer to use locally.",
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev have known CLI bugs that block slicing many Bambu-authored 3MFs — see upstream issues #12426 (segfault on painted multi-extruder files) and #13386 (parameter-range strict-validation reject). Bambu Studio is recommended until the upstream fixes land.',
     useSlicerApi: 'Use Slicer API',
     useSlicerApiDescription: 'When on, "Slice" actions open the in-app slicer modal and call the slicer-API sidecar. When off (default), they hand off to the desktop slicer via URI scheme.',

+ 4 - 1
frontend/src/i18n/locales/es.ts

@@ -1934,7 +1934,10 @@ export default {
     newWindow: 'Ventana nueva',
     embeddedOverlay: 'Superposición integrada',
     preferredSlicer: 'Laminador preferido',
-    preferredSlicerDescription: 'Elija con qué aplicación de laminado abrir los archivos',
+    preferredSlicerDescription: 'Laminador usado para el corte en la aplicación a través del sidecar de la API',
+    openInSlicerLabel: 'Abrir en el laminador',
+    openInSlicerInherit: 'Igual que el laminador de la API',
+    openInSlicerDescription: 'Laminador de escritorio que usa el botón «Abrir en el laminador». Déjelo en «Igual que el laminador de la API» para heredar, o elija un laminador distinto para uso local.',
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev tienen errores conocidos de la CLI que impiden laminar muchos 3MF creados con Bambu — consulte las incidencias del proyecto original #12426 (cierre inesperado en archivos multiextrusor pintados) y #13386 (rechazo por validación estricta del rango de parámetros). Se recomienda Bambu Studio hasta que lleguen las correcciones.',
     useSlicerApi: 'Usar la API del laminador',
     useSlicerApiDescription: 'Cuando está activado, las acciones de «Laminar» abren la ventana del laminador integrada en la aplicación y llaman al contenedor auxiliar de la API del laminador. Cuando está desactivado (predeterminado), delegan en el laminador de escritorio mediante un esquema de URI.',

+ 4 - 1
frontend/src/i18n/locales/fr.ts

@@ -1887,7 +1887,10 @@ export default {
     newWindow: 'Nouvelle fenêtre',
     embeddedOverlay: 'Superposition intégrée',
     preferredSlicer: 'Slicer préféré',
-    preferredSlicerDescription: 'Application pour ouvrir les fichiers',
+    preferredSlicerDescription: 'Slicer utilisé pour le tranchage intégré via le sidecar API',
+    openInSlicerLabel: 'Ouvrir dans le slicer',
+    openInSlicerInherit: 'Identique au slicer API',
+    openInSlicerDescription: 'Slicer de bureau utilisé par le bouton « Ouvrir dans le slicer ». Laissez sur « Identique au slicer API » pour hériter, ou choisissez un slicer différent pour un usage local.',
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev ont des bugs CLI connus qui bloquent le découpage de nombreux 3MF créés par Bambu — voir les issues amont #12426 (segfault sur fichiers multi-extrudeurs peints) et #13386 (rejet de validation stricte des plages de paramètres). Bambu Studio est recommandé jusqu\'à ce que les correctifs amont arrivent.',
     useSlicerApi: 'Utiliser l\'API Slicer',
     useSlicerApiDescription: 'Lorsque activé, les actions "Slice" ouvrent le slicer intégré et appellent le sidecar slicer-API. Lorsque désactivé (par défaut), elles redirigent vers le slicer desktop via le schéma URI.',

+ 4 - 1
frontend/src/i18n/locales/it.ts

@@ -1887,7 +1887,10 @@ export default {
     newWindow: 'Nuova finestra',
     embeddedOverlay: 'Overlay incorporato',
     preferredSlicer: 'Slicer preferito',
-    preferredSlicerDescription: 'Scegli quale applicazione slicer usare per aprire i file',
+    preferredSlicerDescription: 'Slicer utilizzato per lo slicing in-app tramite il sidecar API',
+    openInSlicerLabel: 'Apri nello slicer',
+    openInSlicerInherit: 'Come slicer API',
+    openInSlicerDescription: "Slicer desktop utilizzato dal pulsante 'Apri nello slicer'. Lascia su 'Come slicer API' per ereditare, o scegli uno slicer diverso per l'uso locale.",
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev hanno bug CLI noti che bloccano lo slicing di molti 3MF creati da Bambu — vedi le issue upstream #12426 (segfault su file multi-estrusore dipinti) e #13386 (rifiuto della validazione rigorosa dei range di parametri). Bambu Studio è raccomandato finché non arrivano le correzioni upstream.',
     useSlicerApi: 'Usa Slicer API',
     useSlicerApiDescription: 'Se attivo, le azioni "Slice" aprono il modale slicer integrato e chiamano il sidecar slicer-API. Se disattivo (predefinito), inoltrano al slicer desktop tramite schema URI.',

+ 4 - 1
frontend/src/i18n/locales/ja.ts

@@ -1930,7 +1930,10 @@ export default {
     newWindow: '新しいウィンドウ',
     embeddedOverlay: '埋め込みオーバーレイ',
     preferredSlicer: '優先スライサー',
-    preferredSlicerDescription: 'ファイルを開くスライサーアプリケーションを選択',
+    preferredSlicerDescription: 'APIサイドカー経由のアプリ内スライス処理に使用するスライサー',
+    openInSlicerLabel: 'スライサーで開く',
+    openInSlicerInherit: 'APIスライサーと同じ',
+    openInSlicerDescription: '「スライサーで開く」ボタンが使用するデスクトップスライサー。継承するには「APIスライサーと同じ」のままにするか、ローカルで使用する別のスライサーを選択します。',
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-devには、Bambu作成の3MFの多くをスライスできない既知のCLIバグがあります — 上流イシュー#12426(ペイント済みマルチエクストルーダーファイルでのsegfault)および#13386(パラメータ範囲の厳格検証拒否)を参照。上流の修正がリリースされるまで、Bambu Studioを推奨します。',
     useSlicerApi: 'スライサーAPIを使用',
     useSlicerApiDescription: 'オンの場合、「Slice」アクションはアプリ内スライサーモーダルを開き、slicer-APIサイドカーを呼び出します。オフ(デフォルト)の場合、URIスキーム経由でデスクトップスライサーに引き継がれます。',

+ 4 - 1
frontend/src/i18n/locales/ko.ts

@@ -1805,7 +1805,10 @@ export default {
     newWindow: '새 창',
     embeddedOverlay: '내장 오버레이',
     preferredSlicer: '선호하는 슬라이서',
-    preferredSlicerDescription: '파일을 열 슬라이서 앱을 선택하세요',
+    preferredSlicerDescription: 'API 사이드카를 통한 앱 내 슬라이싱에 사용되는 슬라이서',
+    openInSlicerLabel: '슬라이서에서 열기',
+    openInSlicerInherit: 'API 슬라이서와 동일',
+    openInSlicerDescription: "'슬라이서에서 열기' 버튼이 사용하는 데스크톱 슬라이서입니다. 'API 슬라이서와 동일'로 두어 상속하거나 로컬에서 사용할 다른 슬라이서를 선택하세요.",
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev에는 Bambu 제작 3MF 파일 슬라이싱을 차단하는 알려진 CLI 버그가 있습니다. 업스트림 수정이 출시될 때까지 Bambu Studio를 사용하는 것이 좋습니다.',
     useSlicerApi: '슬라이서 API 사용',
     useSlicerApiDescription: '켜면 "슬라이스" 작업이 앱 내 슬라이서 모달을 열고 슬라이서 API 사이드카를 호출합니다. 끄면 URI 스킴을 통해 데스크톱 슬라이서로 전달합니다.',

+ 4 - 1
frontend/src/i18n/locales/pt-BR.ts

@@ -1887,7 +1887,10 @@ export default {
     newWindow: 'Nova Janela',
     embeddedOverlay: 'Sobreposição Incorporada',
     preferredSlicer: 'Fatiador Preferido',
-    preferredSlicerDescription: 'Escolha qual aplicativo de fatiamento abrirá os arquivos',
+    preferredSlicerDescription: 'Fatiador usado para fatiamento no aplicativo via sidecar da API',
+    openInSlicerLabel: 'Abrir no fatiador',
+    openInSlicerInherit: 'Mesmo do fatiador da API',
+    openInSlicerDescription: "Fatiador de desktop usado pelo botão 'Abrir no fatiador'. Mantenha em 'Mesmo do fatiador da API' para herdar, ou escolha um fatiador diferente para uso local.",
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev têm bugs CLI conhecidos que bloqueiam o fatiamento de muitos 3MFs criados pela Bambu — veja issues upstream #12426 (segfault em arquivos multi-extrusor pintados) e #13386 (rejeição de validação estrita de intervalos de parâmetros). Bambu Studio é recomendado até as correções upstream chegarem.',
     useSlicerApi: 'Usar API do fatiador',
     useSlicerApiDescription: 'Quando ativo, as ações "Slice" abrem o modal do fatiador interno e chamam o sidecar slicer-API. Quando inativo (padrão), redirecionam ao fatiador desktop via esquema URI.',

+ 4 - 1
frontend/src/i18n/locales/tr.ts

@@ -1934,7 +1934,10 @@ export default {
     newWindow: 'Yeni Pencere',
     embeddedOverlay: 'Gömülü Bindirme',
     preferredSlicer: 'Tercih Edilen Dilimleyici',
-    preferredSlicerDescription: 'Dosyaları açmak için hangi dilimleyici uygulamasının kullanılacağını seçin',
+    preferredSlicerDescription: 'Uygulama içi dilimleme için API yardımcı kabıyla kullanılan dilimleyici',
+    openInSlicerLabel: 'Dilimleyicide Aç',
+    openInSlicerInherit: 'API dilimleyicisiyle aynı',
+    openInSlicerDescription: "'Dilimleyicide Aç' düğmesinin kullandığı masaüstü dilimleyici. Devralmak için 'API dilimleyicisiyle aynı' bırakın veya yerel olarak kullanmak için farklı bir dilimleyici seçin.",
     orcaslicerKnownIssuesWarning: "OrcaSlicer 2.3.2 / 2.4.0-dev, birçok Bambu yazılı 3MF'nin dilimlenmesini engelleyen bilinen CLI hatalarına sahiptir — yukarı yöndeki #12426 (boyalı çok ekstrüderli dosyalarda segfault) ve #13386 (parametre aralığı katı doğrulama reddi) sorunlarına bakın. Yukarı yön düzeltmeler gelene kadar Bambu Studio önerilir.",
     useSlicerApi: 'Dilimleyici API kullan',
     useSlicerApiDescription: 'Açıkken, "Dilimle" eylemleri uygulama içi dilimleyici modalini açar ve dilimleyici-API yardımcı bileşenini çağırır. Kapalıyken (varsayılan), URI şeması üzerinden masaüstü dilimleyiciye devredilirler.',

+ 4 - 1
frontend/src/i18n/locales/zh-CN.ts

@@ -1932,7 +1932,10 @@ export default {
     newWindow: '新窗口',
     embeddedOverlay: '嵌入式叠加层',
     preferredSlicer: '首选切片软件',
-    preferredSlicerDescription: '选择要用于打开文件的切片软件',
+    preferredSlicerDescription: '通过 API 边车进行应用内切片所使用的切片软件',
+    openInSlicerLabel: '在切片软件中打开',
+    openInSlicerInherit: '与 API 切片软件相同',
+    openInSlicerDescription: '"在切片软件中打开"按钮使用的桌面切片软件。保持"与 API 切片软件相同"以继承设置,或选择不同的切片软件用于本地使用。',
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev 存在已知 CLI 错误,无法切片许多 Bambu 创建的 3MF — 见上游 issue #12426(绘制的多挤出机文件 segfault)和 #13386(参数范围严格验证拒绝)。在上游修复发布之前,推荐使用 Bambu Studio。',
     useSlicerApi: '使用切片器 API',
     useSlicerApiDescription: '开启时,「切片」操作打开应用内切片器模态并调用 slicer-API sidecar。关闭时(默认),通过 URI 方案交给桌面切片器。',

+ 4 - 1
frontend/src/i18n/locales/zh-TW.ts

@@ -1932,7 +1932,10 @@ export default {
     newWindow: '新視窗',
     embeddedOverlay: '嵌入式疊加層',
     preferredSlicer: '首選切片軟體',
-    preferredSlicerDescription: '選擇要用於開啟檔案的切片軟體',
+    preferredSlicerDescription: '透過 API 邊車進行應用內切片所使用的切片軟體',
+    openInSlicerLabel: '在切片軟體中開啟',
+    openInSlicerInherit: '與 API 切片軟體相同',
+    openInSlicerDescription: '「在切片軟體中開啟」按鈕使用的桌面切片軟體。保持「與 API 切片軟體相同」以繼承設定,或選擇不同的切片軟體以供本地使用。',
     orcaslicerKnownIssuesWarning: 'OrcaSlicer 2.3.2 / 2.4.0-dev 存在已知 CLI 錯誤,無法切片許多 Bambu 建立的 3MF — 見上游 issue #12426(繪製的多擠出機檔案 segfault)和 #13386(參數範圍嚴格驗證拒絕)。在上游修復發布之前,推薦使用 Bambu Studio。',
     useSlicerApi: '使用切片器 API',
     useSlicerApiDescription: '開啟時,「切片」操作開啟應用程式內切片器對話框並呼叫 slicer-API sidecar。關閉時(預設),透過 URI 方案交給桌面切片器。',

+ 5 - 1
frontend/src/pages/ArchivesPage.tsx

@@ -2688,7 +2688,11 @@ export function ArchivesPage() {
   });
 
   const timeFormat: TimeFormat = settings?.time_format || 'system';
-  const preferredSlicer: SlicerType = settings?.preferred_slicer || 'bambu_studio';
+  // Desktop "Open in Slicer" target — falls back to preferred_slicer when the
+  // user hasn't explicitly chosen a different desktop slicer (#1329). This is
+  // ONLY the URI-handoff target; the in-app SliceModal still uses
+  // preferred_slicer for the sidecar.
+  const preferredSlicer: SlicerType = settings?.open_in_slicer || settings?.preferred_slicer || 'bambu_studio';
   const useSlicerApi = settings?.use_slicer_api ?? false;
   const currency = getCurrencySymbol(settings?.currency || 'USD');
 

+ 8 - 2
frontend/src/pages/MakerworldPage.tsx

@@ -179,10 +179,16 @@ export function MakerworldPage() {
   // directly to a printer. The "slice in slicer" action below imports the
   // 3MF and hands it to the user's configured slicer; from there the
   // slicer's own "send to printer" flow takes over.
-  const preferredSlicer: SlicerType = settingsQuery.data?.preferred_slicer || 'bambu_studio';
+  // API-sidecar slicer (in-app SliceModal) is `preferred_slicer`; desktop
+  // "Open in Slicer" handoff respects the `open_in_slicer` override, falling
+  // back to `preferred_slicer` when unset (#1329). The button label shows
+  // whichever slicer this button actually drives — depends on useSlicerApi.
+  const useSlicerApi = settingsQuery.data?.use_slicer_api ?? false;
+  const apiSlicer: SlicerType = settingsQuery.data?.preferred_slicer || 'bambu_studio';
+  const desktopSlicer: SlicerType = settingsQuery.data?.open_in_slicer || apiSlicer;
+  const preferredSlicer: SlicerType = useSlicerApi ? apiSlicer : desktopSlicer;
   const preferredSlicerName =
     preferredSlicer === 'orcaslicer' ? 'OrcaSlicer' : 'Bambu Studio';
-  const useSlicerApi = settingsQuery.data?.use_slicer_api ?? false;
 
   // Slice-via-API modal source. When set, the SliceModal is shown for the
   // referenced library file; it covers MakerWorld's "Slice in <Slicer>" /

+ 33 - 0
frontend/src/pages/SettingsPage.tsx

@@ -997,6 +997,7 @@ export function SettingsPage() {
       Number(settings.library_disk_warning_gb ?? 5) !== Number(localSettings.library_disk_warning_gb ?? 5) ||
       (settings.camera_view_mode ?? 'window') !== (localSettings.camera_view_mode ?? 'window') ||
       (settings.preferred_slicer ?? 'bambu_studio') !== (localSettings.preferred_slicer ?? 'bambu_studio') ||
+      (settings.open_in_slicer ?? null) !== (localSettings.open_in_slicer ?? null) ||
       (settings.use_slicer_api ?? false) !== (localSettings.use_slicer_api ?? false) ||
       (settings.orcaslicer_api_url ?? '') !== (localSettings.orcaslicer_api_url ?? '') ||
       (settings.bambu_studio_api_url ?? '') !== (localSettings.bambu_studio_api_url ?? '') ||
@@ -1082,6 +1083,7 @@ export function SettingsPage() {
         library_disk_warning_gb: localSettings.library_disk_warning_gb,
         camera_view_mode: localSettings.camera_view_mode,
         preferred_slicer: localSettings.preferred_slicer,
+        open_in_slicer: localSettings.open_in_slicer,
         use_slicer_api: localSettings.use_slicer_api,
         orcaslicer_api_url: localSettings.orcaslicer_api_url,
         bambu_studio_api_url: localSettings.bambu_studio_api_url,
@@ -4324,6 +4326,37 @@ export function SettingsPage() {
                   </div>
                 )}
               </div>
+              {/* Desktop "Open in Slicer" override (#1329). Independent of the
+                  API slicer so a user can slice via the Bambu Studio sidecar
+                  but open files locally in OrcaSlicer, or vice versa. */}
+              <div>
+                <label className="block text-sm text-bambu-gray mb-1">
+                  {t('settings.openInSlicerLabel', 'Open in Slicer')}
+                </label>
+                <div className="relative">
+                  <select
+                    value={localSettings.open_in_slicer ?? ''}
+                    onChange={(e) =>
+                      updateSetting(
+                        'open_in_slicer',
+                        e.target.value === '' ? null : (e.target.value as 'bambu_studio' | 'orcaslicer'),
+                      )
+                    }
+                    className="w-full px-3 py-2 pr-10 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none cursor-pointer"
+                  >
+                    <option value="">{t('settings.openInSlicerInherit', 'Same as API slicer')}</option>
+                    <option value="bambu_studio">{t('settings.slicerBambuStudio')}</option>
+                    <option value="orcaslicer">{t('settings.slicerOrcaSlicer')}</option>
+                  </select>
+                  <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
+                </div>
+                <p className="text-xs text-bambu-gray mt-1">
+                  {t(
+                    'settings.openInSlicerDescription',
+                    "Desktop slicer used by the 'Open in Slicer' button. Leave on 'Same as API slicer' to inherit, or pick a different slicer to use locally.",
+                  )}
+                </p>
+              </div>
               <div className="flex items-center justify-between gap-3">
                 <div className="min-w-0">
                   <p className="text-white">{t('settings.useSlicerApi')}</p>

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/index-fm48nbe7.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Bkfk-iWd.js"></script>
+    <script type="module" crossorigin src="/assets/index-fm48nbe7.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-B9tytutS.css">
   </head>
   <body>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini