mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add umami analytics integration
This commit is contained in:
@@ -456,6 +456,7 @@
|
|||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"advanced": "advanced",
|
"advanced": "advanced",
|
||||||
|
"analytics": "analytics",
|
||||||
"generalTab": "general",
|
"generalTab": "general",
|
||||||
"hotkeysTab": "hotkeys",
|
"hotkeysTab": "hotkeys",
|
||||||
"playbackTab": "playback",
|
"playbackTab": "playback",
|
||||||
@@ -565,6 +566,8 @@
|
|||||||
"albumBackground": "album background image",
|
"albumBackground": "album background image",
|
||||||
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
|
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
|
||||||
"albumBackgroundBlur": "album background image blur size",
|
"albumBackgroundBlur": "album background image blur size",
|
||||||
|
"analyticsDisable": "Opt-out of usage based analytics",
|
||||||
|
"analyticsDisable_description": "Anonymized usage data is sent to the developer to help improve the application",
|
||||||
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
|
||||||
"applicationHotkeys": "application hotkeys",
|
"applicationHotkeys": "application hotkeys",
|
||||||
"artistBackground": "artist background image",
|
"artistBackground": "artist background image",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const isAnalyticsDisabled = () => {
|
||||||
|
return localStorage.getItem('umami.disabled') === '1' || process.env.NODE_ENV === 'development';
|
||||||
|
};
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { mutationOptions, useMutation } from '@tanstack/react-query';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
import packageJson from '../../../../../package.json';
|
||||||
|
|
||||||
|
import { isAnalyticsDisabled } from '/@/renderer/features/analytics/hooks/use-analytics-disabled';
|
||||||
|
import {
|
||||||
|
BarAlign,
|
||||||
|
PlayerbarSliderType,
|
||||||
|
SideQueueType,
|
||||||
|
useAuthStore,
|
||||||
|
usePlayerStore,
|
||||||
|
useSettingsStore,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
|
import { ServerType } from '/@/shared/types/domain-types';
|
||||||
|
import {
|
||||||
|
FontType,
|
||||||
|
Platform,
|
||||||
|
PlayerQueueType,
|
||||||
|
PlayerStyle,
|
||||||
|
PlayerType,
|
||||||
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const utils = isElectron() ? window.api.utils : null;
|
||||||
|
|
||||||
|
const getVersion = (): AppTrackerProperties['_version'] => {
|
||||||
|
return packageJson.version;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlatform = (): AppTrackerProperties['_platform'] => {
|
||||||
|
if (!isElectron()) {
|
||||||
|
return Platform.WEB;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils?.isWindows()) {
|
||||||
|
return Platform.WINDOWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils?.isMacOS()) {
|
||||||
|
return Platform.MACOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utils?.isLinux()) {
|
||||||
|
return Platform.LINUX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppTrackerProperties = {
|
||||||
|
_platform: 'unknown' | Platform;
|
||||||
|
_version: string;
|
||||||
|
player: {
|
||||||
|
mediaSession: boolean;
|
||||||
|
playerQueueType: PlayerQueueType;
|
||||||
|
playerStyle: PlayerStyle;
|
||||||
|
playerType: PlayerType;
|
||||||
|
transcoding: boolean;
|
||||||
|
webAudio: boolean;
|
||||||
|
};
|
||||||
|
server: {
|
||||||
|
[ServerType.JELLYFIN]: number;
|
||||||
|
[ServerType.NAVIDROME]: number;
|
||||||
|
[ServerType.SUBSONIC]: number;
|
||||||
|
};
|
||||||
|
settings: {
|
||||||
|
albumBackground: boolean;
|
||||||
|
albumBackgroundBlur: number;
|
||||||
|
artistBackground: boolean;
|
||||||
|
artistBackgroundBlur: number;
|
||||||
|
customCss: boolean;
|
||||||
|
disableAutoUpdate: boolean;
|
||||||
|
discord: boolean;
|
||||||
|
exitToTray: boolean;
|
||||||
|
fontType: FontType;
|
||||||
|
globalHotkeys: boolean;
|
||||||
|
homeFeature: boolean;
|
||||||
|
language: string;
|
||||||
|
lastFM: boolean;
|
||||||
|
lyricsEnableAutoTranslation: boolean;
|
||||||
|
lyricsEnableNeteaseTranslation: boolean;
|
||||||
|
lyricsFetch: boolean;
|
||||||
|
minimizeToTray: boolean;
|
||||||
|
musicBrainz: boolean;
|
||||||
|
nativeAspectRatio: boolean;
|
||||||
|
playerbarSliderType: PlayerbarSliderType;
|
||||||
|
playerbarWaveformAlign: BarAlign;
|
||||||
|
playerbarWaveformBarWidth: number;
|
||||||
|
playerbarWaveformGap: number;
|
||||||
|
playerbarWaveformRadius: number;
|
||||||
|
preventSleepOnPlayback: boolean;
|
||||||
|
releaseChannel: string;
|
||||||
|
resume: boolean;
|
||||||
|
scrobbleEnabled: boolean;
|
||||||
|
sideQueueType: SideQueueType;
|
||||||
|
skipBackwardSeconds: number;
|
||||||
|
skipButtons: boolean;
|
||||||
|
skipForwardSeconds: number;
|
||||||
|
startMinimized: boolean;
|
||||||
|
theme: string;
|
||||||
|
tray: boolean;
|
||||||
|
windowBarStyle: Platform;
|
||||||
|
zoomFactor: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlayerProperties = (): AppTrackerProperties['player'] => {
|
||||||
|
const player = usePlayerStore.getState();
|
||||||
|
const playbackSettings = useSettingsStore.getState().playback;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mediaSession: playbackSettings.mediaSession,
|
||||||
|
playerQueueType: player.player.queueType,
|
||||||
|
playerStyle: player.player.transitionType,
|
||||||
|
playerType: playbackSettings.type,
|
||||||
|
transcoding: playbackSettings.transcode.enabled,
|
||||||
|
webAudio: playbackSettings.webAudio,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSettingsProperties = (): AppTrackerProperties['settings'] => {
|
||||||
|
const settings = useSettingsStore.getState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
albumBackground: settings.general.albumBackground,
|
||||||
|
albumBackgroundBlur: settings.general.albumBackgroundBlur,
|
||||||
|
artistBackground: settings.general.artistBackground,
|
||||||
|
artistBackgroundBlur: settings.general.artistBackgroundBlur,
|
||||||
|
customCss: settings.css.enabled,
|
||||||
|
disableAutoUpdate: settings.window.disableAutoUpdate,
|
||||||
|
discord: settings.discord.enabled,
|
||||||
|
exitToTray: settings.window.exitToTray,
|
||||||
|
fontType: settings.font.type,
|
||||||
|
globalHotkeys: settings.hotkeys.globalMediaHotkeys,
|
||||||
|
homeFeature: settings.general.homeFeature,
|
||||||
|
language: settings.general.language,
|
||||||
|
lastFM: settings.general.lastFM,
|
||||||
|
lyricsEnableAutoTranslation: settings.lyrics.enableAutoTranslation,
|
||||||
|
lyricsEnableNeteaseTranslation: settings.lyrics.enableNeteaseTranslation,
|
||||||
|
lyricsFetch: settings.lyrics.fetch,
|
||||||
|
minimizeToTray: settings.window.minimizeToTray,
|
||||||
|
musicBrainz: settings.general.musicBrainz,
|
||||||
|
nativeAspectRatio: settings.general.nativeAspectRatio,
|
||||||
|
playerbarSliderType: settings.general.playerbarSlider.type as PlayerbarSliderType,
|
||||||
|
playerbarWaveformAlign: settings.general.playerbarSlider.barAlign as BarAlign,
|
||||||
|
playerbarWaveformBarWidth: settings.general.playerbarSlider.barWidth,
|
||||||
|
playerbarWaveformGap: settings.general.playerbarSlider.barGap,
|
||||||
|
playerbarWaveformRadius: settings.general.playerbarSlider.barRadius,
|
||||||
|
preventSleepOnPlayback: settings.window.preventSleepOnPlayback,
|
||||||
|
releaseChannel: settings.window.releaseChannel,
|
||||||
|
resume: settings.general.resume,
|
||||||
|
scrobbleEnabled: settings.playback.scrobble.enabled,
|
||||||
|
sideQueueType: settings.general.sideQueueType,
|
||||||
|
skipBackwardSeconds: settings.general.skipButtons.skipBackwardSeconds,
|
||||||
|
skipButtons: settings.general.skipButtons.enabled,
|
||||||
|
skipForwardSeconds: settings.general.skipButtons.skipForwardSeconds,
|
||||||
|
startMinimized: settings.window.startMinimized,
|
||||||
|
theme: settings.general.theme,
|
||||||
|
tray: settings.window.tray,
|
||||||
|
windowBarStyle: settings.window.windowBarStyle,
|
||||||
|
zoomFactor: settings.general.zoomFactor,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerProperties = (): AppTrackerProperties['server'] => {
|
||||||
|
const auth = useAuthStore.getState();
|
||||||
|
const serverList = auth.serverList;
|
||||||
|
|
||||||
|
return Object.entries(serverList).reduce(
|
||||||
|
(acc, [, server]) => {
|
||||||
|
if (server.type === ServerType.JELLYFIN) {
|
||||||
|
acc[ServerType.JELLYFIN] += 1;
|
||||||
|
} else if (server.type === ServerType.NAVIDROME) {
|
||||||
|
acc[ServerType.NAVIDROME] += 1;
|
||||||
|
} else if (server.type === ServerType.SUBSONIC) {
|
||||||
|
acc[ServerType.SUBSONIC] += 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[ServerType.JELLYFIN]: 0,
|
||||||
|
[ServerType.NAVIDROME]: 0,
|
||||||
|
[ServerType.SUBSONIC]: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppTracker = () => {
|
||||||
|
const { mutate: trackAppMutation } = useMutation(appTrackerMutation);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.umami || isAnalyticsDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProperties = () => {
|
||||||
|
const platform = getPlatform();
|
||||||
|
const version = getVersion();
|
||||||
|
const playerProperties = getPlayerProperties();
|
||||||
|
const serverProperties = getServerProperties();
|
||||||
|
const settingsProperties = getSettingsProperties();
|
||||||
|
|
||||||
|
const properties: AppTrackerProperties = {
|
||||||
|
_platform: platform,
|
||||||
|
_version: version,
|
||||||
|
player: playerProperties,
|
||||||
|
server: serverProperties,
|
||||||
|
settings: settingsProperties,
|
||||||
|
};
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkAndTrack = () => {
|
||||||
|
const lastSentDate = localStorage.getItem('analytics_app_tracker_timestamp');
|
||||||
|
const todayPST = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
// Only send if it's a new day in PST
|
||||||
|
if (lastSentDate !== todayPST) {
|
||||||
|
const properties = getProperties();
|
||||||
|
|
||||||
|
trackAppMutation(properties, {
|
||||||
|
onSettled: () => {
|
||||||
|
logFn.debug(logMsg[LogCategory.ANALYTICS].appTracked, {
|
||||||
|
category: LogCategory.ANALYTICS,
|
||||||
|
meta: { properties },
|
||||||
|
});
|
||||||
|
|
||||||
|
const pstDate = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD');
|
||||||
|
localStorage.setItem('analytics_app_tracker_timestamp', pstDate);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check immediately on mount
|
||||||
|
checkAndTrack();
|
||||||
|
|
||||||
|
// Then check every hour
|
||||||
|
const interval = setInterval(checkAndTrack, 1000 * 60 * 60);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [trackAppMutation]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const appTrackerMutation = mutationOptions({
|
||||||
|
mutationFn: (properties: AppTrackerProperties) => {
|
||||||
|
try {
|
||||||
|
window.umami?.track((props) => ({
|
||||||
|
...props,
|
||||||
|
data: properties,
|
||||||
|
name: 'app',
|
||||||
|
}));
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutationKey: ['analytics', 'settings-tracker'],
|
||||||
|
onSuccess: () => {},
|
||||||
|
retry: false,
|
||||||
|
throwOnError: false,
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { mutationOptions, useMutation } from '@tanstack/react-query';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
|
import { isAnalyticsDisabled } from '/@/renderer/features/analytics/hooks/use-analytics-disabled';
|
||||||
|
import { getRoutePattern } from '/@/renderer/features/analytics/utils/get-route-pattern';
|
||||||
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
|
|
||||||
|
const trackPageView = (routePattern: string) => {
|
||||||
|
window.umami?.track((props) => ({
|
||||||
|
language: props.language,
|
||||||
|
url: routePattern,
|
||||||
|
website: props.website,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePageTracker = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const routePattern = useMemo(() => getRoutePattern(location.pathname), [location.pathname]);
|
||||||
|
|
||||||
|
const { mutate: trackPageViewMutation } = useMutation(pageTrackerMutation);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.umami || isAnalyticsDisabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackPageViewMutation(routePattern, {
|
||||||
|
onSettled: () => {
|
||||||
|
logFn.debug(logMsg[LogCategory.ANALYTICS].pageViewTracked, {
|
||||||
|
category: LogCategory.ANALYTICS,
|
||||||
|
meta: { route: routePattern },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [routePattern, trackPageViewMutation]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageTrackerMutation = mutationOptions({
|
||||||
|
gcTime: 0,
|
||||||
|
mutationFn: (routePattern: string) => {
|
||||||
|
try {
|
||||||
|
trackPageView(routePattern);
|
||||||
|
return Promise.resolve();
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutationKey: ['analytics', 'page-tracker'],
|
||||||
|
|
||||||
|
retry: false,
|
||||||
|
throwOnError: false,
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { matchPath } from 'react-router';
|
||||||
|
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
|
||||||
|
export const getRoutePattern = (pathname: string): string => {
|
||||||
|
const routePatterns = Object.values(AppRoute);
|
||||||
|
|
||||||
|
const sortedRoutes = routePatterns.sort((a, b) => b.split('/').length - a.split('/').length);
|
||||||
|
|
||||||
|
for (const pattern of sortedRoutes) {
|
||||||
|
const match = matchPath(
|
||||||
|
{
|
||||||
|
caseSensitive: false,
|
||||||
|
end: true,
|
||||||
|
path: pattern,
|
||||||
|
},
|
||||||
|
pathname,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to the default route if no pattern matches
|
||||||
|
return '/';
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Fragment } from 'react/jsx-runtime';
|
import { Fragment } from 'react/jsx-runtime';
|
||||||
|
|
||||||
|
import { AnalyticsSettings } from '/@/renderer/features/settings/components/advanced/analytics-settings';
|
||||||
import { ExportImportSettings } from '/@/renderer/features/settings/components/advanced/export-import-settings';
|
import { ExportImportSettings } from '/@/renderer/features/settings/components/advanced/export-import-settings';
|
||||||
import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs';
|
import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs';
|
||||||
import { UpdateSettings } from '/@/renderer/features/settings/components/window/update-settings';
|
import { UpdateSettings } from '/@/renderer/features/settings/components/window/update-settings';
|
||||||
@@ -8,6 +9,7 @@ import { Stack } from '/@/shared/components/stack/stack';
|
|||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ component: UpdateSettings, key: 'update' },
|
{ component: UpdateSettings, key: 'update' },
|
||||||
|
{ component: AnalyticsSettings, key: 'analytics' },
|
||||||
{ component: ExportImportSettings, key: 'export-import' },
|
{ component: ExportImportSettings, key: 'export-import' },
|
||||||
{ component: CacheSettings, key: 'cache' },
|
{ component: CacheSettings, key: 'cache' },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingOption,
|
||||||
|
SettingsSection,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
|
|
||||||
|
export const AnalyticsSettings = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleToggleAnalytics = (disable: boolean) => {
|
||||||
|
if (disable) {
|
||||||
|
localStorage.setItem('umami.disabled', '1');
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('umami.disabled');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const analyticsOptions: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={localStorage.getItem('umami.disabled') === '1'}
|
||||||
|
onChange={(e) => handleToggleAnalytics(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.analyticsDisable_description', { postProcess: 'sentenceCase' }),
|
||||||
|
title: t('setting.analyticsDisable', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsSection
|
||||||
|
options={analyticsOptions}
|
||||||
|
title={t('page.setting.analytics', { postProcess: 'sentenceCase' })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
umami?: {
|
||||||
|
identify(unique_id: string): void;
|
||||||
|
identify(unique_id: string, data: object): void;
|
||||||
|
identify(data: object): void;
|
||||||
|
track(event_name: string, data: object): void;
|
||||||
|
track(
|
||||||
|
callback: (props: {
|
||||||
|
hostname: string;
|
||||||
|
language: string;
|
||||||
|
referrer: string;
|
||||||
|
screen: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
website: string;
|
||||||
|
}) => object,
|
||||||
|
): void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
+21
-16
@@ -1,22 +1,27 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="utf-8" />
|
||||||
<meta charset="utf-8" />
|
<meta http-equiv="Content-Security-Policy" />
|
||||||
<meta http-equiv="Content-Security-Policy" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<script
|
||||||
<title>Feishin</title>
|
defer
|
||||||
<% if (web) { %>
|
src="https://umami.jeffvli.org/script.js"
|
||||||
<link rel="icon" href="./assets/favicon.ico">
|
data-website-id="5120fc56-cffa-4d42-8b6c-9afb6f459251"
|
||||||
|
data-exclude-search="true"
|
||||||
|
data-exclude-hash="true"
|
||||||
|
data-auto-track="false"
|
||||||
|
></script>
|
||||||
|
<title>Feishin</title>
|
||||||
|
<% if (web) { %>
|
||||||
|
<link rel="icon" href="./assets/favicon.ico" />
|
||||||
<script src="settings.js"></script>
|
<script src="settings.js"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="background-color: #000;">
|
|
||||||
<div id="root">
|
|
||||||
<script type="module" src="main.tsx"></script>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<body style="background-color: #000">
|
||||||
|
<div id="root">
|
||||||
|
<script type="module" src="main.tsx"></script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -144,9 +144,15 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Suspense fallback={<Spinner container />}>
|
<MainContentBody />
|
||||||
<Outlet />
|
|
||||||
</Suspense>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function MainContentBody() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Spinner container />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { useAppTracker } from '/@/renderer/features/analytics/hooks/use-app-tracker';
|
||||||
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
|
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
|
||||||
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
import { useIsMobile } from '/@/renderer/hooks/use-is-mobile';
|
||||||
import { DefaultLayout } from '/@/renderer/layouts/default-layout';
|
import { DefaultLayout } from '/@/renderer/layouts/default-layout';
|
||||||
@@ -29,6 +30,8 @@ const ResponsiveLayoutBase = ({ shell }: ResponsiveLayoutProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => {
|
export const ResponsiveLayout = ({ shell }: ResponsiveLayoutProps) => {
|
||||||
|
useAppTracker();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResponsiveLayoutBase shell={shell} />
|
<ResponsiveLayoutBase shell={shell} />
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { LogCategory } from '/@/renderer/utils/logger';
|
||||||
|
|
||||||
|
export const logMsg = {
|
||||||
|
[LogCategory.ANALYTICS]: {
|
||||||
|
appTracked: 'App tracked',
|
||||||
|
pageViewTracked: 'Page view tracked',
|
||||||
|
},
|
||||||
|
[LogCategory.API]: {},
|
||||||
|
[LogCategory.OTHER]: {},
|
||||||
|
[LogCategory.PLAYER]: {},
|
||||||
|
[LogCategory.SYSTEM]: {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export enum LogCategory {
|
||||||
|
ANALYTICS = 'analytics',
|
||||||
|
API = 'api',
|
||||||
|
OTHER = 'other',
|
||||||
|
PLAYER = 'player',
|
||||||
|
SYSTEM = 'system',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogLevel = 'debug' | 'error' | 'info' | 'warn';
|
||||||
|
|
||||||
|
interface LogFn {
|
||||||
|
(
|
||||||
|
message?: string,
|
||||||
|
options?: {
|
||||||
|
category?: string;
|
||||||
|
meta?: any;
|
||||||
|
},
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Logger {
|
||||||
|
debug: LogFn;
|
||||||
|
error: LogFn;
|
||||||
|
info: LogFn;
|
||||||
|
warn: LogFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LOG_LEVEL = process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const NO_OP: LogFn = (_message?: string, ..._optionalParams: any[]) => {};
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
debug: '\x1B[38;2;54;96;146m', // #366092
|
||||||
|
error: '\x1B[38;2;240;0;0m', // #f00000
|
||||||
|
info: '\x1B[38;2;0;125;60m', // #007d3c
|
||||||
|
warn: '\x1B[38;2;225;125;50m', // #e17d32
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounce configuration
|
||||||
|
const DEBOUNCE_INTERVAL = 200; // milliseconds
|
||||||
|
const DEBOUNCE_MAP = new Map<string, { count: number; lastLog: number }>();
|
||||||
|
|
||||||
|
// Periodically flush the debounce map
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, value] of DEBOUNCE_MAP.entries()) {
|
||||||
|
if (now - value.lastLog >= DEBOUNCE_INTERVAL) {
|
||||||
|
const [level, message, category, meta] = JSON.parse(key);
|
||||||
|
const timestampStr = `${dayjs().format('HH:mm:ss')}`;
|
||||||
|
const levelStr = `${colors[level as keyof typeof colors]}[${String(level).toUpperCase().padEnd(5, ' ')}]\x1B[0m`;
|
||||||
|
const countStr = value.count > 1 ? ` (x${value.count})` : '';
|
||||||
|
const categoryStr = category
|
||||||
|
? String(`[${category.padEnd(9, ' ')}]`).toUpperCase()
|
||||||
|
: '';
|
||||||
|
const messageStr = message ? String(message) : '';
|
||||||
|
const logStr = `[${timestampStr}] ${levelStr} ${categoryStr} ${messageStr}${countStr}`;
|
||||||
|
|
||||||
|
if (meta) {
|
||||||
|
console.log(logStr, meta);
|
||||||
|
} else {
|
||||||
|
console.log(logStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBOUNCE_MAP.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, DEBOUNCE_INTERVAL);
|
||||||
|
|
||||||
|
class ConsoleLogger implements Logger {
|
||||||
|
readonly debug: LogFn;
|
||||||
|
readonly error: LogFn;
|
||||||
|
readonly info: LogFn;
|
||||||
|
readonly warn: LogFn;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const level = localStorage.getItem('log_level') || DEFAULT_LOG_LEVEL;
|
||||||
|
|
||||||
|
// Create timestamp wrapper function with colors and debouncing
|
||||||
|
const withTimestamp = (logLevel: string): LogFn => {
|
||||||
|
return (message?: any, options?: { category?: string; meta?: any }) => {
|
||||||
|
const { category, meta } = options || {};
|
||||||
|
const key = JSON.stringify([logLevel, message, category, meta]);
|
||||||
|
const now = Date.now();
|
||||||
|
const existing = DEBOUNCE_MAP.get(key);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
existing.count++;
|
||||||
|
existing.lastLog = now;
|
||||||
|
} else {
|
||||||
|
DEBOUNCE_MAP.set(key, { count: 1, lastLog: now });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
this.error = withTimestamp('error');
|
||||||
|
|
||||||
|
if (level === 'error') {
|
||||||
|
this.warn = NO_OP;
|
||||||
|
this.info = NO_OP;
|
||||||
|
this.debug = NO_OP;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.warn = withTimestamp('warn');
|
||||||
|
|
||||||
|
if (level === 'warn') {
|
||||||
|
this.info = NO_OP;
|
||||||
|
this.debug = NO_OP;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.info = withTimestamp('info');
|
||||||
|
|
||||||
|
if (level === 'info') {
|
||||||
|
this.debug = NO_OP;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debug = withTimestamp('debug');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logFn = new ConsoleLogger();
|
||||||
Reference in New Issue
Block a user