Compare commits

..

2 Commits

Author SHA1 Message Date
fea99d37fe feat: 标签调整宽度和图标,top 3 -> top 4
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 19:00:52 +08:00
520cfd96c7 feat: 别名/屏蔽移到 .env
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 18:17:58 +08:00
8 changed files with 405 additions and 336 deletions

View File

@ -26,3 +26,11 @@ QQ_BOT_TARGET_ID=1234567890
NEXT_PUBLIC_VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:admin@example.com
# Window filters
# Format: keyword1,keyword2
IGNORED_WINDOW_KEYWORDS=NVIDIA Overlay,Twinkle Tray Panel
# Screen Time display name presets
# Format: match=displayName;match=displayName
PROCESS_DISPLAY_NAME_PRESETS=microsoft vs code\code.exe=Visual Studio Code;msedge.exe=Microsoft Edge;windowsTerminal.exe=终端;windowsterminal.exe=终端;blender.exe=Blender;chrome.exe=Google Chrome;steamwebhelper.exe=Steam;steam.exe=Steam;bandizip.exe=Bandizip;explorer.exe=文件资源管理器;devenv.exe=Visual Studio;idea64.exe=IntelliJ IDEA;pycharm64.exe=PyCharm;cursor.exe=Cursor;obsidian.exe=Obsidian;notion.exe=Notion;wechat.exe=微信;qq.exe=QQ;teams.exe=Microsoft Teams;telegram.exe=Telegram

View File

