mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge branch 'noctalia-dev:main' into pr/networking-refactor-pt2
This commit is contained in:
@@ -1018,6 +1018,7 @@
|
||||
"edit-mode-description": "Enable edit mode to move and reposition desktop widgets. When enabled, widgets show a drag outline and can be repositioned.",
|
||||
"edit-mode-exit-button": "Exit edit mode",
|
||||
"edit-mode-grid-snap-label": "Grid snap",
|
||||
"edit-mode-grid-snap-scale-label": "Snap scale",
|
||||
"edit-mode-label": "Edit mode",
|
||||
"enabled-description": "Enable or disable desktop widgets entirely.",
|
||||
"enabled-label": "Enable desktop widgets",
|
||||
|
||||
@@ -539,6 +539,7 @@
|
||||
"enabled": false,
|
||||
"overviewEnabled": true,
|
||||
"gridSnap": false,
|
||||
"gridSnapScale": false,
|
||||
"monitorWidgets": []
|
||||
}
|
||||
}
|
||||
@@ -763,6 +763,7 @@ Singleton {
|
||||
property bool enabled: false
|
||||
property bool overviewEnabled: true
|
||||
property bool gridSnap: false
|
||||
property bool gridSnapScale: false
|
||||
property list<var> monitorWidgets: []
|
||||
// Format: [{ "name": "DP-1", "widgets": [...] }, { "name": "HDMI-1", "widgets": [...] }]
|
||||
}
|
||||
|
||||
@@ -109,6 +109,22 @@ Singleton {
|
||||
save();
|
||||
}
|
||||
|
||||
// Set a usage count directly (used for migration/merging)
|
||||
function recordLauncherUsageMerge(key, count) {
|
||||
let counts = Object.assign({}, adapter.launcherUsage || {});
|
||||
counts[key] = count;
|
||||
adapter.launcherUsage = counts;
|
||||
save();
|
||||
}
|
||||
|
||||
// Remove a usage key (used for cleaning up legacy keys after migration)
|
||||
function clearLauncherUsage(key) {
|
||||
let counts = Object.assign({}, adapter.launcherUsage || {});
|
||||
delete counts[key];
|
||||
adapter.launcherUsage = counts;
|
||||
save();
|
||||
}
|
||||
|
||||
// Debounced save timer
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
@@ -441,6 +441,15 @@ Variants {
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "grid-3x3"
|
||||
visible: Settings.data.desktopWidgets.gridSnap
|
||||
tooltipText: I18n.tr("panels.desktop-widgets.edit-mode-grid-snap-scale-label")
|
||||
colorBg: Settings.data.desktopWidgets.gridSnapScale ? Color.mPrimary : Color.mSurfaceVariant
|
||||
colorFg: Settings.data.desktopWidgets.gridSnapScale ? Color.mOnPrimary : Color.mPrimary
|
||||
onClicked: Settings.data.desktopWidgets.gridSnapScale = !Settings.data.desktopWidgets.gridSnapScale
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "grid-4x4"
|
||||
tooltipText: I18n.tr("panels.desktop-widgets.edit-mode-grid-snap-label")
|
||||
|
||||
@@ -115,6 +115,35 @@ Item {
|
||||
return Math.round(coord / root.gridSize) * root.gridSize;
|
||||
}
|
||||
|
||||
function snapScaleToGrid(scale) {
|
||||
if (!Settings.data.desktopWidgets.gridSnap || !Settings.data.desktopWidgets.gridSnapScale) {
|
||||
return scale;
|
||||
}
|
||||
|
||||
// Get widget's base width
|
||||
var initialWidth = internal.initialWidth;
|
||||
var initialScale = internal.initialScale;
|
||||
if (initialWidth <= 0 || initialScale <= 0) {
|
||||
return scale;
|
||||
}
|
||||
|
||||
// Since initialWidth = baseWidth * initialScale
|
||||
var baseWidth = initialWidth / initialScale;
|
||||
|
||||
// Snap the resulting width with the scale
|
||||
var resultingWidth = baseWidth * scale;
|
||||
var snappedWidth = root.snapToGrid(resultingWidth);
|
||||
|
||||
// Check that the snappedWidth isn't smaller than one grid size
|
||||
if (snappedWidth < root.gridSize) {
|
||||
snappedWidth = root.gridSize;
|
||||
}
|
||||
|
||||
// Return the ratio of the snappedWidth and the baseWidth, which is the new snapped scale
|
||||
var snappedScale = snappedWidth / baseWidth;
|
||||
return Math.max(minScale, Math.min(maxScale, snappedScale));
|
||||
}
|
||||
|
||||
function updateWidgetData(properties) {
|
||||
if (widgetIndex < 0 || !screen || !screen.name) {
|
||||
return;
|
||||
@@ -554,6 +583,8 @@ Item {
|
||||
internal.isScaling = true;
|
||||
internal.initialScale = root.widgetScale;
|
||||
internal.lastScale = root.widgetScale;
|
||||
internal.initialWidth = root.width;
|
||||
internal.initialHeight = root.height;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
@@ -571,6 +602,8 @@ Item {
|
||||
var scaleDelta = diagonalDelta / sensitivity;
|
||||
var newScale = Math.max(root.minScale, Math.min(root.maxScale, internal.initialScale + scaleDelta));
|
||||
|
||||
newScale = root.snapScaleToGrid(newScale);
|
||||
|
||||
if (!isNaN(newScale) && newScale > 0) {
|
||||
root.widgetScale = newScale;
|
||||
internal.lastScale = newScale;
|
||||
@@ -585,6 +618,7 @@ Item {
|
||||
});
|
||||
internal.isScaling = false;
|
||||
internal.operationType = "";
|
||||
root.widgetScale = root.snapScaleToGrid(root.widgetScale);
|
||||
internal.lastScale = root.widgetScale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,13 +77,18 @@ ShapePath {
|
||||
readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(barWidth, barHeight, radius) : false
|
||||
readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(barWidth, barHeight), radius) : 0) : radius
|
||||
|
||||
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
|
||||
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
|
||||
readonly property real _minR: 0.01
|
||||
|
||||
// Helper function for getting corner radius based on state
|
||||
function getCornerRadius(cornerState) {
|
||||
// State -1 = no radius (flat corner)
|
||||
// State -1 = flat corner — use minimum safe radius instead of 0
|
||||
// to prevent degenerate PathArc (zero displacement + zero radius)
|
||||
if (cornerState === -1)
|
||||
return 0;
|
||||
// All other states use effectiveRadius
|
||||
return effectiveRadius;
|
||||
return _minR;
|
||||
// All other states use effectiveRadius (clamped to safe minimum)
|
||||
return Math.max(_minR, effectiveRadius);
|
||||
}
|
||||
|
||||
// Per-corner multipliers and radii based on bar's corner states (handle null bar)
|
||||
@@ -107,13 +112,14 @@ ShapePath {
|
||||
// Mirrors PanelBackground.isRenderable — prevents CurveRenderer crash on zero-area paths.
|
||||
readonly property bool isRenderable: bar !== null && shouldShow && (isFramed ? (screenWidth > 0 && screenHeight > 0) : (barWidth > 0 && barHeight > 0))
|
||||
|
||||
// Extend bar background beyond screen edges where both adjacent corners are flat,
|
||||
// to prevent CurveRenderer antialiasing artifacts on screen-flush edges
|
||||
// Edge overshoot: extend bar background beyond screen edges where both adjacent
|
||||
// corners are flat (state -1) to prevent CurveRenderer antialiasing artifacts.
|
||||
// Uses corner state checks instead of radius === 0 since flat corners now have _minR.
|
||||
readonly property real screenEdgeOvershoot: 2
|
||||
readonly property real topEdgeOvs: (!isFramed && shouldShow && tlRadius === 0 && trRadius === 0 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0
|
||||
readonly property real bottomEdgeOvs: (!isFramed && shouldShow && blRadius === 0 && brRadius === 0 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0
|
||||
readonly property real leftEdgeOvs: (!isFramed && shouldShow && tlRadius === 0 && blRadius === 0 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0
|
||||
readonly property real rightEdgeOvs: (!isFramed && shouldShow && trRadius === 0 && brRadius === 0 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0
|
||||
readonly property real topEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.topRightCornerState === -1 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0
|
||||
readonly property real bottomEdgeOvs: (!isFramed && shouldShow && bar && bar.bottomLeftCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0
|
||||
readonly property real leftEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.bottomLeftCornerState === -1 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0
|
||||
readonly property real rightEdgeOvs: (!isFramed && shouldShow && bar && bar.topRightCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0
|
||||
|
||||
// Auto-hide opacity factor for background fade
|
||||
property real opacityFactor: (bar && bar.isHidden) ? 0 : 1
|
||||
@@ -135,7 +141,9 @@ ShapePath {
|
||||
// all subsequent path elements form a valid non-degenerate off-screen square.
|
||||
// Each edge is split between PathLine and PathArc so no arc has zero displacement,
|
||||
// preventing CurveRenderer triangulation crashes on degenerate arcs.
|
||||
startX: isRenderable ? (isFramed ? 0 : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75
|
||||
// For framed mode the outer path is a full-screen rectangle; _minR offsets at each
|
||||
// corner prevent zero-displacement zero-radius arcs that crash qTriangulate.
|
||||
startX: isRenderable ? (isFramed ? _minR : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75
|
||||
startY: isRenderable ? (isFramed ? 0 : (barMappedPos.y + topEdgeOvs)) : -1
|
||||
|
||||
// ========== PATH DEFINITION ==========
|
||||
@@ -145,58 +153,58 @@ ShapePath {
|
||||
// off-screen square with non-degenerate arcs so CurveRenderer never receives
|
||||
// a zero-area, bare-moveto, or zero-displacement arc path.
|
||||
PathLine {
|
||||
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0
|
||||
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0
|
||||
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
|
||||
}
|
||||
|
||||
// Bar top-right corner (only if not framed)
|
||||
// Top-right corner
|
||||
PathArc {
|
||||
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
|
||||
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75
|
||||
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.trRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.trRadius) : 0
|
||||
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75
|
||||
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0
|
||||
direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY)
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0
|
||||
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0
|
||||
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0
|
||||
}
|
||||
|
||||
// Bar bottom-right corner (only if not framed)
|
||||
// Bottom-right corner
|
||||
PathArc {
|
||||
x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25
|
||||
x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25
|
||||
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
|
||||
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.brRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.brRadius) : 0
|
||||
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0
|
||||
direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY)
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1
|
||||
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1
|
||||
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0
|
||||
}
|
||||
|
||||
// Bar bottom-left corner (only if not framed)
|
||||
// Bottom-left corner
|
||||
PathArc {
|
||||
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
|
||||
y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25
|
||||
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.blRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.blRadius) : 0
|
||||
y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25
|
||||
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0
|
||||
direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY)
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1
|
||||
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1
|
||||
y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1
|
||||
}
|
||||
|
||||
// Bar top-left corner (only if not framed, back to start)
|
||||
// Top-left corner (back to start)
|
||||
PathArc {
|
||||
x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75
|
||||
x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75
|
||||
y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1
|
||||
radiusX: root.isRenderable ? (root.isFramed ? 0 : root.tlRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? 0 : root.tlRadius) : 0
|
||||
radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
|
||||
radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0
|
||||
direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,13 +59,18 @@ ShapePath {
|
||||
readonly property bool shouldFlatten: panelBg ? ShapeCornerHelper.shouldFlatten(panelWidth, panelHeight, radius) : false
|
||||
readonly property real effectiveRadius: shouldFlatten ? ShapeCornerHelper.getFlattenedRadius(Math.min(panelWidth, panelHeight), radius) : radius
|
||||
|
||||
// Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs
|
||||
// that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible.
|
||||
readonly property real _minR: 0.01
|
||||
|
||||
// Helper function for getting corner radius based on state
|
||||
function getCornerRadius(cornerState) {
|
||||
// State -1 = no radius (flat corner)
|
||||
// State -1 = flat corner — use minimum safe radius instead of 0
|
||||
// to prevent degenerate PathArc (zero displacement + zero radius)
|
||||
if (cornerState === -1)
|
||||
return 0;
|
||||
// All other states use effectiveRadius
|
||||
return effectiveRadius;
|
||||
return _minR;
|
||||
// All other states use effectiveRadius (clamped to safe minimum)
|
||||
return Math.max(_minR, effectiveRadius);
|
||||
}
|
||||
|
||||
// Per-corner multipliers and radii based on panelBg's corner states
|
||||
|
||||
@@ -304,9 +304,20 @@ Rectangle {
|
||||
|
||||
// Sort by _score (higher = better match), items without _score go first
|
||||
if (searchText.trim() !== "") {
|
||||
const boostByUsage = Settings.data.appLauncher.sortByMostUsed;
|
||||
|
||||
allResults.sort((a, b) => {
|
||||
const sa = a._score !== undefined ? a._score : 0;
|
||||
const sb = b._score !== undefined ? b._score : 0;
|
||||
let sa = a._score !== undefined ? a._score : 0;
|
||||
let sb = b._score !== undefined ? b._score : 0;
|
||||
|
||||
// Boost scores for frequently used items from tracked providers
|
||||
if (boostByUsage) {
|
||||
if (a.provider && a.provider.trackUsage && a.usageKey)
|
||||
sa += 100.0 * Math.log2(1 + ShellState.getLauncherUsageCount(a.usageKey));
|
||||
if (b.provider && b.provider.trackUsage && b.usageKey)
|
||||
sb += 100.0 * Math.log2(1 + ShellState.getLauncherUsageCount(b.usageKey));
|
||||
}
|
||||
|
||||
return sb - sa;
|
||||
});
|
||||
}
|
||||
@@ -468,6 +479,11 @@ Rectangle {
|
||||
const item = results[selectedIndex];
|
||||
const provider = item.provider || currentProvider;
|
||||
|
||||
// Track usage for providers that opt in (cross-provider "most used" tracking)
|
||||
if (Settings.data.appLauncher.sortByMostUsed && provider && provider.trackUsage && item.usageKey) {
|
||||
ShellState.recordLauncherUsage(item.usageKey);
|
||||
}
|
||||
|
||||
// Check if auto-paste is enabled and provider/item supports it
|
||||
if (Settings.data.appLauncher.autoPasteClipboard && provider && provider.supportsAutoPaste && item.autoPasteText) {
|
||||
if (item.onAutoPaste)
|
||||
|
||||
@@ -14,6 +14,7 @@ Item {
|
||||
property string supportedLayouts: "both"
|
||||
property bool isDefaultProvider: true // This provider handles empty search
|
||||
property bool ignoreDensity: false // Apps should scale with launcher density
|
||||
property bool trackUsage: true // Track usage frequency for "most used" sorting
|
||||
|
||||
// Category support
|
||||
property string selectedCategory: "all"
|
||||
@@ -516,6 +517,7 @@ Item {
|
||||
function createResultEntry(app, score) {
|
||||
return {
|
||||
"appId": getAppKey(app),
|
||||
"usageKey": getAppKey(app),
|
||||
"name": app.name || "Unknown",
|
||||
"description": app.genericName || app.comment || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
@@ -523,10 +525,6 @@ Item {
|
||||
"_score": (score !== undefined ? score : 0),
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
root.recordUsage(app);
|
||||
}
|
||||
|
||||
// Close the launcher/SmartPanel immediately without any animations.
|
||||
// Ensures we are not preventing the future focusing of the app
|
||||
launcher.closeImmediately();
|
||||
@@ -641,8 +639,29 @@ Item {
|
||||
return String(app && app.name ? app.name : "unknown");
|
||||
}
|
||||
|
||||
// Returns the usage count for an app, checking both the canonical key (app.id)
|
||||
// and the legacy command-based key. If a legacy key has usage but the canonical
|
||||
// key doesn't, the counts are migrated automatically.
|
||||
function getUsageCount(app) {
|
||||
return ShellState.getLauncherUsageCount(getAppKey(app));
|
||||
const key = getAppKey(app);
|
||||
let count = ShellState.getLauncherUsageCount(key);
|
||||
|
||||
// Check for legacy command-based key if the primary key is the app ID
|
||||
if (app && app.id && app.command && app.command.join) {
|
||||
const legacyKey = app.command.join(" ");
|
||||
if (legacyKey !== key) {
|
||||
const legacyCount = ShellState.getLauncherUsageCount(legacyKey);
|
||||
if (legacyCount > 0) {
|
||||
// Migrate: merge legacy count into the canonical key
|
||||
count += legacyCount;
|
||||
ShellState.recordLauncherUsageMerge(key, count);
|
||||
ShellState.clearLauncherUsage(legacyKey);
|
||||
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" (${legacyCount}) → "${key}" (${count})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function recordUsage(app) {
|
||||
|
||||
Reference in New Issue
Block a user