mirror of
https://github.com/kunkunsh/kunkun-ext-neohtop.git
synced 2025-04-11 17:29:45 +00:00
Merge pull request #81 from Abdenasser/transaparent-title-bar
custom title bar + removing resizable columns functionality for now
This commit is contained in:
commit
bafb8f520e
@ -5,7 +5,11 @@
|
|||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:app:allow-version"
|
"core:app:allow-version",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-close"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,9 @@
|
|||||||
"title": "NeoHtop",
|
"title": "NeoHtop",
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"minWidth": 1120,
|
"minWidth": 1120,
|
||||||
"minHeight": 700
|
"minHeight": 700,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"hiddenTitle": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
faThumbtack,
|
faThumbtack,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faXmark,
|
faXmark,
|
||||||
faGripLinesVertical,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import Fa from "svelte-fa";
|
import Fa from "svelte-fa";
|
||||||
import type { Process, Column } from "$lib/types";
|
import type { Process, Column } from "$lib/types";
|
||||||
@ -20,88 +19,6 @@
|
|||||||
export let onShowDetails: (process: Process) => void;
|
export let onShowDetails: (process: Process) => void;
|
||||||
export let onKillProcess: (process: Process) => void;
|
export let onKillProcess: (process: Process) => void;
|
||||||
|
|
||||||
interface ResizeState {
|
|
||||||
leftColumnId: string | null;
|
|
||||||
rightColumnId: string | null;
|
|
||||||
startX: number;
|
|
||||||
leftStartWidth: number;
|
|
||||||
rightStartWidth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resizing: ResizeState = {
|
|
||||||
leftColumnId: null,
|
|
||||||
rightColumnId: null,
|
|
||||||
startX: 0,
|
|
||||||
leftStartWidth: 0,
|
|
||||||
rightStartWidth: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store column widths
|
|
||||||
let columnWidths: Record<string, number> = {};
|
|
||||||
|
|
||||||
// Initialize default widths
|
|
||||||
$: {
|
|
||||||
columns.forEach((col) => {
|
|
||||||
if (!columnWidths[col.id]) {
|
|
||||||
columnWidths[col.id] = 100; // Default width
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResizeStart(
|
|
||||||
event: MouseEvent,
|
|
||||||
leftColId: string,
|
|
||||||
rightColId: string,
|
|
||||||
) {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
resizing = {
|
|
||||||
leftColumnId: leftColId,
|
|
||||||
rightColumnId: rightColId,
|
|
||||||
startX: event.pageX,
|
|
||||||
leftStartWidth: columnWidths[leftColId],
|
|
||||||
rightStartWidth: columnWidths[rightColId],
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleResizeMove);
|
|
||||||
document.addEventListener("mouseup", handleResizeEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResizeMove(event: MouseEvent) {
|
|
||||||
if (!resizing.leftColumnId || !resizing.rightColumnId) return;
|
|
||||||
|
|
||||||
const delta = event.pageX - resizing.startX;
|
|
||||||
|
|
||||||
// Ensure minimum width (50px) for both columns
|
|
||||||
const newLeftWidth = Math.max(50, resizing.leftStartWidth + delta);
|
|
||||||
const totalWidth = resizing.leftStartWidth + resizing.rightStartWidth;
|
|
||||||
const newRightWidth = Math.max(50, totalWidth - newLeftWidth);
|
|
||||||
|
|
||||||
// Only update if both columns maintain minimum width
|
|
||||||
if (newLeftWidth >= 50 && newRightWidth >= 50) {
|
|
||||||
columnWidths = {
|
|
||||||
...columnWidths,
|
|
||||||
[resizing.leftColumnId]: newLeftWidth,
|
|
||||||
[resizing.rightColumnId]: newRightWidth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResizeEnd() {
|
|
||||||
resizing = {
|
|
||||||
leftColumnId: null,
|
|
||||||
rightColumnId: null,
|
|
||||||
startX: 0,
|
|
||||||
leftStartWidth: 0,
|
|
||||||
rightStartWidth: 0,
|
|
||||||
};
|
|
||||||
document.removeEventListener("mousemove", handleResizeMove);
|
|
||||||
document.removeEventListener("mouseup", handleResizeEnd);
|
|
||||||
|
|
||||||
// Optional: Save to localStorage
|
|
||||||
localStorage.setItem("columnWidths", JSON.stringify(columnWidths));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSortIndicator(field: keyof Process) {
|
function getSortIndicator(field: keyof Process) {
|
||||||
if (sortConfig.field !== field) return "↕";
|
if (sortConfig.field !== field) return "↕";
|
||||||
return sortConfig.direction === "asc" ? "↑" : "↓";
|
return sortConfig.direction === "asc" ? "↑" : "↓";
|
||||||
@ -173,38 +90,20 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{#each columns.filter((col) => col.visible) as column, i}
|
{#each columns.filter((col) => col.visible) as column}
|
||||||
<th
|
<th class="sortable" on:click={() => onToggleSort(column.id)}>
|
||||||
data-column={column.id}
|
<div class="th-content">
|
||||||
style="width: {columnWidths[column.id]}px"
|
{column.label}
|
||||||
>
|
<span
|
||||||
<div class="th-content" on:click={() => onToggleSort(column.id)}>
|
class="sort-indicator"
|
||||||
<div class="th-label">
|
class:active={sortConfig.field === column.id}
|
||||||
{column.label}
|
>
|
||||||
<span
|
{getSortIndicator(column.id)}
|
||||||
class="sort-indicator"
|
</span>
|
||||||
class:active={sortConfig.field === column.id}
|
|
||||||
>
|
|
||||||
{getSortIndicator(column.id)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{#if i < columns.filter((col) => col.visible).length - 1}
|
|
||||||
<div
|
|
||||||
class="resize-handle"
|
|
||||||
on:mousedown|stopPropagation={(e) =>
|
|
||||||
handleResizeStart(
|
|
||||||
e,
|
|
||||||
column.id,
|
|
||||||
columns.filter((col) => col.visible)[i + 1].id,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Fa icon={faGripLinesVertical} size="xs" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
<th class="col-actions">Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -353,61 +252,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.th-content {
|
.th-content {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-label {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: col-resize;
|
|
||||||
color: var(--overlay0);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show handle on header hover */
|
|
||||||
th:hover .resize-handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle:hover {
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active state during resize */
|
|
||||||
.resize-handle:active {
|
|
||||||
color: var(--blue);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-indicator {
|
.sort-indicator {
|
||||||
display: inline-flex;
|
color: var(--overlay0);
|
||||||
opacity: 0;
|
font-size: 12px;
|
||||||
transition: opacity 0.2s ease;
|
opacity: 0.5;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-indicator.active {
|
.sort-indicator.active {
|
||||||
|
color: var(--blue);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
th:hover .sort-indicator {
|
.sortable:hover .sort-indicator {
|
||||||
opacity: 0.5;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.high-usage {
|
.high-usage {
|
||||||
@ -443,6 +306,7 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
background: var(--base);
|
||||||
border-left: 1px solid var(--surface0);
|
border-left: 1px solid var(--surface0);
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
@ -563,66 +427,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
|
||||||
position: relative;
|
|
||||||
min-width: 50px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle {
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: col-resize;
|
|
||||||
color: var(--overlay0);
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show handle on header hover */
|
|
||||||
th:hover .resize-handle {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resize-handle:hover {
|
|
||||||
color: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Active state during resize */
|
|
||||||
.resize-handle:active {
|
|
||||||
color: var(--blue);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optional: Add a subtle background on hover */
|
|
||||||
.resize-handle:hover::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: -4px;
|
|
||||||
background: var(--surface0);
|
|
||||||
border-radius: 4px;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make sure the table doesn't shrink columns */
|
|
||||||
table {
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
transition: width 0.05s ease;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
112
src/lib/components/TitleBar.svelte
Normal file
112
src/lib/components/TitleBar.svelte
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Window } from "@tauri-apps/api/window";
|
||||||
|
|
||||||
|
const appWindow = new Window("main");
|
||||||
|
let isMaximized = false;
|
||||||
|
|
||||||
|
async function toggleMaximize() {
|
||||||
|
isMaximized = await appWindow.isMaximized();
|
||||||
|
if (isMaximized) {
|
||||||
|
await appWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
await appWindow.maximize();
|
||||||
|
}
|
||||||
|
isMaximized = await appWindow.isMaximized();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="title-bar" data-tauri-drag-region>
|
||||||
|
<div class="title">
|
||||||
|
<div class="neon">NeoHtop</div>
|
||||||
|
<div class="scanlines"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title-bar {
|
||||||
|
height: 32px;
|
||||||
|
/* background: var(--mantle); */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text);
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px var(--text),
|
||||||
|
0 0 10px var(--text),
|
||||||
|
0 0 20px var(--blue),
|
||||||
|
0 0 40px var(--blue),
|
||||||
|
0 0 80px var(--blue);
|
||||||
|
animation: flicker 3s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0%,
|
||||||
|
19.999%,
|
||||||
|
22%,
|
||||||
|
62.999%,
|
||||||
|
64%,
|
||||||
|
64.999%,
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px var(--text),
|
||||||
|
0 0 10px var(--text),
|
||||||
|
0 0 20px var(--blue),
|
||||||
|
0 0 40px var(--blue),
|
||||||
|
0 0 80px var(--blue);
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
21.999%,
|
||||||
|
63%,
|
||||||
|
63.999%,
|
||||||
|
65%,
|
||||||
|
69.999% {
|
||||||
|
opacity: 0.4;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanlines {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 50%,
|
||||||
|
rgba(0, 0, 0, 0.2) 51%
|
||||||
|
);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
animation: scanlines 0.6s steps(40) infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanlines {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -7,7 +7,6 @@
|
|||||||
faPause,
|
faPause,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faArrowRotateRight,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
export let searchTerm: string;
|
export let searchTerm: string;
|
||||||
export let statusFilter: string = "all";
|
export let statusFilter: string = "all";
|
||||||
@ -54,11 +53,16 @@
|
|||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<div class="search-input-wrapper">
|
<div class="search-input-wrapper">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="text"
|
||||||
placeholder="Search processes"
|
placeholder="Search processes"
|
||||||
bind:value={searchTerm}
|
bind:value={searchTerm}
|
||||||
class="search-input"
|
class="search-input"
|
||||||
/>
|
/>
|
||||||
|
{#if searchTerm}
|
||||||
|
<button class="btn-clear" on:click={() => (searchTerm = "")}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-group">
|
<div class="toolbar-group">
|
||||||
@ -214,7 +218,9 @@
|
|||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
padding: 6px 12px;
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
padding-right: 70px;
|
||||||
border: 1px solid var(--surface1);
|
border: 1px solid var(--surface1);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -223,6 +229,43 @@
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--subtext0);
|
||||||
|
background: var(--surface1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:hover {
|
||||||
|
background: var(--surface2);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
background: var(--overlay0);
|
||||||
|
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E")
|
||||||
|
no-repeat 50% 50%;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E")
|
||||||
|
no-repeat 50% 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::-webkit-search-cancel-button:hover {
|
||||||
|
background: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.search-input:hover {
|
.search-input:hover {
|
||||||
background-color: var(--surface1);
|
background-color: var(--surface1);
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import { formatMemorySize, formatStatus } from "$lib/utils";
|
import { formatMemorySize, formatStatus } from "$lib/utils";
|
||||||
import { themeStore } from "$lib/stores";
|
import { themeStore } from "$lib/stores";
|
||||||
import type { Process, SystemStats, Column } from "$lib/types";
|
import type { Process, SystemStats, Column } from "$lib/types";
|
||||||
|
import TitleBar from "$lib/components/TitleBar.svelte";
|
||||||
|
|
||||||
let processes: Process[] = [];
|
let processes: Process[] = [];
|
||||||
let systemStats: SystemStats | null = null;
|
let systemStats: SystemStats | null = null;
|
||||||
@ -47,20 +48,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "memory_usage",
|
id: "memory_usage",
|
||||||
label: "Memory",
|
label: "RAM",
|
||||||
visible: true,
|
visible: true,
|
||||||
format: (v) => (v / (1024 * 1024)).toFixed(1) + " MB",
|
format: (v) => (v / (1024 * 1024)).toFixed(1) + " MB",
|
||||||
},
|
},
|
||||||
{ id: "command", label: "Command", visible: false },
|
|
||||||
{ id: "ppid", label: "Parent PID", visible: false },
|
|
||||||
{ id: "environ", label: "Environment Variables", visible: false },
|
|
||||||
{ id: "root", label: "Root", visible: false },
|
|
||||||
{
|
{
|
||||||
id: "virtual_memory",
|
id: "virtual_memory",
|
||||||
label: "Virtual Memory",
|
label: "VIRT",
|
||||||
visible: false,
|
visible: true,
|
||||||
format: (v) => formatMemorySize(v),
|
format: (v) => formatMemorySize(v),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "disk_usage",
|
||||||
|
label: "Disk R/W",
|
||||||
|
visible: true,
|
||||||
|
format: (v) =>
|
||||||
|
`${(v[0] / (1024 * 1024)).toFixed(1)} / ${(v[1] / (1024 * 1024)).toFixed(1)} MB`,
|
||||||
|
},
|
||||||
|
{ id: "ppid", label: "Parent PID", visible: false },
|
||||||
|
{ id: "root", label: "Root", visible: false },
|
||||||
|
{ id: "command", label: "Command", visible: false },
|
||||||
|
{ id: "environ", label: "Environment Variables", visible: false },
|
||||||
|
{ id: "session_id", label: "Session ID", visible: false },
|
||||||
{
|
{
|
||||||
id: "start_time",
|
id: "start_time",
|
||||||
label: "Start Time",
|
label: "Start Time",
|
||||||
@ -70,7 +79,7 @@
|
|||||||
{
|
{
|
||||||
id: "run_time",
|
id: "run_time",
|
||||||
label: "Run Time",
|
label: "Run Time",
|
||||||
visible: false,
|
visible: true,
|
||||||
format: (v) => {
|
format: (v) => {
|
||||||
const seconds = v; // v is the time the process has been running in seconds
|
const seconds = v; // v is the time the process has been running in seconds
|
||||||
const hours = Math.floor(seconds / 3600);
|
const hours = Math.floor(seconds / 3600);
|
||||||
@ -79,14 +88,6 @@
|
|||||||
return `${hours}h ${minutes}m ${remainingSeconds}s`; // Format as HH:MM:SS
|
return `${hours}h ${minutes}m ${remainingSeconds}s`; // Format as HH:MM:SS
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "disk_usage",
|
|
||||||
label: "Disk Usage read/write",
|
|
||||||
visible: false,
|
|
||||||
format: (v) =>
|
|
||||||
`${(v[0] / (1024 * 1024)).toFixed(1)} / ${(v[1] / (1024 * 1024)).toFixed(1)} MB`,
|
|
||||||
},
|
|
||||||
{ id: "session_id", label: "Session ID", visible: false },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let sortConfig = {
|
let sortConfig = {
|
||||||
@ -245,9 +246,18 @@
|
|||||||
selectedProcessPid = null;
|
selectedProcessPid = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let minLoadingTimer: ReturnType<typeof setTimeout>;
|
||||||
|
const MIN_LOADING_TIME = 2000; // Show loading screen for at least 2 seconds
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
const loadingPromise = Promise.all([getProcesses()]);
|
||||||
|
const timerPromise = new Promise((resolve) => {
|
||||||
|
minLoadingTimer = setTimeout(resolve, MIN_LOADING_TIME);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([getProcesses()]);
|
// Wait for both the data to load AND the minimum time to pass
|
||||||
|
await Promise.all([loadingPromise, timerPromise]);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
@ -257,50 +267,58 @@
|
|||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (intervalId) clearInterval(intervalId);
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
if (minLoadingTimer) clearTimeout(minLoadingTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
<div class="spinner"></div>
|
<div class="title-wrapper">
|
||||||
<span class="loading-text">Loading processes...</span>
|
<div class="neon-title">NeoHtop</div>
|
||||||
|
<div class="neon-flare"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cyber-spinner"></div>
|
||||||
|
<span class="loading-text">System Initialization...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<main>
|
<div class="app-container">
|
||||||
{#if systemStats}
|
<TitleBar />
|
||||||
<StatsBar {systemStats} />
|
<main>
|
||||||
{/if}
|
{#if systemStats}
|
||||||
|
<StatsBar {systemStats} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<ToolBar
|
<ToolBar
|
||||||
bind:searchTerm
|
bind:searchTerm
|
||||||
bind:statusFilter
|
bind:statusFilter
|
||||||
bind:itemsPerPage
|
bind:itemsPerPage
|
||||||
bind:currentPage
|
bind:currentPage
|
||||||
bind:refreshRate
|
bind:refreshRate
|
||||||
bind:isFrozen
|
bind:isFrozen
|
||||||
{totalPages}
|
{totalPages}
|
||||||
totalResults={filteredProcesses.length}
|
totalResults={filteredProcesses.length}
|
||||||
bind:columns
|
bind:columns
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="alert">{error}</div>
|
<div class="alert">{error}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ProcessTable
|
<ProcessTable
|
||||||
processes={paginatedProcesses}
|
processes={paginatedProcesses}
|
||||||
{columns}
|
{columns}
|
||||||
{systemStats}
|
{systemStats}
|
||||||
{sortConfig}
|
{sortConfig}
|
||||||
{pinnedProcesses}
|
{pinnedProcesses}
|
||||||
onToggleSort={toggleSort}
|
onToggleSort={toggleSort}
|
||||||
onTogglePin={togglePin}
|
onTogglePin={togglePin}
|
||||||
onShowDetails={showProcessDetails}
|
onShowDetails={showProcessDetails}
|
||||||
onKillProcess={confirmKillProcess}
|
onKillProcess={confirmKillProcess}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ProcessDetailsModal
|
<ProcessDetailsModal
|
||||||
@ -320,14 +338,6 @@
|
|||||||
onConfirm={handleConfirmKill}
|
onConfirm={handleConfirmKill}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>NeoHtop - Modern System Monitor</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="A modern, web-based system monitoring interface inspired by htop"
|
|
||||||
/>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(:root) {
|
:global(:root) {
|
||||||
--base: #1e1e2e;
|
--base: #1e1e2e;
|
||||||
@ -365,10 +375,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
height: 100vh;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: min-content;
|
min-width: min-content;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
@ -388,6 +399,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: linear-gradient(135deg, var(--base) 0%, var(--mantle) 100%);
|
background: linear-gradient(135deg, var(--base) 0%, var(--mantle) 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-content {
|
.loading-content {
|
||||||
@ -395,16 +408,61 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
@keyframes glitch {
|
||||||
width: 40px;
|
0%,
|
||||||
height: 40px;
|
100% {
|
||||||
border: 3px solid var(--surface0);
|
transform: translate(0);
|
||||||
border-top-color: var(--blue);
|
}
|
||||||
border-radius: 50%;
|
20% {
|
||||||
animation: spin 1s ease-in-out infinite;
|
transform: translate(-2px, 2px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
}
|
||||||
|
40% {
|
||||||
|
transform: translate(-2px, -2px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translate(2px, -2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch-2 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translate(1px, 1px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch-3 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translate(-1px, -1px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(1px, 1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scanlines {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
@ -431,4 +489,122 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-title {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 64px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text);
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px var(--text),
|
||||||
|
0 0 10px var(--text),
|
||||||
|
0 0 20px var(--blue),
|
||||||
|
0 0 40px var(--blue),
|
||||||
|
0 0 80px var(--blue);
|
||||||
|
animation: pulse 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-flare {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
transparent 45%,
|
||||||
|
var(--blue) 48%,
|
||||||
|
var(--text) 50%,
|
||||||
|
var(--blue) 52%,
|
||||||
|
transparent 55%
|
||||||
|
);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: flare 4s ease-in-out infinite;
|
||||||
|
opacity: 0.1;
|
||||||
|
filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyber-spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 20px;
|
||||||
|
background: transparent;
|
||||||
|
border: 3px solid var(--surface0);
|
||||||
|
border-top: 3px solid var(--blue);
|
||||||
|
border-right: 3px solid var(--blue);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cyber-spinner::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
left: -3px;
|
||||||
|
right: -3px;
|
||||||
|
bottom: -3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: var(--blue);
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
animation: glow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px var(--text),
|
||||||
|
0 0 10px var(--text),
|
||||||
|
0 0 20px var(--blue),
|
||||||
|
0 0 40px var(--blue),
|
||||||
|
0 0 80px var(--blue);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
text-shadow:
|
||||||
|
0 0 10px var(--text),
|
||||||
|
0 0 20px var(--text),
|
||||||
|
0 0 40px var(--blue),
|
||||||
|
0 0 80px var(--blue),
|
||||||
|
0 0 120px var(--blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flare {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
background-position: 200% 200%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow {
|
||||||
|
from {
|
||||||
|
text-shadow:
|
||||||
|
0 0 2px var(--text),
|
||||||
|
0 0 4px var(--text),
|
||||||
|
0 0 6px var(--blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user