@ -13,11 +13,7 @@ import {
YAxis
} from 'recharts';
import {
CalendarDays,
Clock3,
Loader2,
Sparkles,
WandSparkles
} from 'lucide-react';
import { ScreenTimeResponse } from '../types';
import { formatDuration, getLocalDateInputValue } from '../utils';
@ -28,30 +24,27 @@ interface ScreenTimeOverviewChartProps {
loading: boolean;
refreshing: boolean;
onSelectDate: (date: string, nextUnit?: 'day' | 'week') => void;
onSpotlightAppClick: (processId: string) => void;
}
interface WeekChartDatum {
const SPOTLIGHT_COLORS = ['#2f7cff', '#17c3d1', '#ff9f43', '#ff6b6b'] as const;
const SPOTLIGHT_KEYS = ['spotlight_0', 'spotlight_1', 'spotlight_2', 'spotlight_3'] as const;
type SpotlightKey = typeof SPOTLIGHT_KEYS[number];
type WeekChartDatum = Record<SpotlightKey, number> & {
date: string;
label: string;
totalDurationSeconds: number;
mutedDuration: number;
selectedRemainder: number;
spotlight_0: number;
spotlight_1: number;
spotlight_2: number;
isSelected: boolean;
}
};
interface HourChartDatum {
type HourChartDatum = Record<SpotlightKey, number> & {
hour: number;
label: string;
totalDurationSeconds: number;
spotlight_0: number;
spotlight_1: number;
spotlight_2: number;
otherDurationSeconds: number;
}
};
interface TooltipPayloadRow<T> {
payload: T;
@ -66,11 +59,10 @@ interface RoundedBarShapeProps {
payload?: Record<string, unknown>;
}
const SPOTLIGHT_COLORS = ['#2f7cff', '#17c3d1', '#ff9f43'];
const OTHER_COLOR = '#d4d7df';
const MUTED_BAR_COLOR = '#cfd3dc';
const WEEK_STACK_KEYS = ['mutedDuration', 'spotlight_0', 'spotlight_1', 'spotlight_2', 'selectedRemainder'] as const;
const HOUR_STACK_KEYS = ['spotlight_0', 'spotlight_1', 'spotlight_2', 'otherDurationSeconds'] as const;
const WEEK_STACK_KEYS = ['mutedDuration', ...SPOTLIGHT_KEYS, 'selectedRemainder'] as const;
const HOUR_STACK_KEYS = [...SPOTLIGHT_KEYS, 'otherDurationSeconds'] as const;
const getNumericValue = (value: unknown) => {
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
@ -157,19 +149,25 @@ const formatUpdateTime = (value: string | null) => {
const OverviewSkeleton = () => {
return (
<div className="rounded-[2rem] border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="screen-time-skeleton h-6 w-36 rounded-full" />
<div className="mt-4 screen-time-skeleton h-14 w-56 rounded-2xl" />
<div className="mt-8 rounded-[1.75rem] bg-gray-50 p-5 dark:bg-gray-900/60">
<div className="flex h-40 items-end gap-3">
{[56, 88, 62, 76, 50, 68, 18].map((height, index) => (
<div key={index} className="flex flex-1 flex-col items-center gap-3">
<div className="screen-time-skeleton w-full rounded-t-2xl" style={{ height }} />
<div className="screen-time-skeleton h-4 w-4 rounded-full" />
</div>
<div className="rounded-[1.75rem] border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="screen-time-skeleton h-5 w-24 rounded-full" />
<div className="mt-2 screen-time-skeleton h-10 w-44 rounded-xl" />
</div>
<div className="grid w-full grid-cols-3 gap-2 sm:w-64">
{[0, 1, 2].map((index) => (
<div key={index} className="screen-time-skeleton h-12 rounded-2xl" />
))}
</div>
<div className="mt-8 flex h-32 items-end gap-2">
</div>
<div className="mt-4 grid gap-5 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.35fr)]">
<div className="flex h-36 items-end gap-2">
{[56, 88, 62, 76, 50, 68, 18].map((height, index) => (
<div key={index} className="screen-time-skeleton flex-1 rounded-t-2xl" style={{ height }} />
))}
</div>
<div className="flex h-36 items-end gap-1.5">
{Array.from({ length: 18 }).map((_, index) => (
<div
key={index}
@ -200,9 +198,6 @@ function WeekTooltip({
<div className="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
{formatDuration(datum.totalDurationSeconds)}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{datum.isSelected ? '当前选中日,点击其他日期可跳转' : '点击查看这一天'}
</div>
</div>
);
}
@ -219,10 +214,10 @@ function HourTooltip({
if (!active || !payload || payload.length === 0) return null;
const datum = payload[0].payload;
const rows = [0, 1, 2]
.map((index) => ({
label: spotlightNames[index],
value: datum[`spotlight_${index}` as keyof HourChartDatum] as number,
const rows: Array<{ label: string; value: number; color: string }> = SPOTLIGHT_KEYS
.map((key, index) => ({
label: spotlightNames[index] ?? '',
value: datum[key],
color: SPOTLIGHT_COLORS[index]
}))
.filter((row) => row.label && row.value > 0);
@ -262,7 +257,6 @@ export default function ScreenTimeOverviewChart({
loading,
refreshing,
onSelectDate,
onSpotlightAppClick
}: ScreenTimeOverviewChartProps) {
const weekChartData = useMemo(() => {
if (!data) return [] as WeekChartDatum[];
@ -270,32 +264,46 @@ export default function ScreenTimeOverviewChart({
const spotlightTotal = data.overview.selectedDay.spotlightApps
.reduce((sum, app) => sum + app.durationSeconds, 0);
return data.overview.week.days.map((day) => ({
date: day.date,
label: day.weekdayLabel,
totalDurationSeconds: day.totalDurationSeconds,
mutedDuration: day.isSelected ? 0 : day.totalDurationSeconds,
selectedRemainder: day.isSelected ? Math.max(day.totalDurationSeconds - spotlightTotal, 0) : 0,
spotlight_0: day.isSelected ? (data.overview.selectedDay.spotlightApps[0]?.durationSeconds ?? 0) : 0,
spotlight_1: day.isSelected ? (data.overview.selectedDay.spotlightApps[1]?.durationSeconds ?? 0) : 0,
spotlight_2: day.isSelected ? (data.overview.selectedDay.spotlightApps[2]?.durationSeconds ?? 0) : 0,
isSelected: day.isSelected
}));
return data.overview.week.days.map((day) => {
const spotlightDurations = Object.fromEntries(
SPOTLIGHT_KEYS.map((key, index) => [
key,
day.isSelected ? (data.overview.selectedDay.spotlightApps[index]?.durationSeconds ?? 0) : 0
])
) as Record<SpotlightKey, number>;
return {
date: day.date,
label: day.weekdayLabel,
totalDurationSeconds: day.totalDurationSeconds,
mutedDuration: day.isSelected ? 0 : day.totalDurationSeconds,
selectedRemainder: day.isSelected ? Math.max(day.totalDurationSeconds - spotlightTotal, 0) : 0,
...spotlightDurations,
isSelected: day.isSelected
};
});
}, [data]);
const hourChartData = useMemo(() => {
if (!data) return [] as HourChartDatum[];
const spotlightApps = data.overview.selectedDay.spotlightApps;
return data.overview.selectedDay.hours.map((hour) => ({
hour: hour.hour,
label: hour.label,
totalDurationSeconds: hour.totalDurationSeconds,
spotlight_0: spotlightApps[0] ? hour.spotlightDurations[spotlightApps[0].processId] ?? 0 : 0,
spotlight_1: spotlightApps[1] ? hour.spotlightDurations[spotlightApps[1].processId] ?? 0 : 0,
spotlight_2: spotlightApps[2] ? hour.spotlightDurations[spotlightApps[2].processId] ?? 0 : 0,
otherDurationSeconds: hour.otherDurationSeconds
}));
return data.overview.selectedDay.hours.map((hour) => {
const spotlightDurations = Object.fromEntries(
SPOTLIGHT_KEYS.map((key, index) => [
key,
spotlightApps[index] ? hour.spotlightDurations[spotlightApps[index].processId] ?? 0 : 0
])
) as Record<SpotlightKey, number>;
return {
hour: hour.hour,
label: hour.label,
totalDurationSeconds: hour.totalDurationSeconds,
...spotlightDurations,
otherDurationSeconds: hour.otherDurationSeconds
};
});
}, [data]);
const spotlightNames = data?.overview.selectedDay.spotlightApps.map((app) => app.processName) ?? [];
@ -303,6 +311,8 @@ export default function ScreenTimeOverviewChart({
if (!data) return 0;
return Math.max(...data.overview.week.days.map((day) => day.totalDurationSeconds), 0);
}, [data]);
const primaryLabel = data ? (unit === 'week' ? '本周' : formatSelectedDateHeadline(data.overview.selectedDay.date)) : '';
const selectedDayDuration = data?.overview.selectedDay.totalDurationSeconds ?? 0;
if (loading && !data) {
return <OverviewSkeleton />;
@ -311,225 +321,218 @@ export default function ScreenTimeOverviewChart({
if (!data) return null;
return (
<div className="relative overflow-hidden rounded-[2rem] border border-gray-200 bg-[radial-gradient(circle_at_top_left,rgba(59,130,246,0.10),transparent_28%),linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)] p-6 shadow-sm dark:border-gray-700 dark:bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_28%),linear-gradient(180deg,#111827_0%,#0f172a_100%)]">
<div className="relative overflow-hidden rounded-[1.75rem] border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800">
{refreshing && (
<div className="absolute inset-0 z-20 flex items-start justify-end bg-white/35 p-4 backdrop-blur-[2px] dark:bg-gray-950/25">
<div className="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1.5 text-xs font-medium text-gray-700 shadow-lg dark:bg-gray-900/90 dark:text-gray-200">
<div className="absolute inset-0 z-20 flex items-start justify-end bg-white/40 p-3 backdrop-blur-[2px] dark:bg-gray-950/25">
<div className="inline-flex items-center gap-2 rounded-full bg-white/95 px-3 py-1 text-xs font-medium text-gray-700 shadow-lg dark:bg-gray-900/95 dark:text-gray-200">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
</div>
</div>
)}
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<div className="flex items-center gap-2 text-sm font-medium text-slate-500 dark:text-slate-300">
<CalendarDays className="h-4 w-4" />
{formatSelectedDateHeadline(data.overview.selectedDay.date)}
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
<div className="text-sm font-medium text-gray-500 dark:text-gray-400">
{primaryLabel}
</div>
<div className="mt-2 text-4xl font-semibold tracking-tight text-gray-950 dark:text-white sm:text-5xl">
<div className="mt-1 text-4xl font-semibold text-gray-950 dark:text-white sm:text-5xl">
{formatDuration(data.totalDurationSeconds)}
</div>
<div className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{unit === 'week' ? '本周总屏幕时间' : '当天总屏幕时间'}
<span className="mx-2 text-gray-300 dark:text-gray-600"></span>
{formatDuration(data.overview.selectedDay.totalDurationSeconds)}
<div className="mt-1 text-xs text-gray-400 dark:text-gray-500">
{formatUpdateTime(data.lastRecordedAt)}
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3 lg:min-w-[22rem]">
<div className="rounded-2xl bg-white/80 px-4 py-3 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-900/70 dark:ring-white/10">
<div className="text-xs text-gray-500 dark:text-gray-400"></div>
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
<div className="grid grid-cols-3 gap-2 sm:min-w-[22rem]">
<div className="rounded-2xl bg-gray-50 px-3 py-2 dark:bg-gray-900/70">
<div className="text-[11px] text-gray-500 dark:text-gray-400"></div>
<div className="mt-0.5 truncate text-sm font-semibold text-gray-900 dark:text-white">
{formatDuration(data.overview.week.averageDurationSeconds)}
</div>
</div>
<div className="rounded-2xl bg-white/80 px-4 py-3 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-900/70 dark:ring-white/10">
<div className="text-xs text-gray-500 dark:text-gray-400"></div>
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
<div className="rounded-2xl bg-gray-50 px-3 py-2 dark:bg-gray-900/70">
<div className="text-[11px] text-gray-500 dark:text-gray-400"></div>
<div className="mt-0.5 truncate text-sm font-semibold text-gray-900 dark:text-white">
{formatDuration(selectedDayDuration)}
</div>
</div>
<div className="rounded-2xl bg-gray-50 px-3 py-2 dark:bg-gray-900/70">
<div className="text-[11px] text-gray-500 dark:text-gray-400"></div>
<div className="mt-0.5 text-sm font-semibold text-gray-900 dark:text-white">
{data.apps.length}
</div>
</div>
<div className="rounded-2xl bg-white/80 px-4 py-3 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-900/70 dark:ring-white/10">
<div className="text-xs text-gray-500 dark:text-gray-400"></div>
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
{formatDuration(data.estimatedSampleSeconds)}
</div>
</div>
</div>
</div>
<div className="mt-6 rounded-[1.75rem] border border-white/70 bg-white/70 p-5 shadow-inner shadow-white/40 backdrop-blur dark:border-gray-700/70 dark:bg-gray-900/60 dark:shadow-black/20">
<div className="flex items-center justify-between gap-4">
<div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-200"></div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
<div className="mt-4 grid gap-5 xl:grid-cols-[minmax(0,0.88fr)_minmax(0,1.35fr)]">
<section className="min-w-0">
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white"></h3>
<div className="truncate text-xs text-gray-500 dark:text-gray-400">
{formatDuration(data.overview.week.averageDurationSeconds)}
</div>
</div>
<div className="inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:bg-blue-500/10 dark:text-blue-300">
<Sparkles className="h-3.5 w-3.5" />
</div>
</div>
<div className="mt-4 h-48">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={weekChartData} barCategoryGap="20%">
<CartesianGrid vertical={false} strokeDasharray="4 4" stroke="#d9dde5" />
<XAxis axisLine={false} tickLine={false} dataKey="label" tick={{ fill: '#94a3b8', fontSize: 12 }} />
<YAxis
axisLine={false}
tickLine={false}
orientation="right"
tick={{ fill: '#b0b7c3', fontSize: 12 }}
tickFormatter={formatAxisDuration}
/>
<ReferenceLine y={data.overview.week.averageDurationSeconds} stroke="#94a3b8" strokeDasharray="4 6" />
<Tooltip content={<WeekTooltip />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
<div className="mt-2 h-36 sm:h-40">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={weekChartData} barCategoryGap="26%" margin={{ top: 8, right: 0, left: 0, bottom: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="3 5" stroke="#e0e4eb" />
<XAxis
axisLine={false}
tickLine={false}
dataKey="label"
height={22}
tick={{ fill: '#9ca3af', fontSize: 12 }}
/>
<YAxis
axisLine={false}
tickLine={false}
orientation="right"
width={34}
tick={{ fill: '#b0b7c3', fontSize: 12 }}
tickFormatter={formatAxisDuration}
/>
<ReferenceLine y={data.overview.week.averageDurationSeconds} stroke="#9ca3af" strokeDasharray="4 6" />
<Tooltip content={<WeekTooltip />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
<Bar dataKey="mutedDuration" stackId="week" shape={createRoundedTopShape('mutedDuration', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
const day = data.overview.week.days[index];
if (day) {
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
}
}}>
{weekChartData.map((entry) => (
<Cell
key={`muted-${entry.date}`}
cursor="pointer"
fill={entry.isSelected ? 'transparent' : MUTED_BAR_COLOR}
/>
))}
</Bar>
{[0, 1, 2].map((index) => (
<Bar
key={`spotlight-${index}`}
dataKey={`spotlight_${index}`}
stackId="week"
shape={createRoundedTopShape(`spotlight_${index}`, WEEK_STACK_KEYS, 12)}
onClick={(_, clickedIndex) => {
const day = data.overview.week.days[clickedIndex];
if (day) {
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
}
}}
>
<Bar dataKey="mutedDuration" stackId="week" shape={createRoundedTopShape('mutedDuration', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
const day = data.overview.week.days[index];
if (day) {
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
}
}}>
{weekChartData.map((entry) => (
<Cell
key={`spotlight-cell-${index}-${entry.date}`}
key={`muted-${entry.date}`}
cursor="pointer"
fill={entry.isSelected ? SPOTLIGHT_COLORS[index] : 'transparent'}
fill={entry.isSelected ? 'transparent' : MUTED_BAR_COLOR}
/>
))}
</Bar>
))}
<Bar dataKey="selectedRemainder" stackId="week" shape={createRoundedTopShape('selectedRemainder', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
const day = data.overview.week.days[index];
if (day) {
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
}
}}>
{weekChartData.map((entry) => (
<Cell
key={`remainder-${entry.date}`}
cursor="pointer"
fill={entry.isSelected ? OTHER_COLOR : 'transparent'}
{SPOTLIGHT_KEYS.map((key, index) => (
<Bar
key={key}
dataKey={key}
stackId="week"
shape={createRoundedTopShape(key, WEEK_STACK_KEYS, 12)}
onClick={(_, clickedIndex) => {
const day = data.overview.week.days[clickedIndex];
if (day) {
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
}
}}
>
{weekChartData.map((entry) => (
<Cell
key={`spotlight-cell-${key}-${entry.date}`}
cursor="pointer"
fill={entry.isSelected ? SPOTLIGHT_COLORS[index] : 'transparent'}
/>
))}
</Bar>
))}
<Bar dataKey="selectedRemainder" stackId="week" shape={createRoundedTopShape('selectedRemainder', WEEK_STACK_KEYS, 12)} onClick={(_, index) => {
const day = data.overview.week.days[index];
if (day) {
onSelectDate(day.date, unit === 'week' ? 'day' : undefined);
}
}}>
{weekChartData.map((entry) => (
<Cell
key={`remainder-${entry.date}`}
cursor="pointer"
fill={entry.isSelected ? OTHER_COLOR : 'transparent'}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<span> {formatDuration(data.overview.week.totalDurationSeconds)}</span>
<span> {formatDuration(maxWeekDuration)}</span>
</div>
</section>
<section className="min-w-0">
<div className="flex items-baseline justify-between gap-3">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white"></h3>
<div className="truncate text-xs text-gray-500 dark:text-gray-400">
{formatDuration(selectedDayDuration)}
</div>
</div>
<div className="mt-2 h-36 sm:h-40">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={hourChartData} barCategoryGap={2} margin={{ top: 8, right: 0, left: 0, bottom: 0 }}>
<CartesianGrid vertical={false} strokeDasharray="3 5" stroke="#e0e4eb" />
<XAxis
axisLine={false}
tickLine={false}
dataKey="label"
height={22}
interval={5}
tick={{ fill: '#9ca3af', fontSize: 12 }}
/>
<YAxis
axisLine={false}
tickLine={false}
orientation="right"
width={38}
tick={{ fill: '#b0b7c3', fontSize: 12 }}
tickFormatter={formatAxisDuration}
/>
<Tooltip content={<HourTooltip spotlightNames={spotlightNames} />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
{SPOTLIGHT_KEYS.map((key, index) => (
<Bar
key={`hour-${key}`}
dataKey={key}
stackId="hours"
fill={SPOTLIGHT_COLORS[index]}
shape={createRoundedTopShape(key, HOUR_STACK_KEYS, 10)}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<span> {formatDuration(data.overview.week.totalDurationSeconds)}</span>
<span> {formatDuration(maxWeekDuration)}</span>
</div>
<div className="mt-7 flex items-center justify-between gap-4">
<div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-200"></div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
24
</div>
</div>
<div className="inline-flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-600 dark:bg-gray-800 dark:text-gray-300">
<Clock3 className="h-3.5 w-3.5" />
{formatDuration(data.overview.selectedDay.totalDurationSeconds)}
</div>
</div>
<div className="mt-4 h-52">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={hourChartData} barCategoryGap={2}>
<CartesianGrid vertical={false} strokeDasharray="4 4" stroke="#d9dde5" />
<XAxis
axisLine={false}
tickLine={false}
dataKey="label"
interval={2}
tick={{ fill: '#94a3b8', fontSize: 12 }}
/>
<YAxis
axisLine={false}
tickLine={false}
orientation="right"
tick={{ fill: '#b0b7c3', fontSize: 12 }}
tickFormatter={formatAxisDuration}
/>
<Tooltip content={<HourTooltip spotlightNames={spotlightNames} />} cursor={{ fill: 'rgba(148, 163, 184, 0.08)' }} />
{[0, 1, 2].map((index) => (
<Bar
key={`hour-spotlight-${index}`}
dataKey={`spotlight_${index}`}
dataKey="otherDurationSeconds"
stackId="hours"
fill={SPOTLIGHT_COLORS[index]}
shape={createRoundedTopShape(`spotlight_${index}`, HOUR_STACK_KEYS, 10)}
fill={OTHER_COLOR}
shape={createRoundedTopShape('otherDurationSeconds', HOUR_STACK_KEYS, 10)}
/>
))}
<Bar
dataKey="otherDurationSeconds"
stackId="hours"
fill={OTHER_COLOR}
shape={createRoundedTopShape('otherDurationSeconds', HOUR_STACK_KEYS, 10)}
/>
</BarChart>
</ResponsiveContainer>
</div>
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-5 flex flex-wrap gap-3">
{data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => (
<button
key={app.processId}
onClick={() => onSpotlightAppClick(app.processId)}
className="flex min-w-36 flex-1 items-start gap-3 rounded-2xl bg-white/80 px-4 py-3 text-left shadow-sm ring-1 ring-black/5 transition-transform hover:-translate-y-0.5 dark:bg-gray-900/70 dark:ring-white/10"
>
<span className="mt-1 h-3 w-3 rounded-full" style={{ backgroundColor: SPOTLIGHT_COLORS[index] }} />
<span className="min-w-0 flex-1">
<span className="flex items-center gap-2 text-xs uppercase tracking-[0.18em] text-gray-400 dark:text-gray-500">
<WandSparkles className="h-3.5 w-3.5" />
Focus {index + 1}
<div className="mt-3 grid grid-cols-2 gap-2">
{data.overview.selectedDay.spotlightApps.length > 0 ? data.overview.selectedDay.spotlightApps.map((app, index) => (
<button
key={app.processId}
className="flex min-w-0 items-center gap-2 rounded-2xl bg-gray-50 px-3 py-2 text-left transition-colors hover:bg-gray-100 dark:bg-gray-900/70 dark:hover:bg-gray-900"
>
<span
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold text-white"
style={{ backgroundColor: SPOTLIGHT_COLORS[index] }}
>
{index + 1}
</span>
<span className="mt-1 block truncate text-sm font-semibold text-gray-900 dark:text-white" title={app.processName}>
{app.processName}
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-gray-900 dark:text-white" title={app.processName}>
{app.processName}
</span>
<span className="block truncate text-xs text-gray-500 dark:text-gray-400">
{formatDuration(app.durationSeconds)} · {app.percentage}%
</span>
</span>
<span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
{formatDuration(app.durationSeconds)} · {app.percentage}%
</span>
</span>
</button>
)) : (
<div className="rounded-2xl bg-gray-50 px-4 py-3 text-sm text-gray-500 dark:bg-gray-900/60 dark:text-gray-400">
</div>
)}
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<span>{formatUpdateTime(data.lastRecordedAt)}</span>
<span>Focus </span>
</button>
)) : (
<div className="col-span-2 rounded-2xl bg-gray-50 px-3 py-2 text-sm text-gray-500 dark:bg-gray-900/70 dark:text-gray-400">
</div>
)}
</div>
</section>
</div>
</div>
);

View File

@ -28,7 +28,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [expandedApps, setExpandedApps] = useState<Set<string>>(new Set());
const [highlightedProcessId, setHighlightedProcessId] = useState<string | null>(null);
const [initialLoading, setInitialLoading] = useState(true);
const abortControllerRef = React.useRef<AbortController | null>(null);
@ -125,29 +124,24 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
setSelectedDate(date);
};
const handleSpotlightAppClick = (processId: string) => {
setHighlightedProcessId(processId);
setExpandedApps((previous) => new Set(previous).add(processId));
};
return (
<div className="space-y-6">
<div className="rounded-[2rem] border border-gray-200 bg-[linear-gradient(180deg,#ffffff_0%,#f8fafc_100%)] p-5 shadow-sm dark:border-gray-700 dark:bg-[linear-gradient(180deg,#111827_0%,#0f172a_100%)]">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="text-sm font-medium text-blue-600 dark:text-blue-400">
Screen Time
<div className="space-y-4">
<div className="rounded-[1.75rem] border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="min-w-0">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400">
使
</div>
<h2 className="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
<h2 className="mt-0.5 truncate text-xl font-semibold text-gray-900 dark:text-white">
{periodLabel}
</h2>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<div className="inline-flex rounded-full bg-gray-100 p-1 dark:bg-gray-700/70">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="inline-flex rounded-full bg-gray-100 p-0.5 dark:bg-gray-700/70">
<button
onClick={() => setUnit('day')}
className={`rounded-full px-5 py-2 text-sm font-medium ${unit === 'day'
className={`rounded-full px-4 py-1.5 text-sm font-medium ${unit === 'day'
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-900 dark:text-blue-400'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
}`}
@ -156,7 +150,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
</button>
<button
onClick={() => setUnit('week')}
className={`rounded-full px-5 py-2 text-sm font-medium ${unit === 'week'
className={`rounded-full px-4 py-1.5 text-sm font-medium ${unit === 'week'
? 'bg-white text-blue-600 shadow-sm dark:bg-gray-900 dark:text-blue-400'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
}`}
@ -168,13 +162,13 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
<div className="flex items-center gap-2">
<button
onClick={() => shiftPeriod(-1)}
className="rounded-lg border border-gray-200 p-2 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
title={unit === 'week' ? '上一周' : '上一天'}
>
<ChevronLeft className="h-4 w-4" />
</button>
<label className="flex items-center gap-2 rounded-xl border border-gray-200 px-3 py-2 text-sm text-gray-700 dark:border-gray-600 dark:text-gray-200">
<label className="flex h-9 items-center gap-2 rounded-full border border-gray-200 px-3 text-sm text-gray-700 dark:border-gray-600 dark:text-gray-200">
<Calendar className="h-4 w-4 text-gray-400" />
<input
type="date"
@ -186,7 +180,7 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
<button
onClick={() => shiftPeriod(1)}
className="rounded-lg border border-gray-200 p-2 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 text-gray-600 hover:border-blue-500 hover:text-blue-600 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-400 dark:hover:text-blue-400"
title={unit === 'week' ? '下一周' : '下一天'}
>
<ChevronRight className="h-4 w-4" />
@ -202,7 +196,6 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
loading={loading || initialLoading}
refreshing={isRefreshing}
onSelectDate={handleOverviewDateSelect}
onSpotlightAppClick={handleSpotlightAppClick}
/>
{error && (
@ -211,14 +204,16 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
</div>
)}
<div className="relative overflow-hidden rounded-[2rem] border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="border-b border-gray-200 px-5 py-4 dark:border-gray-700">
<div className="relative overflow-hidden rounded-[1.75rem] border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="border-b border-gray-200 px-4 py-3 dark:border-gray-700">
<div className="flex items-center justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">使</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
5
</p>
<h3 className="text-base font-semibold text-gray-900 dark:text-white">使</h3>
{screenTime && (
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{screenTime.apps.length} · {formatDuration(screenTime.totalDurationSeconds)}
</div>
)}
</div>
{isRefreshing && (
@ -231,14 +226,14 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
</div>
{loading && !screenTime && (
<div className="flex items-center justify-center gap-3 px-6 py-12 text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center justify-center gap-3 px-6 py-8 text-sm text-gray-500 dark:text-gray-400">
<Loader2 className="h-5 w-5 animate-spin" />
使...
</div>
)}
{!loading && screenTime && screenTime.apps.length === 0 && !error && (
<div className="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400">
<div className="px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
</div>
)}
@ -248,56 +243,47 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
{screenTime.apps.map((app, index) => {
const isExpanded = expandedApps.has(app.processId);
const processWidth = maxDurationSeconds > 0
? Math.max((app.durationSeconds / maxDurationSeconds) * 100, 4)
? Math.max((app.durationSeconds / maxDurationSeconds) * 100, 2)
: 0;
return (
<div
key={app.processId}
className={`px-5 py-4 transition-colors ${highlightedProcessId === app.processId
? 'bg-blue-50/70 dark:bg-blue-500/8'
: ''
}`}
className={`px-4 py-3 transition-colors`}
>
<button
onClick={() => toggleExpanded(app.processId)}
className="w-full text-left"
>
<div className="flex items-start gap-4">
<div className="mt-1 text-sm font-semibold text-gray-400 dark:text-gray-500">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-2xl bg-gray-100 text-xs font-semibold text-gray-500 dark:bg-gray-700 dark:text-gray-300">
{String(index + 1).padStart(2, '0')}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="h-4 w-4 text-gray-400" />
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400" />
) : (
<ChevronRight className="h-4 w-4 text-gray-400" />
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-gray-400" />
)}
<div className="truncate text-base font-semibold text-gray-900 dark:text-white" title={app.processName}>
{app.processName}
</div>
{highlightedProcessId === app.processId && (
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-medium text-blue-700 dark:bg-blue-500/15 dark:text-blue-300">
Focus
</span>
)}
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-300">
<span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{app.titleCount}
</span>
</div>
<div className="mt-2 flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400">
<span>{formatDuration(app.durationSeconds)}</span>
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<span>{app.percentage}%</span>
<span className="h-1 w-1 rounded-full bg-gray-300 dark:bg-gray-600" />
<span className="truncate" title={app.processPath || '路径未知'}>
{app.processPath || '路径未知'}
</span>
</div>
<div className="mt-2 truncate text-xs text-gray-500 dark:text-gray-400" title={app.processPath || '路径未知'}>
: {app.processPath || '路径未知'}
</div>
<div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
<div
className="h-full rounded-full bg-blue-600 dark:bg-blue-500"
style={{ width: `${processWidth}%` }}
@ -312,17 +298,17 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
</button>
{isExpanded && (
<div className="ml-10 mt-4 rounded-2xl bg-gray-50 p-4 dark:bg-gray-900/50">
<div className="space-y-3">
<div className="ml-12 mt-3 rounded-2xl bg-gray-50 px-3 py-2.5 dark:bg-gray-900/50">
<div className="space-y-2.5">
{app.titles.map((title) => (
<div key={`${app.processId}-${title.title}`}>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-gray-800 dark:text-gray-100" title={title.title}>
{title.title}
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{title.percentage}%
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{title.percentage}%
</div>
</div>
@ -331,10 +317,10 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
</div>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white dark:bg-gray-800">
<div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-white dark:bg-gray-800">
<div
className="h-full rounded-full bg-emerald-500 dark:bg-emerald-400"
style={{ width: `${Math.max(title.percentage, 4)}%` }}
style={{ width: `${Math.max(title.percentage, 2)}%` }}
/>
</div>
</div>
@ -342,8 +328,8 @@ export default function ScreenTimeTab({ hostname }: ScreenTimeTabProps) {
</div>
{app.titleCount > app.titles.length && (
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
{app.titleCount - app.titles.length}
<div className="mt-3 text-xs text-gray-500 dark:text-gray-400">
{app.titleCount - app.titles.length}
</div>
)}
</div>

View File

@ -633,7 +633,7 @@ export default function ScreenshotsTab({
{
!(navigator as any).connection?.saveData &&
records.map(record => record.screenshots).flat()
.filter((_, index) => Math.abs(index - currentFrameIndex) <= 20)
.filter((_, index) => Math.abs(index - currentFrameIndex) <= 5)
.map(screenshot => <link rel="preload" key={screenshot.fileId} href={`/screenshots/${screenshot.fileId}`} as="image" />)
}
{/* 图片预览区域 */}

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, BarChart3, Star } from 'lucide-react';
import { ArrowLeft, BarChart3, Star, KeySquare, ChartNoAxesGantt } from 'lucide-react';
import ScreenshotsTab from './components/ScreenshotsTab';
import ScreenTimeTab from './components/ScreenTimeTab';
import StarredTab from './components/StarredTab';
@ -59,15 +59,15 @@ export default function HostDetail() {
{/* 选项卡导航 */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
<nav className="-mb-px flex space-x-6">
<button
onClick={() => setActiveTab('screenshots')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === 'screenshots'
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${activeTab === 'screenshots'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
线
><ChartNoAxesGantt className="h-4 w-4 mr-1"/>
线
</button>
<button
onClick={() => setActiveTab('screen-time')}
@ -77,7 +77,7 @@ export default function HostDetail() {
}`}
>
<BarChart3 className="h-4 w-4 mr-1" />
使
使
</button>
<button
onClick={() => setActiveTab('starred')}
@ -87,16 +87,17 @@ export default function HostDetail() {
}`}
>
<Star className="h-4 w-4 mr-1" />
</button>
<button
onClick={() => setActiveTab('credentials')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${activeTab === 'credentials'
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center ${activeTab === 'credentials'
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<KeySquare className="h-4 w-4 mr-1" />
</button>
</nav>
</div>

View File

@ -2,11 +2,13 @@ import { NextRequest, NextResponse } from 'next/server'
import { win32 } from 'path'
import { prisma } from '@/lib/prisma'
import { withCors } from '@/lib/middleware'
import { PROCESS_DISPLAY_NAME_PRESETS } from '@/lib/config'
import { Prisma } from '@prisma/client'
const MAX_GAP_MS = 5 * 60 * 1000
const DEFAULT_SAMPLE_MS = 30 * 1000
const MIN_SAMPLE_MS = 5 * 1000
const SPOTLIGHT_APP_LIMIT = 4
const LABEL_TOKEN_THRESHOLD_RATIO = 0.5
const HOUR_MS = 60 * 60 * 1000
const DAY_MS = 24 * HOUR_MS
@ -44,29 +46,6 @@ interface ScreenTimeRecordRow {
path: string | null
}
const PROCESS_DISPLAY_NAME_PRESETS = [
{ match: 'microsoft vs code\\code.exe', displayName: 'Visual Studio Code' },
{ match: 'msedge.exe', displayName: 'Microsoft Edge' },
{ match: 'windowsTerminal.exe', displayName: '终端' },
{ match: 'windowsterminal.exe', displayName: '终端' },
{ match: 'blender.exe', displayName: 'Blender' },
{ match: 'chrome.exe', displayName: 'Google Chrome' },
{ match: 'steamwebhelper.exe', displayName: 'Steam' },
{ match: 'steam.exe', displayName: 'Steam' },
{ match: 'bandizip.exe', displayName: 'Bandizip' },
{ match: 'explorer.exe', displayName: '文件资源管理器' },
{ match: 'devenv.exe', displayName: 'Visual Studio' },
{ match: 'idea64.exe', displayName: 'IntelliJ IDEA' },
{ match: 'pycharm64.exe', displayName: 'PyCharm' },
{ match: 'cursor.exe', displayName: 'Cursor' },
{ match: 'obsidian.exe', displayName: 'Obsidian' },
{ match: 'notion.exe', displayName: 'Notion' },
{ match: 'wechat.exe', displayName: '微信' },
{ match: 'qq.exe', displayName: 'QQ' },
{ match: 'teams.exe', displayName: 'Microsoft Teams' },
{ match: 'telegram.exe', displayName: 'Telegram' }
] as const
const cleanTitle = (title: string) => title.replace(/\s+/g, ' ').trim()
const trimLabelSeparators = (label: string) => label
@ -480,7 +459,7 @@ async function handleScreenTime(req: NextRequest) {
const aggregateSummary = buildAppList(aggregateUsageMap)
const selectedDaySummary = buildAppList(selectedDayUsageMap)
const spotlightApps = selectedDaySummary.apps.slice(0, 3).map(app => ({
const spotlightApps = selectedDaySummary.apps.slice(0, SPOTLIGHT_APP_LIMIT).map(app => ({
processId: app.processId,
processName: app.processName,
durationSeconds: app.durationSeconds,

View File

@ -1,3 +1,90 @@
export interface ProcessDisplayNamePreset {
match: string
displayName: string
}
const DEFAULT_IGNORED_WINDOW_KEYWORDS = [
'NVIDIA Overlay',
'Twinkle Tray Panel'
]
const DEFAULT_PROCESS_DISPLAY_NAME_PRESETS: ProcessDisplayNamePreset[] = [
{ match: 'microsoft vs code\\code.exe', displayName: 'Visual Studio Code' },
{ match: 'msedge.exe', displayName: 'Microsoft Edge' },
{ match: 'windowsTerminal.exe', displayName: '终端' },
{ match: 'windowsterminal.exe', displayName: '终端' },
{ match: 'blender.exe', displayName: 'Blender' },
{ match: 'chrome.exe', displayName: 'Google Chrome' },
{ match: 'steamwebhelper.exe', displayName: 'Steam' },
{ match: 'steam.exe', displayName: 'Steam' },
{ match: 'bandizip.exe', displayName: 'Bandizip' },
{ match: 'explorer.exe', displayName: '文件资源管理器' },
{ match: 'devenv.exe', displayName: 'Visual Studio' },
{ match: 'idea64.exe', displayName: 'IntelliJ IDEA' },
{ match: 'pycharm64.exe', displayName: 'PyCharm' },
{ match: 'cursor.exe', displayName: 'Cursor' },
{ match: 'obsidian.exe', displayName: 'Obsidian' },
{ match: 'notion.exe', displayName: 'Notion' },
{ match: 'wechat.exe', displayName: '微信' },
{ match: 'qq.exe', displayName: 'QQ' },
{ match: 'teams.exe', displayName: 'Microsoft Teams' },
{ match: 'telegram.exe', displayName: 'Telegram' }
]
const parseCommaSeparatedEnv = (value: string | undefined, fallback: string[]) => {
if (!value?.trim()) {
return fallback
}
const parsed = value
.split(',')
.map(item => item.trim())
.filter(Boolean)
return parsed.length > 0 ? parsed : fallback
}
const parseProcessDisplayNamePresetsEnv = (
value: string | undefined,
fallback: ProcessDisplayNamePreset[]
) => {
if (!value?.trim()) {
return fallback
}
const parsed = value
.split(';')
.map(item => item.trim())
.filter(Boolean)
.map((item) => {
const separatorIndex = item.indexOf('=')
if (separatorIndex <= 0 || separatorIndex === item.length - 1) {
return null
}
const match = item.slice(0, separatorIndex).trim()
const displayName = item.slice(separatorIndex + 1).trim()
if (!match || !displayName) {
return null
}
return { match, displayName }
})
.filter((item): item is ProcessDisplayNamePreset => item !== null)
return parsed.length > 0 ? parsed : fallback
}
export const IGNORED_WINDOW_KEYWORDS = parseCommaSeparatedEnv(
process.env.IGNORED_WINDOW_KEYWORDS,
DEFAULT_IGNORED_WINDOW_KEYWORDS
)
export const PROCESS_DISPLAY_NAME_PRESETS = parseProcessDisplayNamePresetsEnv(
process.env.PROCESS_DISPLAY_NAME_PRESETS,
DEFAULT_PROCESS_DISPLAY_NAME_PRESETS
)
export const config = {
port: process.env.PORT || 3000,
auth: {
@ -5,6 +92,12 @@ export const config = {
password: process.env.AUTH_PASSWORD || 'password',
secret: process.env.AUTH_SECRET || 'winupdate-neo-secret-key'
},
windows: {
ignoredKeywords: IGNORED_WINDOW_KEYWORDS
},
screenTime: {
processDisplayNamePresets: PROCESS_DISPLAY_NAME_PRESETS
},
// For file upload settings
upload: {
maxFileSize: 10 * 1024 * 1024, // 10MB

View File

@ -1,7 +1,6 @@
export const IGNORED_WINDOW_KEYWORDS = [
'NVIDIA Overlay',
'Twinkle Tray Panel'
] as const
import { IGNORED_WINDOW_KEYWORDS } from './config'
export { IGNORED_WINDOW_KEYWORDS }
const normalizedIgnoredWindowKeywords = IGNORED_WINDOW_KEYWORDS.map(keyword => keyword.toLowerCase())