diff --git a/components.d.ts b/components.d.ts index 2da2181..d71d442 100644 --- a/components.d.ts +++ b/components.d.ts @@ -8,6 +8,7 @@ export {} declare module 'vue' { export interface GlobalComponents { AButton: typeof import('ant-design-vue/es')['Button'] + AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] ADivider: typeof import('ant-design-vue/es')['Divider'] AForm: typeof import('ant-design-vue/es')['Form'] AFormItem: typeof import('ant-design-vue/es')['FormItem'] diff --git a/src/App.vue b/src/App.vue index 2738761..5778bc0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -84,15 +84,24 @@
نسخه جدید منتشر شد!
- -
+
+
h(LoadingOutlined, { const showSplash = ref(true) const popVisible = ref(false) +const GAROOSI_SUPPORT_URL = 'https://ble.ir/garoosigallery' + +const supportLink = computed(() => { + if (config.config?.name === 'garoosi') { + return GAROOSI_SUPPORT_URL + } + return buildSupportMessengerUrl( + supportMessengerSettings.value.messenger, + supportMessengerSettings.value.messengerId + ) +}) + +const showSupportButton = computed(() => !!supportLink.value) + +const { + dragging: supportDragging, + fabStyle: supportButtonStyle, + handlePointerDown: onSupportPointerDown, + handlePointerMove: onSupportPointerMove, + handlePointerUp: supportPointerUp, + handlePointerCancel: onSupportPointerCancel +} = useDraggableFloatingButton('khaneghi_support_button_position', { + width: 110, + height: 44, + defaultBottomOffset: 120 +}) + +function openSupportLink() { + if (!supportLink.value) return + window.open(supportLink.value, '_blank', 'noopener,noreferrer') +} + +function handleSupportPointerUp(event: PointerEvent) { + if (supportPointerUp(event)) { + openSupportLink() + } +} + const antTheme = computed(() => { if (!config.config?.color) return null @@ -164,6 +217,7 @@ const antTheme = computed(() => { onMounted(async () => { await config.getConfigData() + await loadSupportMessengerFromSettings() console.log(config?.config?.color) checkVersion() setTimeout(() => { @@ -235,18 +289,20 @@ function update() { #support-button { position: fixed; - bottom: calc(80px + var(--app-credit-footer-offset, 28px) + 12px); - right: 20px; - width: 70px; - height: 70px; - //border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - //box-shadow: 0 4px 10px rgba(0,0,0,0.3); - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; z-index: 9999; + cursor: grab; + user-select: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border-radius: 0.5rem; + + &.is-dragging { + cursor: grabbing; + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22); + } + + img { + pointer-events: none; + } } @media screen and (min-width: 426px) { diff --git a/src/utils/useDraggableFloatingButton.ts b/src/utils/useDraggableFloatingButton.ts new file mode 100644 index 0000000..1bb61e8 --- /dev/null +++ b/src/utils/useDraggableFloatingButton.ts @@ -0,0 +1,165 @@ +import { computed, onMounted, onUnmounted, ref } from 'vue' + +const DRAG_THRESHOLD = 6 + +type FabPosition = { x: number; y: number } + +type DraggableFloatingOptions = { + width?: number + height?: number + defaultBottomOffset?: number + defaultRightOffset?: number +} + +export function useDraggableFloatingButton( + storageKey: string, + options: DraggableFloatingOptions = {} +) { + const width = options.width ?? 96 + const height = options.height ?? 44 + const defaultBottomOffset = options.defaultBottomOffset ?? 120 + const defaultRightOffset = options.defaultRightOffset ?? 20 + + const pos = ref({ x: 0, y: 0 }) + const dragging = ref(false) + + let pointerId: number | null = null + let startX = 0 + let startY = 0 + let originX = 0 + let originY = 0 + let hasMoved = false + + const fabStyle = computed(() => ({ + left: `${pos.value.x}px`, + top: `${pos.value.y}px`, + width: `${width}px`, + touchAction: 'none' + })) + + function getViewportWidth() { + return Math.min(window.innerWidth, 426) + } + + function getHorizontalOffset() { + const vw = window.innerWidth + const contentWidth = getViewportWidth() + return Math.max(0, (vw - contentWidth) / 2) + } + + function resetToDefault() { + const vh = window.innerHeight + const offsetX = getHorizontalOffset() + const contentWidth = getViewportWidth() + + pos.value = { + x: offsetX + contentWidth - width - defaultRightOffset, + y: vh - defaultBottomOffset - height + } + } + + function clampToViewport() { + const vw = window.innerWidth + const vh = window.innerHeight + + pos.value = { + x: Math.max(0, Math.min(vw - width, pos.value.x)), + y: Math.max(0, Math.min(vh - height, pos.value.y)) + } + } + + function loadPosition() { + try { + const raw = localStorage.getItem(storageKey) + if (!raw) return false + const parsed = JSON.parse(raw) as FabPosition + if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') { + pos.value = parsed + clampToViewport() + return true + } + } catch { + // ignore + } + return false + } + + function savePosition() { + localStorage.setItem(storageKey, JSON.stringify(pos.value)) + } + + function handleResize() { + clampToViewport() + savePosition() + } + + function handlePointerDown(event: PointerEvent) { + dragging.value = true + hasMoved = false + pointerId = event.pointerId + startX = event.clientX + startY = event.clientY + originX = pos.value.x + originY = pos.value.y + ;(event.currentTarget as HTMLElement)?.setPointerCapture?.(event.pointerId) + } + + function handlePointerMove(event: PointerEvent) { + if (!dragging.value || pointerId !== event.pointerId) return + + const dx = event.clientX - startX + const dy = event.clientY - startY + + if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) { + hasMoved = true + } + + pos.value = { + x: originX + dx, + y: originY + dy + } + clampToViewport() + } + + function finishPointer(event: PointerEvent) { + if (!dragging.value || pointerId !== event.pointerId) return false + + dragging.value = false + pointerId = null + savePosition() + + const clicked = !hasMoved + hasMoved = false + return clicked + } + + function handlePointerUp(event: PointerEvent) { + return finishPointer(event) + } + + function handlePointerCancel(event: PointerEvent) { + finishPointer(event) + return false + } + + onMounted(() => { + if (!loadPosition()) { + resetToDefault() + } + window.addEventListener('resize', handleResize) + }) + + onUnmounted(() => { + window.removeEventListener('resize', handleResize) + }) + + return { + dragging, + fabStyle, + handlePointerDown, + handlePointerMove, + handlePointerUp, + handlePointerCancel, + resetToDefault + } +} diff --git a/src/views/base/index.vue b/src/views/base/index.vue index 82f34f7..fde1d30 100644 --- a/src/views/base/index.vue +++ b/src/views/base/index.vue @@ -118,8 +118,6 @@ :src="getProjectNavLogo(config.config?.avatar)" />
-