diff --git a/src-tauri/crates/settings/src/commands.rs b/src-tauri/crates/settings/src/commands.rs index cc540970d2..ad5fa202d8 100644 --- a/src-tauri/crates/settings/src/commands.rs +++ b/src-tauri/crates/settings/src/commands.rs @@ -86,6 +86,14 @@ mod tests { assert!(result.is_ok()); } + + #[test] + fn app_license_reads_the_repository_license() { + let license = app_license_read(); + + assert!(license.starts_with("GNU AFFERO GENERAL PUBLIC LICENSE")); + assert!(license.contains("Version 3, 19 November 2007")); + } } /// Reset settings by deleting the file. @@ -108,6 +116,16 @@ pub async fn settings_get_path() -> Result { Ok(file_io::get_settings_path().to_string_lossy().to_string()) } +/// Return the license shipped with the application. +/// +/// Embedding the repository's root LICENSE at compile time keeps the viewer +/// synchronized with the license that is shipped in each build without +/// duplicating its contents in frontend code. +#[tauri::command] +pub fn app_license_read() -> String { + include_str!("../../../../LICENSE").to_string() +} + /// Write the JSON Schema file alongside the settings file. /// The schema enables autocomplete in external editors (VS Code, etc.). #[tauri::command] diff --git a/src-tauri/src/commands/handler_list.inc b/src-tauri/src/commands/handler_list.inc index 3e32645164..4596127bec 100644 --- a/src-tauri/src/commands/handler_list.inc +++ b/src-tauri/src/commands/handler_list.inc @@ -766,6 +766,7 @@ settings::commands::settings_write_partial, settings::commands::settings_reset, settings::commands::settings_get_path, settings::commands::settings_write_schema, +settings::commands::app_license_read, // Codex config (~/.codex/config.toml) agent_cli::codex::commands::codex_config_read, agent_cli::codex::commands::codex_config_write_partial, diff --git a/src/api/tauri/license.test.ts b/src/api/tauri/license.test.ts new file mode 100644 index 0000000000..e7305173de --- /dev/null +++ b/src/api/tauri/license.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it, vi } from "vitest"; + +import { readAppLicense } from "./license"; + +const invoke = vi.hoisted(() => vi.fn()); + +vi.mock("@tauri-apps/api/core", () => ({ invoke })); + +describe("readAppLicense", () => { + it("requests the license from the desktop runtime", async () => { + invoke.mockResolvedValue("GNU AFFERO GENERAL PUBLIC LICENSE"); + + await expect(readAppLicense()).resolves.toContain( + "GNU AFFERO GENERAL PUBLIC LICENSE" + ); + expect(invoke).toHaveBeenCalledWith("app_license_read"); + }); +}); diff --git a/src/api/tauri/license.ts b/src/api/tauri/license.ts new file mode 100644 index 0000000000..8287b8dd16 --- /dev/null +++ b/src/api/tauri/license.ts @@ -0,0 +1,6 @@ +import { invoke } from "@tauri-apps/api/core"; + +/** Read the license text embedded in the running desktop application. */ +export function readAppLicense(): Promise { + return invoke("app_license_read"); +} diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 414ddd33fc..50fc0675f0 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -217,7 +217,13 @@ "skin": "Skin", "accent": "Akzent", "linkSkinVariants": "Hell und Dunkel koppeln", - "linkSkinVariantsDesc": "Verwendet einen Skin und Akzent für beide Modi. Nur Skins mit heller und dunkler Variante können gekoppelt werden" + "linkSkinVariantsDesc": "Verwendet einen Skin und Akzent für beide Modi. Nur Skins mit heller und dunkler Variante können gekoppelt werden", + "license": "Lizenz", + "licenseDesc": "Die mit dieser App gebündelte Lizenz anzeigen", + "viewLicense": "Lizenz anzeigen", + "licenseTitle": "ORG2-Lizenz", + "licenseLoading": "Lizenz wird geladen…", + "licenseLoadFailed": "Die App-Lizenz konnte nicht geladen werden" }, "background": { "title": "Hintergrund", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 068a9282b0..d593bd0530 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -295,7 +295,13 @@ "rainbow": "Rainbow", "dark": "Dark tile", "light": "Light tile" - } + }, + "license": "License", + "licenseDesc": "View the license bundled with this app", + "viewLicense": "View license", + "licenseTitle": "ORG2 license", + "licenseLoading": "Loading license…", + "licenseLoadFailed": "Unable to load the application license" }, "background": { "title": "Background", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 1b9f1620cc..7d2478670e 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -217,7 +217,13 @@ "skin": "Skin", "accent": "Acento", "linkSkinVariants": "Vincular claro y oscuro", - "linkSkinVariantsDesc": "Usa una sola skin y un solo acento para ambos modos. Solo se pueden vincular las skins que incluyen una variante clara y una oscura" + "linkSkinVariantsDesc": "Usa una sola skin y un solo acento para ambos modos. Solo se pueden vincular las skins que incluyen una variante clara y una oscura", + "license": "Licencia", + "licenseDesc": "Ver la licencia incluida con esta aplicación", + "viewLicense": "Ver licencia", + "licenseTitle": "Licencia de ORG2", + "licenseLoading": "Cargando licencia…", + "licenseLoadFailed": "No se puede cargar la licencia de la aplicación" }, "background": { "title": "Fondo", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index c092cef39c..9158540b85 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -217,7 +217,13 @@ "skin": "Skin", "accent": "Accent", "linkSkinVariants": "Lier clair et sombre", - "linkSkinVariantsDesc": "Utiliser un seul skin et un seul accent pour les deux modes. Seuls les skins proposant une variante claire et une variante sombre peuvent être liés" + "linkSkinVariantsDesc": "Utiliser un seul skin et un seul accent pour les deux modes. Seuls les skins proposant une variante claire et une variante sombre peuvent être liés", + "license": "Licence", + "licenseDesc": "Afficher la licence incluse avec cette application", + "viewLicense": "Afficher la licence", + "licenseTitle": "Licence ORG2", + "licenseLoading": "Chargement de la licence…", + "licenseLoadFailed": "Impossible de charger la licence de l’application" }, "background": { "title": "Arrière-plan", diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index f3ac69931e..7378aa0d8f 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -217,7 +217,13 @@ "skin": "スキン", "accent": "アクセント", "linkSkinVariants": "ライトとダークを連動", - "linkSkinVariantsDesc": "両方のモードで 1 つのスキンとアクセントを使用します。ライトとダークの両バリアントを備えたスキンのみ連動できます" + "linkSkinVariantsDesc": "両方のモードで 1 つのスキンとアクセントを使用します。ライトとダークの両バリアントを備えたスキンのみ連動できます", + "license": "ライセンス", + "licenseDesc": "このアプリに同梱されているライセンスを表示します", + "viewLicense": "ライセンスを表示", + "licenseTitle": "ORG2 のライセンス", + "licenseLoading": "ライセンスを読み込み中…", + "licenseLoadFailed": "アプリのライセンスを読み込めません" }, "background": { "title": "背景", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index e4737c0247..a0d4f03ca0 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -217,7 +217,13 @@ "skin": "스킨", "accent": "강조색", "linkSkinVariants": "라이트와 다크 연동", - "linkSkinVariantsDesc": "두 모드에 하나의 스킨과 강조색을 사용합니다. 라이트와 다크 변형을 모두 제공하는 스킨만 연동할 수 있습니다" + "linkSkinVariantsDesc": "두 모드에 하나의 스킨과 강조색을 사용합니다. 라이트와 다크 변형을 모두 제공하는 스킨만 연동할 수 있습니다", + "license": "라이선스", + "licenseDesc": "이 앱에 포함된 라이선스 보기", + "viewLicense": "라이선스 보기", + "licenseTitle": "ORG2 라이선스", + "licenseLoading": "라이선스 로드 중…", + "licenseLoadFailed": "앱 라이선스를 불러올 수 없습니다" }, "background": { "title": "배경", diff --git a/src/i18n/locales/pl/settings.json b/src/i18n/locales/pl/settings.json index 95d29bc268..a670082f06 100644 --- a/src/i18n/locales/pl/settings.json +++ b/src/i18n/locales/pl/settings.json @@ -217,7 +217,13 @@ "skin": "Skórka", "accent": "Akcent", "linkSkinVariants": "Powiąż jasny i ciemny", - "linkSkinVariantsDesc": "Używaj jednej skórki i akcentu w obu trybach. Powiązać można tylko skórki, które mają wariant jasny i ciemny" + "linkSkinVariantsDesc": "Używaj jednej skórki i akcentu w obu trybach. Powiązać można tylko skórki, które mają wariant jasny i ciemny", + "license": "Licencja", + "licenseDesc": "Wyświetl licencję dołączoną do tej aplikacji", + "viewLicense": "Wyświetl licencję", + "licenseTitle": "Licencja ORG2", + "licenseLoading": "Wczytywanie licencji…", + "licenseLoadFailed": "Nie można wczytać licencji aplikacji" }, "background": { "title": "Tło", diff --git a/src/i18n/locales/pt/settings.json b/src/i18n/locales/pt/settings.json index 62467077c6..413c40c2d7 100644 --- a/src/i18n/locales/pt/settings.json +++ b/src/i18n/locales/pt/settings.json @@ -217,7 +217,13 @@ "skin": "Skin", "accent": "Destaque", "linkSkinVariants": "Vincular claro e escuro", - "linkSkinVariantsDesc": "Usa um único skin e destaque para os dois modos. Só é possível vincular skins que incluem uma variante clara e uma escura" + "linkSkinVariantsDesc": "Usa um único skin e destaque para os dois modos. Só é possível vincular skins que incluem uma variante clara e uma escura", + "license": "Licença", + "licenseDesc": "Ver a licença incluída com este aplicativo", + "viewLicense": "Ver licença", + "licenseTitle": "Licença do ORG2", + "licenseLoading": "Carregando licença…", + "licenseLoadFailed": "Não foi possível carregar a licença do aplicativo" }, "background": { "title": "Plano de fundo", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index fa3953bb87..1f0e5d877e 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -217,7 +217,13 @@ "skin": "Скин", "accent": "Акцент", "linkSkinVariants": "Связать светлый и тёмный", - "linkSkinVariantsDesc": "Использовать один скин и акцент для обоих режимов. Связать можно только скины, имеющие светлый и тёмный варианты" + "linkSkinVariantsDesc": "Использовать один скин и акцент для обоих режимов. Связать можно только скины, имеющие светлый и тёмный варианты", + "license": "Лицензия", + "licenseDesc": "Просмотреть лицензию, включённую в это приложение", + "viewLicense": "Просмотреть лицензию", + "licenseTitle": "Лицензия ORG2", + "licenseLoading": "Загрузка лицензии…", + "licenseLoadFailed": "Не удалось загрузить лицензию приложения" }, "background": { "title": "Фон", diff --git a/src/i18n/locales/tr/settings.json b/src/i18n/locales/tr/settings.json index dad828b60e..6f707afa63 100644 --- a/src/i18n/locales/tr/settings.json +++ b/src/i18n/locales/tr/settings.json @@ -217,7 +217,13 @@ "skin": "Skin", "accent": "Vurgu rengi", "linkSkinVariants": "Açık ve koyuyu bağla", - "linkSkinVariantsDesc": "Her iki mod için tek bir skin ve vurgu rengi kullan. Yalnızca hem açık hem koyu varyantı olan skin'ler bağlanabilir" + "linkSkinVariantsDesc": "Her iki mod için tek bir skin ve vurgu rengi kullan. Yalnızca hem açık hem koyu varyantı olan skin'ler bağlanabilir", + "license": "Lisans", + "licenseDesc": "Bu uygulamayla birlikte gelen lisansı görüntüle", + "viewLicense": "Lisansı görüntüle", + "licenseTitle": "ORG2 lisansı", + "licenseLoading": "Lisans yükleniyor…", + "licenseLoadFailed": "Uygulama lisansı yüklenemedi" }, "background": { "title": "Arka Plan", diff --git a/src/i18n/locales/vi/settings.json b/src/i18n/locales/vi/settings.json index bb4e78cbc0..4975eea4f8 100644 --- a/src/i18n/locales/vi/settings.json +++ b/src/i18n/locales/vi/settings.json @@ -217,7 +217,13 @@ "skin": "Skin", "accent": "Màu nhấn", "linkSkinVariants": "Liên kết sáng và tối", - "linkSkinVariantsDesc": "Dùng một skin và màu nhấn cho cả hai chế độ. Chỉ các skin có cả biến thể sáng và tối mới có thể liên kết" + "linkSkinVariantsDesc": "Dùng một skin và màu nhấn cho cả hai chế độ. Chỉ các skin có cả biến thể sáng và tối mới có thể liên kết", + "license": "Giấy phép", + "licenseDesc": "Xem giấy phép đi kèm ứng dụng này", + "viewLicense": "Xem giấy phép", + "licenseTitle": "Giấy phép ORG2", + "licenseLoading": "Đang tải giấy phép…", + "licenseLoadFailed": "Không thể tải giấy phép của ứng dụng" }, "background": { "title": "Hình nền", diff --git a/src/i18n/locales/zh-Hant/settings.json b/src/i18n/locales/zh-Hant/settings.json index f4e04ce286..cd94428d2c 100644 --- a/src/i18n/locales/zh-Hant/settings.json +++ b/src/i18n/locales/zh-Hant/settings.json @@ -217,7 +217,13 @@ "skin": "外觀主題", "accent": "強調色", "linkSkinVariants": "連動淺色與深色", - "linkSkinVariantsDesc": "兩種模式使用同一套外觀主題和強調色。只有同時提供淺色與深色變體的外觀主題才能連動" + "linkSkinVariantsDesc": "兩種模式使用同一套外觀主題和強調色。只有同時提供淺色與深色變體的外觀主題才能連動", + "license": "授權條款", + "licenseDesc": "查看此應用程式隨附的授權條款", + "viewLicense": "查看授權條款", + "licenseTitle": "ORG2 授權條款", + "licenseLoading": "正在載入授權條款…", + "licenseLoadFailed": "無法載入應用程式授權條款" }, "background": { "title": "背景", diff --git a/src/i18n/locales/zh/settings.json b/src/i18n/locales/zh/settings.json index 0e9dfe414e..b4e29638fb 100644 --- a/src/i18n/locales/zh/settings.json +++ b/src/i18n/locales/zh/settings.json @@ -295,7 +295,13 @@ "rainbow": "彩虹", "dark": "深色", "light": "浅色" - } + }, + "license": "许可证", + "licenseDesc": "查看此应用附带的许可证", + "viewLicense": "查看许可证", + "licenseTitle": "ORG2 许可证", + "licenseLoading": "正在加载许可证…", + "licenseLoadFailed": "无法加载应用许可证" }, "background": { "title": "背景", diff --git a/src/modules/MainApp/Settings/sections/GeneralSection.tsx b/src/modules/MainApp/Settings/sections/GeneralSection.tsx index 3447583547..a05a7b1330 100644 --- a/src/modules/MainApp/Settings/sections/GeneralSection.tsx +++ b/src/modules/MainApp/Settings/sections/GeneralSection.tsx @@ -86,6 +86,7 @@ import { HighRefreshRateRow, } from "./HighRefreshRateRow"; import HttpVersionSettingsBlock from "./HttpVersionSettingsBlock"; +import LicenseModal from "./LicenseModal"; export const GENERAL_TAB_KEYS = { GENERAL: "general", @@ -145,6 +146,7 @@ const GeneralSection: React.FC = ({ const GeneralTabBody: React.FC = () => { const { t, i18n } = useTranslation("settings"); + const [licenseModalVisible, setLicenseModalVisible] = useState(false); const [timezone, setTimezone] = useAtom(timezoneAtom); const [chatAppearance, updateChatAppearance] = useAtom( chatAppearancePersistAtom @@ -516,6 +518,21 @@ const GeneralTabBody: React.FC = () => { /> )} + + + + + + + + {licenseModalVisible && ( + setLicenseModalVisible(false)} /> + )} ); }; diff --git a/src/modules/MainApp/Settings/sections/LicenseModal.tsx b/src/modules/MainApp/Settings/sections/LicenseModal.tsx new file mode 100644 index 0000000000..314295d499 --- /dev/null +++ b/src/modules/MainApp/Settings/sections/LicenseModal.tsx @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { readAppLicense } from "@src/api/tauri/license"; +import Button from "@src/components/Button"; +import Modal from "@src/scaffold/ModalSystem"; + +interface LicenseModalProps { + visible: boolean; + onClose: () => void; +} + +const LicenseModal: React.FC = ({ visible, onClose }) => { + const { t } = useTranslation("settings"); + const [licenseText, setLicenseText] = useState(null); + const [loadError, setLoadError] = useState(false); + + useEffect(() => { + if (!visible) return; + + let cancelled = false; + + readAppLicense() + .then((text) => { + if (!cancelled) setLicenseText(text); + }) + .catch(() => { + if (!cancelled) setLoadError(true); + }); + + return () => { + cancelled = true; + }; + }, [visible]); + + return ( + + + + } + > +
+ {loadError ? ( +

+ {t("general.licenseLoadFailed")} +

+ ) : licenseText === null ? ( +

{t("general.licenseLoading")}

+ ) : ( +
+            {licenseText}
+          
+ )} +
+
+ ); +}; + +export default LicenseModal;