fedora
This commit is contained in:
@@ -0,0 +1,473 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
/*
|
||||
Noctalia is not strictly a Material Design project, it supports both some predefined
|
||||
color schemes and dynamic color generation from the wallpaper.
|
||||
|
||||
We ultimately decided to use a restricted set of colors that follows the
|
||||
Material Design 3 naming convention.
|
||||
|
||||
NOTE: All color names are prefixed with 'm' (e.g., mPrimary) to prevent QML from
|
||||
misinterpreting them as signals (e.g., the 'onPrimary' property name).
|
||||
*/
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool reloadColors: false
|
||||
|
||||
// Suppress transition animations until the first colors.json load completes
|
||||
property bool skipTransition: true
|
||||
|
||||
// Flag indicating theme colors are currently transitioning (for widgets to disable their own animations)
|
||||
property bool isTransitioning: false
|
||||
|
||||
// Timer to reset isTransitioning after animation completes
|
||||
Timer {
|
||||
id: transitionTimer
|
||||
interval: Style.animationSlowest + 50 // Small buffer after animation
|
||||
onTriggered: root.isTransitioning = false
|
||||
}
|
||||
|
||||
// --- Key Colors: These are the main accent colors that define your app's style
|
||||
property color mPrimary: defaultColors.mPrimary
|
||||
property color mOnPrimary: defaultColors.mOnPrimary
|
||||
property color mSecondary: defaultColors.mSecondary
|
||||
property color mOnSecondary: defaultColors.mOnSecondary
|
||||
property color mTertiary: defaultColors.mTertiary
|
||||
property color mOnTertiary: defaultColors.mOnTertiary
|
||||
|
||||
// --- Utility Colors: These colors serve specific, universal purposes like indicating errors
|
||||
property color mError: defaultColors.mError
|
||||
property color mOnError: defaultColors.mOnError
|
||||
|
||||
// --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy
|
||||
property color mSurface: defaultColors.mSurface
|
||||
property color mOnSurface: defaultColors.mOnSurface
|
||||
|
||||
property color mSurfaceVariant: defaultColors.mSurfaceVariant
|
||||
property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
|
||||
|
||||
property color mOutline: defaultColors.mOutline
|
||||
property color mShadow: defaultColors.mShadow
|
||||
|
||||
property color mHover: defaultColors.mHover
|
||||
property color mOnHover: defaultColors.mOnHover
|
||||
|
||||
// --- Color transition animations ---
|
||||
Behavior on mPrimary {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnPrimary {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mSecondary {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnSecondary {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mTertiary {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnTertiary {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mError {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnError {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mSurface {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnSurface {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mSurfaceVariant {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnSurfaceVariant {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOutline {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mShadow {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mHover {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on mOnHover {
|
||||
enabled: !root.skipTransition
|
||||
ColorAnimation {
|
||||
duration: Style.animationSlowest
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to start transition and update a color
|
||||
function startTransition() {
|
||||
root.isTransitioning = true;
|
||||
transitionTimer.restart();
|
||||
}
|
||||
|
||||
// Update colors when customColorsData changes (imperative assignment enables Behavior animations)
|
||||
Connections {
|
||||
target: customColorsData
|
||||
function onMPrimaryChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mPrimary = customColorsData.mPrimary;
|
||||
}
|
||||
function onMOnPrimaryChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnPrimary = customColorsData.mOnPrimary;
|
||||
}
|
||||
function onMSecondaryChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mSecondary = customColorsData.mSecondary;
|
||||
}
|
||||
function onMOnSecondaryChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnSecondary = customColorsData.mOnSecondary;
|
||||
}
|
||||
function onMTertiaryChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mTertiary = customColorsData.mTertiary;
|
||||
}
|
||||
function onMOnTertiaryChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnTertiary = customColorsData.mOnTertiary;
|
||||
}
|
||||
function onMErrorChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mError = customColorsData.mError;
|
||||
}
|
||||
function onMOnErrorChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnError = customColorsData.mOnError;
|
||||
}
|
||||
function onMSurfaceChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mSurface = customColorsData.mSurface;
|
||||
}
|
||||
function onMOnSurfaceChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnSurface = customColorsData.mOnSurface;
|
||||
}
|
||||
function onMSurfaceVariantChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mSurfaceVariant = customColorsData.mSurfaceVariant;
|
||||
}
|
||||
function onMOnSurfaceVariantChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnSurfaceVariant = customColorsData.mOnSurfaceVariant;
|
||||
}
|
||||
function onMOutlineChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOutline = customColorsData.mOutline;
|
||||
}
|
||||
function onMShadowChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mShadow = customColorsData.mShadow;
|
||||
}
|
||||
function onMHoverChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mHover = customColorsData.mHover;
|
||||
}
|
||||
function onMOnHoverChanged() {
|
||||
if (!root.skipTransition) {
|
||||
startTransition();
|
||||
}
|
||||
root.mOnHover = customColorsData.mOnHover;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveColorKey(key) {
|
||||
switch (key) {
|
||||
case "primary":
|
||||
return root.mPrimary;
|
||||
case "secondary":
|
||||
return root.mSecondary;
|
||||
case "tertiary":
|
||||
return root.mTertiary;
|
||||
case "error":
|
||||
return root.mError;
|
||||
default:
|
||||
return root.mOnSurface;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOnColorKey(key) {
|
||||
switch (key) {
|
||||
case "primary":
|
||||
return root.mOnPrimary;
|
||||
case "secondary":
|
||||
return root.mOnSecondary;
|
||||
case "tertiary":
|
||||
return root.mOnTertiary;
|
||||
case "error":
|
||||
return root.mOnError;
|
||||
default:
|
||||
return root.mSurface;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveColorKeyOptional(key) {
|
||||
switch (key) {
|
||||
case "primary":
|
||||
return root.mPrimary;
|
||||
case "secondary":
|
||||
return root.mSecondary;
|
||||
case "tertiary":
|
||||
return root.mTertiary;
|
||||
case "error":
|
||||
return root.mError;
|
||||
default:
|
||||
return "transparent";
|
||||
}
|
||||
}
|
||||
|
||||
// Adaptive opacity calculation: automatically makes light mode more transparent
|
||||
function adaptiveOpacity(baseOpacity) {
|
||||
return Settings.data.colorSchemes.darkMode ? baseOpacity : Math.pow(baseOpacity, 1.5);
|
||||
}
|
||||
|
||||
function smartAlpha(baseColor, minAlpha = 0.4) {
|
||||
if (!Settings.data.ui.translucentWidgets)
|
||||
return baseColor;
|
||||
|
||||
let alpha = Math.max(adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity), minAlpha);
|
||||
|
||||
// Combine with the base color's existing alpha
|
||||
let resultAlpha = Math.max(0, baseColor.a - (1.0 - alpha));
|
||||
return Qt.alpha(baseColor, resultAlpha);
|
||||
}
|
||||
|
||||
readonly property var colorKeyModel: [
|
||||
{
|
||||
"key": "none",
|
||||
"name": I18n.tr("common.none")
|
||||
},
|
||||
{
|
||||
"key": "primary",
|
||||
"name": I18n.tr("common.primary")
|
||||
},
|
||||
{
|
||||
"key": "secondary",
|
||||
"name": I18n.tr("common.secondary")
|
||||
},
|
||||
{
|
||||
"key": "tertiary",
|
||||
"name": I18n.tr("common.tertiary")
|
||||
},
|
||||
{
|
||||
"key": "error",
|
||||
"name": I18n.tr("common.error")
|
||||
}
|
||||
]
|
||||
|
||||
// --------------------------------
|
||||
// Default colors: Noctalia (default) dark — must match Assets/ColorScheme/Noctalia-default
|
||||
QtObject {
|
||||
id: defaultColors
|
||||
|
||||
readonly property color mPrimary: "#fff59b"
|
||||
readonly property color mOnPrimary: "#0e0e43"
|
||||
|
||||
readonly property color mSecondary: "#a9aefe"
|
||||
readonly property color mOnSecondary: "#0e0e43"
|
||||
|
||||
readonly property color mTertiary: "#9BFECE"
|
||||
readonly property color mOnTertiary: "#0e0e43"
|
||||
|
||||
readonly property color mError: "#FD4663"
|
||||
readonly property color mOnError: "#0e0e43"
|
||||
|
||||
readonly property color mSurface: "#070722"
|
||||
readonly property color mOnSurface: "#f3edf7"
|
||||
|
||||
readonly property color mSurfaceVariant: "#11112d"
|
||||
readonly property color mOnSurfaceVariant: "#7c80b4"
|
||||
|
||||
readonly property color mOutline: "#21215F"
|
||||
readonly property color mShadow: "#070722"
|
||||
|
||||
readonly property color mHover: "#9BFECE"
|
||||
readonly property color mOnHover: "#0e0e43"
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// FileView to load custom colors data from colors.json
|
||||
FileView {
|
||||
id: customColorsFile
|
||||
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: {
|
||||
Logger.d("Color", "Reloading colors from disk");
|
||||
reloadColors = true;
|
||||
reload();
|
||||
}
|
||||
onAdapterUpdated: {
|
||||
Logger.d("Color", "Writing colors to disk");
|
||||
writeAdapter();
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root.skipTransition) {
|
||||
Qt.callLater(function () {
|
||||
root.skipTransition = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger initial load when path changes from empty to actual path
|
||||
onPathChanged: {
|
||||
if (path !== undefined) {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (reloadColors) {
|
||||
reloadColors = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.skipTransition) {
|
||||
Qt.callLater(function () {
|
||||
root.skipTransition = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Error code 2 = ENOENT (No such file or directory)
|
||||
if (error === 2 || error.toString().includes("No such file")) {
|
||||
// File doesn't exist, create it with default values
|
||||
writeAdapter();
|
||||
}
|
||||
}
|
||||
JsonAdapter {
|
||||
id: customColorsData
|
||||
|
||||
property color mPrimary: defaultColors.mPrimary
|
||||
property color mOnPrimary: defaultColors.mOnPrimary
|
||||
|
||||
property color mSecondary: defaultColors.mSecondary
|
||||
property color mOnSecondary: defaultColors.mOnSecondary
|
||||
|
||||
property color mTertiary: defaultColors.mTertiary
|
||||
property color mOnTertiary: defaultColors.mOnTertiary
|
||||
|
||||
property color mError: defaultColors.mError
|
||||
property color mOnError: defaultColors.mOnError
|
||||
|
||||
property color mSurface: defaultColors.mSurface
|
||||
property color mOnSurface: defaultColors.mOnSurface
|
||||
|
||||
property color mSurfaceVariant: defaultColors.mSurfaceVariant
|
||||
property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
|
||||
|
||||
property color mOutline: defaultColors.mOutline
|
||||
property color mShadow: defaultColors.mShadow
|
||||
|
||||
property color mHover: defaultColors.mHover
|
||||
property color mOnHover: defaultColors.mOnHover
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Public API
|
||||
function go(search, targets, options) {
|
||||
return _go(search, targets, options);
|
||||
}
|
||||
|
||||
function single(search, target) {
|
||||
return _single(search, target);
|
||||
}
|
||||
|
||||
function highlight(result, open, close) {
|
||||
if (open === undefined)
|
||||
open = '<b>';
|
||||
if (close === undefined)
|
||||
close = '</b>';
|
||||
return _highlight(result, open, close);
|
||||
}
|
||||
|
||||
function prepare(target) {
|
||||
return _prepare(target);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
return _cleanup();
|
||||
}
|
||||
|
||||
// Internal implementation
|
||||
readonly property var _INFINITY: Infinity
|
||||
readonly property var _NEGATIVE_INFINITY: -Infinity
|
||||
readonly property var _NULL: null
|
||||
property var _noResults: {
|
||||
let r = [];
|
||||
r.total = 0;
|
||||
return r;
|
||||
}
|
||||
property var _noTarget: _prepare('')
|
||||
|
||||
property var _preparedCache: new Map()
|
||||
property var _preparedSearchCache: new Map()
|
||||
|
||||
property var _matchesSimple: []
|
||||
property var _matchesStrict: []
|
||||
property var _nextBeginningIndexesChanges: []
|
||||
property var _keysSpacesBestScores: []
|
||||
property var _allowPartialMatchScores: []
|
||||
property var _tmpTargets: []
|
||||
property var _tmpResults: []
|
||||
property var _q: _fastpriorityqueue()
|
||||
|
||||
function _fastpriorityqueue() {
|
||||
var e = [], o = 0, a = {};
|
||||
var v = function (r) {
|
||||
for (var a = 0, vc = e[a], c = 1; c < o; ) {
|
||||
var s = c + 1;
|
||||
a = c;
|
||||
if (s < o && e[s]._score < e[c]._score)
|
||||
a = s;
|
||||
e[a - 1 >> 1] = e[a];
|
||||
c = 1 + (a << 1);
|
||||
}
|
||||
for (var f = a - 1 >> 1; a > 0 && vc._score < e[f]._score; f = (a = f) - 1 >> 1)
|
||||
e[a] = e[f];
|
||||
e[a] = vc;
|
||||
};
|
||||
a.add = function (r) {
|
||||
var ac = o;
|
||||
e[o++] = r;
|
||||
for (var vc = ac - 1 >> 1; ac > 0 && r._score < e[vc]._score; vc = (ac = vc) - 1 >> 1)
|
||||
e[ac] = e[vc];
|
||||
e[ac] = r;
|
||||
};
|
||||
a.poll = function () {
|
||||
if (o !== 0) {
|
||||
var ac = e[0];
|
||||
e[0] = e[--o];
|
||||
v();
|
||||
return ac;
|
||||
}
|
||||
};
|
||||
a.peek = function () {
|
||||
if (o !== 0)
|
||||
return e[0];
|
||||
};
|
||||
a.replaceTop = function (r) {
|
||||
e[0] = r;
|
||||
v();
|
||||
};
|
||||
return a;
|
||||
}
|
||||
|
||||
function _createResult() {
|
||||
return {
|
||||
target: '',
|
||||
obj: _NULL,
|
||||
_score: _NEGATIVE_INFINITY,
|
||||
_indexes: [],
|
||||
_targetLower: '',
|
||||
_targetLowerCodes: _NULL,
|
||||
_nextBeginningIndexes: _NULL,
|
||||
_bitflags: 0,
|
||||
get indexes() {
|
||||
return this._indexes.slice(0, this._indexes.len).sort((a, b) => a - b);
|
||||
},
|
||||
set indexes(idx) {
|
||||
this._indexes = idx;
|
||||
},
|
||||
highlight: function (open, close) {
|
||||
return root._highlight(this, open, close);
|
||||
},
|
||||
get score() {
|
||||
return root._normalizeScore(this._score);
|
||||
},
|
||||
set score(s) {
|
||||
this._score = root._denormalizeScore(s);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _createKeysResult(len) {
|
||||
var arr = new Array(len);
|
||||
arr._score = _NEGATIVE_INFINITY;
|
||||
arr.obj = _NULL;
|
||||
Object.defineProperty(arr, 'score', {
|
||||
get: function () {
|
||||
return root._normalizeScore(this._score);
|
||||
},
|
||||
set: function (s) {
|
||||
this._score = root._denormalizeScore(s);
|
||||
}
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
function _new_result(target, options) {
|
||||
var result = _createResult();
|
||||
result.target = target;
|
||||
result.obj = options.obj ?? _NULL;
|
||||
result._score = options._score ?? _NEGATIVE_INFINITY;
|
||||
result._indexes = options._indexes ?? [];
|
||||
result._targetLower = options._targetLower ?? '';
|
||||
result._targetLowerCodes = options._targetLowerCodes ?? _NULL;
|
||||
result._nextBeginningIndexes = options._nextBeginningIndexes ?? _NULL;
|
||||
result._bitflags = options._bitflags ?? 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
function _normalizeScore(score) {
|
||||
if (score === _NEGATIVE_INFINITY)
|
||||
return 0;
|
||||
if (score > 1)
|
||||
return score;
|
||||
return Math.E ** (((-score + 1) ** 0.04307 - 1) * -2);
|
||||
}
|
||||
|
||||
function _denormalizeScore(normalizedScore) {
|
||||
if (normalizedScore === 0)
|
||||
return _NEGATIVE_INFINITY;
|
||||
if (normalizedScore > 1)
|
||||
return normalizedScore;
|
||||
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307);
|
||||
}
|
||||
|
||||
function _remove_accents(str) {
|
||||
return str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
function _prepareLowerInfo(str) {
|
||||
str = _remove_accents(str);
|
||||
var strLen = str.length;
|
||||
var lower = str.toLowerCase();
|
||||
var lowerCodes = [];
|
||||
var bitflags = 0;
|
||||
var containsSpace = false;
|
||||
|
||||
for (var i = 0; i < strLen; ++i) {
|
||||
var lowerCode = lowerCodes[i] = lower.charCodeAt(i);
|
||||
if (lowerCode === 32) {
|
||||
containsSpace = true;
|
||||
continue;
|
||||
}
|
||||
var bit = lowerCode >= 97 && lowerCode <= 122 ? lowerCode - 97 : lowerCode >= 48 && lowerCode <= 57 ? 26 : lowerCode <= 127 ? 30 : 31;
|
||||
bitflags |= 1 << bit;
|
||||
}
|
||||
return {
|
||||
lowerCodes: lowerCodes,
|
||||
bitflags: bitflags,
|
||||
containsSpace: containsSpace,
|
||||
_lower: lower
|
||||
};
|
||||
}
|
||||
|
||||
function _prepareBeginningIndexes(target) {
|
||||
var targetLen = target.length;
|
||||
var beginningIndexes = [];
|
||||
var beginningIndexesLen = 0;
|
||||
var wasUpper = false;
|
||||
var wasAlphanum = false;
|
||||
for (var i = 0; i < targetLen; ++i) {
|
||||
var targetCode = target.charCodeAt(i);
|
||||
var isUpper = targetCode >= 65 && targetCode <= 90;
|
||||
var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57;
|
||||
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum;
|
||||
wasUpper = isUpper;
|
||||
wasAlphanum = isAlphanum;
|
||||
if (isBeginning)
|
||||
beginningIndexes[beginningIndexesLen++] = i;
|
||||
}
|
||||
return beginningIndexes;
|
||||
}
|
||||
|
||||
function _prepareNextBeginningIndexes(target) {
|
||||
target = _remove_accents(target);
|
||||
var targetLen = target.length;
|
||||
var beginningIndexes = _prepareBeginningIndexes(target);
|
||||
var nextBeginningIndexes = [];
|
||||
var lastIsBeginning = beginningIndexes[0];
|
||||
var lastIsBeginningI = 0;
|
||||
for (var i = 0; i < targetLen; ++i) {
|
||||
if (lastIsBeginning > i) {
|
||||
nextBeginningIndexes[i] = lastIsBeginning;
|
||||
} else {
|
||||
lastIsBeginning = beginningIndexes[++lastIsBeginningI];
|
||||
nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning;
|
||||
}
|
||||
}
|
||||
return nextBeginningIndexes;
|
||||
}
|
||||
|
||||
function _prepareSearch(search) {
|
||||
if (typeof search === 'number')
|
||||
search = '' + search;
|
||||
else if (typeof search !== 'string')
|
||||
search = '';
|
||||
search = search.trim();
|
||||
var info = _prepareLowerInfo(search);
|
||||
|
||||
var spaceSearches = [];
|
||||
if (info.containsSpace) {
|
||||
var searches = search.split(/\s+/);
|
||||
searches = [...new Set(searches)];
|
||||
for (var i = 0; i < searches.length; i++) {
|
||||
if (searches[i] === '')
|
||||
continue;
|
||||
var _info = _prepareLowerInfo(searches[i]);
|
||||
spaceSearches.push({
|
||||
lowerCodes: _info.lowerCodes,
|
||||
_lower: searches[i].toLowerCase(),
|
||||
containsSpace: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
lowerCodes: info.lowerCodes,
|
||||
_lower: info._lower,
|
||||
containsSpace: info.containsSpace,
|
||||
bitflags: info.bitflags,
|
||||
spaceSearches: spaceSearches
|
||||
};
|
||||
}
|
||||
|
||||
function _prepare(target) {
|
||||
if (typeof target === 'number')
|
||||
target = '' + target;
|
||||
else if (typeof target !== 'string')
|
||||
target = '';
|
||||
var info = _prepareLowerInfo(target);
|
||||
return _new_result(target, {
|
||||
_targetLower: info._lower,
|
||||
_targetLowerCodes: info.lowerCodes,
|
||||
_bitflags: info.bitflags
|
||||
});
|
||||
}
|
||||
|
||||
function _cleanup() {
|
||||
_preparedCache.clear();
|
||||
_preparedSearchCache.clear();
|
||||
}
|
||||
|
||||
function _isPrepared(x) {
|
||||
return typeof x === 'object' && typeof x._bitflags === 'number';
|
||||
}
|
||||
|
||||
function _getPrepared(target) {
|
||||
if (target.length > 999)
|
||||
return _prepare(target);
|
||||
var targetPrepared = _preparedCache.get(target);
|
||||
if (targetPrepared !== undefined)
|
||||
return targetPrepared;
|
||||
targetPrepared = _prepare(target);
|
||||
_preparedCache.set(target, targetPrepared);
|
||||
return targetPrepared;
|
||||
}
|
||||
|
||||
function _getPreparedSearch(search) {
|
||||
if (search.length > 999)
|
||||
return _prepareSearch(search);
|
||||
var searchPrepared = _preparedSearchCache.get(search);
|
||||
if (searchPrepared !== undefined)
|
||||
return searchPrepared;
|
||||
searchPrepared = _prepareSearch(search);
|
||||
_preparedSearchCache.set(search, searchPrepared);
|
||||
return searchPrepared;
|
||||
}
|
||||
|
||||
function _getValue(obj, prop) {
|
||||
var tmp = obj[prop];
|
||||
if (tmp !== undefined)
|
||||
return tmp;
|
||||
if (typeof prop === 'function')
|
||||
return prop(obj);
|
||||
var segs = prop;
|
||||
if (!Array.isArray(prop))
|
||||
segs = prop.split('.');
|
||||
var len = segs.length;
|
||||
var i = -1;
|
||||
while (obj && (++i < len))
|
||||
obj = obj[segs[i]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
function _single(search, target) {
|
||||
if (!search || !target)
|
||||
return _NULL;
|
||||
var preparedSearch = _getPreparedSearch(search);
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
var searchBitflags = preparedSearch.bitflags;
|
||||
if ((searchBitflags & target._bitflags) !== searchBitflags)
|
||||
return _NULL;
|
||||
return _algorithm(preparedSearch, target);
|
||||
}
|
||||
|
||||
function _highlight(result, open, close) {
|
||||
if (open === undefined)
|
||||
open = '<b>';
|
||||
if (close === undefined)
|
||||
close = '</b>';
|
||||
var callback = typeof open === 'function' ? open : undefined;
|
||||
|
||||
var target = result.target;
|
||||
var targetLen = target.length;
|
||||
var indexes = result.indexes;
|
||||
var highlighted = '';
|
||||
var matchI = 0;
|
||||
var indexesI = 0;
|
||||
var opened = false;
|
||||
var parts = [];
|
||||
|
||||
for (var i = 0; i < targetLen; ++i) {
|
||||
var ch = target[i];
|
||||
if (indexes[indexesI] === i) {
|
||||
++indexesI;
|
||||
if (!opened) {
|
||||
opened = true;
|
||||
if (callback) {
|
||||
parts.push(highlighted);
|
||||
highlighted = '';
|
||||
} else {
|
||||
highlighted += open;
|
||||
}
|
||||
}
|
||||
if (indexesI === indexes.length) {
|
||||
if (callback) {
|
||||
highlighted += ch;
|
||||
parts.push(callback(highlighted, matchI++));
|
||||
highlighted = '';
|
||||
parts.push(target.substr(i + 1));
|
||||
} else {
|
||||
highlighted += ch + close + target.substr(i + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (opened) {
|
||||
opened = false;
|
||||
if (callback) {
|
||||
parts.push(callback(highlighted, matchI++));
|
||||
highlighted = '';
|
||||
} else {
|
||||
highlighted += close;
|
||||
}
|
||||
}
|
||||
}
|
||||
highlighted += ch;
|
||||
}
|
||||
return callback ? parts : highlighted;
|
||||
}
|
||||
|
||||
function _all(targets, options) {
|
||||
var results = [];
|
||||
results.total = targets.length;
|
||||
var limit = options?.limit || _INFINITY;
|
||||
|
||||
if (options?.key) {
|
||||
for (var i = 0; i < targets.length; i++) {
|
||||
var obj = targets[i];
|
||||
var target = _getValue(obj, options.key);
|
||||
if (target == _NULL)
|
||||
continue;
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
var result = _new_result(target.target, {
|
||||
_score: target._score,
|
||||
obj: obj
|
||||
});
|
||||
results.push(result);
|
||||
if (results.length >= limit)
|
||||
return results;
|
||||
}
|
||||
} else if (options?.keys) {
|
||||
for (var i = 0; i < targets.length; i++) {
|
||||
var obj = targets[i];
|
||||
var objResults = _createKeysResult(options.keys.length);
|
||||
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
|
||||
var target = _getValue(obj, options.keys[keyI]);
|
||||
if (!target) {
|
||||
objResults[keyI] = _noTarget;
|
||||
continue;
|
||||
}
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
target._score = _NEGATIVE_INFINITY;
|
||||
target._indexes.len = 0;
|
||||
objResults[keyI] = target;
|
||||
}
|
||||
objResults.obj = obj;
|
||||
objResults._score = _NEGATIVE_INFINITY;
|
||||
results.push(objResults);
|
||||
if (results.length >= limit)
|
||||
return results;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < targets.length; i++) {
|
||||
var target = targets[i];
|
||||
if (target == _NULL)
|
||||
continue;
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
target._score = _NEGATIVE_INFINITY;
|
||||
target._indexes.len = 0;
|
||||
results.push(target);
|
||||
if (results.length >= limit)
|
||||
return results;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function _algorithm(preparedSearch, prepared, allowSpaces, allowPartialMatch) {
|
||||
if (allowSpaces === undefined)
|
||||
allowSpaces = false;
|
||||
if (allowPartialMatch === undefined)
|
||||
allowPartialMatch = false;
|
||||
|
||||
if (allowSpaces === false && preparedSearch.containsSpace)
|
||||
return _algorithmSpaces(preparedSearch, prepared, allowPartialMatch);
|
||||
|
||||
var searchLower = preparedSearch._lower;
|
||||
var searchLowerCodes = preparedSearch.lowerCodes;
|
||||
var searchLowerCode = searchLowerCodes[0];
|
||||
var targetLowerCodes = prepared._targetLowerCodes;
|
||||
var searchLen = searchLowerCodes.length;
|
||||
var targetLen = targetLowerCodes.length;
|
||||
var searchI = 0;
|
||||
var targetI = 0;
|
||||
var matchesSimpleLen = 0;
|
||||
|
||||
for (; ; ) {
|
||||
var isMatch = searchLowerCode === targetLowerCodes[targetI];
|
||||
if (isMatch) {
|
||||
_matchesSimple[matchesSimpleLen++] = targetI;
|
||||
++searchI;
|
||||
if (searchI === searchLen)
|
||||
break;
|
||||
searchLowerCode = searchLowerCodes[searchI];
|
||||
}
|
||||
++targetI;
|
||||
if (targetI >= targetLen)
|
||||
return _NULL;
|
||||
}
|
||||
|
||||
searchI = 0;
|
||||
var successStrict = false;
|
||||
var matchesStrictLen = 0;
|
||||
|
||||
var nextBeginningIndexes = prepared._nextBeginningIndexes;
|
||||
if (nextBeginningIndexes === _NULL)
|
||||
nextBeginningIndexes = prepared._nextBeginningIndexes = _prepareNextBeginningIndexes(prepared.target);
|
||||
targetI = _matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[_matchesSimple[0] - 1];
|
||||
|
||||
var backtrackCount = 0;
|
||||
if (targetI !== targetLen)
|
||||
for (; ; ) {
|
||||
if (targetI >= targetLen) {
|
||||
if (searchI <= 0)
|
||||
break;
|
||||
++backtrackCount;
|
||||
if (backtrackCount > 200)
|
||||
break;
|
||||
--searchI;
|
||||
var lastMatch = _matchesStrict[--matchesStrictLen];
|
||||
targetI = nextBeginningIndexes[lastMatch];
|
||||
} else {
|
||||
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI];
|
||||
if (isMatch) {
|
||||
_matchesStrict[matchesStrictLen++] = targetI;
|
||||
++searchI;
|
||||
if (searchI === searchLen) {
|
||||
successStrict = true;
|
||||
break;
|
||||
}
|
||||
++targetI;
|
||||
} else {
|
||||
targetI = nextBeginningIndexes[targetI];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, _matchesSimple[0]);
|
||||
var isSubstring = !!~substringIndex;
|
||||
var isSubstringBeginning = !isSubstring ? false : substringIndex === 0 || prepared._nextBeginningIndexes[substringIndex - 1] === substringIndex;
|
||||
|
||||
if (isSubstring && !isSubstringBeginning) {
|
||||
for (var i = 0; i < nextBeginningIndexes.length; i = nextBeginningIndexes[i]) {
|
||||
if (i <= substringIndex)
|
||||
continue;
|
||||
for (var s = 0; s < searchLen; s++)
|
||||
if (searchLowerCodes[s] !== prepared._targetLowerCodes[i + s])
|
||||
break;
|
||||
if (s === searchLen) {
|
||||
substringIndex = i;
|
||||
isSubstringBeginning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var calculateScore = function (matches) {
|
||||
var score = 0;
|
||||
var extraMatchGroupCount = 0;
|
||||
for (var i = 1; i < searchLen; ++i) {
|
||||
if (matches[i] - matches[i - 1] !== 1) {
|
||||
score -= matches[i];
|
||||
++extraMatchGroupCount;
|
||||
}
|
||||
}
|
||||
var unmatchedDistance = matches[searchLen - 1] - matches[0] - (searchLen - 1);
|
||||
score -= (12 + unmatchedDistance) * extraMatchGroupCount;
|
||||
if (matches[0] !== 0)
|
||||
score -= matches[0] * matches[0] * 0.2;
|
||||
if (!successStrict) {
|
||||
score *= 1000;
|
||||
} else {
|
||||
var uniqueBeginningIndexes = 1;
|
||||
for (var i = nextBeginningIndexes[0]; i < targetLen; i = nextBeginningIndexes[i])
|
||||
++uniqueBeginningIndexes;
|
||||
if (uniqueBeginningIndexes > 24)
|
||||
score *= (uniqueBeginningIndexes - 24) * 10;
|
||||
}
|
||||
score -= (targetLen - searchLen) / 2;
|
||||
if (isSubstring)
|
||||
score /= 1 + searchLen * searchLen * 1;
|
||||
if (isSubstringBeginning)
|
||||
score /= 1 + searchLen * searchLen * 1;
|
||||
score -= (targetLen - searchLen) / 2;
|
||||
return score;
|
||||
};
|
||||
|
||||
var matchesBest, score;
|
||||
if (!successStrict) {
|
||||
if (isSubstring)
|
||||
for (var i = 0; i < searchLen; ++i)
|
||||
_matchesSimple[i] = substringIndex + i;
|
||||
matchesBest = _matchesSimple;
|
||||
score = calculateScore(matchesBest);
|
||||
} else {
|
||||
if (isSubstringBeginning) {
|
||||
for (var i = 0; i < searchLen; ++i)
|
||||
_matchesSimple[i] = substringIndex + i;
|
||||
matchesBest = _matchesSimple;
|
||||
score = calculateScore(_matchesSimple);
|
||||
} else {
|
||||
matchesBest = _matchesStrict;
|
||||
score = calculateScore(_matchesStrict);
|
||||
}
|
||||
}
|
||||
|
||||
prepared._score = score;
|
||||
for (var i = 0; i < searchLen; ++i)
|
||||
prepared._indexes[i] = matchesBest[i];
|
||||
prepared._indexes.len = searchLen;
|
||||
|
||||
var result = _createResult();
|
||||
result.target = prepared.target;
|
||||
result._score = prepared._score;
|
||||
result._indexes = prepared._indexes;
|
||||
return result;
|
||||
}
|
||||
|
||||
function _algorithmSpaces(preparedSearch, target, allowPartialMatch) {
|
||||
var seen_indexes = new Set();
|
||||
var score = 0;
|
||||
var result = _NULL;
|
||||
|
||||
var first_seen_index_last_search = 0;
|
||||
var searches = preparedSearch.spaceSearches;
|
||||
var searchesLen = searches.length;
|
||||
var changeslen = 0;
|
||||
|
||||
var resetNextBeginningIndexes = function () {
|
||||
for (let i = changeslen - 1; i >= 0; i--)
|
||||
target._nextBeginningIndexes[_nextBeginningIndexesChanges[i * 2 + 0]] = _nextBeginningIndexesChanges[i * 2 + 1];
|
||||
};
|
||||
|
||||
var hasAtLeast1Match = false;
|
||||
for (var i = 0; i < searchesLen; ++i) {
|
||||
_allowPartialMatchScores[i] = _NEGATIVE_INFINITY;
|
||||
var search = searches[i];
|
||||
result = _algorithm(search, target);
|
||||
|
||||
if (allowPartialMatch) {
|
||||
if (result === _NULL)
|
||||
continue;
|
||||
hasAtLeast1Match = true;
|
||||
} else {
|
||||
if (result === _NULL) {
|
||||
resetNextBeginningIndexes();
|
||||
return _NULL;
|
||||
}
|
||||
}
|
||||
|
||||
var isTheLastSearch = i === searchesLen - 1;
|
||||
if (!isTheLastSearch) {
|
||||
var indexes = result._indexes;
|
||||
var indexesIsConsecutiveSubstring = true;
|
||||
for (let j = 0; j < indexes.len - 1; j++) {
|
||||
if (indexes[j + 1] - indexes[j] !== 1) {
|
||||
indexesIsConsecutiveSubstring = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (indexesIsConsecutiveSubstring) {
|
||||
var newBeginningIndex = indexes[indexes.len - 1] + 1;
|
||||
var toReplace = target._nextBeginningIndexes[newBeginningIndex - 1];
|
||||
for (let j = newBeginningIndex - 1; j >= 0; j--) {
|
||||
if (toReplace !== target._nextBeginningIndexes[j])
|
||||
break;
|
||||
target._nextBeginningIndexes[j] = newBeginningIndex;
|
||||
_nextBeginningIndexesChanges[changeslen * 2 + 0] = j;
|
||||
_nextBeginningIndexesChanges[changeslen * 2 + 1] = toReplace;
|
||||
changeslen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
score += result._score / searchesLen;
|
||||
_allowPartialMatchScores[i] = result._score / searchesLen;
|
||||
|
||||
if (result._indexes[0] < first_seen_index_last_search) {
|
||||
score -= (first_seen_index_last_search - result._indexes[0]) * 2;
|
||||
}
|
||||
first_seen_index_last_search = result._indexes[0];
|
||||
|
||||
for (var j = 0; j < result._indexes.len; ++j)
|
||||
seen_indexes.add(result._indexes[j]);
|
||||
}
|
||||
|
||||
if (allowPartialMatch && !hasAtLeast1Match)
|
||||
return _NULL;
|
||||
|
||||
resetNextBeginningIndexes();
|
||||
|
||||
var allowSpacesResult = _algorithm(preparedSearch, target, true);
|
||||
if (allowSpacesResult !== _NULL && allowSpacesResult._score > score) {
|
||||
if (allowPartialMatch) {
|
||||
for (var i = 0; i < searchesLen; ++i) {
|
||||
_allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen;
|
||||
}
|
||||
}
|
||||
return allowSpacesResult;
|
||||
}
|
||||
|
||||
if (allowPartialMatch)
|
||||
result = target;
|
||||
result._score = score;
|
||||
|
||||
var idx = 0;
|
||||
for (let index of seen_indexes)
|
||||
result._indexes[idx++] = index;
|
||||
result._indexes.len = idx;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _go(search, targets, options) {
|
||||
if (!search)
|
||||
return options?.all ? _all(targets, options) : _noResults;
|
||||
|
||||
var preparedSearch = _getPreparedSearch(search);
|
||||
var searchBitflags = preparedSearch.bitflags;
|
||||
var containsSpace = preparedSearch.containsSpace;
|
||||
|
||||
var threshold = _denormalizeScore(options?.threshold ?? 0.35);
|
||||
var limit = options?.limit || _INFINITY;
|
||||
|
||||
var resultsLen = 0;
|
||||
var limitedCount = 0;
|
||||
var targetsLen = targets.length;
|
||||
|
||||
function push_result(result) {
|
||||
if (resultsLen < limit) {
|
||||
_q.add(result);
|
||||
++resultsLen;
|
||||
} else {
|
||||
++limitedCount;
|
||||
if (result._score > _q.peek()._score)
|
||||
_q.replaceTop(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.key) {
|
||||
var key = options.key;
|
||||
for (var i = 0; i < targetsLen; ++i) {
|
||||
var obj = targets[i];
|
||||
var target = _getValue(obj, key);
|
||||
if (!target)
|
||||
continue;
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
if ((searchBitflags & target._bitflags) !== searchBitflags)
|
||||
continue;
|
||||
var result = _algorithm(preparedSearch, target);
|
||||
if (result === _NULL)
|
||||
continue;
|
||||
if (result._score < threshold)
|
||||
continue;
|
||||
result.obj = obj;
|
||||
push_result(result);
|
||||
}
|
||||
} else if (options?.keys) {
|
||||
var keys = options.keys;
|
||||
var keysLen = keys.length;
|
||||
|
||||
outer: for (var i = 0; i < targetsLen; ++i) {
|
||||
var obj = targets[i];
|
||||
var keysBitflags = 0;
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
var key = keys[keyI];
|
||||
var target = _getValue(obj, key);
|
||||
if (!target) {
|
||||
_tmpTargets[keyI] = _noTarget;
|
||||
continue;
|
||||
}
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
_tmpTargets[keyI] = target;
|
||||
keysBitflags |= target._bitflags;
|
||||
}
|
||||
|
||||
if ((searchBitflags & keysBitflags) !== searchBitflags)
|
||||
continue;
|
||||
|
||||
if (containsSpace)
|
||||
for (let j = 0; j < preparedSearch.spaceSearches.length; j++)
|
||||
_keysSpacesBestScores[j] = _NEGATIVE_INFINITY;
|
||||
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
target = _tmpTargets[keyI];
|
||||
if (target === _noTarget) {
|
||||
_tmpResults[keyI] = _noTarget;
|
||||
continue;
|
||||
}
|
||||
|
||||
_tmpResults[keyI] = _algorithm(preparedSearch, target, false, containsSpace);
|
||||
if (_tmpResults[keyI] === _NULL) {
|
||||
_tmpResults[keyI] = _noTarget;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (containsSpace)
|
||||
for (let j = 0; j < preparedSearch.spaceSearches.length; j++) {
|
||||
if (_allowPartialMatchScores[j] > -1000) {
|
||||
if (_keysSpacesBestScores[j] > _NEGATIVE_INFINITY) {
|
||||
var tmp = (_keysSpacesBestScores[j] + _allowPartialMatchScores[j]) / 4;
|
||||
if (tmp > _keysSpacesBestScores[j])
|
||||
_keysSpacesBestScores[j] = tmp;
|
||||
}
|
||||
}
|
||||
if (_allowPartialMatchScores[j] > _keysSpacesBestScores[j])
|
||||
_keysSpacesBestScores[j] = _allowPartialMatchScores[j];
|
||||
}
|
||||
}
|
||||
|
||||
if (containsSpace) {
|
||||
for (let j = 0; j < preparedSearch.spaceSearches.length; j++)
|
||||
if (_keysSpacesBestScores[j] === _NEGATIVE_INFINITY)
|
||||
continue outer;
|
||||
} else {
|
||||
var hasAtLeast1Match = false;
|
||||
for (let j = 0; j < keysLen; j++)
|
||||
if (_tmpResults[j]._score !== _NEGATIVE_INFINITY) {
|
||||
hasAtLeast1Match = true;
|
||||
break;
|
||||
}
|
||||
if (!hasAtLeast1Match)
|
||||
continue;
|
||||
}
|
||||
|
||||
var objResults = _createKeysResult(keysLen);
|
||||
for (let j = 0; j < keysLen; j++)
|
||||
objResults[j] = _tmpResults[j];
|
||||
|
||||
var score;
|
||||
if (containsSpace) {
|
||||
score = 0;
|
||||
for (let j = 0; j < preparedSearch.spaceSearches.length; j++)
|
||||
score += _keysSpacesBestScores[j];
|
||||
} else {
|
||||
score = _NEGATIVE_INFINITY;
|
||||
for (let j = 0; j < keysLen; j++) {
|
||||
var res = objResults[j];
|
||||
if (res._score > -1000) {
|
||||
if (score > _NEGATIVE_INFINITY) {
|
||||
var tmp = (score + res._score) / 4;
|
||||
if (tmp > score)
|
||||
score = tmp;
|
||||
}
|
||||
}
|
||||
if (res._score > score)
|
||||
score = res._score;
|
||||
}
|
||||
}
|
||||
|
||||
objResults.obj = obj;
|
||||
objResults._score = score;
|
||||
|
||||
if (options?.scoreFn) {
|
||||
score = options.scoreFn(objResults);
|
||||
if (!score)
|
||||
continue;
|
||||
score = _denormalizeScore(score);
|
||||
objResults._score = score;
|
||||
}
|
||||
|
||||
if (score < threshold)
|
||||
continue;
|
||||
push_result(objResults);
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < targetsLen; ++i) {
|
||||
var target = targets[i];
|
||||
if (!target)
|
||||
continue;
|
||||
if (!_isPrepared(target))
|
||||
target = _getPrepared(target);
|
||||
if ((searchBitflags & target._bitflags) !== searchBitflags)
|
||||
continue;
|
||||
var result = _algorithm(preparedSearch, target);
|
||||
if (result === _NULL)
|
||||
continue;
|
||||
if (result._score < threshold)
|
||||
continue;
|
||||
push_result(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultsLen === 0)
|
||||
return _noResults;
|
||||
var results = new Array(resultsLen);
|
||||
for (var i = resultsLen - 1; i >= 0; --i)
|
||||
results[i] = _q.poll();
|
||||
results.total = resultsLen + limitedCount;
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool isLoaded: false
|
||||
property string langCode: ""
|
||||
property var locale: Qt.locale()
|
||||
property string systemDetectedLangCode: ""
|
||||
property string fullLocaleCode: "" // Preserves regional locale variants
|
||||
property var translations: ({})
|
||||
property var fallbackTranslations: ({})
|
||||
|
||||
// Static list of available translations — update when adding/removing translation files
|
||||
property var availableLanguages: ["en", "en-GB", "cs", "de", "es", "fr", "hu", "it", "ja", "ko-KR", "ku", "nl", "nn-HN", "nn-NO", "pl", "pt", "ru", "sv", "tr", "uk-UA", "vi", "zh-CN", "zh-TW"]
|
||||
|
||||
// Default date format per language (used by lock screen, etc.)
|
||||
readonly property var dateFormats: ({
|
||||
"de": "dddd, d. MMMM",
|
||||
"en": "dddd, MMMM d",
|
||||
"es": "dddd, d 'de' MMMM",
|
||||
"fr": "dddd d MMMM",
|
||||
"hu": "dddd, MMMM d.",
|
||||
"it": "dddd d MMMM",
|
||||
"ja": "yyyy年M月d日 dddd",
|
||||
"ko": "yyyy년 M월 d일 dddd",
|
||||
"ku": "dddd, dê MMMM",
|
||||
"nl": "dddd d MMMM",
|
||||
"nn": "dddd d. MMMM",
|
||||
"pl": "dddd, d MMMM",
|
||||
"pt": "dddd, d 'de' MMMM",
|
||||
"ru": "dddd, d MMMM",
|
||||
"sv": "dddd d MMMM",
|
||||
"tr": "dddd, d MMMM",
|
||||
"uk": "dddd, d MMMM",
|
||||
"vi": "dddd, d MMMM",
|
||||
"zh": "yyyy年M月d日 dddd"
|
||||
})
|
||||
|
||||
// Signals for reactive updates
|
||||
signal languageChanged(string newLanguage)
|
||||
signal translationsLoaded
|
||||
|
||||
// FileView to load translation files
|
||||
property FileView translationFile: FileView {
|
||||
id: fileView
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text());
|
||||
root.translations = data;
|
||||
Logger.i("I18n", `Loaded translations for "${root.langCode}"`);
|
||||
|
||||
root.isLoaded = true;
|
||||
root.translationsLoaded();
|
||||
|
||||
// Load English fallback for non-English languages (only after main file succeeds)
|
||||
if (root.langCode !== "en") {
|
||||
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`;
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.e("I18n", `Failed to parse translation file: ${e}`);
|
||||
setLanguage("en");
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
if (root.langCode === "en") {
|
||||
Logger.e("I18n", `Failed to load English translation file: ${error}`);
|
||||
// English also failed - still emit signal to unblock startup
|
||||
root.isLoaded = true;
|
||||
root.translationsLoaded();
|
||||
return;
|
||||
}
|
||||
|
||||
// Qt.callLater is needed because FileView doesn't re-trigger when path
|
||||
// is changed inside its own onLoadFailed handler
|
||||
var strippedCode = stripScript(root.langCode);
|
||||
if (strippedCode !== root.langCode) {
|
||||
Logger.d("I18n", `Translation file for "${root.langCode}" not found, trying "${strippedCode}"`);
|
||||
root.langCode = strippedCode;
|
||||
Qt.callLater(loadTranslations);
|
||||
} else {
|
||||
// Try language-only code (e.g. "zh-CN" → "zh")
|
||||
var shortCode = root.langCode.substring(0, 2);
|
||||
if (shortCode !== root.langCode) {
|
||||
Logger.d("I18n", `Translation file for "${root.langCode}" not found, trying "${shortCode}"`);
|
||||
root.langCode = shortCode;
|
||||
Qt.callLater(loadTranslations);
|
||||
} else {
|
||||
Logger.w("I18n", `Translation file for "${root.langCode}" not found, falling back to English`);
|
||||
root.langCode = "en";
|
||||
root.fullLocaleCode = "en";
|
||||
root.locale = Qt.locale("en");
|
||||
Qt.callLater(loadTranslations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to load fallback translation files
|
||||
property FileView fallbackTranslationFile: FileView {
|
||||
id: fallbackFileView
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
try {
|
||||
var data = JSON.parse(text());
|
||||
root.fallbackTranslations = data;
|
||||
Logger.d("I18n", `Loaded english fallback translations`);
|
||||
} catch (e) {
|
||||
Logger.e("I18n", `Failed to parse fallback translation file: ${e}`);
|
||||
}
|
||||
}
|
||||
onLoadFailed: function (error) {
|
||||
Logger.e("I18n", `Failed to load fallback translation file: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Correct language when settings finish loading from disk (or user changes it)
|
||||
Connections {
|
||||
target: Settings.data.general
|
||||
function onLanguageChanged() {
|
||||
var userLang = Settings.data.general.language;
|
||||
if (userLang !== "" && userLang !== root.langCode && availableLanguages.includes(userLang)) {
|
||||
Logger.i("I18n", `Applying user language preference: "${userLang}"`);
|
||||
setLanguage(userLang);
|
||||
} else if (userLang === "" && root.systemDetectedLangCode !== "" && root.systemDetectedLangCode !== root.langCode) {
|
||||
Logger.i("I18n", `Language reset to default, reverting to system language: "${root.systemDetectedLangCode}"`);
|
||||
setLanguage(root.systemDetectedLangCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("I18n", "Service started");
|
||||
|
||||
var lang = determineFastLanguage();
|
||||
langCode = lang.code;
|
||||
fullLocaleCode = lang.fullLocale;
|
||||
locale = Qt.locale(lang.fullLocale);
|
||||
systemDetectedLangCode = lang.code;
|
||||
Logger.i("I18n", `Loading "${lang.code}" (locale: "${lang.fullLocale}")`);
|
||||
loadTranslations();
|
||||
}
|
||||
|
||||
// Strip 4-letter script subtag from a BCP 47 tag (e.g. "fr-Latn-FR" → "fr-FR")
|
||||
function stripScript(tag) {
|
||||
var parts = tag.split("-");
|
||||
var result = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (parts[i].length === 4 && /^[A-Za-z]{4}$/.test(parts[i])) {
|
||||
continue;
|
||||
}
|
||||
result.push(parts[i]);
|
||||
}
|
||||
return result.join("-");
|
||||
}
|
||||
|
||||
// Determine the best language match against availableLanguages
|
||||
function determineFastLanguage() {
|
||||
// User preference from Settings (defaults to "" if not yet loaded from disk)
|
||||
var userLang = Settings.data.general.language;
|
||||
if (userLang !== "" && availableLanguages.includes(userLang)) {
|
||||
return {
|
||||
code: userLang,
|
||||
fullLocale: userLang
|
||||
};
|
||||
}
|
||||
|
||||
// Match system locale against available translations
|
||||
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
|
||||
var fullLang = Qt.locale().uiLanguages[i];
|
||||
|
||||
// Exact match (e.g. "zh-CN")
|
||||
if (availableLanguages.includes(fullLang)) {
|
||||
return {
|
||||
code: fullLang,
|
||||
fullLocale: fullLang
|
||||
};
|
||||
}
|
||||
|
||||
// Script-stripped match (e.g. "zh-Hans-CN" → "zh-CN")
|
||||
var stripped = stripScript(fullLang);
|
||||
if (stripped !== fullLang && availableLanguages.includes(stripped)) {
|
||||
return {
|
||||
code: stripped,
|
||||
fullLocale: fullLang
|
||||
};
|
||||
}
|
||||
|
||||
// Language-only match (e.g. "fr-FR" → "fr")
|
||||
var short_ = fullLang.substring(0, 2);
|
||||
if (availableLanguages.includes(short_)) {
|
||||
return {
|
||||
code: short_,
|
||||
fullLocale: fullLang
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: "en",
|
||||
fullLocale: "en"
|
||||
};
|
||||
}
|
||||
|
||||
function dateFormat() {
|
||||
var lang = langCode.split("-")[0];
|
||||
return dateFormats[lang] || "dddd, d MMMM";
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function setLanguage(newLangCode, fullLocale) {
|
||||
if (typeof fullLocale === "undefined") {
|
||||
fullLocale = newLangCode;
|
||||
}
|
||||
|
||||
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
|
||||
langCode = newLangCode;
|
||||
fullLocaleCode = fullLocale;
|
||||
locale = Qt.locale(fullLocale);
|
||||
Logger.i("I18n", `Language set to "${langCode}" with locale "${fullLocale}"`);
|
||||
languageChanged(langCode);
|
||||
loadTranslations();
|
||||
} else if (!availableLanguages.includes(newLangCode)) {
|
||||
Logger.w("I18n", `Language "${newLangCode}" is not available`);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
function loadTranslations() {
|
||||
if (langCode === "")
|
||||
return;
|
||||
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`;
|
||||
fileView.path = filePath;
|
||||
isLoaded = false;
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Check if a translation exists
|
||||
function hasTranslation(key) {
|
||||
if (!isLoaded)
|
||||
return false;
|
||||
|
||||
const keys = key.split(".");
|
||||
var value = translations;
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Get all translation keys (useful for debugging)
|
||||
function getAllKeys(obj, prefix) {
|
||||
if (typeof obj === "undefined")
|
||||
obj = translations;
|
||||
if (typeof prefix === "undefined")
|
||||
prefix = "";
|
||||
|
||||
var keys = [];
|
||||
for (var key in (obj || {})) {
|
||||
const value = obj[key];
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof value === "object" && value !== null) {
|
||||
keys = keys.concat(getAllKeys(value, fullKey));
|
||||
} else if (typeof value === "string") {
|
||||
keys.push(fullKey);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Reload translations (useful for development)
|
||||
function reload() {
|
||||
Logger.d("I18n", "Reloading translations");
|
||||
loadTranslations();
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Main translation function
|
||||
function tr(key, interpolations) {
|
||||
if (typeof interpolations === "undefined")
|
||||
interpolations = {};
|
||||
|
||||
if (!isLoaded) {
|
||||
//Logger.d("I18n", "Translations not loaded yet")
|
||||
return key;
|
||||
}
|
||||
|
||||
// Navigate nested keys (e.g., "menu.file.open")
|
||||
const keys = key.split(".");
|
||||
|
||||
// Look-up translation in the active language
|
||||
var value = translations;
|
||||
var notFound = false;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]];
|
||||
} else {
|
||||
Logger.d("I18n", `Translation key "${key}" not found at part "${keys[i]}"`);
|
||||
Logger.d("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`);
|
||||
notFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to english if not found
|
||||
if (notFound && availableLanguages.includes("en") && langCode !== "en") {
|
||||
value = fallbackTranslations;
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (value && typeof value === "object" && keys[i] in value) {
|
||||
value = value[keys[i]];
|
||||
} else {
|
||||
// Indicate this key does not even exists in the english fallback
|
||||
return `!!${key}!!`;
|
||||
}
|
||||
}
|
||||
} else if (notFound) {
|
||||
// No fallback available
|
||||
return `!!${key}!!`;
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
Logger.d("I18n", `Translation key "${key}" is not a string`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Handle interpolations (e.g., "Hello {name}!")
|
||||
var result = value;
|
||||
for (var placeholder in interpolations) {
|
||||
const regex = new RegExp(`\\{${placeholder}\\}`, 'g');
|
||||
result = result.replace(regex, interpolations[placeholder]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Plural translation function
|
||||
function trp(key, count, interpolations) {
|
||||
if (typeof interpolations === "undefined") {
|
||||
interpolations = {};
|
||||
}
|
||||
|
||||
// Use key for singular, key-plural for plural
|
||||
const realKey = count === 1 ? key : `${key}-plural`;
|
||||
|
||||
// Merge interpolations with count
|
||||
var finalInterpolations = {
|
||||
"count": count
|
||||
};
|
||||
for (var prop in interpolations) {
|
||||
finalInterpolations[prop] = interpolations[prop];
|
||||
}
|
||||
|
||||
return tr(realKey, finalInterpolations);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Expose the font family name for easy access
|
||||
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
|
||||
readonly property string defaultIcon: IconsTabler.defaultIcon
|
||||
readonly property var icons: IconsTabler.icons
|
||||
readonly property var aliases: IconsTabler.aliases
|
||||
readonly property string fontPath: "/Assets/Fonts/tabler/noctalia-tabler-icons.ttf"
|
||||
|
||||
// Current active font loader
|
||||
property FontLoader currentFontLoader: null
|
||||
property int fontVersion: 0
|
||||
|
||||
// Create a unique cache-busting path
|
||||
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
|
||||
|
||||
// Signal emitted when font is reloaded
|
||||
signal fontReloaded
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Icons", "Service started");
|
||||
loadFontWithCacheBusting();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Quickshell
|
||||
function onReloadCompleted() {
|
||||
Logger.d("Icons", "Quickshell reload completed - forcing font reload");
|
||||
reloadFont();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
function get(iconName) {
|
||||
// Check in aliases first
|
||||
if (aliases[iconName] !== undefined) {
|
||||
iconName = aliases[iconName];
|
||||
}
|
||||
|
||||
// Find the appropriate codepoint
|
||||
return icons[iconName];
|
||||
}
|
||||
|
||||
function loadFontWithCacheBusting() {
|
||||
Logger.d("Icons", "Loading font with cache busting");
|
||||
|
||||
// Destroy old loader first
|
||||
if (currentFontLoader) {
|
||||
currentFontLoader.destroy();
|
||||
currentFontLoader = null;
|
||||
}
|
||||
|
||||
// Create new loader with cache-busting URL
|
||||
currentFontLoader = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
FontLoader {
|
||||
source: "${cacheBustingPath}"
|
||||
}
|
||||
`, root, "dynamicFontLoader_" + fontVersion);
|
||||
|
||||
// Connect to the new loader's status changes
|
||||
currentFontLoader.statusChanged.connect(function () {
|
||||
if (currentFontLoader.status === FontLoader.Ready) {
|
||||
Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")");
|
||||
fontReloaded();
|
||||
} else if (currentFontLoader.status === FontLoader.Error) {
|
||||
Logger.e("Icons", "Font failed to load (version " + fontVersion + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadFont() {
|
||||
Logger.d("Icons", "Forcing font reload...");
|
||||
fontVersion++;
|
||||
loadFontWithCacheBusting();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
function getKeybindString(event) {
|
||||
let keyStr = "";
|
||||
if (event.modifiers & Qt.ControlModifier)
|
||||
keyStr += "Ctrl+";
|
||||
if (event.modifiers & Qt.AltModifier)
|
||||
keyStr += "Alt+";
|
||||
if (event.modifiers & Qt.ShiftModifier)
|
||||
keyStr += "Shift+";
|
||||
|
||||
let keyName = "";
|
||||
let rawText = event.text;
|
||||
|
||||
if (event.key >= Qt.Key_A && event.key <= Qt.Key_Z || event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
|
||||
keyName = String.fromCharCode(event.key);
|
||||
} else if (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F12) {
|
||||
keyName = "F" + (event.key - Qt.Key_F1 + 1);
|
||||
} else if (rawText && rawText.length > 0 && rawText.charCodeAt(0) > 31 && rawText.charCodeAt(0) !== 127) {
|
||||
keyName = rawText.toUpperCase();
|
||||
|
||||
if (event.modifiers & Qt.ShiftModifier) {
|
||||
const shiftMap = {
|
||||
"!": "1",
|
||||
"\"": "2",
|
||||
"§": "3",
|
||||
"$": "4",
|
||||
"%": "5",
|
||||
"&": "6",
|
||||
"/": "7",
|
||||
"(": "8",
|
||||
")": "9",
|
||||
"=": "0",
|
||||
"@": "2",
|
||||
"#": "3",
|
||||
"^": "6",
|
||||
"*": "8"
|
||||
};
|
||||
if (shiftMap[keyName]) {
|
||||
keyName = shiftMap[keyName];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
keyName = "Esc";
|
||||
break;
|
||||
case Qt.Key_Space:
|
||||
keyName = "Space";
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
keyName = "Return";
|
||||
break;
|
||||
case Qt.Key_Enter:
|
||||
keyName = "Enter";
|
||||
break;
|
||||
case Qt.Key_Tab:
|
||||
keyName = "Tab";
|
||||
break;
|
||||
case Qt.Key_Backspace:
|
||||
keyName = "Backspace";
|
||||
break;
|
||||
case Qt.Key_Delete:
|
||||
keyName = "Del";
|
||||
break;
|
||||
case Qt.Key_Insert:
|
||||
keyName = "Ins";
|
||||
break;
|
||||
case Qt.Key_Home:
|
||||
keyName = "Home";
|
||||
break;
|
||||
case Qt.Key_End:
|
||||
keyName = "End";
|
||||
break;
|
||||
case Qt.Key_PageUp:
|
||||
keyName = "PgUp";
|
||||
break;
|
||||
case Qt.Key_PageDown:
|
||||
keyName = "PgDn";
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
keyName = "Left";
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
keyName = "Right";
|
||||
break;
|
||||
case Qt.Key_Up:
|
||||
keyName = "Up";
|
||||
break;
|
||||
case Qt.Key_Down:
|
||||
keyName = "Down";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyName)
|
||||
return "";
|
||||
return keyStr + keyName;
|
||||
}
|
||||
|
||||
function checkKey(event, settingName, settings) {
|
||||
// Accept both simplified names ("remove") and full property names ("keyRemove")
|
||||
// so callers are less error-prone.
|
||||
var normalized = String(settingName || "");
|
||||
if (!normalized)
|
||||
return false;
|
||||
|
||||
var propName = normalized.startsWith("key") ? normalized : ("key" + normalized.charAt(0).toUpperCase() + normalized.slice(1));
|
||||
var boundKeys = settings.data.general.keybinds[propName];
|
||||
if (!boundKeys || boundKeys.length === 0)
|
||||
return false;
|
||||
var eventString = getKeybindString(event);
|
||||
for (var i = 0; i < boundKeys.length; i++) {
|
||||
if (boundKeys[i] === eventString)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a keybind string conflicts with any other existing keybinds.
|
||||
* @param {string} keyStr - The keybind string to check (e.g., "Ctrl+A").
|
||||
* @param {string} currentPath - The settings path of the keybind being edited (to skip checking itself).
|
||||
* @param {object} data - The settings data object (from Settings.data).
|
||||
* @returns {string|null} - The name of the conflicting action, or null if no conflict.
|
||||
*/
|
||||
function getKeybindConflict(keyStr, currentPath, data) {
|
||||
if (!keyStr || !data)
|
||||
return null;
|
||||
|
||||
const searchKey = String(keyStr).trim().toLowerCase();
|
||||
|
||||
// 1. Check navigation keybinds
|
||||
const navKeybinds = data.general ? data.general.keybinds : null;
|
||||
const navMap = {
|
||||
"keyUp": "Navigation: Up",
|
||||
"keyDown": "Navigation: Down",
|
||||
"keyLeft": "Navigation: Left",
|
||||
"keyRight": "Navigation: Right",
|
||||
"keyEnter": "Navigation: Enter",
|
||||
"keyEscape": "Navigation: Escape"
|
||||
};
|
||||
|
||||
if (navKeybinds) {
|
||||
for (const prop in navMap) {
|
||||
const fullPath = "general.keybinds." + prop;
|
||||
if (fullPath === currentPath)
|
||||
continue;
|
||||
|
||||
const boundKeys = navKeybinds[prop];
|
||||
if (boundKeys && boundKeys.length !== undefined) {
|
||||
for (let i = 0; i < boundKeys.length; i++) {
|
||||
if (String(boundKeys[i]).trim().toLowerCase() === searchKey) {
|
||||
return navMap[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check session menu power options
|
||||
const sessionMenu = data.sessionMenu;
|
||||
if (sessionMenu && sessionMenu.powerOptions) {
|
||||
const powerOptions = sessionMenu.powerOptions;
|
||||
for (let i = 0; i < powerOptions.length; i++) {
|
||||
const entry = powerOptions[i];
|
||||
const fullPath = "sessionMenu.powerOptions[" + i + "].keybind";
|
||||
if (fullPath === currentPath)
|
||||
continue;
|
||||
|
||||
if (entry.keybind && String(entry.keybind).trim().toLowerCase() === searchKey) {
|
||||
// Capitalize action name
|
||||
const actionName = entry.action ? entry.action.charAt(0).toUpperCase() + entry.action.slice(1) : "Unknown";
|
||||
return "Session Menu: " + actionName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function _formatMessage(...args) {
|
||||
var t = Time.getFormattedTimestamp();
|
||||
if (args.length > 1) {
|
||||
const maxLength = 14;
|
||||
var module = args.shift().substring(0, maxLength).padStart(maxLength, " ");
|
||||
return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ");
|
||||
} else {
|
||||
return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
function _getStackTrace() {
|
||||
try {
|
||||
throw new Error("Stack trace");
|
||||
} catch (e) {
|
||||
return e.stack;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug log (only when Settings.isDebug is true)
|
||||
function d(...args) {
|
||||
if (Settings?.isDebug) {
|
||||
var msg = _formatMessage(...args);
|
||||
console.debug(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Info log (always visible)
|
||||
function i(...args) {
|
||||
var msg = _formatMessage(...args);
|
||||
console.info(msg);
|
||||
}
|
||||
|
||||
// Warning log (always visible)
|
||||
function w(...args) {
|
||||
var msg = _formatMessage(...args);
|
||||
console.warn(msg);
|
||||
}
|
||||
|
||||
// Error log (always visible)
|
||||
function e(...args) {
|
||||
var msg = _formatMessage(...args);
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
function callStack() {
|
||||
var stack = _getStackTrace();
|
||||
Logger.i("Debug", "--------------------------");
|
||||
Logger.i("Debug", "Current call stack");
|
||||
// Split the stack into lines and log each one
|
||||
var stackLines = stack.split('\n');
|
||||
for (var i = 0; i < stackLines.length; i++) {
|
||||
var line = stackLines[i].trim(); // Remove leading/trailing whitespace
|
||||
if (line.length > 0) {
|
||||
// Only log non-empty lines
|
||||
Logger.i("Debug", `- ${line}`);
|
||||
}
|
||||
}
|
||||
Logger.i("Debug", "--------------------------");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate from version < 27 to version 27
|
||||
// Converts settingsPanelAttachToBar boolean to settingsPanelMode string
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v27");
|
||||
|
||||
// Check rawJson for old property (adapter doesn't expose removed properties)
|
||||
if (rawJson?.ui?.settingsPanelAttachToBar !== undefined) {
|
||||
if (rawJson.ui.settingsPanelAttachToBar === true) {
|
||||
adapter.ui.settingsPanelMode = "attached";
|
||||
} else {
|
||||
adapter.ui.settingsPanelMode = "centered";
|
||||
}
|
||||
logger.i("Settings", "Migrated settingsPanelAttachToBar to settingsPanelMode: " + adapter.ui.settingsPanelMode);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate TaskbarGrouped widgets to Workspace with showApplications: true
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v28");
|
||||
|
||||
// Check bar widgets in all sections
|
||||
const sections = ["left", "center", "right"];
|
||||
|
||||
for (const section of sections) {
|
||||
if (!rawJson?.bar?.widgets?.[section])
|
||||
continue;
|
||||
|
||||
const widgets = rawJson.bar.widgets[section];
|
||||
for (let i = 0; i < widgets.length; i++) {
|
||||
if (widgets[i].id === "TaskbarGrouped") {
|
||||
// Convert TaskbarGrouped to Workspace with showApplications
|
||||
const oldWidget = widgets[i];
|
||||
adapter.bar.widgets[section][i] = {
|
||||
id: "Workspace",
|
||||
showApplications: true,
|
||||
labelMode: oldWidget.labelMode || "index",
|
||||
hideUnoccupied: oldWidget.hideUnoccupied || false,
|
||||
showLabelsOnlyWhenOccupied: oldWidget.showLabelsOnlyWhenOccupied ?? true,
|
||||
colorizeIcons: oldWidget.colorizeIcons || false
|
||||
};
|
||||
logger.i("Settings", "Migrated TaskbarGrouped to Workspace in " + section + " section");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate systemMonitor.enableNvidiaGpu to systemMonitor.enableDgpuMonitoring
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v32");
|
||||
|
||||
// Check rawJson for old property (adapter doesn't expose removed properties)
|
||||
if (rawJson?.systemMonitor?.enableNvidiaGpu !== undefined) {
|
||||
adapter.systemMonitor.enableDgpuMonitoring = rawJson.systemMonitor.enableNvidiaGpu;
|
||||
logger.i("Settings", "Migrated systemMonitor.enableNvidiaGpu to enableDgpuMonitoring: " + adapter.systemMonitor.enableDgpuMonitoring);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate wallpaper.randomEnabled to wallpaper.wallpaperChangeMode
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v33");
|
||||
|
||||
if (rawJson?.wallpaper?.randomEnabled !== undefined) {
|
||||
// We'll keep randomEnabled for backward compatibility but set wallpaperChangeMode
|
||||
if (rawJson.wallpaper.randomEnabled === true) {
|
||||
adapter.wallpaper.wallpaperChangeMode = "random";
|
||||
logger.i("Settings", "Migrated wallpaper.randomEnabled=true to wallpaperChangeMode=random");
|
||||
} else {
|
||||
adapter.wallpaper.wallpaperChangeMode = "random";
|
||||
logger.i("Settings", "Set wallpaperChangeMode=random (randomEnabled was false)");
|
||||
}
|
||||
} else {
|
||||
adapter.wallpaper.wallpaperChangeMode = "random";
|
||||
logger.i("Settings", "Set default wallpaperChangeMode=random");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate bar.transparent to bar.backgroundOpacity + bar.useSeparateOpacity
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v35");
|
||||
|
||||
if (rawJson?.bar?.transparent !== undefined) {
|
||||
if (rawJson.bar.transparent === true) {
|
||||
adapter.bar.backgroundOpacity = 0;
|
||||
adapter.bar.useSeparateOpacity = true;
|
||||
logger.i("Settings", "Migrated bar.transparent=true to backgroundOpacity=0, useSeparateOpacity=true");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Clear legacy emoji usage cache to adopt new format
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v36");
|
||||
|
||||
const usagePath = Settings.cacheDir + "emoji_usage.json";
|
||||
|
||||
if (!usagePath.endsWith("emoji_usage.json")) {
|
||||
logger.w("Settings", "Skipping emoji usage cleanup due to unexpected path: " + usagePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure dir exists then remove the file
|
||||
Quickshell.execDetached(["sh", "-c", `mkdir -p "${Settings.cacheDir}" && rm -f -- "${usagePath}"`]);
|
||||
logger.i("Settings", "Cleared legacy emoji usage file at: " + usagePath);
|
||||
} catch (e) {
|
||||
logger.w("Settings", "Failed to clear emoji usage cache: " + e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Rename WiFi widgets/shortcuts to Network
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v37 (WiFi → Network)");
|
||||
|
||||
let changed = false;
|
||||
|
||||
function migrateArray(contextName, rawArr, adapterArr, setItem) {
|
||||
if (!adapterArr)
|
||||
return;
|
||||
const len = adapterArr.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
// Prefer raw item if present, otherwise use adapter item
|
||||
const item = rawArr && rawArr[i] !== undefined ? rawArr[i] : adapterArr[i];
|
||||
|
||||
if (item === undefined || item === null)
|
||||
continue;
|
||||
|
||||
// Case 1: simple string entries
|
||||
if (typeof item === "string") {
|
||||
if (item === "WiFi") {
|
||||
setItem(i, "Network");
|
||||
changed = true;
|
||||
logger.i("Settings", `Migrated ${contextName}[${i}] from WiFi to Network (string entry)`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case 2: object entries with id
|
||||
let id = item.id !== undefined ? item.id : (item.widgetId !== undefined ? item.widgetId : undefined);
|
||||
if (id === "WiFi") {
|
||||
var newObj = {};
|
||||
for (var key in item)
|
||||
newObj[key] = item[key];
|
||||
if (newObj.id !== undefined)
|
||||
newObj.id = "Network";
|
||||
if (newObj.widgetId !== undefined && newObj.id === undefined)
|
||||
newObj.widgetId = "Network"; // fallback if older schema used widgetId
|
||||
setItem(i, newObj);
|
||||
changed = true;
|
||||
logger.i("Settings", `Migrated ${contextName}[${i}] from WiFi to Network (object entry)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bar widgets: left/center/right
|
||||
const sections = ["left", "center", "right"];
|
||||
for (const section of sections) {
|
||||
const rawArr = rawJson?.bar?.widgets?.[section];
|
||||
const adapterArr = adapter?.bar?.widgets?.[section];
|
||||
migrateArray(`bar.widgets.${section}`, rawArr, adapterArr, function (i, v) {
|
||||
adapter.bar.widgets[section][i] = v;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Control Center shortcuts: left/right
|
||||
const rawLeft = rawJson?.controlCenter?.shortcuts?.left;
|
||||
const adLeft = adapter?.controlCenter?.shortcuts?.left;
|
||||
migrateArray("controlCenter.shortcuts.left", rawLeft, adLeft, function (i, v) {
|
||||
adapter.controlCenter.shortcuts.left[i] = v;
|
||||
});
|
||||
|
||||
const rawRight = rawJson?.controlCenter?.shortcuts?.right;
|
||||
const adRight = adapter?.controlCenter?.shortcuts?.right;
|
||||
migrateArray("controlCenter.shortcuts.right", rawRight, adRight, function (i, v) {
|
||||
adapter.controlCenter.shortcuts.right[i] = v;
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
logger.i("Settings", "No WiFi widget IDs found to migrate; leaving settings unchanged");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration38", "Migrating bar margins from percentages to integers");
|
||||
|
||||
// Use rawJson to read original values, adapter to write new values (following migration patterns)
|
||||
const rawVertical = rawJson?.bar?.marginVertical;
|
||||
const rawHorizontal = rawJson?.bar?.marginHorizontal;
|
||||
|
||||
// Only migrate if values exist and are percentages (<= 1.0)
|
||||
if (rawVertical !== undefined && typeof rawVertical === 'number' && rawVertical <= 1.0) {
|
||||
const marginXL = 18; // Standard value of Style.marginXL
|
||||
adapter.bar.marginVertical = Math.round(rawVertical * marginXL);
|
||||
logger.d("Migration38", "Converted marginVertical from " + rawVertical + " to " + adapter.bar.marginVertical + "px");
|
||||
}
|
||||
|
||||
if (rawHorizontal !== undefined && typeof rawHorizontal === 'number' && rawHorizontal <= 1.0) {
|
||||
const marginXL = 18; // Standard value of Style.marginXL
|
||||
adapter.bar.marginHorizontal = Math.round(rawHorizontal * marginXL);
|
||||
logger.d("Migration38", "Converted marginHorizontal from " + rawHorizontal + " to " + adapter.bar.marginHorizontal + "px");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// List of all template IDs that existed as booleans in the old format
|
||||
readonly property var templateIds: ["gtk", "qt", "kcolorscheme", "alacritty", "kitty", "ghostty", "foot", "wezterm", "fuzzel", "discord", "pywalfox", "vicinae", "walker", "code", "spicetify", "telegram", "cava", "yazi", "emacs", "niri", "hyprland", "mango", "zed", "helix", "zenBrowser"]
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration39", "Migrating templates from boolean format to activeTemplates array");
|
||||
|
||||
// Check if old format exists (any boolean template property)
|
||||
const oldTemplates = rawJson?.templates;
|
||||
if (!oldTemplates) {
|
||||
logger.d("Migration39", "No templates section found, skipping migration");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if already migrated (has activeTemplates array)
|
||||
if (Array.isArray(oldTemplates.activeTemplates)) {
|
||||
logger.d("Migration39", "Already has activeTemplates array, skipping migration");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build the new activeTemplates array from old boolean values
|
||||
const activeTemplates = [];
|
||||
for (const templateId of templateIds) {
|
||||
if (oldTemplates[templateId] === true) {
|
||||
activeTemplates.push({
|
||||
"id": templateId,
|
||||
"enabled": true
|
||||
});
|
||||
logger.d("Migration40", "Migrated enabled template: " + templateId);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the new format
|
||||
adapter.templates.activeTemplates = activeTemplates;
|
||||
logger.i("Migration40", "Migrated " + activeTemplates.length + " templates to new array format");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration42", "Migrating randomEnabled to automationEnabled");
|
||||
|
||||
const wallpaper = rawJson?.wallpaper;
|
||||
if (!wallpaper) {
|
||||
logger.d("Migration42", "No wallpaper section found, skipping migration");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if already migrated (has automationEnabled)
|
||||
if (wallpaper.automationEnabled !== undefined) {
|
||||
logger.d("Migration42", "Already has automationEnabled, skipping migration");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Migrate randomEnabled to automationEnabled
|
||||
const oldValue = wallpaper.randomEnabled ?? false;
|
||||
adapter.wallpaper.automationEnabled = oldValue;
|
||||
logger.i("Migration42", "Migrated randomEnabled=" + oldValue + " to automationEnabled=" + oldValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration43", "Migrating recursiveSearch to viewMode");
|
||||
|
||||
const wallpaper = rawJson?.wallpaper;
|
||||
if (!wallpaper) {
|
||||
logger.d("Migration43", "No wallpaper section found, skipping migration");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if already migrated (has viewMode)
|
||||
if (wallpaper.viewMode !== undefined) {
|
||||
logger.d("Migration43", "Already has viewMode, skipping migration");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Migrate recursiveSearch to viewMode
|
||||
const oldValue = wallpaper.recursiveSearch ?? false;
|
||||
const newValue = oldValue ? "recursive" : "single";
|
||||
adapter.wallpaper.viewMode = newValue;
|
||||
logger.i("Migration43", "Migrated recursiveSearch=" + oldValue + " to viewMode=" + newValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration44", "Updating PAM pam/password.conf");
|
||||
|
||||
const configDir = Settings.configDir;
|
||||
const pamConfigDir = configDir + "pam";
|
||||
const pamConfigFile = pamConfigDir + "/password.conf";
|
||||
const pamConfigDirEsc = pamConfigDir.replace(/'/g, "'\\''");
|
||||
const pamConfigFileEsc = pamConfigFile.replace(/'/g, "'\\''");
|
||||
|
||||
// Ensure directory exists
|
||||
Quickshell.execDetached(["mkdir", "-p", pamConfigDir]);
|
||||
|
||||
// Generate the PAM config file content (updated version)
|
||||
var configContent = "auth sufficient pam_fprintd.so timeout=-1\n";
|
||||
configContent += "auth sufficient /run/current-system/sw/lib/security/pam_fprintd.so timeout=-1 # for NixOS\n";
|
||||
configContent += "auth required pam_unix.so\n";
|
||||
|
||||
// Write the config file using heredoc
|
||||
var script = `cat > '${pamConfigFileEsc}' << 'EOF'\n`;
|
||||
script += configContent;
|
||||
script += "EOF\n";
|
||||
Quickshell.execDetached(["sh", "-c", script]);
|
||||
|
||||
logger.d("Migration44", "PAM config file updated at:", pamConfigFile);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
/**
|
||||
* Migration 45: Migrate 'floating' bar setting to 'barType'
|
||||
*/
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration45", "Migrating bar settings...");
|
||||
|
||||
if (rawJson?.bar?.floating) {
|
||||
adapter.bar.barType = "floating";
|
||||
} else {
|
||||
adapter.bar.barType = "simple";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration46", "Removing legacy PAM configuration file");
|
||||
|
||||
const configDir = Settings.configDir;
|
||||
const pamConfigDir = configDir + "pam";
|
||||
// Remove the entire pam directory if it exists
|
||||
const script = `rm -rf '${pamConfigDir}'`;
|
||||
Quickshell.execDetached(["sh", "-c", script]);
|
||||
|
||||
logger.d("Migration46", "Cleaned up legacy PAM config");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Migration47", "Removing network_stats.json cache");
|
||||
|
||||
const networkStatsFile = Settings.cacheDir + "network_stats.json";
|
||||
Quickshell.execDetached(["rm", "-f", networkStatsFile]);
|
||||
|
||||
logger.d("Migration47", "Removed network_stats.json");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate battery thresholds from notifications to systemMonitor
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v48");
|
||||
|
||||
if (rawJson?.notifications?.batteryWarningThreshold !== undefined) {
|
||||
adapter.systemMonitor.batteryWarningThreshold = rawJson.notifications.batteryWarningThreshold;
|
||||
logger.i("Settings", "Migrated notifications.batteryWarningThreshold to systemMonitor: " + adapter.systemMonitor.batteryWarningThreshold);
|
||||
}
|
||||
|
||||
if (rawJson?.notifications?.batteryCriticalThreshold !== undefined) {
|
||||
adapter.systemMonitor.batteryCriticalThreshold = rawJson.notifications.batteryCriticalThreshold;
|
||||
logger.i("Settings", "Migrated notifications.batteryCriticalThreshold to systemMonitor: " + adapter.systemMonitor.batteryCriticalThreshold);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Remove old launcher_app_usage.json (usage tracking moved to ShellState)
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v49");
|
||||
|
||||
Quickshell.execDetached(["rm", "-f", Settings.cacheDir + "launcher_app_usage.json"]);
|
||||
logger.i("Settings", "Removed old launcher_app_usage.json");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Migrate keybinds from single strings to arrays of strings
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v50");
|
||||
|
||||
const keybinds = rawJson?.general?.keybinds;
|
||||
if (!keybinds)
|
||||
return true;
|
||||
|
||||
const keys = ["keyUp", "keyDown", "keyLeft", "keyRight", "keyEnter", "keyEscape"];
|
||||
for (const key of keys) {
|
||||
if (keybinds[key] !== undefined && typeof keybinds[key] === "string") {
|
||||
adapter.general.keybinds[key] = [keybinds[key]];
|
||||
logger.i("Settings", "Migrated keybinds." + key + " from string to array: [" + keybinds[key] + "]");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Add default keybinds (1-6) to session menu power options if none are defined
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v53");
|
||||
|
||||
const powerOptions = rawJson?.sessionMenu?.powerOptions;
|
||||
if (!powerOptions || !Array.isArray(powerOptions))
|
||||
return true;
|
||||
|
||||
// Check if any power option has a keybind defined
|
||||
const hasAnyKeybind = powerOptions.some(opt => opt.keybind && opt.keybind !== "");
|
||||
if (hasAnyKeybind)
|
||||
return true;
|
||||
|
||||
// No keybinds defined — apply defaults matching the action order
|
||||
const defaultKeybinds = {
|
||||
"lock": "1",
|
||||
"suspend": "2",
|
||||
"hibernate": "3",
|
||||
"reboot": "4",
|
||||
"logout": "5",
|
||||
"shutdown": "6"
|
||||
};
|
||||
|
||||
for (let i = 0; i < powerOptions.length; i++) {
|
||||
const action = powerOptions[i].action;
|
||||
if (defaultKeybinds[action]) {
|
||||
adapter.sessionMenu.powerOptions[i].keybind = defaultKeybinds[action];
|
||||
logger.i("Settings", "Set keybind '" + defaultKeybinds[action] + "' for session menu action: " + action);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Add numpad Enter as a second default keybind for keyEnter
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v54");
|
||||
|
||||
const keybinds = rawJson?.general?.keybinds;
|
||||
if (!keybinds)
|
||||
return true;
|
||||
|
||||
const keyEnter = keybinds.keyEnter;
|
||||
if (!keyEnter || !Array.isArray(keyEnter))
|
||||
return true;
|
||||
|
||||
// Only add "Enter" if the first entry is "Return" and "Enter" isn't already present
|
||||
if (keyEnter[0] === "Return" && !keyEnter.includes("Enter")) {
|
||||
var updated = Array.from(keyEnter);
|
||||
updated.splice(1, 0, "Enter");
|
||||
adapter.general.keybinds.keyEnter = updated;
|
||||
logger.i("Settings", "Added 'Enter' (numpad) as second default keybind for keyEnter");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v55");
|
||||
|
||||
// Check if the old setting exists
|
||||
if (rawJson.controlCenter && rawJson.controlCenter.openAtMouseOnBarRightClick !== undefined) {
|
||||
if (!rawJson.bar)
|
||||
rawJson.bar = {};
|
||||
|
||||
rawJson.bar.rightClickFollowMouse = rawJson.controlCenter.openAtMouseOnBarRightClick;
|
||||
delete rawJson.controlCenter.openAtMouseOnBarRightClick;
|
||||
|
||||
logger.i("Settings", "Successfully moved openAtMouseOnBarRightClick to bar.rightClickFollowMouse");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v56 (Color Scheme Migration)");
|
||||
|
||||
const scriptPath = Quickshell.shellDir + "/Scripts/python/src/theming/migrate-colorschemes.py";
|
||||
const configDir = Settings.configDir;
|
||||
|
||||
logger.i("Settings", `Running color scheme migration script: ${scriptPath} with configDir: ${configDir}`);
|
||||
|
||||
// Run the migration script detached
|
||||
Quickshell.execDetached(["python3", scriptPath, configDir]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v57 (cavaFrameRate -> spectrumFrameRate)");
|
||||
|
||||
if (rawJson && rawJson.audio && rawJson.audio.cavaFrameRate !== undefined) {
|
||||
adapter.audio.spectrumFrameRate = rawJson.audio.cavaFrameRate;
|
||||
logger.i("Settings", "Migrated cavaFrameRate:", rawJson.audio.cavaFrameRate, "-> spectrumFrameRate");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v58 (dock.dockType: static -> attached)");
|
||||
|
||||
if (rawJson && rawJson.dock && rawJson.dock.dockType === "static") {
|
||||
adapter.dock.dockType = "attached";
|
||||
logger.i("Settings", "Migrated dock.dockType: static -> attached");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
function migrate(adapter, logger, rawJson) {
|
||||
logger.i("Settings", "Migrating settings to v59 (wallpaper.transitionType: string -> array)");
|
||||
|
||||
if (rawJson && rawJson.wallpaper && typeof rawJson.wallpaper.transitionType === "string") {
|
||||
var oldValue = rawJson.wallpaper.transitionType;
|
||||
var newValue;
|
||||
|
||||
if (oldValue === "random") {
|
||||
newValue = ["fade", "disc", "stripes", "wipe", "pixelate", "honeycomb"];
|
||||
} else if (oldValue === "none") {
|
||||
newValue = [];
|
||||
} else {
|
||||
newValue = [oldValue];
|
||||
}
|
||||
|
||||
adapter.wallpaper.transitionType = newValue;
|
||||
logger.i("Settings", "Migrated wallpaper.transitionType:", oldValue, "->", JSON.stringify(newValue));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// Map of version number to migration component
|
||||
readonly property var migrations: ({
|
||||
27: migration27Component,
|
||||
28: migration28Component,
|
||||
32: migration32Component,
|
||||
33: migration33Component,
|
||||
35: migration35Component,
|
||||
36: migration36Component,
|
||||
37: migration37Component,
|
||||
38: migration38Component,
|
||||
40: migration40Component,
|
||||
42: migration42Component,
|
||||
43: migration43Component,
|
||||
44: migration44Component,
|
||||
45: migration45Component,
|
||||
46: migration46Component,
|
||||
47: migration47Component,
|
||||
48: migration48Component,
|
||||
49: migration49Component,
|
||||
50: migration50Component,
|
||||
53: migration53Component,
|
||||
54: migration54Component,
|
||||
55: migration55Component,
|
||||
56: migration56Component,
|
||||
57: migration57Component,
|
||||
58: migration58Component,
|
||||
59: migration59Component
|
||||
})
|
||||
|
||||
// Migration components
|
||||
property Component migration27Component: Migration27 {}
|
||||
property Component migration28Component: Migration28 {}
|
||||
property Component migration32Component: Migration32 {}
|
||||
property Component migration33Component: Migration33 {}
|
||||
property Component migration35Component: Migration35 {}
|
||||
property Component migration36Component: Migration36 {}
|
||||
property Component migration37Component: Migration37 {}
|
||||
property Component migration38Component: Migration38 {}
|
||||
property Component migration40Component: Migration40 {}
|
||||
property Component migration42Component: Migration42 {}
|
||||
property Component migration43Component: Migration43 {}
|
||||
property Component migration44Component: Migration44 {}
|
||||
property Component migration45Component: Migration45 {}
|
||||
property Component migration46Component: Migration46 {}
|
||||
property Component migration47Component: Migration47 {}
|
||||
property Component migration48Component: Migration48 {}
|
||||
property Component migration49Component: Migration49 {}
|
||||
property Component migration50Component: Migration50 {}
|
||||
property Component migration53Component: Migration53 {}
|
||||
property Component migration54Component: Migration54 {}
|
||||
property Component migration55Component: Migration55 {}
|
||||
property Component migration56Component: Migration56 {}
|
||||
property Component migration57Component: Migration57 {}
|
||||
property Component migration58Component: Migration58 {}
|
||||
property Component migration59Component: Migration59 {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,287 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../Helpers/QtObj2JS.js" as QtObj2JS
|
||||
import qs.Services.Power
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
|
||||
// Centralized shell state management for small cache files
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string stateFile: ""
|
||||
property bool isLoaded: false
|
||||
|
||||
// State properties for different services
|
||||
readonly property alias data: adapter
|
||||
|
||||
// Signals for state changes
|
||||
signal displayStateChanged
|
||||
signal notificationsStateChanged
|
||||
signal changelogStateChanged
|
||||
signal colorSchemesListChanged
|
||||
|
||||
Component.onCompleted: {
|
||||
// Setup state file path (needs Settings to be available)
|
||||
Qt.callLater(() => {
|
||||
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
|
||||
stateFile = Settings.cacheDir + "shell-state.json";
|
||||
stateFileView.path = stateFile;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// FileView for shell state
|
||||
FileView {
|
||||
id: stateFileView
|
||||
printErrors: false
|
||||
watchChanges: false
|
||||
|
||||
adapter: JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
// CompositorService: display scales
|
||||
property var display: ({})
|
||||
|
||||
// NotificationService: notification state
|
||||
property var notificationsState: ({
|
||||
lastSeenTs: 0
|
||||
})
|
||||
|
||||
// UpdateService: changelog state
|
||||
property var changelogState: ({
|
||||
lastSeenVersion: ""
|
||||
})
|
||||
|
||||
// SchemeDownloader: color schemes list
|
||||
property var colorSchemesList: ({
|
||||
schemes: [],
|
||||
timestamp: 0
|
||||
})
|
||||
|
||||
// UI state: settings panel, etc.
|
||||
property var ui: ({
|
||||
settingsSidebarExpanded: true
|
||||
})
|
||||
|
||||
// Telemetry state
|
||||
property var telemetry: ({
|
||||
instanceId: ""
|
||||
})
|
||||
|
||||
// Launcher app usage counts
|
||||
property var launcherUsage: ({})
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
root.isLoaded = true;
|
||||
Logger.d("ShellState", "Loaded state file");
|
||||
}
|
||||
|
||||
onLoadFailed: error => {
|
||||
if (error === 2) {
|
||||
// File doesn't exist, will be created on first write
|
||||
root.isLoaded = true;
|
||||
Logger.d("ShellState", "State file doesn't exist, will create on first write");
|
||||
} else {
|
||||
Logger.e("ShellState", "Failed to load state file:", error);
|
||||
root.isLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Launcher usage
|
||||
function getLauncherUsageCount(key) {
|
||||
const m = adapter.launcherUsage;
|
||||
if (!m)
|
||||
return 0;
|
||||
const v = m[key];
|
||||
return typeof v === 'number' && isFinite(v) ? v : 0;
|
||||
}
|
||||
|
||||
function recordLauncherUsage(key) {
|
||||
let counts = Object.assign({}, adapter.launcherUsage || {});
|
||||
counts[key] = getLauncherUsageCount(key) + 1;
|
||||
adapter.launcherUsage = counts;
|
||||
save();
|
||||
}
|
||||
|
||||
// Migrate usage from one key to another, merging counts in a single save
|
||||
function migrateLauncherUsage(fromKey, toKey) {
|
||||
let counts = Object.assign({}, adapter.launcherUsage || {});
|
||||
const fromCount = typeof counts[fromKey] === 'number' && isFinite(counts[fromKey]) ? counts[fromKey] : 0;
|
||||
const toCount = typeof counts[toKey] === 'number' && isFinite(counts[toKey]) ? counts[toKey] : 0;
|
||||
counts[toKey] = toCount + fromCount;
|
||||
delete counts[fromKey];
|
||||
adapter.launcherUsage = counts;
|
||||
save();
|
||||
}
|
||||
|
||||
// Debounced save timer
|
||||
Timer {
|
||||
id: saveTimer
|
||||
interval: 500
|
||||
onTriggered: performSave()
|
||||
}
|
||||
|
||||
property bool saveQueued: false
|
||||
|
||||
function save() {
|
||||
saveQueued = true;
|
||||
saveTimer.restart();
|
||||
}
|
||||
|
||||
function performSave() {
|
||||
if (!saveQueued || !stateFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveQueued = false;
|
||||
|
||||
try {
|
||||
// Ensure cache directory exists
|
||||
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
try {
|
||||
stateFileView.writeAdapter();
|
||||
Logger.d("ShellState", "Saved state file");
|
||||
} catch (writeError) {
|
||||
Logger.e("ShellState", "Failed to write state file:", writeError);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.e("ShellState", "Failed to save state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience functions for each service
|
||||
|
||||
// Display state (CompositorService)
|
||||
function setDisplay(displayData) {
|
||||
adapter.display = displayData;
|
||||
save();
|
||||
displayStateChanged();
|
||||
}
|
||||
|
||||
function getDisplay() {
|
||||
return adapter.display || {};
|
||||
}
|
||||
|
||||
// Notifications state (NotificationService)
|
||||
function setNotificationsState(stateData) {
|
||||
adapter.notificationsState = stateData;
|
||||
save();
|
||||
notificationsStateChanged();
|
||||
}
|
||||
|
||||
function getNotificationsState() {
|
||||
return adapter.notificationsState || {
|
||||
lastSeenTs: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Changelog state (UpdateService)
|
||||
function setChangelogState(stateData) {
|
||||
adapter.changelogState = stateData;
|
||||
save();
|
||||
changelogStateChanged();
|
||||
}
|
||||
|
||||
function getChangelogState() {
|
||||
return adapter.changelogState || {
|
||||
lastSeenVersion: ""
|
||||
};
|
||||
}
|
||||
|
||||
// Color schemes list (SchemeDownloader)
|
||||
function setColorSchemesList(listData) {
|
||||
adapter.colorSchemesList = listData;
|
||||
save();
|
||||
colorSchemesListChanged();
|
||||
}
|
||||
|
||||
function getColorSchemesList() {
|
||||
return adapter.colorSchemesList || {
|
||||
schemes: [],
|
||||
timestamp: 0
|
||||
};
|
||||
}
|
||||
|
||||
// UI state
|
||||
function setUiState(stateData) {
|
||||
adapter.ui = stateData;
|
||||
save();
|
||||
}
|
||||
|
||||
function getUiState() {
|
||||
return adapter.ui || {
|
||||
settingsSidebarExpanded: true
|
||||
};
|
||||
}
|
||||
|
||||
function setSettingsSidebarExpanded(expanded) {
|
||||
let uiState = getUiState();
|
||||
uiState.settingsSidebarExpanded = expanded;
|
||||
setUiState(uiState);
|
||||
}
|
||||
|
||||
function getSettingsSidebarExpanded() {
|
||||
return getUiState().settingsSidebarExpanded !== false; // default to true
|
||||
}
|
||||
|
||||
// Telemetry state
|
||||
function setTelemetryState(stateData) {
|
||||
adapter.telemetry = stateData;
|
||||
save();
|
||||
}
|
||||
|
||||
function getTelemetryState() {
|
||||
return adapter.telemetry || {
|
||||
instanceId: ""
|
||||
};
|
||||
}
|
||||
|
||||
function getTelemetryInstanceId() {
|
||||
return getTelemetryState().instanceId || "";
|
||||
}
|
||||
|
||||
function setTelemetryInstanceId(instanceId) {
|
||||
let state = getTelemetryState();
|
||||
state.instanceId = instanceId;
|
||||
setTelemetryState(state);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
function buildStateSnapshot() {
|
||||
try {
|
||||
const settingsData = QtObj2JS.qtObjectToPlainObject(Settings.data);
|
||||
const shellStateData = ShellState?.data ? QtObj2JS.qtObjectToPlainObject(ShellState.data) || {} : {};
|
||||
|
||||
return {
|
||||
settings: settingsData,
|
||||
state: {
|
||||
doNotDisturb: NotificationService.doNotDisturb,
|
||||
noctaliaPerformanceMode: PowerProfileService.noctaliaPerformanceMode,
|
||||
barVisible: BarService.isVisible,
|
||||
openedPanel: PanelService.openedPanel?.objectName || "",
|
||||
lockScreenActive: PanelService.lockScreen?.active || false,
|
||||
wallpapers: WallpaperService.getWallpapersEffectiveMap(),
|
||||
desktopWidgetsEditMode: DesktopWidgetRegistry.editMode || false,
|
||||
// -------------
|
||||
display: shellStateData.display || {},
|
||||
notificationsState: shellStateData.notificationsState || {},
|
||||
changelogState: shellStateData.changelogState || {},
|
||||
colorSchemesList: shellStateData.colorSchemesList || {},
|
||||
ui: shellStateData.ui || {}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.e("Settings", "Failed to build state snapshot:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services.Power
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Font size
|
||||
readonly property real fontSizeXXS: 8
|
||||
readonly property real fontSizeXS: 9
|
||||
readonly property real fontSizeS: 10
|
||||
readonly property real fontSizeM: 11
|
||||
readonly property real fontSizeL: 13
|
||||
readonly property real fontSizeXL: 16
|
||||
readonly property real fontSizeXXL: 18
|
||||
readonly property real fontSizeXXXL: 24
|
||||
|
||||
// Font weight
|
||||
readonly property int fontWeightRegular: 400
|
||||
readonly property int fontWeightMedium: 500
|
||||
readonly property int fontWeightSemiBold: 600
|
||||
readonly property int fontWeightBold: 700
|
||||
|
||||
// Container Radii: major layout sections (sidebars, cards, content panels)
|
||||
readonly property int radiusXXXS: Math.round(3 * Settings.data.general.radiusRatio)
|
||||
readonly property int radiusXXS: Math.round(4 * Settings.data.general.radiusRatio)
|
||||
readonly property int radiusXS: Math.round(8 * Settings.data.general.radiusRatio)
|
||||
readonly property int radiusS: Math.round(12 * Settings.data.general.radiusRatio)
|
||||
readonly property int radiusM: Math.round(16 * Settings.data.general.radiusRatio)
|
||||
readonly property int radiusL: Math.round(20 * Settings.data.general.radiusRatio)
|
||||
|
||||
// Input radii: interactive elements (buttons, toggles, text fields)
|
||||
readonly property int iRadiusXXXS: Math.round(3 * Settings.data.general.iRadiusRatio)
|
||||
readonly property int iRadiusXXS: Math.round(4 * Settings.data.general.iRadiusRatio)
|
||||
readonly property int iRadiusXS: Math.round(8 * Settings.data.general.iRadiusRatio)
|
||||
readonly property int iRadiusS: Math.round(12 * Settings.data.general.iRadiusRatio)
|
||||
readonly property int iRadiusM: Math.round(16 * Settings.data.general.iRadiusRatio)
|
||||
readonly property int iRadiusL: Math.round(20 * Settings.data.general.iRadiusRatio)
|
||||
|
||||
readonly property int screenRadius: Math.round(20 * Settings.data.general.screenRadiusRatio)
|
||||
|
||||
// Border
|
||||
readonly property int borderS: Math.max(1, Math.round(1 * uiScaleRatio))
|
||||
readonly property int borderM: Math.max(1, Math.round(2 * uiScaleRatio))
|
||||
readonly property int borderL: Math.max(1, Math.round(3 * uiScaleRatio))
|
||||
|
||||
// Margins (for margins and spacing)
|
||||
readonly property int marginXXXS: Math.round(1 * uiScaleRatio)
|
||||
readonly property int marginXXS: Math.round(2 * uiScaleRatio)
|
||||
readonly property int marginXS: Math.round(4 * uiScaleRatio)
|
||||
readonly property int marginS: Math.round(6 * uiScaleRatio)
|
||||
readonly property int marginM: Math.round(9 * uiScaleRatio)
|
||||
readonly property int marginL: Math.round(13 * uiScaleRatio)
|
||||
readonly property int marginXL: Math.round(18 * uiScaleRatio)
|
||||
|
||||
// Double margins, for proper container sizing only (e.g. height: id.implicitHeight + Style.margin2M)
|
||||
readonly property int margin2XXXS: marginXXXS * 2
|
||||
readonly property int margin2XXS: marginXXS * 2
|
||||
readonly property int margin2XS: marginXS * 2
|
||||
readonly property int margin2S: marginS * 2
|
||||
readonly property int margin2M: marginM * 2
|
||||
readonly property int margin2L: marginL * 2
|
||||
readonly property int margin2XL: marginXL * 2
|
||||
|
||||
// Opacity
|
||||
readonly property real opacityNone: 0.0
|
||||
readonly property real opacityLight: 0.25
|
||||
readonly property real opacityMedium: 0.5
|
||||
readonly property real opacityHeavy: 0.75
|
||||
readonly property real opacityAlmost: 0.95
|
||||
readonly property real opacityFull: 1.0
|
||||
|
||||
// Shadows
|
||||
readonly property real shadowOpacity: 0.85
|
||||
readonly property real shadowBlur: 1.0
|
||||
readonly property int shadowBlurMax: 22
|
||||
readonly property real shadowHorizontalOffset: Settings.data.general.shadowOffsetX
|
||||
readonly property real shadowVerticalOffset: Settings.data.general.shadowOffsetY
|
||||
|
||||
// Animation duration (ms)
|
||||
readonly property int animationFaster: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(75 / Settings.data.general.animationSpeed)
|
||||
readonly property int animationFast: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
|
||||
readonly property int animationNormal: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
|
||||
readonly property int animationSlow: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
|
||||
readonly property int animationSlowest: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
|
||||
|
||||
// Delays
|
||||
readonly property int tooltipDelay: 300
|
||||
readonly property int tooltipDelayLong: 1200
|
||||
readonly property int pillDelay: 500
|
||||
|
||||
// Widgets base size
|
||||
readonly property real baseWidgetSize: 33
|
||||
readonly property real sliderWidth: 200
|
||||
|
||||
readonly property real uiScaleRatio: Settings.data.general.scaleRatio
|
||||
|
||||
// Bar Height
|
||||
readonly property real barHeight: {
|
||||
let h;
|
||||
switch (Settings.data.bar.density) {
|
||||
case "mini":
|
||||
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 23 : 21;
|
||||
break;
|
||||
case "compact":
|
||||
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25;
|
||||
break;
|
||||
case "comfortable":
|
||||
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37;
|
||||
break;
|
||||
case "spacious":
|
||||
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 49 : 47;
|
||||
break;
|
||||
default:
|
||||
case "default":
|
||||
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31;
|
||||
}
|
||||
return toOdd(h);
|
||||
}
|
||||
|
||||
// Capsule Height
|
||||
// Note: capsule must always be smaller than barHeight to account for border rendering
|
||||
// Qt Quick Rectangle borders are drawn centered on edges (half inside, half outside)
|
||||
readonly property real capsuleHeight: {
|
||||
let h;
|
||||
switch (Settings.data.bar.density) {
|
||||
case "mini":
|
||||
h = Math.round(barHeight * 0.90);
|
||||
break;
|
||||
case "compact":
|
||||
h = Math.round(barHeight * 0.85);
|
||||
break;
|
||||
case "comfortable":
|
||||
h = Math.round(barHeight * 0.75);
|
||||
break;
|
||||
case "spacious":
|
||||
h = Math.round(barHeight * 0.65);
|
||||
break;
|
||||
default:
|
||||
h = Math.round(barHeight * 0.82);
|
||||
break;
|
||||
}
|
||||
return toOdd(h);
|
||||
}
|
||||
|
||||
// The base/default font size for all texts in the bar
|
||||
readonly property real _barBaseFontSize: Math.max(1, (Style.barHeight / Style.capsuleHeight) * Style.fontSizeXXS)
|
||||
readonly property real barFontSize: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? _barBaseFontSize * 0.9 * Settings.data.bar.fontScale : _barBaseFontSize * Settings.data.bar.fontScale
|
||||
|
||||
readonly property color capsuleColor: Settings.data.bar.showCapsule ? Qt.alpha(Settings.data.bar.capsuleColorKey !== "none" ? Color.resolveColorKey(Settings.data.bar.capsuleColorKey) : Color.mSurfaceVariant, Settings.data.bar.capsuleOpacity) : "transparent"
|
||||
|
||||
readonly property color capsuleBorderColor: Settings.data.bar.showOutline ? Color.mPrimary : "transparent"
|
||||
readonly property int capsuleBorderWidth: Settings.data.bar.showOutline ? Style.borderS : 0
|
||||
|
||||
readonly property color boxBorderColor: Settings.data.ui.boxBorderEnabled ? Color.mOutline : "transparent"
|
||||
|
||||
// Pixel-perfect utility for centering content without subpixel positioning
|
||||
function pixelAlignCenter(containerSize, contentSize) {
|
||||
return Math.round((containerSize - contentSize) / 2);
|
||||
}
|
||||
|
||||
// Ensures a number is always odd (rounds down to nearest odd)
|
||||
function toOdd(n) {
|
||||
return Math.floor(n / 2) * 2 + 1;
|
||||
}
|
||||
|
||||
// Ensures a number is always even (rounds down to nearest even)
|
||||
function toEven(n) {
|
||||
return Math.floor(n / 2) * 2;
|
||||
}
|
||||
|
||||
// Get bar height for a specific density and orientation
|
||||
function getBarHeightForDensity(density, isVertical) {
|
||||
let h;
|
||||
switch (density) {
|
||||
case "mini":
|
||||
h = isVertical ? 23 : 21;
|
||||
break;
|
||||
case "compact":
|
||||
h = isVertical ? 27 : 25;
|
||||
break;
|
||||
case "comfortable":
|
||||
h = isVertical ? 39 : 37;
|
||||
break;
|
||||
case "spacious":
|
||||
h = isVertical ? 49 : 47;
|
||||
break;
|
||||
default:
|
||||
case "default":
|
||||
h = isVertical ? 33 : 31;
|
||||
}
|
||||
return toOdd(h);
|
||||
}
|
||||
|
||||
// Get capsule height for a specific density and bar height
|
||||
function getCapsuleHeightForDensity(density, barHeight) {
|
||||
let h;
|
||||
switch (density) {
|
||||
case "mini":
|
||||
h = Math.round(barHeight * 0.90);
|
||||
break;
|
||||
case "compact":
|
||||
h = Math.round(barHeight * 0.85);
|
||||
break;
|
||||
case "comfortable":
|
||||
h = Math.round(barHeight * 0.75);
|
||||
break;
|
||||
case "spacious":
|
||||
h = Math.round(barHeight * 0.65);
|
||||
break;
|
||||
default:
|
||||
h = Math.round(barHeight * 0.82);
|
||||
break;
|
||||
}
|
||||
return toOdd(h);
|
||||
}
|
||||
|
||||
// Get bar font size for a specific bar height, capsule height, and orientation
|
||||
function getBarFontSizeForDensity(barHeight, capsuleHeight, isVertical) {
|
||||
const baseFontSize = Math.max(1, (barHeight / capsuleHeight) * Style.fontSizeXXS);
|
||||
return isVertical ? baseFontSize * 0.9 * Settings.data.bar.fontScale : baseFontSize * Settings.data.bar.fontScale;
|
||||
}
|
||||
|
||||
// Convenience functions for per-screen bar sizing
|
||||
function getBarHeightForScreen(screenName) {
|
||||
var density = Settings.getBarDensityForScreen(screenName);
|
||||
var position = Settings.getBarPositionForScreen(screenName);
|
||||
var isVertical = position === "left" || position === "right";
|
||||
return getBarHeightForDensity(density, isVertical);
|
||||
}
|
||||
|
||||
function getCapsuleHeightForScreen(screenName) {
|
||||
var barHeight = getBarHeightForScreen(screenName);
|
||||
var density = Settings.getBarDensityForScreen(screenName);
|
||||
return getCapsuleHeightForDensity(density, barHeight);
|
||||
}
|
||||
|
||||
function getBarFontSizeForScreen(screenName) {
|
||||
var barHeight = getBarHeightForScreen(screenName);
|
||||
var capsuleHeight = getCapsuleHeightForScreen(screenName);
|
||||
var position = Settings.getBarPositionForScreen(screenName);
|
||||
var isVertical = position === "left" || position === "right";
|
||||
return getBarFontSizeForDensity(barHeight, capsuleHeight, isVertical);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
// Manual overrides for tricky apps
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
"gnome-tweaks": "org.gnome.tweaks",
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
})
|
||||
|
||||
// Dynamic fixups
|
||||
property var regexSubstitutions: [
|
||||
{
|
||||
"regex": /^steam_app_(\d+)$/,
|
||||
"replace": "steam_icon_$1"
|
||||
},
|
||||
{
|
||||
"regex": /Minecraft.*/,
|
||||
"replace": "minecraft-launcher"
|
||||
},
|
||||
{
|
||||
"regex": /.*polkit.*/,
|
||||
"replace": "system-lock-screen"
|
||||
},
|
||||
{
|
||||
"regex": /gcr.prompter/,
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
|
||||
property list<DesktopEntry> entryList: []
|
||||
property var preppedNames: []
|
||||
property var preppedIcons: []
|
||||
property var preppedIds: []
|
||||
|
||||
Component.onCompleted: refreshEntries()
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries.applications
|
||||
function onValuesChanged() {
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
if (typeof DesktopEntries === 'undefined')
|
||||
return;
|
||||
|
||||
const values = Array.from(DesktopEntries.applications.values);
|
||||
if (values) {
|
||||
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
|
||||
updatePreppedData();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreppedData() {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return;
|
||||
|
||||
const list = Array.from(entryList);
|
||||
preppedNames = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.name} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIcons = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIds = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.id} `),
|
||||
entry: a
|
||||
}));
|
||||
}
|
||||
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = findAppEntry(appId);
|
||||
if (entry) {
|
||||
return iconFromName(entry.icon, fallback);
|
||||
}
|
||||
|
||||
return iconFromName(appId, fallback);
|
||||
}
|
||||
|
||||
// Robust lookup strategy
|
||||
function findAppEntry(str) {
|
||||
if (!str || str.length === 0)
|
||||
return null;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (result = checkHeuristic(str))
|
||||
return result;
|
||||
if (result = checkSubstitutions(str))
|
||||
return result;
|
||||
if (result = checkRegex(str))
|
||||
return result;
|
||||
if (result = checkSimpleTransforms(str))
|
||||
return result;
|
||||
if (result = checkFuzzySearch(str))
|
||||
return result;
|
||||
if (result = checkCleanMatch(str))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lookup Helpers ---
|
||||
|
||||
function checkHeuristic(str) {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(str);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSubstitutions(str) {
|
||||
let effectiveStr = substitutions[str];
|
||||
if (!effectiveStr)
|
||||
effectiveStr = substitutions[str.toLowerCase()];
|
||||
|
||||
if (effectiveStr && effectiveStr !== str) {
|
||||
return findAppEntry(effectiveStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkRegex(str) {
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const sub = regexSubstitutions[i];
|
||||
const replaced = str.replace(sub.regex, sub.replace);
|
||||
if (replaced !== str) {
|
||||
return findAppEntry(replaced);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSimpleTransforms(str) {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
const lower = str.toLowerCase();
|
||||
|
||||
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
const variant = variants[i];
|
||||
if (variant) {
|
||||
const entry = DesktopEntries.byId(variant);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkFuzzySearch(str) {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return null;
|
||||
|
||||
// Check filenames (IDs) first
|
||||
if (preppedIds.length > 0) {
|
||||
let results = fuzzyQuery(str, preppedIds);
|
||||
if (results.length === 0) {
|
||||
const underscored = str.replace(/-/g, '_').toLowerCase();
|
||||
if (underscored !== str)
|
||||
results = fuzzyQuery(underscored, preppedIds);
|
||||
}
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then icons
|
||||
if (preppedIcons.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedIcons);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then names
|
||||
if (preppedNames.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedNames);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkCleanMatch(str) {
|
||||
if (!str || str.length <= 3)
|
||||
return null;
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
// Aggressive fallback: strip all separators
|
||||
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
|
||||
const list = Array.from(entryList);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const entry = list[i];
|
||||
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
|
||||
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search, preppedData) {
|
||||
if (!search || !preppedData || preppedData.length === 0)
|
||||
return [];
|
||||
return FuzzySort.go(search, preppedData, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
if (!iconName || iconName.length === 0)
|
||||
return false;
|
||||
if (iconName.startsWith("/"))
|
||||
return true;
|
||||
|
||||
const path = Quickshell.iconPath(iconName, true);
|
||||
return path && path.length > 0 && !path.includes("image-missing");
|
||||
}
|
||||
|
||||
function getFromReverseDomain(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
function normalizeWithHyphens(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
// Deprecated shim
|
||||
function guessIcon(str) {
|
||||
const entry = findAppEntry(str);
|
||||
return entry ? entry.icon : "image-missing";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Current date
|
||||
property var now: new Date()
|
||||
|
||||
// Unix timestamp of the last update
|
||||
property real _lastUpdateTs: Date.now()
|
||||
|
||||
// Signal emitted when a significant time jump is detected (e.g. system resume)
|
||||
signal resumed
|
||||
|
||||
// Returns a Unix Timestamp (in seconds)
|
||||
readonly property int timestamp: {
|
||||
return Math.floor(root.now / 1000);
|
||||
}
|
||||
|
||||
// Timer state (for countdown/stopwatch)
|
||||
property bool timerRunning: false
|
||||
property bool timerStopwatchMode: false
|
||||
property int timerRemainingSeconds: 0
|
||||
property int timerTotalSeconds: 0
|
||||
property int timerElapsedSeconds: 0
|
||||
property bool timerSoundPlaying: false
|
||||
property int timerStartTimestamp: 0 // Unix timestamp when timer was started
|
||||
property int timerPausedAt: 0 // Value when paused (for resuming)
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: false
|
||||
onTriggered: {
|
||||
var newTime = new Date();
|
||||
var currentTs = newTime.getTime();
|
||||
|
||||
// Detect time jump (e.g. system resume) - threshold: 5 seconds
|
||||
if (currentTs - root._lastUpdateTs > 5000) {
|
||||
Logger.i("Time", "Time jump detected (" + Math.round((currentTs - root._lastUpdateTs) / 1000) + "s) - likely system resume");
|
||||
root.resumed();
|
||||
}
|
||||
root._lastUpdateTs = currentTs;
|
||||
|
||||
root.now = newTime;
|
||||
|
||||
// Update timer if running
|
||||
if (root.timerRunning && root.timerStartTimestamp > 0) {
|
||||
const elapsedSinceStart = root.timestamp - root.timerStartTimestamp;
|
||||
|
||||
if (root.timerStopwatchMode) {
|
||||
root.timerElapsedSeconds = root.timerPausedAt + elapsedSinceStart;
|
||||
} else {
|
||||
root.timerRemainingSeconds = root.timerTotalSeconds - elapsedSinceStart;
|
||||
if (root.timerRemainingSeconds <= 0) {
|
||||
root.timerOnFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust next interval to sync with the start of the next second
|
||||
var msIntoSecond = newTime.getMilliseconds();
|
||||
if (msIntoSecond > 100) {
|
||||
// If we're more than 100ms into the second, adjust for next time
|
||||
updateTimer.interval = 1000 - msIntoSecond + 10; // +10ms buffer
|
||||
updateTimer.restart();
|
||||
} else {
|
||||
updateTimer.interval = 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Start by syncing to the next second boundary
|
||||
var now = new Date();
|
||||
var msUntilNextSecond = 1000 - now.getMilliseconds();
|
||||
updateTimer.interval = msUntilNextSecond + 10; // +10ms buffer
|
||||
updateTimer.restart();
|
||||
}
|
||||
|
||||
// Formats a Date object into a YYYYMMDD-HHMMSS string.
|
||||
function getFormattedTimestamp(date) {
|
||||
if (!date) {
|
||||
date = new Date();
|
||||
}
|
||||
const year = date.getFullYear();
|
||||
|
||||
// getMonth() is zero-based, so we add 1
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
|
||||
}
|
||||
|
||||
// Format an easy to read approximate duration ex: 4h 32m
|
||||
// Used to display the time remaining on the Battery widget, computer uptime, etc..
|
||||
function formatVagueHumanReadableDuration(totalSeconds) {
|
||||
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
|
||||
return '0s';
|
||||
}
|
||||
|
||||
// Floor the input to handle decimal seconds
|
||||
totalSeconds = Math.floor(totalSeconds);
|
||||
|
||||
const days = Math.floor(totalSeconds / 86400);
|
||||
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
const parts = [];
|
||||
if (days)
|
||||
parts.push(`${days}d`);
|
||||
if (hours)
|
||||
parts.push(`${hours}h`);
|
||||
if (minutes)
|
||||
parts.push(`${minutes}m`);
|
||||
|
||||
// Only show seconds if no hours and no minutes
|
||||
if (!hours && !minutes) {
|
||||
parts.push(`${seconds}s`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// Format a date into
|
||||
function formatRelativeTime(date) {
|
||||
if (!date)
|
||||
return "";
|
||||
const diff = Date.now() - date.getTime();
|
||||
if (diff < 60000)
|
||||
return I18n.tr("notifications.time.now");
|
||||
if (diff < 120000)
|
||||
return I18n.tr("notifications.time.diff-m");
|
||||
if (diff < 3600000)
|
||||
return I18n.tr("notifications.time.diff-mm", {
|
||||
"diff": Math.floor(diff / 60000)
|
||||
});
|
||||
if (diff < 7200000)
|
||||
return I18n.tr("notifications.time.diff-h");
|
||||
if (diff < 86400000)
|
||||
return I18n.tr("notifications.time.diff-hh", {
|
||||
"diff": Math.floor(diff / 3600000)
|
||||
});
|
||||
if (diff < 172800000)
|
||||
return I18n.tr("notifications.time.diff-d");
|
||||
return I18n.tr("notifications.time.diff-dd", {
|
||||
"diff": Math.floor(diff / 86400000)
|
||||
});
|
||||
}
|
||||
|
||||
// Timer functions
|
||||
function timerStart() {
|
||||
if (root.timerStopwatchMode) {
|
||||
root.timerRunning = true;
|
||||
root.timerStartTimestamp = root.timestamp;
|
||||
root.timerPausedAt = root.timerElapsedSeconds;
|
||||
} else {
|
||||
if (root.timerRemainingSeconds <= 0) {
|
||||
return;
|
||||
}
|
||||
root.timerRunning = true;
|
||||
root.timerTotalSeconds = root.timerRemainingSeconds;
|
||||
root.timerStartTimestamp = root.timestamp;
|
||||
root.timerPausedAt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function timerPause() {
|
||||
if (root.timerRunning) {
|
||||
// Calculate and set remainingSeconds BEFORE changing timerRunning
|
||||
// This ensures the UI sees the correct value when it reacts to timerRunning changing
|
||||
if (root.timerStopwatchMode) {
|
||||
root.timerPausedAt = root.timerElapsedSeconds;
|
||||
} else {
|
||||
// Calculate remaining seconds at the exact moment of pause
|
||||
// Use current time directly to avoid stale timestamp issues
|
||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||
const elapsedSinceStart = currentTimestamp - root.timerStartTimestamp;
|
||||
const currentRemaining = root.timerTotalSeconds - elapsedSinceStart;
|
||||
root.timerPausedAt = Math.max(0, currentRemaining);
|
||||
|
||||
// CRITICAL: Update timerRemainingSeconds to the paused value BEFORE changing timerRunning
|
||||
// This ensures UI sees correct value when it reacts to timerRunning change
|
||||
root.timerRemainingSeconds = root.timerPausedAt;
|
||||
}
|
||||
}
|
||||
root.timerRunning = false;
|
||||
root.timerStartTimestamp = 0;
|
||||
// Stop any repeating notification sound when pausing
|
||||
SoundService.stopSound("alarm-beep.wav");
|
||||
root.timerSoundPlaying = false;
|
||||
}
|
||||
|
||||
function timerReset() {
|
||||
root.timerRunning = false;
|
||||
root.timerStartTimestamp = 0;
|
||||
if (root.timerStopwatchMode) {
|
||||
root.timerElapsedSeconds = 0;
|
||||
root.timerPausedAt = 0;
|
||||
} else {
|
||||
root.timerRemainingSeconds = 0;
|
||||
root.timerTotalSeconds = 0;
|
||||
root.timerPausedAt = 0;
|
||||
}
|
||||
// Stop any repeating notification sound
|
||||
SoundService.stopSound("alarm-beep.wav");
|
||||
root.timerSoundPlaying = false;
|
||||
}
|
||||
|
||||
function timerOnFinished() {
|
||||
root.timerRunning = false;
|
||||
root.timerRemainingSeconds = 0;
|
||||
// Play notification sound with repeat at lower volume
|
||||
root.timerSoundPlaying = true;
|
||||
SoundService.playSound("alarm-beep.wav", {
|
||||
repeat: true,
|
||||
volume: 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user