Files
HyprArch/airootfs/etc/calamares/modules/mobile/mobile.qml

391 lines
14 KiB
QML

/* SPDX-FileCopyrightText: 2020 Oliver Smith <ollieparanoid@postmarketos.org>
* SPDX-License-Identifier: GPL-3.0-or-later */
import io.calamares.core 1.0
import io.calamares.ui 1.0
import QtQuick 2.10
import QtQuick.Controls 2.10
import QtQuick.Layouts 1.3
import org.kde.kirigami 2.7 as Kirigami
import QtGraphicalEffects 1.0
import QtQuick.Window 2.3
import QtQuick.VirtualKeyboard 2.1
Page
{
property var screen: "welcome"
property var screenPrevious: []
property var titles: {
"welcome": null, /* titlebar disabled */
"install_target": "Installation target",
"install_target_confirm": "Warning",
"user_pass": "User password",
"ssh_confirm": "SSH server",
"ssh_credentials": "SSH credentials",
"fs_selection": "Root filesystem",
"fde_confirm": "Full disk encryption",
"fde_pass": "Full disk encryption",
"install_confirm": "Ready to install",
"wait": null
}
property var features: [
{"name": "welcome",
"screens": ["welcome"]},
{"name": "installTarget",
"screens": ["install_target", "install_target_confirm"]},
{"name": "userPassword",
"screens": ["user_pass"]},
{"name": "sshd",
"screens": ["ssh_confirm", "ssh_credentials"]},
{"name": "fsType",
"screens": ["fs_selection"]},
{"name": "fde",
"screens": ["fde_confirm", "fde_pass"]},
{"name": "installConfirm",
"screens": ["install_confirm", "wait"]}
]
property var featureIdByScreen: (function() {
/* Put "features" above into an index of screen name -> feature id:
* featureIdByScreen = {"welcome": 0, "user_pass": 1, ...} */
var ret = {};
for (var i=0; i<features.length; i++) {
for (var j=0; j<features[i]["screens"].length; j++) {
ret[ features[i]["screens"][j] ] = i;
}
}
return ret;
}())
/* Only allow characters, that can be typed in with osk-sdl
* (src/keyboard.cpp). Details in big comment in validatePassword(). */
property var allowed_chars:
/* layer 0 */ "abcdefghijklmnopqrstuvwxyz" +
/* layer 1 */ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
/* layer 2 */ "1234567890" + "@#$%&-_+()" + ",\"':;!?" +
/* layer 3 */ "~`|·√πτ÷×¶" + "©®£€¥^°*{}" + "\\/<>=[]" +
/* bottom row */ " ."
Item {
id: appContainer
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
anchors.bottom: inputPanel.top
Item {
width: parent.width
height: parent.height
Rectangle {
id: mobileNavigation
width: parent.width
height: 30
color: "#e6e4e1"
Layout.fillWidth: true
border.width: 1
border.color: "#a7a7a7"
anchors.left: parent.left
anchors.right: parent.right
RowLayout {
width: parent.width
height: parent.height
spacing: 6
Button {
Layout.leftMargin: 6
id: mobileBack
text: "<"
background: Rectangle {
implicitWidth: 10
implicitHeight: 7
border.color: "#c1bab5"
border.width: 1
radius: 4
color: mobileBack.down ? "#dbdbdb" : "#f2f2f2"
}
onClicked: navBack()
}
Rectangle {
implicitHeight: 10
Layout.fillWidth: true
color: "#e6e4e1"
Text {
id: mobileTitle
text: ""
color: "#303638"
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
}
Rectangle {
color: "#e6e4e1"
Layout.rightMargin: 6
implicitWidth: 32
implicitHeight: 30
id: filler
}
}
}
Loader {
id: load
anchors.left: parent.left
anchors.top: mobileNavigation.bottom
anchors.right: parent.right
}
}
}
InputPanel {
id: inputPanel
y: Qt.inputMethod.visible ? parent.height - inputPanel.height : parent.height
visible: config.builtinVirtualKeyboard
anchors.left: parent.left
anchors.right: parent.right
}
Timer {
id: timer
}
function skipFeatureInstallTarget() {
return config.targetDeviceRootInternal == "";
}
/* Navigation related */
function navTo(name, historyPush=true) {
console.log("Navigating to screen: " + name);
if (historyPush)
screenPrevious.push(screen);
screen = name;
load.source = name + ".qml";
mobileNavigation.visible = (titles[name] !== null);
mobileTitle.text = "<b>" + titles[name] + "</b>";
Qt.inputMethod.hide();
}
function navFinish() {
/* Show a waiting screen and wait a second (so it can render). The big
* comment in Config.cpp::runPartitionJobThenLeave() explains why this
* is necessary. */
navTo("wait");
timer.interval = 1000;
timer.repeat = false;
timer.triggered.connect(function() {
/* Trigger Config.cpp::runPartitionJobThenLeave(). (We could expose
* the function directly with qmlRegisterSingletonType somehow, but
* I haven't seen existing Calamares code do that with the Config
* object, so just use the side effect of setting the variable, as
* done in existing code of Calamares modules.) */
config.runPartitionJobThenLeave = 1
});
timer.start();
}
function navNextFeature() {
var id;
/* Skip disabled features */
for (id = featureIdByScreen[screen] + 1; id < features.length; id++) {
/* First letter uppercase */
var name = features[id]["name"];
var nameUp = name.charAt(0).toUpperCase() + name.slice(1);
/* Check config.Feature<Name> */
var configOption = "feature" + nameUp;
if (config[configOption] === false) {
console.log("Skipping feature (disabled in config): " + name);
continue;
}
/* Check skipFeature<Name>() */
var funcName = "skipFeature" + nameUp;
if (eval("typeof " + funcName) === "function"
&& eval(funcName + "()")) {
console.log("Skipping feature (skip function): " + name);
continue;
}
break;
}
console.log("Navigating to feature: " + features[id]["name"]);
return navTo(features[id]["screens"][0]);
}
function navNext() {
var featureId = featureIdByScreen[screen];
var featureScreens = features[featureId]["screens"];
for (var i = 0; i<featureScreens.length; i++) {
/* Seek ahead until i is current screen */
if (featureScreens[i] != screen)
continue;
/* Navigate to next screen in same feature */
if (i + 1 < featureScreens.length) {
var screenNext = featureScreens[i + 1];
return navTo(screenNext);
}
/* Screen is last in feature */
return navNextFeature();
}
console.log("ERROR: navNext() failed for screen: " + screen);
}
function navBack() {
if (screenPrevious.length)
return navTo(screenPrevious.pop(), false);
ViewManager.back();
}
function onActivate() {
navTo(screen, false);
}
/* Input validation: show/clear failures */
function validationFailure(errorText, message="") {
errorText.text = message;
errorText.visible = true;
return false;
}
function validationFailureClear(errorText) {
errorText.text = "";
errorText.visible = false;
return true;
}
/* Input validation: user-screens (fde_pass, user_pass, ssh_credentials) */
function validatePin(userPin, userPinRepeat, errorText) {
var pin = userPin.text;
var repeat = userPinRepeat.text;
if (pin == "")
return validationFailure(errorText);
if (!pin.match(/^[0-9]*$/))
return validationFailure(errorText,
"Only digits are allowed.");
if (pin.length < 5)
return validationFailure(errorText,
"Too short: needs at least 5 digits.");
if (repeat == "")
return validationFailure(errorText);
if (repeat != pin)
return validationFailure(errorText,
"The PINs don't match.");
return validationFailureClear(errorText);
}
function validateUsername(username, errorText, extraReservedUsernames = []) {
var name = username.text;
var reserved = config.reservedUsernames.concat(extraReservedUsernames);
/* Validate characters */
for (var i = 0; i < name.length; i++) {
if (i) {
if (!name[i].match(/^[a-z0-9_-]$/))
return validationFailure(errorText,
"Characters must be lowercase" +
" letters, numbers,<br>" +
" underscores or minus signs.");
} else {
if (!name[i].match(/^[a-z_]$/))
return validationFailure(errorText,
"First character must be a" +
" lowercase letter or an" +
" underscore.");
}
}
/* Validate against reserved usernames */
for (var i = 0; i < reserved.length; i++) {
if (name == reserved[i])
return validationFailure(errorText, "Username '" +
reserved[i] +
"' is reserved.");
}
/* Passed */
return validationFailureClear(errorText);
}
function validateSshdUsername(username, errorText) {
return validateUsername(username, errorText, [config.username]);
}
function validateSshdPassword(password, passwordRepeat, errorText) {
var pass = password.text;
var repeat = passwordRepeat.text;
if (pass == "")
return validationFailure(errorText);
if (pass.length < 6)
return validationFailure(errorText,
"Too short: needs at least 6" +
" digits/characters.");
if (repeat == "")
return validationFailure(errorText);
if (pass != repeat)
return validationFailure(errorText, "Passwords don't match.");
return validationFailureClear(errorText);
}
function check_chars(input) {
for (var i = 0; i < input.length; i++) {
if (allowed_chars.indexOf(input[i]) == -1)
return false;
}
return true;
}
function allowed_chars_multiline() {
/* return allowed_chars split across multiple lines */
var step = 20;
var ret = "";
for (var i = 0; i < allowed_chars.length + step; i += step)
ret += allowed_chars.slice(i, i + step) + "\n";
return ret.trim();
}
function validatePassword(password, passwordRepeat, errorText) {
var pass = password.text;
var repeat = passwordRepeat.text;
if (pass == "")
return validationFailure(errorText);
/* This function gets called for the FDE password and for the user
* password. As of writing, all distributions shipping the mobile
* module are using osk-sdl to type in the FDE password after the
* installation, and another keyboard after booting up, to type in the
* user password. The osk-sdl password has the same keys as
* squeekboard's default layout, and other keyboards should be able to
* type these characters in as well. For now, verify that the password
* only contains characters that can be typed in by osk-sdl. If you
* need this to be more sophisticated, feel free to submit patches to
* make this more configurable. */
if (!check_chars(pass))
return validationFailure(errorText,
"The password must only contain" +
" these characters, others can possibly" +
" not be typed in after installation:\n" +
"\n" +
allowed_chars_multiline());
if (pass.length < 6)
return validationFailure(errorText,
"Too short: needs at least 6" +
" digits/characters.");
if (repeat == "")
return validationFailure(errorText);
if (pass != repeat)
return validationFailure(errorText, "Passwords don't match.");
return validationFailureClear(errorText);
}
}