+
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)"
/>
-