refactor(logging): route frontend console calls through the logger
TRACES: | DR-204 484 ungated `console.*` calls across 63 non-test frontend files shipped to end users with no way to turn them off. Mechanical substitution, no control flow, error handling or message semantics changed: console.log / console.debug -> log.debug console.info -> log.info console.warn -> log.warn console.error -> log.error Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope now carries them; scope names that already existed are preserved verbatim (`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename where a file had none. `src/routes/player/[id]/+page.svelte` keeps its `NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than flattening them into the page scope. `grep -rn 'console\.' src/` now matches nothing outside the tests and the facade itself.
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
import { downloads } from "$lib/stores/downloads";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("AlbumDownloadButton");
|
||||
|
||||
interface Props {
|
||||
albumId: string;
|
||||
@@ -60,7 +63,7 @@
|
||||
try {
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
log.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,7 +98,7 @@
|
||||
await downloads.refresh(userId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Album download operation failed:", error);
|
||||
log.error("Album download operation failed:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("ArtistDetailView");
|
||||
|
||||
interface Props {
|
||||
artist: MediaItem;
|
||||
@@ -47,7 +50,7 @@
|
||||
});
|
||||
albums = albumsResult.items.filter(item => item.kind === "album");
|
||||
} catch (e) {
|
||||
console.warn("Failed to load albums:", e);
|
||||
log.warn("Failed to load albums:", e);
|
||||
} finally {
|
||||
albumsLoading = false;
|
||||
}
|
||||
@@ -62,7 +65,7 @@
|
||||
});
|
||||
topTracks = tracksResult.items.filter(item => item.kind === "track");
|
||||
} catch (e) {
|
||||
console.warn("Failed to load tracks:", e);
|
||||
log.warn("Failed to load tracks:", e);
|
||||
} finally {
|
||||
tracksLoading = false;
|
||||
}
|
||||
@@ -82,14 +85,14 @@
|
||||
.slice(0, 6);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load related artists:", e);
|
||||
log.warn("Failed to load related artists:", e);
|
||||
} finally {
|
||||
artistsLoading = false;
|
||||
}
|
||||
|
||||
singlesLoading = false;
|
||||
} catch (e) {
|
||||
console.error("Error loading artist content:", e);
|
||||
log.error("Error loading artist content:", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<script lang="ts">
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { isServerReachable } from "$lib/stores/connectivity";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("ClearHistoryButton");
|
||||
|
||||
interface Props {
|
||||
/** Series or season id to clear. */
|
||||
@@ -51,7 +54,7 @@
|
||||
await auth.getRepository().clearWatchHistory(itemId);
|
||||
onCleared?.();
|
||||
} catch (e) {
|
||||
console.error("Failed to clear watch history:", e);
|
||||
log.error("Failed to clear watch history:", e);
|
||||
alert(
|
||||
`Could not clear watch history: ${e instanceof Error ? e.message : String(e)}`
|
||||
);
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import DownloadButtonCore from "./DownloadButtonCore.svelte";
|
||||
import type { DownloadState } from "./DownloadButtonCore.svelte";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("DownloadButton");
|
||||
|
||||
/**
|
||||
* Single audio track download button
|
||||
@@ -39,7 +42,7 @@
|
||||
});
|
||||
|
||||
async function handleClick() {
|
||||
console.log("🖱️ Download button clicked! Current status:", status);
|
||||
log.debug("🖱️ Download button clicked! Current status:", status);
|
||||
if (isProcessing) return;
|
||||
|
||||
isProcessing = true;
|
||||
@@ -63,25 +66,25 @@
|
||||
// Start download
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
log.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
|
||||
console.log("🎯 Starting download for item:", itemId);
|
||||
log.debug("🎯 Starting download for item:", itemId);
|
||||
|
||||
// Get stream URL
|
||||
const streamUrl = await repo.getAudioStreamUrl(itemId);
|
||||
console.log(" Stream URL obtained:", streamUrl?.substring(0, 50) + "...");
|
||||
log.debug(" Stream URL obtained:", streamUrl?.substring(0, 50) + "...");
|
||||
if (!streamUrl) {
|
||||
throw new Error("Failed to get stream URL");
|
||||
}
|
||||
|
||||
// Get target directory
|
||||
const targetDir = await commands.storageGetPath();
|
||||
console.log(" Target directory:", targetDir);
|
||||
log.debug(" Target directory:", targetDir);
|
||||
|
||||
// Queue and start download in single atomic operation
|
||||
const downloadId = await commands.downloadItemAndStart({
|
||||
@@ -93,16 +96,16 @@
|
||||
artistName: artistName || null,
|
||||
albumName: albumName || null,
|
||||
});
|
||||
console.log(" Download queued and started with ID:", downloadId);
|
||||
log.debug(" Download queued and started with ID:", downloadId);
|
||||
|
||||
// Refresh downloads list
|
||||
await downloads.refresh(userId);
|
||||
} catch (e) {
|
||||
console.error("❌ Failed to start download:", e);
|
||||
log.error("❌ Failed to start download:", e);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Download operation failed:", error);
|
||||
log.error("Download operation failed:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
|
||||
import { useOfflineFilterReload } from "$lib/composables/useOfflineFilterReload";
|
||||
import type { Genre, MediaItem, ItemType } from "$lib/api/types";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("GenericGenreBrowser");
|
||||
|
||||
/**
|
||||
* Generic genre browser supporting Movies, Music Albums, and TV Series
|
||||
@@ -92,7 +95,7 @@
|
||||
genres = result.sort((a, b) => a.name.localeCompare(b.name));
|
||||
applyFilter();
|
||||
} catch (e) {
|
||||
console.error("Failed to load genres:", e);
|
||||
log.error("Failed to load genres:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -115,7 +118,7 @@
|
||||
});
|
||||
genreItems = result.items;
|
||||
} catch (e) {
|
||||
console.error("Failed to load genre items:", e);
|
||||
log.error("Failed to load genre items:", e);
|
||||
} finally {
|
||||
loadingItems = false;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
import TrackList from "./TrackList.svelte";
|
||||
import AlphabetScrollBar from "./AlphabetScrollBar.svelte";
|
||||
import { excludePodcasts } from "$lib/utils/podcastFilter";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("GenericMediaListPage");
|
||||
|
||||
/**
|
||||
* Generic media list page supporting Albums, Artists, Playlists, and Tracks
|
||||
@@ -154,7 +157,7 @@
|
||||
items = excludePodcasts(result.items);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to load ${config.itemType}:`, e);
|
||||
log.error(`Failed to load ${config.itemType}:`, e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||
import { favoriteOverrides, resolveIsFavorite } from "$lib/stores/favorites";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("MediaCard");
|
||||
|
||||
interface Props {
|
||||
item: MediaItem | Library;
|
||||
@@ -171,7 +174,7 @@
|
||||
media.albumName ?? undefined
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("[MediaCard] Failed to queue download:", err);
|
||||
log.error("Failed to queue download:", err);
|
||||
queueError = "Failed to queue";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import LibraryGrid from "./LibraryGrid.svelte";
|
||||
import CachedImage from "$lib/components/common/CachedImage.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("PersonDetailView");
|
||||
|
||||
interface Props {
|
||||
person: MediaItem;
|
||||
@@ -34,7 +37,7 @@
|
||||
movies = result.items.filter(item => item.kind === "movie");
|
||||
series = result.items.filter(item => item.kind === "series");
|
||||
} catch (e) {
|
||||
console.error("Failed to load filmography:", e);
|
||||
log.error("Failed to load filmography:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
import FavoriteButton from "$lib/components/FavoriteButton.svelte";
|
||||
import { favoriteOverrides } from "$lib/stores/favorites";
|
||||
import { formatDuration } from "$lib/utils/duration";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("PlaylistDetail");
|
||||
|
||||
interface Props {
|
||||
playlist: MediaItem;
|
||||
@@ -40,7 +43,7 @@
|
||||
const repo = auth.getRepository();
|
||||
entries = await repo.getPlaylistItems(playlist.id);
|
||||
} catch (e) {
|
||||
console.error("[PlaylistDetail] Failed to load items:", e);
|
||||
log.error("Failed to load items:", e);
|
||||
toast.error("Failed to load playlist items");
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -62,7 +65,7 @@
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[PlaylistDetail] Failed to play all:", e);
|
||||
log.error("Failed to play all:", e);
|
||||
toast.error("Failed to play playlist");
|
||||
}
|
||||
}
|
||||
@@ -82,7 +85,7 @@
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[PlaylistDetail] Failed to shuffle play:", e);
|
||||
log.error("Failed to shuffle play:", e);
|
||||
toast.error("Failed to shuffle playlist");
|
||||
}
|
||||
}
|
||||
@@ -100,7 +103,7 @@
|
||||
playlist.name = trimmed;
|
||||
toast.success("Playlist renamed");
|
||||
} catch (e) {
|
||||
console.error("[PlaylistDetail] Failed to rename:", e);
|
||||
log.error("Failed to rename:", e);
|
||||
toast.error("Failed to rename playlist");
|
||||
editName = playlist.name;
|
||||
} finally {
|
||||
@@ -115,7 +118,7 @@
|
||||
toast.success("Playlist deleted");
|
||||
goto("/library");
|
||||
} catch (e) {
|
||||
console.error("[PlaylistDetail] Failed to delete:", e);
|
||||
log.error("Failed to delete:", e);
|
||||
toast.error("Failed to delete playlist");
|
||||
} finally {
|
||||
showDeleteConfirm = false;
|
||||
@@ -129,7 +132,7 @@
|
||||
entries = entries.filter(e => e.playlistItemId !== entry.playlistItemId);
|
||||
toast.success("Track removed");
|
||||
} catch (e) {
|
||||
console.error("[PlaylistDetail] Failed to remove track:", e);
|
||||
log.error("Failed to remove track:", e);
|
||||
toast.error("Failed to remove track");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import type { MediaItem, MediaKind, Person } from "$lib/api/types";
|
||||
import MediaCard from "./MediaCard.svelte";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("RelatedItemsSection");
|
||||
|
||||
interface Props {
|
||||
currentItemId: string;
|
||||
@@ -57,7 +60,7 @@
|
||||
return; // Success - return early
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load similar items from API:", e);
|
||||
log.warn("Failed to load similar items from API:", e);
|
||||
// Fall through to genre-based loading
|
||||
}
|
||||
}
|
||||
@@ -78,7 +81,7 @@
|
||||
|
||||
items = result.items.filter(item => item.id !== currentItemId);
|
||||
} catch (e) {
|
||||
console.warn("Failed to load related items by genre:", e);
|
||||
log.warn("Failed to load related items by genre:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@
|
||||
const artistAlbums = result.items.filter(item => item.id !== currentItemId);
|
||||
items = [...items, ...artistAlbums];
|
||||
} catch (e) {
|
||||
console.warn("Failed to load albums by artist:", e);
|
||||
log.warn("Failed to load albums by artist:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +109,7 @@
|
||||
relatedItems = uniqueItems;
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Failed to load related items";
|
||||
console.error("Error loading related items:", e);
|
||||
log.error("Error loading related items:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("SeasonDownloadButton");
|
||||
|
||||
interface Props {
|
||||
seasonId: string;
|
||||
@@ -46,11 +49,11 @@
|
||||
try {
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
log.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("📺 Starting season download for:", seasonName, "quality:", quality);
|
||||
log.debug("📺 Starting season download for:", seasonName, "quality:", quality);
|
||||
|
||||
// Get target directory
|
||||
const targetDir = await commands.storageGetPath();
|
||||
@@ -67,7 +70,7 @@
|
||||
quality
|
||||
);
|
||||
|
||||
console.log(`✅ Queued ${downloadIds.length} episodes for download`);
|
||||
log.debug(`✅ Queued ${downloadIds.length} episodes for download`);
|
||||
|
||||
// Pin the season item
|
||||
await downloads.pinItem(seasonId);
|
||||
@@ -77,9 +80,9 @@
|
||||
// rest as slots free up.
|
||||
const handle = auth.getRepository().getHandle();
|
||||
await commands.enqueueVideoDownloads(handle, downloadIds, targetDir);
|
||||
console.log(" Episodes enqueued; backend pump will start them");
|
||||
log.debug(" Episodes enqueued; backend pump will start them");
|
||||
} catch (error) {
|
||||
console.error("Failed to start season download:", error);
|
||||
log.error("Failed to start season download:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("SeriesDownloadButton");
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
@@ -40,11 +43,11 @@
|
||||
try {
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
log.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("📺 Starting series download for:", seriesName, "quality:", quality);
|
||||
log.debug("📺 Starting series download for:", seriesName, "quality:", quality);
|
||||
|
||||
// Get target directory
|
||||
const targetDir = await commands.storageGetPath();
|
||||
@@ -59,7 +62,7 @@
|
||||
quality
|
||||
);
|
||||
|
||||
console.log(` Queued ${downloadIds.length} episodes for download`);
|
||||
log.debug(` Queued ${downloadIds.length} episodes for download`);
|
||||
|
||||
// Pin the series item
|
||||
await downloads.pinItem(seriesId);
|
||||
@@ -69,9 +72,9 @@
|
||||
// rest as slots free up.
|
||||
const handle = auth.getRepository().getHandle();
|
||||
await commands.enqueueVideoDownloads(handle, downloadIds, targetDir);
|
||||
console.log(" Episodes enqueued; backend pump will start them");
|
||||
log.debug(" Episodes enqueued; backend pump will start them");
|
||||
} catch (error) {
|
||||
console.error("Failed to start series download:", error);
|
||||
log.error("Failed to start series download:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
import AddToPlaylistModal from "$lib/components/playlist/AddToPlaylistModal.svelte";
|
||||
import { calculateMenuPosition, type MenuPosition } from "$lib/utils/menuPosition";
|
||||
import { formatDuration } from "$lib/utils/duration";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("TrackList");
|
||||
|
||||
/** Queue context for remote transfer - what type of queue is this? */
|
||||
export type QueueContext =
|
||||
@@ -55,7 +58,7 @@
|
||||
|
||||
// If this is an album, use the backend album command (more efficient)
|
||||
if (context && context.type === "album") {
|
||||
console.log(`[TrackList] Playing track: "${track.name}" (ID: ${track.id}, index in list: ${index})`);
|
||||
log.debug(`Playing track: "${track.name}" (ID: ${track.id}, index in list: ${index})`);
|
||||
await playerController.playAlbumTrack({
|
||||
albumId: context.albumId,
|
||||
albumName: context.albumName,
|
||||
@@ -91,7 +94,7 @@
|
||||
// Queue will auto-update from Rust backend event
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||
console.error("Failed to play track:", errorMessage);
|
||||
log.error("Failed to play track:", errorMessage);
|
||||
toast.error(`Failed to play track: ${errorMessage}`, 5000);
|
||||
} finally {
|
||||
isPlayingTrack = null;
|
||||
@@ -145,9 +148,9 @@
|
||||
try {
|
||||
// Queue store now handles everything in Rust - just pass the track
|
||||
await queue.addToQueue(track, position);
|
||||
console.log(`Added "${track.name}" to queue (${position})`);
|
||||
log.debug(`Added "${track.name}" to queue (${position})`);
|
||||
} catch (e) {
|
||||
console.error("Failed to add to queue:", e);
|
||||
log.error("Failed to add to queue:", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("VideoDownloadButton");
|
||||
|
||||
interface Props {
|
||||
itemId: string;
|
||||
@@ -57,17 +60,17 @@
|
||||
try {
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
log.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = auth.getRepository();
|
||||
|
||||
console.log("🎬 Starting video download for item:", itemId, "quality:", quality);
|
||||
log.debug("🎬 Starting video download for item:", itemId, "quality:", quality);
|
||||
|
||||
// Get stream URL based on quality
|
||||
const streamUrl = await repo.getVideoDownloadUrl(itemId, quality);
|
||||
console.log(" Stream URL obtained");
|
||||
log.debug(" Stream URL obtained");
|
||||
|
||||
// Get target directory
|
||||
const targetDir = await commands.storageGetPath();
|
||||
@@ -85,7 +88,7 @@
|
||||
filePath = `videos/${safeName}.mp4`;
|
||||
}
|
||||
|
||||
console.log(" File path:", filePath);
|
||||
log.debug(" File path:", filePath);
|
||||
|
||||
// Queue download with video metadata
|
||||
const downloadId = await downloads.downloadVideo(
|
||||
@@ -101,16 +104,16 @@
|
||||
episodeNumber,
|
||||
seasonNumber
|
||||
);
|
||||
console.log(" Download queued with ID:", downloadId);
|
||||
log.debug(" Download queued with ID:", downloadId);
|
||||
|
||||
// Pin the item metadata
|
||||
await downloads.pinItem(itemId);
|
||||
|
||||
// Actually start the download
|
||||
await commands.startDownload(downloadId, streamUrl, targetDir);
|
||||
console.log(" Download started");
|
||||
log.debug(" Download started");
|
||||
} catch (error) {
|
||||
console.error("Failed to start video download:", error);
|
||||
log.error("Failed to start video download:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { syncService } from "$lib/services/syncService";
|
||||
import { createLogger } from "$lib/utils/logger";
|
||||
|
||||
const log = createLogger("WatchedToggleButton");
|
||||
|
||||
interface Props {
|
||||
/** Episode, season or series id. */
|
||||
@@ -81,7 +84,7 @@
|
||||
} catch (e) {
|
||||
// Put the button back where it was — the change did not happen.
|
||||
optimistic = null;
|
||||
console.error("Failed to change watched state:", e);
|
||||
log.error("Failed to change watched state:", e);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user