From 6d0c89e45f6a6635921085963be2c69d4f1059e0 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Sun, 16 Nov 2025 21:10:54 +0200 Subject: [PATCH 01/60] feat: updated translations, fixed windows warn skip-ci --- docs/body.md | 148 +++++++++++++---------------- docs/mock_release.json | 12 +-- src-tauri/src/commands/settings.rs | 1 + src/i18n/locales/en.json | 5 +- src/i18n/locales/ru.json | 5 +- src/i18n/locales/ua.json | 5 +- src/i18n/locales/zh_cn.json | 5 +- 7 files changed, 91 insertions(+), 90 deletions(-) diff --git a/docs/body.md b/docs/body.md index 0ce48e2..7415d61 100644 --- a/docs/body.md +++ b/docs/body.md @@ -1,98 +1,86 @@ -COLLAPSELOADER 0.2.0 (FABRIC) | BETA ERA +CollapseLoader 0.2.3 (NullPtr) -The project has moved from alpha to beta testing +<< CHANGELOG >> +[~] fixes for most of the interface, lag, freezes, etc. -Important notice: -There may be bugs with Fabric — please report them immediately in Discord: https://collapseloader.org/discord +<< LINKS >> + +[VirusTotal](https://www.virustotal.com/gui/file-analysis/YWMxNjhjYmY1ZDA2YmVkNGEwODEzMWM4ZGZhYTU1M2Y6MTc2MzMxOTAzMg==/detection) -[+] added fabric support — a long-awaited feature -[+] added profile avatars -[+] added client filters -[~] optimized some tabs -[~] redesigned parts of the UI
do not read, for internal updater -```changelog +```json { - "entries": [ + "entries": [ + { + "version": "v0.2.3", + "date": "2025-11-16", + "highlights": [ + "Bug fixes & interface fixes", + "DPI Bypass" + ], + "changes": [ + { + "category": "improvement", + "description_key": "updater.changelogs.improvement.v0_2_3.bug_fixes", + "icon": "✨" + }, { - "version": "v0.2.0", - "date": "2025-09-16", - "highlights": [ - "Переход из альфа в бета", - "Добавлена поддержка Fabric" - ], - "changes": [ - { - "category": "feature", - "description_key": "updater.changelogs.feature.v0_2_0.fabric_support", - "icon": "✨" - }, - { - "category": "feature", - "description_key": "updater.changelogs.feature.v0_2_0.avatars_added", - "icon": "🖼️" - }, - { - "category": "feature", - "description_key": "updater.changelogs.feature.v0_2_0.client_filters", - "icon": "🔎" - }, - { - "category": "improvement", - "description_key": "updater.changelogs.improvement.v0_2_0.optimized_tabs", - "icon": "⚡" - }, - { - "category": "improvement", - "description_key": "updater.changelogs.improvement.v0_2_0.redesigned_ui", - "icon": "🎨" - } - ] + "category": "feature", + "description_key": "updater.changelogs.feature.v0_2_3.dpi_bypass", + "icon": "🛠" } - ], - "translations": { - "en": { - "updater": { - "changelogs": { - "feature": { - "v0_2_0": { - "fabric_support": "Added Fabric support", - "avatars_added": "Added profile avatars", - "client_filters": "Added client filters" - } - }, - "improvement": { - "v0_2_0": { - "optimized_tabs": "Optimized some tabs", - "redesigned_ui": "Redesigned parts of the UI" - } - } - } + ] + } + ], + "translations": { + "en": { + "updater": { + "categories": { + "feature": "Feature", + "improvement": "Improvement", + "bugfix": "Bugfix", + "other": "Other" + }, + "changelogs": { + "feature": { + "v0_2_3": { + "dpi_bypass": "DPI Bypass, now the loader works anywhere in the world, even where it is banned, powered by Zapret" + } + }, + "improvement": { + "v0_2_3": { + "bug_fixes": "Fixes for most of the interface, lag, freezes, etc." } + } + } + } + }, + "ru": { + "updater": { + "categories": { + "feature": "Фича", + "improvement": "Улучшение", + "bugfix": "Исправление ошибки", + "other": "Другое" }, - "ru": { - "updater": { - "changelogs": { - "feature": { - "v0_2_0": { - "fabric_support": "Добавлена поддержка Fabric", - "avatars_added": "Добавлены аватарки для профилей", - "client_filters": "Добавлены фильтры для клиентов" - } - }, - "improvement": { - "v0_2_0": { - "optimized_tabs": "Оптимизированы некоторые вкладки", - "redesigned_ui": "Переделаны некоторые части интерфейса" - } - } - } + "changelogs": { + "feature": { + "v0_2_3": { + "dpi_bypass": "DPI Bypass, теперь лоадер работает с любой точки мира, даже там где он запрещён, powered by Zapret" + } + }, + "improvement": { + "v0_2_3": { + "bug_fixes": "Исправления большей части интерфейса, лагов, фризов и т.д" } + } } + } } + } } ``` diff --git a/docs/mock_release.json b/docs/mock_release.json index a86fa3e..328630c 100644 --- a/docs/mock_release.json +++ b/docs/mock_release.json @@ -1,15 +1,15 @@ { - "tag_name": "v0.2.1", - "name": "v0.2.1", + "tag_name": "v0.2.3", + "name": "v0.2.3", "body": "#file:body.md", - "html_url": "https://github.com/dest4590/CollapseLoader/releases/tag/v0.2.1", + "html_url": "https://github.com/dest4590/CollapseLoader/releases/tag/v0.2.3", "assets": [ { - "name": "CollapseLoader-0.2.1.msi", - "browser_download_url": "https://example.com/CollapseLoader-0.2.1.msi", + "name": "CollapseLoader-0.2.3.msi", + "browser_download_url": "https://example.com/CollapseLoader-0.2.3.msi", "size": 100 } ], - "published_at": "2025-08-29T12:00:00Z", + "published_at": "2025-11-16T12:00:00Z", "prerelease": false } diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index c2f2feb..044301d 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -36,6 +36,7 @@ pub fn save_settings(input_settings: InputSettings) -> Result<(), String> { let config_path = current_settings.config_path.clone(); let old_discord_rpc_enabled = current_settings.discord_rpc_enabled.value; + #[cfg(target_os = "windows")] let old_dpi_bypass_enabled = current_settings.dpi_bypass.value; let discord_rpc_changed = old_discord_rpc_enabled != input_settings.discord_rpc_enabled.value; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2af4208..92f98fa 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -988,7 +988,10 @@ "show_more": "Show {count} more versions", "show_less": "Show less", "release_types": {}, - "categories": {}, + "categories": { + "feature": "Feature", + "improvement": "Improvement" + }, "no_update": "Updates not found" }, "custom_clients": { diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 4a8a318..6937db8 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -987,7 +987,10 @@ "up_to_date": "У вас установлена последняя версия!", "show_more": "Показать еще {count} версий", "show_less": "Показать меньше", - "categories": {}, + "categories": { + "feature": "Функционал", + "improvement": "Улучшение" + }, "no_update": "Обновления не найдены" }, "custom_clients": { diff --git a/src/i18n/locales/ua.json b/src/i18n/locales/ua.json index 5b3fe6e..3565dbb 100644 --- a/src/i18n/locales/ua.json +++ b/src/i18n/locales/ua.json @@ -987,7 +987,10 @@ "show_more": "Показати ще {count} версій", "show_less": "Показати менше", "release_types": {}, - "categories": {}, + "categories": { + "feature": "Функціонал", + "improvement": "Поліпшення" + }, "no_update": "Оновлень не знайдено" }, "custom_clients": { diff --git a/src/i18n/locales/zh_cn.json b/src/i18n/locales/zh_cn.json index bbf6c5f..0c50a79 100644 --- a/src/i18n/locales/zh_cn.json +++ b/src/i18n/locales/zh_cn.json @@ -976,7 +976,10 @@ "show_more": "再显示 {count} 个版本", "show_less": "收起", "release_types": {}, - "categories": {}, + "categories": { + "feature": "功能", + "improvement": "改善" + }, "no_update": "未找到更新" }, "custom_clients": { From 1f7153b8e29cb6581676c38130b895e1cfe21437 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Mon, 17 Nov 2025 23:27:23 +0200 Subject: [PATCH 02/60] feat: improved filters menu skip-ci --- src/components/common/FiltersMenu.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/common/FiltersMenu.vue b/src/components/common/FiltersMenu.vue index 294f68b..f0bbd38 100644 --- a/src/components/common/FiltersMenu.vue +++ b/src/components/common/FiltersMenu.vue @@ -1,6 +1,6 @@ diff --git a/src/components/features/friends/FriendRequestCard.vue b/src/components/features/friends/FriendRequestCard.vue index 6515753..591d2c8 100644 --- a/src/components/features/friends/FriendRequestCard.vue +++ b/src/components/features/friends/FriendRequestCard.vue @@ -41,11 +41,12 @@ import { useModal } from '../../../services/modalService'; import UserAvatar from '../../ui/UserAvatar.vue'; import CancelFriendRequestConfirmModal from '../../modals/social/friends/CancelFriendRequestConfirmModal.vue'; import type { Friend } from '../../../services/userService'; -import { globalUserStatus } from '../../../composables/useUserStatus'; +import { useStreamerMode } from '../../../composables/useStreamerMode'; import { computed } from 'vue'; const { t } = useI18n(); const { showModal, hideModal } = useModal(); +const streamer = useStreamerMode(); const props = defineProps<{ user: Friend; @@ -61,17 +62,11 @@ const emit = defineEmits<{ }>(); const displayNickname = computed(() => { - if (globalUserStatus.isStreamer.value) { - return '??????'; - } - return props.user.nickname || props.user.username; + return streamer.getDisplayName(props.user.nickname, props.user.username); }); const displayUsername = computed(() => { - if (globalUserStatus.isStreamer.value) { - return 'unknown'; - } - return props.user.username; + return streamer.getDisplayUsername(props.user.username); }); const confirmCancel = () => { diff --git a/src/components/modals/social/friends/AddFriendModal.vue b/src/components/modals/social/friends/AddFriendModal.vue index 5c7e092..5080b0c 100644 --- a/src/components/modals/social/friends/AddFriendModal.vue +++ b/src/components/modals/social/friends/AddFriendModal.vue @@ -4,7 +4,7 @@ @@ -84,8 +84,7 @@ - @@ -288,6 +286,7 @@ import { } from 'lucide-vue-next'; import { useI18n } from 'vue-i18n'; import { globalUserStatus } from '../composables/useUserStatus'; +import { useStreamerMode } from '../composables/useStreamerMode'; import getRoleBadge from '../utils/roleBadge'; import { formatDate } from '../utils/utils'; import { marketplaceService } from '../services/marketplaceService'; @@ -303,6 +302,7 @@ defineEmits(['change-view']); const { t } = useI18n(); const { addToast } = useToast(); const { showModal, hideModal } = useModal(); +const streamer = useStreamerMode(); const userProfile = ref(null); const loading = ref(true); @@ -326,18 +326,12 @@ const presetsCountLabel = computed(() => { const displayNickname = computed(() => { if (!userProfile.value) return ''; - if (globalUserStatus.isStreamer.value) { - return '???'; - } - return userProfile.value.nickname || userProfile.value.username; + return streamer.getDisplayName(userProfile.value.nickname, userProfile.value.username); }); const displayUsername = computed(() => { if (!userProfile.value) return ''; - if (globalUserStatus.isStreamer.value) { - return '???'; - } - return userProfile.value.username; + return streamer.getDisplayUsername(userProfile.value.username); }); const roleBadge = computed(() => { From 1e2bc9b01e00daf9471a370e77aec0351ba3dc86 Mon Sep 17 00:00:00 2001 From: dest4590 Date: Fri, 21 Nov 2025 15:34:18 +0200 Subject: [PATCH 08/60] feat: improved expanded card, and fixed bug with scrollbar skip-ci --- .../features/clients/ClientCard.vue | 661 ++++++++++++------ src/composables/useStreamerMode.ts | 12 - src/views/Home.vue | 83 ++- 3 files changed, 512 insertions(+), 244 deletions(-) diff --git a/src/components/features/clients/ClientCard.vue b/src/components/features/clients/ClientCard.vue index c97e7c6..c07d0b5 100644 --- a/src/components/features/clients/ClientCard.vue +++ b/src/components/features/clients/ClientCard.vue @@ -1,5 +1,5 @@ @@ -655,212 +873,232 @@ onBeforeUnmount(() => { - - - - -
-
-

- {{ client.name }} -
-
- -
-
-

- -
- + + +
+
+
+
+

+ {{ client.name }} +
+
+ +
+
+

+ +
+ +
+
- -
- + -
- - -
-
- - {{ currentInstallStatus?.action }} - {{ client.name }} - - - {{ currentInstallStatus?.percentage }}% - -
-
-
-
-
-
- - + +
+
+ + {{ currentInstallStatus?.action }} + {{ client.name }} + + + {{ currentInstallStatus?.percentage }}% + +
+
+
+
+
+
+ + +
+
-
-
- -
-
-
-
-

{{ t('client.details.loading') }}

-
+
+
+
+
+

{{ t('client.details.loading') }}

+
-
- +
+ -
- -
-
-
-
-
{{ - t('client.details.source_link') }}
-
- - {{ clientDetails.source_link }} - - -
-
-
-
{{ - t('client.details.created') }}
-
- {{ new Date(clientDetails.created_at).toLocaleDateString('en-GB') }} -
+
+ +
+
+
+
+
{{ + t('client.details.source_link') }}
+
+ + {{ clientDetails.source_link }} + + +
+
+
+
{{ + t('client.details.created') }}
+
+ {{ new + Date(clientDetails.created_at).toLocaleDateString('en-GB') + }} +
+
+
-
-
-
-

- - {{ t('client.details.changelog') }} -

-
+
+

+ + {{ t('client.details.changelog') }} +

+
-
-
    -
  • - -
    - - {{ new Date(entry.created_at).toLocaleDateString(undefined, { - month: 'short', day: 'numeric' - }) }} - -
    -
    - -
    -
    -
    - v{{ entry.version }} -
    -
    - {{ entry.content }} -
    -
    -
    -
  • -
-
-
- -

{{ - t('client.details.no_changelog') }}

-

{{ - t('client.details.no_changelog_desc') }}

-
-
+
+
    +
  • + +
    + + {{ new + Date(entry.created_at).toLocaleDateString(undefined, { + month: 'short', day: 'numeric' + }) }} + +
    +
    + +
    +
    +
    + v{{ entry.version }} +
    +
    + {{ entry.content }} +
    +
    +
    +
  • +
+
+
+ +

{{ + t('client.details.no_changelog') }}

+

{{ + t('client.details.no_changelog_desc') }}

+
+
-
-
-
-
-
- -
- +
+
+
+
+
+ +
+ +
+
+
+

{{ t('client.details.no_screenshots') }}

+
-
-
-

{{ t('client.details.no_screenshots') }}

-
+
- +
+ + +
+
+
+
+
@@ -992,11 +1230,21 @@ onBeforeUnmount(() => { position: relative; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - padding: 1rem; + /* padding: 1rem; - Moved to inner container */ - scrollbar-gutter: stable both-edges; + /* scrollbar-gutter: stable both-edges; - Removed */ -webkit-overflow-scrolling: touch; - scrollbar-width: thin; + /* scrollbar-width: thin; - Removed */ + will-change: transform, width, height, top, left; +} + +.custom-scrollbar-hide { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.custom-scrollbar-hide::-webkit-scrollbar { + display: none; } .card-actions { @@ -1018,18 +1266,15 @@ onBeforeUnmount(() => { scrollbar-width: thin; } -.client-card::-webkit-scrollbar, .timeline::-webkit-scrollbar { width: 10px; height: 10px; } -.client-card::-webkit-scrollbar-track, .timeline::-webkit-scrollbar-track { background: transparent; } -.client-card::-webkit-scrollbar-thumb, .timeline::-webkit-scrollbar-thumb { background-color: rgba(100, 100, 100, 0.35); border-radius: 999px; @@ -1037,9 +1282,7 @@ onBeforeUnmount(() => { background-clip: content-box; } -.client-card[style*="position: fixed"] { - scrollbar-gutter: stable; -} + .status-section { border-top: 1px solid rgba(255, 255, 255, 0.1); diff --git a/src/composables/useStreamerMode.ts b/src/composables/useStreamerMode.ts index 5e714b6..b4526ce 100644 --- a/src/composables/useStreamerMode.ts +++ b/src/composables/useStreamerMode.ts @@ -6,7 +6,6 @@ function readStoredFlag(key: string): boolean { try { return localStorage.getItem(key) === 'true'; } catch (e) { - // localStorage may not be available in some environments (SSR), or read can fail. console.warn('useStreamerMode: failed to read storage', e); return false; } @@ -36,17 +35,13 @@ function emitChange(enabled: boolean) { } } -/** Mask arbitrary string using maskChar and preserve character count (handles unicode code points) */ function maskString(input: string | undefined | null, maskChar = '*'): string { if (!input) return ''; - // Use spread to correctly handle unicode characters return maskChar.repeat([...input].length); } -/** Convenient placeholders used when streamer mode is enabled */ function maskName(name?: string): string { if (!name) return 'User'; - // Keep last char and mask the rest for a friendlier look const chars = [...name]; if (chars.length <= 1) return chars[0] || 'U'; return maskString(chars.slice(0, -1).join('')) + chars[chars.length - 1]; @@ -55,14 +50,12 @@ function maskName(name?: string): string { function maskUsername(username?: string): string { if (!username) return 'user'; const chars = [...username]; - // Keep up to 2 leading chars for readability const lead = chars.slice(0, 2).join(''); return lead + maskString(chars.slice(2).join('')); } function maskEmail(email?: string): string { if (!email) return 'unknown@*****.***'; - // Basic email mask: keep the domain's TLD and mask the local part const [local, domain] = email.split('@'); if (!domain) return maskString(email); const domainParts = domain.split('.'); @@ -73,7 +66,6 @@ function maskEmail(email?: string): string { } export function useStreamerMode() { - // Expose readonly computed ref to prevent direct mutation from consumers const isEnabled = computed(() => isStreamerModeEnabled.value); function setStreamerMode(enabled: boolean) { @@ -114,12 +106,9 @@ export function useStreamerMode() { return isStreamerModeEnabled.value ? masker(value) : (value || ''); } - // Note: the storage event listener is installed once at module init to avoid - // adding duplicate listeners every time the composable is used. return { isStreamerModeEnabled: isEnabled, - // alias for convenience enabled: isEnabled, toggleStreamerMode, setStreamerMode, @@ -137,7 +126,6 @@ export function useStreamerMode() { export default useStreamerMode; -// Install storage listener once at module initialization to keep state in sync if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { if (!storageListenerInstalled) { window.addEventListener('storage', (e: StorageEvent) => { diff --git a/src/views/Home.vue b/src/views/Home.vue index d5afad2..f449f56 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -149,28 +149,39 @@ const debouncedClientSortKey = ref(clientSortKey.value); const debouncedClientSortOrder = ref(clientSortOrder.value); const applyFiltersAndSort = () => { - if (JSON.stringify(debouncedActiveFilters.value) !== JSON.stringify(activeFilters.value)) { + const filtersChanged = JSON.stringify(debouncedActiveFilters.value) !== JSON.stringify(activeFilters.value); + const sortKeyChanged = debouncedClientSortKey.value !== clientSortKey.value; + const sortOrderChanged = debouncedClientSortOrder.value !== clientSortOrder.value; + + if (!filtersChanged && !sortKeyChanged && !sortOrderChanged) { + return; + } + + if (filtersChanged) { debouncedActiveFilters.value = { ...activeFilters.value }; } - if (debouncedClientSortKey.value !== clientSortKey.value) { + if (sortKeyChanged) { debouncedClientSortKey.value = clientSortKey.value; } - if (debouncedClientSortOrder.value !== clientSortOrder.value) { + if (sortOrderChanged) { debouncedClientSortOrder.value = clientSortOrder.value; } }; let filtersDebounceTimer: number | null = null; +const isFilterUpdateScheduled = ref(false); const scheduleFilterUpdate = () => { if (filtersDebounceTimer !== null) { clearTimeout(filtersDebounceTimer); } + isFilterUpdateScheduled.value = true; filtersDebounceTimer = window.setTimeout(() => { applyFiltersAndSort(); filtersDebounceTimer = null; - }, 100); + isFilterUpdateScheduled.value = false; + }, 200); }; watch( @@ -278,8 +289,8 @@ const isCtrlPressed = ref(false); let ctrlPressTimer: number | null = null; const blurHandler = () => { - if (ctrlPressTimer != null) { - clearTimeout(ctrlPressTimer as number); + if (ctrlPressTimer !== null) { + clearTimeout(ctrlPressTimer); ctrlPressTimer = null; } isCtrlPressed.value = false; @@ -287,8 +298,8 @@ const blurHandler = () => { const visibilityHandler = () => { if (document.hidden) { - if (ctrlPressTimer != null) { - clearTimeout(ctrlPressTimer as number); + if (ctrlPressTimer !== null) { + clearTimeout(ctrlPressTimer); ctrlPressTimer = null; } isCtrlPressed.value = false; @@ -442,6 +453,10 @@ const allClients = computed(() => { let filteredClientsCache: { key: string; result: Client[] } = { key: '', result: [] }; const filteredClients = computed(() => { + if (isFilterUpdateScheduled.value) { + return filteredClientsCache.result; + } + const filters = debouncedActiveFilters.value; const cacheKey = `${allClients.value.length}-${debouncedSearchQuery.value}-${filters.fabric}-${filters.vanilla}-${filters.installed}-${debouncedClientSortKey.value}-${debouncedClientSortOrder.value}-${favoriteClients.value.length}`; @@ -449,6 +464,11 @@ const filteredClients = computed(() => { return filteredClientsCache.result; } + if (allClients.value.length === 0) { + filteredClientsCache = { key: cacheKey, result: [] }; + return []; + } + const query = debouncedSearchQuery.value.trim(); const queryLower = query ? query.toLowerCase() : ''; const hasActiveFilters = filters.fabric || filters.vanilla || filters.installed; @@ -1078,9 +1098,9 @@ const setupEventListeners = async () => { }; const invalidateClientCaches = () => { - filteredClientsCache = { key: '', result: [] }; - selectedClientsDataCache = { key: '', result: [] }; - downloadingClientsCache = { key: '', result: false }; + if (filteredClientsCache.key) filteredClientsCache = { key: '', result: [] }; + if (selectedClientsDataCache.key) selectedClientsDataCache = { key: '', result: [] }; + if (downloadingClientsCache.key) downloadingClientsCache = { key: '', result: false }; }; const updateClientInstallStatus = (filename: string) => { @@ -1263,7 +1283,7 @@ const handleClientClick = (client: Client, event: MouseEvent) => { } selectedClients.value = newSelection; } else { - selectedClients.value = new Set(); + clearSelection(); } }; @@ -1272,7 +1292,9 @@ const handleExpandedStateChanged = (clientId: number, isExpanded: boolean) => { }; const clearSelection = () => { - selectedClients.value = new Set(); + if (selectedClients.value.size > 0) { + selectedClients.value = new Set(); + } }; const isClientSelected = (clientId: number): boolean => { @@ -1493,17 +1515,19 @@ const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Control' && expandedClientId.value === null) { if (isCtrlPressed.value) return; - if (ctrlPressTimer === null) { - ctrlPressTimer = window.setTimeout(() => { - isCtrlPressed.value = true; - ctrlPressTimer = null; - }, 100); + if (ctrlPressTimer !== null) { + clearTimeout(ctrlPressTimer); } + + ctrlPressTimer = window.setTimeout(() => { + isCtrlPressed.value = true; + ctrlPressTimer = null; + }, 50); return; } if (event.key === 'Escape') { - selectedClients.value = new Set(); + clearSelection(); } if ( @@ -1521,8 +1545,8 @@ const handleKeyDown = (event: KeyboardEvent) => { const handleKeyUp = (event: KeyboardEvent) => { if (event.key === 'Control') { - if (ctrlPressTimer != null) { - clearTimeout(ctrlPressTimer as number); + if (ctrlPressTimer !== null) { + clearTimeout(ctrlPressTimer); ctrlPressTimer = null; } isCtrlPressed.value = false; @@ -1532,7 +1556,7 @@ const handleKeyUp = (event: KeyboardEvent) => { const handleDocumentClick = (event: MouseEvent) => { const target = event.target as HTMLElement; if (!target.closest('.client-card')) { - selectedClients.value = new Set(); + clearSelection(); } }; @@ -1729,7 +1753,20 @@ onBeforeUnmount(() => {
+ :style="{ 'animation-delay': (!hasStaggerPlayed ? index * 0.07 + 's' : '0s') }" v-memo="[ + client.id, + isClientRunning(client.id), + isClientInstalling(client), + installationStatus.get(client.filename), + isRequirementsInProgress, + isAnyClientDownloading, + isClientFavorite(client.id), + isClientSelected(client.id), + isCtrlPressed, + expandedClientId, + hashVerifyingClients.has(client.id), + isAnyCardExpanded + ]"> Date: Fri, 21 Nov 2025 17:50:37 +0200 Subject: [PATCH 09/60] feat: simplified friends service skip-ci --- src/composables/useFriends.ts | 106 ++-------------------------------- 1 file changed, 5 insertions(+), 101 deletions(-) diff --git a/src/composables/useFriends.ts b/src/composables/useFriends.ts index b644b46..ce9f172 100644 --- a/src/composables/useFriends.ts +++ b/src/composables/useFriends.ts @@ -22,15 +22,6 @@ interface FriendRequest { updated_at: string; } -interface FriendsMetrics { - totalFriends: number; - onlineFriends: number; - lastBulkUpdate: number; - cacheHitRate: number; - statusUpdateCount: number; - avgStatusResponseTime: number; -} - interface GlobalFriendsState { friends: Friend[]; sentRequests: FriendRequest[]; @@ -39,7 +30,6 @@ interface GlobalFriendsState { isLoaded: boolean; lastUpdated: string | null; lastStatusUpdate: number; - bulkUpdateCount: number; } const globalFriendsState = reactive({ @@ -49,31 +39,12 @@ const globalFriendsState = reactive({ isLoading: false, isLoaded: false, lastUpdated: null, - lastStatusUpdate: 0, - bulkUpdateCount: 0 -}); - -const friendsMetrics = reactive({ - totalFriends: 0, - onlineFriends: 0, - lastBulkUpdate: 0, - cacheHitRate: 0, - statusUpdateCount: 0, - avgStatusResponseTime: 0 + lastStatusUpdate: 0 }); const isStatusLoading = ref(false); -const previousReceivedCount = ref(0); const statusUpdateInterval: { current: NodeJS.Timeout | null } = { current: null }; - -const statusUpdateConfig = { - baseInterval: 45000, - maxInterval: 180000, - currentInterval: 45000, - backoffMultiplier: 1.3, - consecutiveNoChanges: 0, - maxNoChanges: 4 -}; +const STATUS_UPDATE_INTERVAL = 45000; const isAuthenticated = computed(() => !!localStorage.getItem('authToken')); @@ -125,22 +96,11 @@ export function useFriends() { optimized: batchData.performance_info?.optimized || false }); - checkForNewRequests({ - sent: batchData.requests?.sent || [], - received: batchData.requests?.received || [] - }); - globalFriendsState.friends = batchData.friends || []; globalFriendsState.sentRequests = batchData.requests?.sent || []; globalFriendsState.receivedRequests = batchData.requests?.received || []; globalFriendsState.lastUpdated = new Date().toISOString(); globalFriendsState.isLoaded = true; - globalFriendsState.bulkUpdateCount++; - - friendsMetrics.totalFriends = globalFriendsState.friends.length; - friendsMetrics.onlineFriends = onlineFriendsCount.value; - friendsMetrics.lastBulkUpdate = Date.now(); - friendsMetrics.cacheHitRate = apiClient.getCacheStats().hitRate; if (!statusUpdateInterval.current) { startStatusUpdates(); @@ -172,9 +132,6 @@ export function useFriends() { globalFriendsState.lastUpdated = new Date().toISOString(); globalFriendsState.isLoaded = true; - friendsMetrics.totalFriends = globalFriendsState.friends.length; - friendsMetrics.onlineFriends = onlineFriendsCount.value; - console.log('Friends data loaded successfully via fallback method'); } catch (fallbackError) { console.error('Fallback friends data loading also failed:', fallbackError); @@ -192,7 +149,6 @@ export function useFriends() { try { isStatusLoading.value = true; - friendsMetrics.statusUpdateCount++; const statusesData = await apiClient.get('/auth/friends/status/'); @@ -215,8 +171,6 @@ export function useFriends() { }); const responseTime = Date.now() - startTime; - friendsMetrics.avgStatusResponseTime = friendsMetrics.avgStatusResponseTime * 0.8 + responseTime * 0.2; - friendsMetrics.onlineFriends = onlineFriendsCount.value; globalFriendsState.lastStatusUpdate = Date.now(); console.log(`Friend statuses updated in ${responseTime}ms (${hasChanges ? 'with changes' : 'no changes'})`); @@ -245,13 +199,12 @@ export function useFriends() { return; } - const hasChanges = await updateFriendStatuses(); - adjustStatusUpdateFrequency(hasChanges); + await updateFriendStatuses(); }; runStatusUpdate(); - statusUpdateInterval.current = setInterval(runStatusUpdate, statusUpdateConfig.currentInterval); + statusUpdateInterval.current = setInterval(runStatusUpdate, STATUS_UPDATE_INTERVAL); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { @@ -259,50 +212,7 @@ export function useFriends() { } }); - console.log(`Started status updates with ${statusUpdateConfig.currentInterval}ms interval`); - }; - - - const adjustStatusUpdateFrequency = (hasChanges: boolean): void => { - if (hasChanges) { - statusUpdateConfig.currentInterval = statusUpdateConfig.baseInterval; - statusUpdateConfig.consecutiveNoChanges = 0; - } else { - statusUpdateConfig.consecutiveNoChanges++; - - if (statusUpdateConfig.consecutiveNoChanges >= statusUpdateConfig.maxNoChanges) { - const newInterval = Math.min( - statusUpdateConfig.currentInterval * statusUpdateConfig.backoffMultiplier, - statusUpdateConfig.maxInterval - ); - - if (newInterval !== statusUpdateConfig.currentInterval) { - statusUpdateConfig.currentInterval = newInterval; - console.log(`Increased status update interval to ${newInterval}ms (no changes for ${statusUpdateConfig.consecutiveNoChanges} updates)`); - } - } - } - - if (statusUpdateInterval.current) { - clearInterval(statusUpdateInterval.current); - statusUpdateInterval.current = setInterval(async () => { - if (isAuthenticated.value && globalFriendsState.friends.length > 0) { - const changes = await updateFriendStatuses(); - adjustStatusUpdateFrequency(changes); - } - }, statusUpdateConfig.currentInterval); - } - }; - - - const checkForNewRequests = (currentRequests: { sent: FriendRequest[]; received: FriendRequest[] }): FriendRequest[] => { - if (currentRequests.received.length > previousReceivedCount.value) { - const knownRequestIds = new Set(globalFriendsState.receivedRequests.map(req => req.id)); - const newRequests = currentRequests.received.filter(req => !knownRequestIds.has(req.id)); - previousReceivedCount.value = currentRequests.received.length; - return newRequests; - } - return []; + console.log(`Started status updates with ${STATUS_UPDATE_INTERVAL}ms interval`); }; const searchUsers = async (query: string): Promise => { @@ -372,7 +282,6 @@ export function useFriends() { const index = globalFriendsState.friends.findIndex(friend => friend.id === userId); if (index > -1) { globalFriendsState.friends.splice(index, 1); - friendsMetrics.totalFriends = globalFriendsState.friends.length; } console.log('Friend removed'); @@ -404,15 +313,11 @@ export function useFriends() { globalFriendsState.isLoaded = false; globalFriendsState.lastUpdated = null; globalFriendsState.lastStatusUpdate = 0; - previousReceivedCount.value = 0; if (statusUpdateInterval.current) { clearInterval(statusUpdateInterval.current); statusUpdateInterval.current = null; } - - friendsMetrics.totalFriends = 0; - friendsMetrics.onlineFriends = 0; }; @@ -454,7 +359,6 @@ export function useFriends() { removeFriend, getOnlineFriends, - checkForNewRequests, startStatusUpdates, stopStatusUpdates, From 0b803be15fef129b8c81bf0611bb1b9582f1274d Mon Sep 17 00:00:00 2001 From: dest4590 Date: Fri, 21 Nov 2025 20:04:46 +0200 Subject: [PATCH 10/60] feat: UI/UX improve to search and home client-list skip-ci --- src/components/common/SearchBar.vue | 64 +++++++++++++------ .../features/clients/ClientCard.vue | 27 ++++---- src/i18n/locales/en.json | 3 +- src/i18n/locales/ru.json | 3 +- src/i18n/locales/ua.json | 3 +- src/i18n/locales/zh_cn.json | 3 +- src/views/Home.vue | 60 +++++++++++++++-- 7 files changed, 121 insertions(+), 42 deletions(-) diff --git a/src/components/common/SearchBar.vue b/src/components/common/SearchBar.vue index 6c25cf8..e3e2db7 100644 --- a/src/components/common/SearchBar.vue +++ b/src/components/common/SearchBar.vue @@ -1,22 +1,28 @@ \ No newline at end of file diff --git a/src/components/features/clients/ClientCard.vue b/src/components/features/clients/ClientCard.vue index c07d0b5..403acd9 100644 --- a/src/components/features/clients/ClientCard.vue +++ b/src/components/features/clients/ClientCard.vue @@ -110,7 +110,7 @@ const updateScrollbar = () => { const height = Math.max(visibleRatio * clientHeight, 30); thumbHeight.value = height; - const maxThumbTop = clientHeight - height; + const maxThumbTop = clientHeight - height - 20; const maxScrollTop = scrollHeight - clientHeight; if (maxScrollTop > 0) { @@ -152,7 +152,7 @@ const onScrollbarDrag = (event: MouseEvent) => { const maxScrollTop = scrollHeight - clientHeight; let newThumbTop = dragStartTop.value + deltaY; - newThumbTop = Math.max(0, Math.min(newThumbTop, maxThumbTop)); + newThumbTop = Math.max(0, Math.min(newThumbTop, maxThumbTop - 20)); thumbTop.value = newThumbTop; @@ -875,7 +875,10 @@ onBeforeUnmount(() => { @@ -933,10 +936,10 @@ onBeforeUnmount(() => { {{ t('home.download') - }} + }} {{ t('home.unavailable') - }} + }} {{ client.meta.size || '0' }} MB @@ -1089,10 +1092,14 @@ onBeforeUnmount(() => {
+
+ class="custom-scrollbar-track absolute right-1 top-1 bottom-1 w-1.5 m-1 h-[95%] bg-base-content/5 rounded-full z-50 transition-opacity duration-200" + :class="{ + 'opacity-0': !isExpanded || isCollapsing, + 'opacity-100': isExpanded && !isCollapsing + }">
@@ -1230,11 +1237,7 @@ onBeforeUnmount(() => { position: relative; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - /* padding: 1rem; - Moved to inner container */ - - /* scrollbar-gutter: stable both-edges; - Removed */ -webkit-overflow-scrolling: touch; - /* scrollbar-width: thin; - Removed */ will-change: transform, width, height, top, left; } @@ -1282,8 +1285,6 @@ onBeforeUnmount(() => { background-clip: content-box; } - - .status-section { border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 1rem; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 92f98fa..32398c5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -408,7 +408,8 @@ }, "filters": { "installed": "Installed" - } + }, + "clear_filters": "Clear filters" }, "theme": { "actions": { diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 6937db8..5fcb211 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -408,7 +408,8 @@ }, "filters": { "installed": "Установленные" - } + }, + "clear_filters": "Отчистить фильтры" }, "theme": { "actions": { diff --git a/src/i18n/locales/ua.json b/src/i18n/locales/ua.json index 3565dbb..5be075f 100644 --- a/src/i18n/locales/ua.json +++ b/src/i18n/locales/ua.json @@ -408,7 +408,8 @@ }, "filters": { "installed": "Встановлені" - } + }, + "clear_filters": "Очистити фільтри" }, "theme": { "actions": { diff --git a/src/i18n/locales/zh_cn.json b/src/i18n/locales/zh_cn.json index 0c50a79..2e33cbb 100644 --- a/src/i18n/locales/zh_cn.json +++ b/src/i18n/locales/zh_cn.json @@ -408,7 +408,8 @@ }, "filters": { "installed": "已安装" - } + }, + "clear_filters": "清除筛选条件" }, "theme": { "actions": { diff --git a/src/views/Home.vue b/src/views/Home.vue index f449f56..c865cf5 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -75,6 +75,8 @@ const { addToast } = useToast(); const { showModal, hideModal } = useModal(); const statusInterval = ref(null); +const searchBarRef = ref(null); + const HOME_ANIM_KEY = 'homeAnimPlayed'; const hasAnimatedBefore = ref(false); try { @@ -168,6 +170,16 @@ const applyFiltersAndSort = () => { } }; +const clearAllFilters = () => { + searchQuery.value = ''; + debouncedSearchQuery.value = ''; + activeFilters.value = { + fabric: false, + vanilla: false, + installed: false + }; +}; + let filtersDebounceTimer: number | null = null; const isFilterUpdateScheduled = ref(false); @@ -184,6 +196,13 @@ const scheduleFilterUpdate = () => { }, 200); }; +const onBeforeLeave = (el: Element) => { + const card = el as HTMLElement; + card.style.left = `${card.offsetLeft}px`; + card.style.top = `${card.offsetTop}px`; + card.style.width = `${card.offsetWidth}px`; +}; + watch( activeFilters, (val) => { @@ -1512,6 +1531,12 @@ const selectAllClients = () => { }; const handleKeyDown = (event: KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key === 'f') { + event.preventDefault(); + searchBarRef.value?.focus(); + return; + } + if (event.key === 'Control' && expandedClientId.value === null) { if (isCtrlPressed.value) return; @@ -1712,8 +1737,8 @@ onBeforeUnmount(() => { @@ -55,7 +51,7 @@ const props = defineProps({ currentTheme: { type: String, default: 'dark' }, }); -const emit = defineEmits(['update:show']); +defineEmits(['update:show']); const progressPercent = computed(() => Math.min(100, Math.max(0, (props.currentProgress / (props.totalSteps || 1)) * 100)) @@ -106,8 +102,6 @@ watch(() => props.show, (val) => { } }); -const skipIntro = () => emit('update:show', false); - onBeforeUnmount(() => { if (_progressRaf) cancelAnimationFrame(_progressRaf); if (_bumpTimer) window.clearTimeout(_bumpTimer); diff --git a/src/views/Settings.vue b/src/views/Settings.vue index f5bc2be..3e89bc3 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -797,7 +797,7 @@ const handleToastPositionChange = (position: ToastPosition) => {
{{ t('settings.offline_warning') - }} + }}
@@ -806,7 +806,7 @@ const handleToastPositionChange = (position: ToastPosition) => { {{ t('settings.no_cloud_data') - }} + }}
@@ -836,11 +836,10 @@ const handleToastPositionChange = (position: ToastPosition) => {
- +