🏡 index : quine-global/quine-anaconda-webui.git

/*
 * Copyright (C) 2022 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from "cockpit";

import { getKeyboardConfigurationAction, getKeyboardLayoutsAction, getLanguageAction, getLanguagesAction, setLanguageKickstartedAction } from "../actions/localization-actions.js";

import { convertToCockpitLang, getLangCookie, setLangCookie } from "../helpers/language.js";
import { debug, error } from "../helpers/log.js";
import { _callClient, _getProperty, _setProperty } from "./helpers.js";

import { setLocale } from "./boss.js";

const OBJECT_PATH = "/org/fedoraproject/Anaconda/Modules/Localization";
const INTERFACE_NAME = "org.fedoraproject.Anaconda.Modules.Localization";

const callClient = (...args) => {
    return _callClient(LocalizationClient, OBJECT_PATH, INTERFACE_NAME, ...args);
};
const setProperty = (...args) => {
    return _setProperty(LocalizationClient, OBJECT_PATH, INTERFACE_NAME, ...args);
};
const getProperty = (...args) => {
    return _getProperty(LocalizationClient, OBJECT_PATH, INTERFACE_NAME, ...args);
};

export class LocalizationClient {
    constructor (address, dispatch) {
        if (LocalizationClient.instance && (!address || LocalizationClient.instance.address === address)) {
            return LocalizationClient.instance;
        }

        LocalizationClient.instance?.client.close();

        LocalizationClient.instance = this;

        this.client = cockpit.dbus(
            INTERFACE_NAME,
            { address, bus: "none", superuser: "try" }
        );
        this.address = address;
        this.dispatch = dispatch;
    }

    async init (args = {}) {
        this.client.addEventListener("close", () => error("Localization client closed"));

        this.startEventMonitor();

        await this.initData(args);
    }

    async initData ({ automatedInstall = false } = {}) {
        const languageKickstarted = await getLanguageKickstarted();
        this.dispatch(setLanguageKickstartedAction({
            languageKickstarted: Boolean(languageKickstarted)
        }));

        await this.dispatch(getLanguageAction());

        // Apply runtime locale for kickstarted automated installs
        // (LanguageSelector.componentDidMount won't run since the wizard
        //  jumps to review, so we must set locale here)
        if (automatedInstall && languageKickstarted) {
            const language = await getLanguage();
            applyKickstartLanguage(language);
        }

        await this.dispatch(getLanguagesAction());
        await this.dispatch(getKeyboardLayoutsAction());
        await this.dispatch(getKeyboardConfigurationAction());
    }

    stopEventMonitor () {
        this._subscription?.remove();
    }

    startEventMonitor () {
        this._subscription = this.client.subscribe(
            { },
            async (path, iface, signal, args) => {
                switch (signal) {
                case "CompositorSelectedLayoutChanged":
                case "CompositorLayoutsChanged":
                    await this.dispatch(getKeyboardConfigurationAction());
                    break;
                case "PropertiesChanged":
                    if (args[0] === INTERFACE_NAME && Object.hasOwn(args[1], "Language")) {
                        await this.dispatch(getLanguageAction());

                        /* FIXME: On each locale change, KeyboardLayouts must be refetched since they are localized.
                         * Currently, a race condition in the backend) causes the Language property to update,
                         * but the returned KeyboardLayouts still are translated with the previous locale.
                         * Workaround this by dispatching the KeyboardLayouts action with small delay.
                         */
                        setTimeout(async () => {
                            this.dispatch(getKeyboardConfigurationAction());
                            this.dispatch(getKeyboardLayoutsAction());
                        }, 500);
                    } else {
                        debug(`Unhandled signal on ${path}: ${iface}.${signal}`, JSON.stringify(args));
                    }
                    break;
                default:
                    debug(`Unhandled signal on ${path}: ${iface}.${signal}`, JSON.stringify(args));
                }
            });
    }
}

/**
 * @returns {Promise}           Resolves a list of language ids
 */
export const getLanguages = () => {
    return callClient("GetLanguages", []);
};

/**
 * @returns {Promise}           The language the system will use
 */
export const getLanguage = () => {
    return getProperty("Language");
};

/**
 * @param {string} lang         Language id
 *
 * @returns {Promise}           Resolves a language data object
 */
export const getLanguageData = ({ lang }) => {
    return callClient("GetLanguageData", [lang]);
};

/**
 * @param {string} lang         Language id
 *
 * @returns {Promise}           Resolves a list of locales ids
 */
export const getLocales = ({ lang }) => {
    return callClient("GetLocales", [lang]);
};

/**
 * @returns {Promise}           Resolves a list of common locales id's.
 */
export const getCommonLocales = () => {
    return callClient("GetCommonLocales");
};

/**
 * @param {string} lang         Locale id
 *
 * @returns {Promise}           Resolves a locale data object
 */
export const getLocaleData = ({ locale }) => {
    return callClient("GetLocaleData", [locale]);
};

/**
 * @param {string} lang         Language id
 */
export const setLanguage = ({ lang }) => {
    return setProperty("Language", cockpit.variant("s", lang));
};

export const setXLayouts = ({ layouts }) => {
    return setProperty("XLayouts", cockpit.variant("as", layouts));
};

export const setCompositorLayouts = ({ layouts }) => {
    return callClient("SetCompositorLayouts", [layouts, []]);
};

export const getKeyboardConfiguration = async ({ onFail, onSuccess }) => {
    const task = await callClient("GetKeyboardConfigurationWithTask");
    const taskProxy = new LocalizationClient().client.proxy(
        "org.fedoraproject.Anaconda.Task",
        task
    );

    const getTaskResult = async () => {
        const result = await taskProxy.GetResult();
        return onSuccess(result.v);
    };

    const addEventListeners = () => {
        taskProxy.addEventListener("Stopped", () => taskProxy.Finish().catch(onFail));
        taskProxy.addEventListener("Succeeded", getTaskResult);
    };

    taskProxy.wait(() => {
        addEventListeners();
        taskProxy.Start().catch(onFail);
    });
};

/**
 * @returns {Promise}           Resolves a list of locale keyboards
 */
export const getKeyboardLayouts = async () => {
    const keyboards = await callClient("GetKeyboardLayouts", []);
    return keyboards;
};

/**
 * @returns {Promise<string[]>}   Current X keyboard layouts
 */
export const getXLayouts = () => {
    return getProperty("XLayouts");
};

/**
 * @returns {Promise<boolean>}   Whether the language was set in a kickstart
 */
export const getLanguageKickstarted = () => {
    return getProperty("LanguageKickstarted");
};

/**
 * Apply a kickstart language to the running installer UI.
 * Mirrors LanguageSelector.componentDidMount() logic.
 */
const applyKickstartLanguage = (language) => {
    const cockpitLang = convertToCockpitLang({ lang: language });
    if (getLangCookie() !== cockpitLang) {
        setLangCookie({ cockpitLang });
        window.location.reload(true);
    }
    setLocale({ locale: language });
};

export const setXKeyboardDefaults = async () => {
    // FIXME: Reset XLayouts before calling SetXKeyboardDefaults. Without this reset, the
    // backend would see existing layouts and think they came from
    // kickstart, preventing new defaults from being applied.
    await setXLayouts({ layouts: [] });
    await callClient("SetXKeyboardDefaults");
};