diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f79b85d..6a11091 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -234,30 +234,41 @@ jobs: with: fetch-depth: 0 + - name: Check if release already exists + id: check_release + run: | + VERSION="${{ needs.prepare.outputs.version }}" + RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "") + if [ -n "$RELEASE_EXISTS" ]; then + echo "Release v$VERSION already exists. Skipping release creation." + echo "SKIP_RELEASE=true" >> $GITHUB_ENV + else + echo "Release v$VERSION does not exist. Proceeding with release creation." + echo "SKIP_RELEASE=false" >> $GITHUB_ENV + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download all artifacts + if: env.SKIP_RELEASE == 'false' uses: actions/download-artifact@v4 with: path: artifacts + - name: Update CHANGELOG + if: env.SKIP_RELEASE == 'false' + id: changelog + uses: requarks/changelog-action@v1 + with: + token: ${{ github.token }} + tag: ${{ github.ref_name }} + - name: Generate Release Body + if: env.SKIP_RELEASE == 'false' id: release_body run: | VERSION="${{ needs.prepare.outputs.version }}" - - # Get the most recent release tag (v* tags only) - LAST_TAG=$(git describe --match "v*" --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1` 2>/dev/null || echo "") - - if [ -n "$LAST_TAG" ]; then - echo "Debug: Found last release tag: $LAST_TAG" - CHANGES=$(git log ${LAST_TAG}..HEAD --pretty=format:"- %s") - else - echo "Debug: No previous release tag found, using first commit" - CHANGES=$(git log --pretty=format:"- %s") - fi - - echo "Debug: Changelog content:" - echo "$CHANGES" - + # Calculate hashes with corrected paths WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }') WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }') @@ -278,10 +289,9 @@ jobs: echo "Red Hat: $REDHAT_HASH" RELEASE_BODY=$(cat <<-EOF - ## ♻️ Changelog - - $CHANGES - + + ${{ needs.create-release.outputs.changelog }} + ## ⬇️ Downloads - [Windows (x64)](https://github.com/${{ github.repository }}/releases/download/v${VERSION}/Qopy-${VERSION}_x64.msi) - ${WINDOWS_64_HASH} @@ -299,6 +309,7 @@ jobs: echo "EOF" >> $GITHUB_ENV - name: Create Release + if: env.SKIP_RELEASE == 'false' uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/GET_STARTED.md b/GET_STARTED.md index 548a5e9..a793041 100644 --- a/GET_STARTED.md +++ b/GET_STARTED.md @@ -1,6 +1,6 @@ # Get Started -The default hotkey for Qopy is Windows+V which is also the hotkey for the default clipboard manager to turn that off follow [this guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md#disable-windowsv-for-default-clipboard-manager). +The default hotkey for Qopy is Windows+V which is also the hotkey for the default clipboard manager to turn that off follow [this guide](https://github.com/0PandaDEV/Qopy/blob/main/GET_STARTED.md#disable-windowsv-for-default-clipboard-manager). All the data of Qopy is stored inside of a SQLite database. @@ -12,7 +12,7 @@ All the data of Qopy is stored inside of a SQLite database. ## Disable Windows+V for default clipboard manager -https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620 + To disable the default clipboard manager popup from windows open Command prompt and run this command diff --git a/app.vue b/app.vue index 2d69efc..a60f802 100644 --- a/app.vue +++ b/app.vue @@ -1,27 +1,36 @@ - + \ No newline at end of file + diff --git a/assets/css/index.scss b/assets/css/index.scss index 2fb355a..5feb44b 100644 --- a/assets/css/index.scss +++ b/assets/css/index.scss @@ -22,7 +22,7 @@ $mutedtext: #78756f; position: fixed; top: 0; left: 0; - height: 54px; + height: 56px; background-color: transparent; outline: none; border: none; @@ -35,10 +35,10 @@ $mutedtext: #78756f; .results { position: absolute; - width: 284px; - top: 53px; + width: 286px; + top: 55px; left: 0; - height: calc(100vh - 95px); + height: 417px; border-right: 1px solid $divider; display: flex; flex-direction: column; @@ -46,6 +46,7 @@ $mutedtext: #78756f; padding-bottom: 8px; overflow-y: auto; overflow-x: hidden; + z-index: 3; .result { height: 40px; @@ -59,6 +60,7 @@ $mutedtext: #78756f; overflow: hidden; text-overflow: clip; white-space: nowrap; + color: $text; } .result { @@ -96,20 +98,22 @@ $mutedtext: #78756f; .content { position: absolute; - top: 53px; - left: 284px; - height: calc(100vh - 254px); + top: 55px; + left: 285px; + height: 220px; font-family: CommitMono !important; font-size: 12px; letter-spacing: 1; border-radius: 10px; - width: calc(100vw - 286px); + width: 465px; white-space: pre-wrap; word-wrap: break-word; display: flex; flex-direction: column; align-items: center; overflow: hidden; + z-index: 2; + color: $text; &:not(:has(.image)) { padding: 8px; @@ -128,7 +132,7 @@ $mutedtext: #78756f; } .bottom-bar { - height: 40px; + height: 39px; width: calc(100vw - 2px); backdrop-filter: blur(18px); background-color: hsla(40, 3%, 16%, 0.8); @@ -215,18 +219,20 @@ $mutedtext: #78756f; display: flex; flex-direction: column; gap: 14px; - bottom: 40px; - left: 284px; + bottom: 39px; + left: 285px; height: 160px; - width: calc(100vw - 286px); + width: 465px; border-top: 1px solid $divider; background-color: $primary; padding: 14px; + z-index: 1; .title { font-family: SFRoundedSemiBold; font-size: 12px; letter-spacing: 0.6px; + color: $text; } .info-content { diff --git a/assets/css/settings.scss b/assets/css/settings.scss index c593334..7f2eed0 100644 --- a/assets/css/settings.scss +++ b/assets/css/settings.scss @@ -36,42 +36,118 @@ $mutedtext: #78756f; } } -.keybind-container { +p { + font-family: SFRoundedMedium; +} + +.settings-container { + width: 100%; + margin-top: 26px; + position: relative; + font-size: 12px; + font-family: SFRoundedMedium; + + .settings { + position: absolute; + left: 50%; + transform: translateX(-50%); + margin-left: -26px; + display: flex; + gap: 24px; + + .names { + display: flex; + flex-direction: column; + gap: 16px; + + p { + font-family: SFRoundedSemiBold; + color: $text2; + display: flex; + justify-content: right; + } + } + + .actions { + display: flex; + flex-direction: column; + gap: 16px; + color: $mutedtext; + } + } +} + +.launch { display: flex; - flex-direction: column; align-items: center; - justify-content: center; - height: 100vh; gap: 6px; - .title { - font-size: 20px; - font-weight: 800; + input[type="checkbox"] { + appearance: none; + width: 14px; + height: 14px; + background-color: transparent; + border-radius: 5px; + border: 1px solid $mutedtext; + position: relative; + cursor: pointer; + transition: background-color 0.2s; + + &:checked { + ~ .checkmark { + opacity: 1; + } + } } - .keybind-input { - padding: 6px; - border: 1px solid $divider; - color: $text2; - display: flex; - border-radius: 13px; - outline: none; - gap: 6px; + .checkmark { + height: 14px; + width: 14px; + position: absolute; + opacity: 0; + transition: opacity 0.2s; + } - .key { - color: $text2; - font-family: SFRoundedMedium; - background-color: $divider; - padding: 6px 8px; - border-radius: 8px; - } + p { + color: $text2; } +} - .keybind-input:focus { - border: 1px solid rgba(255, 255, 255, 0.2); +.keybind-input { + width: min-content; + white-space: nowrap; + padding: 6px; + border: 1px solid $divider; + color: $text2; + display: flex; + border-radius: 10px; + outline: none; + gap: 4px; + + .key { + color: $text2; + font-family: SFRoundedMedium; + background-color: $divider; + padding: 2px 6px; + border-radius: 6px; + font-size: 14px; } } +.keybind-input:focus { + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.empty-keybind { + border-color: rgba(255, 82, 82, 0.298); +} + +.top-bar { + width: 100%; + height: 56px; + border-bottom: 1px solid $divider; +} + .bottom-bar { height: 40px; width: calc(100vw - 2px); @@ -136,6 +212,15 @@ $mutedtext: #78756f; background-color: transparent; transition: all 0.2s; cursor: pointer; + + p { + color: $text; + } + + &.disabled { + pointer-events: none; + opacity: 0.5; + } } .actions:hover { diff --git a/package.json b/package.json index aad552e..de5e05e 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,12 @@ "sass-embedded": "1.83.0", "uuid": "11.0.3", "vue": "3.5.13", - "wrdu-keyboard": "1.1.1" + "wrdu-keyboard": "3.0.0" }, "overrides": { "chokidar": "^3.6.0" + }, + "patchedDependencies": { + "wrdu-keyboard@3.0.0": "patches/wrdu-keyboard@3.0.0.patch" } -} \ No newline at end of file +} diff --git a/pages/index.vue b/pages/index.vue index c902e9b..8f1ad50 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -65,10 +65,17 @@ + @error=" + ($event.target as HTMLImageElement).src = + '../public/icons/Link.svg' + " /> - - + + {{ selectedItem?.content || "" }} @@ -135,9 +146,7 @@ {{ row.label }} - + {{ row.value }} @@ -153,12 +162,19 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; import "overlayscrollbars/overlayscrollbars.css"; import { app, window } from "@tauri-apps/api"; import { platform } from "@tauri-apps/plugin-os"; -import { enable, isEnabled } from "@tauri-apps/plugin-autostart"; import { listen } from "@tauri-apps/api/event"; import { useNuxtApp } from "#app"; import { invoke } from "@tauri-apps/api/core"; import { HistoryItem, ContentType } from "~/types/types"; -import type { InfoText, InfoImage, InfoFile, InfoLink, InfoColor, InfoCode } from "~/types/types"; +import type { + InfoText, + InfoImage, + InfoFile, + InfoLink, + InfoColor, + InfoCode, +} from "~/types/types"; +import { Key } from "wrdu-keyboard/key"; interface GroupedHistory { label: string; @@ -188,8 +204,8 @@ const imageSizes = shallowRef>({}); const lastUpdateTime = ref(Date.now()); const imageLoadError = ref(false); const imageLoading = ref(false); -const pageTitle = ref(''); -const pageOgImage = ref(''); +const pageTitle = ref(""); +const pageOgImage = ref(""); const keyboard = useKeyboard(); @@ -583,41 +599,35 @@ const setupEventListeners = async (): Promise => { searchInput.value?.blur(); }); - keyboard.down("ArrowDown", (event) => { - event.preventDefault(); + keyboard.prevent.down([Key.DownArrow], (event) => { selectNext(); }); - keyboard.down("ArrowUp", (event) => { - event.preventDefault(); + keyboard.prevent.down([Key.UpArrow], (event) => { selectPrevious(); }); - keyboard.down("Enter", (event) => { - event.preventDefault(); + keyboard.prevent.down([Key.Enter], (event) => { pasteSelectedItem(); }); - keyboard.down("Escape", (event) => { - event.preventDefault(); + keyboard.prevent.down([Key.Escape], (event) => { hideApp(); }); - keyboard.down("all", (event) => { - const isMacActionCombo = - os.value === "macos" && - (event.code === "MetaLeft" || event.code === "MetaRight") && - event.key === "k"; + switch (os.value) { + case "macos": + keyboard.prevent.down([Key.LeftMeta, Key.K], (event) => {}); - const isOtherOsActionCombo = - os.value !== "macos" && - (event.code === "ControlLeft" || event.code === "ControlRight") && - event.key === "k"; + keyboard.prevent.down([Key.RightMeta, Key.K], (event) => {}); + break; - if (isMacActionCombo || isOtherOsActionCombo) { - event.preventDefault(); - } - }); + case "linux" || "windows": + keyboard.prevent.down([Key.LeftControl, Key.K], (event) => {}); + + keyboard.prevent.down([Key.RightControl, Key.K], (event) => {}); + break; + } }; const hideApp = async (): Promise => { @@ -646,7 +656,7 @@ watch(searchQuery, () => { onMounted(async () => { try { - os.value = await platform(); + os.value = platform(); await loadHistoryChunk(); resultsContainer.value @@ -655,10 +665,6 @@ onMounted(async () => { ?.viewport?.addEventListener("scroll", handleScroll); await setupEventListeners(); - - if (!(await isEnabled())) { - await enable(); - } } catch (error) { console.error("Error during onMounted:", error); } @@ -686,27 +692,33 @@ const formatFileSize = (bytes: number): string => { const fetchPageMeta = async (url: string) => { try { - const [title, ogImage] = await invoke('fetch_page_meta', { url }) as [string, string | null]; + const [title, ogImage] = (await invoke("fetch_page_meta", { url })) as [ + string, + string | null + ]; pageTitle.value = title; if (ogImage) { pageOgImage.value = ogImage; } } catch (error) { - console.error('Error fetching page meta:', error); - pageTitle.value = 'Error loading title'; + console.error("Error fetching page meta:", error); + pageTitle.value = "Error loading title"; } }; -watch(() => selectedItem.value, (newItem) => { - if (newItem?.content_type === ContentType.Link) { - pageTitle.value = 'Loading...'; - pageOgImage.value = ''; - fetchPageMeta(newItem.content); - } else { - pageTitle.value = ''; - pageOgImage.value = ''; +watch( + () => selectedItem.value, + (newItem) => { + if (newItem?.content_type === ContentType.Link) { + pageTitle.value = "Loading..."; + pageOgImage.value = ""; + fetchPageMeta(newItem.content); + } else { + pageTitle.value = ""; + pageOgImage.value = ""; + } } -}); +); const getInfo = computed(() => { if (!selectedItem.value) return null; @@ -716,7 +728,10 @@ const getInfo = computed(() => { copied: selectedItem.value.timestamp, }; - const infoMap: Record InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode> = { + const infoMap: Record< + ContentType, + () => InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode + > = { [ContentType.Text]: () => ({ ...baseInfo, content_type: ContentType.Text, @@ -747,20 +762,21 @@ const getInfo = computed(() => { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); - + const rNorm = r / 255; const gNorm = g / 255; const bNorm = b / 255; - + const max = Math.max(rNorm, gNorm, bNorm); const min = Math.min(rNorm, gNorm, bNorm); - let h = 0, s = 0; + let h = 0, + s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - + switch (max) { case rNorm: h = (gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0); @@ -780,14 +796,16 @@ const getInfo = computed(() => { content_type: ContentType.Color, hex: hex, rgb: `rgb(${r}, ${g}, ${b})`, - hsl: `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`, + hsl: `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round( + l * 100 + )}%)`, }; }, [ContentType.Code]: () => ({ ...baseInfo, content_type: ContentType.Code, language: selectedItem.value!.language ?? "Unknown", - lines: selectedItem.value!.content.split('\n').length, + lines: selectedItem.value!.content.split("\n").length, }), }; @@ -799,24 +817,37 @@ const infoRows = computed(() => { const commonRows = [ { label: "Source", value: getInfo.value.source, isUrl: false }, - { label: "Content Type", value: getInfo.value.content_type.charAt(0).toUpperCase() + getInfo.value.content_type.slice(1), isUrl: false }, + { + label: "Content Type", + value: + getInfo.value.content_type.charAt(0).toUpperCase() + + getInfo.value.content_type.slice(1), + isUrl: false, + }, ]; - const typeSpecificRows: Record> = { + const typeSpecificRows: Record< + ContentType, + Array<{ label: string; value: string | number; isUrl?: boolean }> + > = { [ContentType.Text]: [ { label: "Characters", value: (getInfo.value as InfoText).characters }, { label: "Words", value: (getInfo.value as InfoText).words }, ], [ContentType.Image]: [ { label: "Dimensions", value: (getInfo.value as InfoImage).dimensions }, - { label: "Image size", value: formatFileSize((getInfo.value as InfoImage).size) }, + { + label: "Image size", + value: formatFileSize((getInfo.value as InfoImage).size), + }, ], [ContentType.File]: [ { label: "Path", value: (getInfo.value as InfoFile).path }, ], [ContentType.Link]: [ - ...((getInfo.value as InfoLink).title && (getInfo.value as InfoLink).title !== 'Loading...' - ? [{ label: "Title", value: (getInfo.value as InfoLink).title || '' }] + ...((getInfo.value as InfoLink).title && + (getInfo.value as InfoLink).title !== "Loading..." + ? [{ label: "Title", value: (getInfo.value as InfoLink).title || "" }] : []), { label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true }, { label: "Characters", value: (getInfo.value as InfoLink).characters }, @@ -832,8 +863,9 @@ const infoRows = computed(() => { ], }; - const specificRows = typeSpecificRows[getInfo.value.content_type] - .filter(row => row.value !== ""); + const specificRows = typeSpecificRows[getInfo.value.content_type].filter( + (row) => row.value !== "" + ); return [ ...commonRows, diff --git a/pages/settings.vue b/pages/settings.vue index 6fd6417..d4814e0 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -1,8 +1,10 @@ - - - Back + + + + Back + @@ -10,7 +12,10 @@ Qopy - + Save @@ -23,25 +28,61 @@ - - Record a new Hotkey - - Click here - - - {{ keyToDisplay(key) }} - - + + + + Startup + Qopy Hotkey + + + + + + + + + + + + + Launch Qopy at login + + + Click here + + + {{ keyToLabel(key) }} + + + + @@ -52,62 +93,43 @@ import { invoke } from "@tauri-apps/api/core"; import { onMounted, onUnmounted, reactive, ref } from "vue"; import { platform } from "@tauri-apps/plugin-os"; import { useRouter } from "vue-router"; +import { Key } from "wrdu-keyboard/key"; +import { KeyValues, KeyLabels } from "../types/keys"; +import { disable, enable } from "@tauri-apps/plugin-autostart"; -const activeModifiers = reactive>(new Set()); +const activeModifiers = reactive>(new Set()); const isKeybindInputFocused = ref(false); -const keybind = ref([]); +const keybind = ref([]); const keybindInput = ref(null); const lastBlurTime = ref(0); const os = ref(""); const router = useRouter(); const keyboard = useKeyboard(); - -const keyToDisplayMap: Record = { - " ": "Space", - Alt: "Alt", - AltLeft: "Alt L", - AltRight: "Alt R", - ArrowDown: "↓", - ArrowLeft: "←", - ArrowRight: "→", - ArrowUp: "↑", - Control: "Ctrl", - ControlLeft: "Ctrl L", - ControlRight: "Ctrl R", - Enter: "↵", - Meta: "Meta", - MetaLeft: "Meta L", - MetaRight: "Meta R", - Shift: "⇧", - ShiftLeft: "⇧ L", - ShiftRight: "⇧ R", -}; +const showEmptyKeybindError = ref(false); +const autostart = ref(false); +const { $settings } = useNuxtApp(); const modifierKeySet = new Set([ - "Alt", - "AltLeft", - "AltRight", - "Control", - "ControlLeft", - "ControlRight", - "Meta", - "MetaLeft", - "MetaRight", - "Shift", - "ShiftLeft", - "ShiftRight", + KeyValues.AltLeft, + KeyValues.AltRight, + KeyValues.ControlLeft, + KeyValues.ControlRight, + KeyValues.MetaLeft, + KeyValues.MetaRight, + KeyValues.ShiftLeft, + KeyValues.ShiftRight, ]); -const isModifier = (key: string): boolean => { +const isModifier = (key: KeyValues): boolean => { return modifierKeySet.has(key); }; -const keyToDisplay = (key: string): string => { - return keyToDisplayMap[key] || key; +const keyToLabel = (key: KeyValues): string => { + return KeyLabels[key] || key; }; const updateKeybind = () => { - const modifiers = Array.from(activeModifiers).sort(); + const modifiers = Array.from(activeModifiers); const nonModifiers = keybind.value.filter((key) => !isModifier(key)); keybind.value = [...modifiers, ...nonModifiers]; }; @@ -115,19 +137,20 @@ const updateKeybind = () => { const onBlur = () => { isKeybindInputFocused.value = false; lastBlurTime.value = Date.now(); + showEmptyKeybindError.value = false; }; const onFocus = () => { isKeybindInputFocused.value = true; activeModifiers.clear(); keybind.value = []; + showEmptyKeybindError.value = false; }; const onKeyDown = (event: KeyboardEvent) => { - event.preventDefault(); - const key = event.code; + const key = event.code as KeyValues; - if (key === "Escape") { + if (key === KeyValues.Escape) { if (keybindInput.value) { keybindInput.value.blur(); } @@ -142,45 +165,79 @@ const onKeyDown = (event: KeyboardEvent) => { } updateKeybind(); + showEmptyKeybindError.value = false; }; const saveKeybind = async () => { - console.log("New:", keybind.value); - const oldKeybind = await invoke("get_keybind"); - console.log("Old:", oldKeybind); - await invoke("save_keybind", { keybind: keybind.value }); + if (keybind.value.length > 0) { + await $settings.saveSetting("keybind", JSON.stringify(keybind.value)); + router.push("/"); + } else { + showEmptyKeybindError.value = true; + } +}; + +const toggleAutostart = async () => { + if (autostart.value === true) { + await enable(); + } else { + await disable(); + } + await $settings.saveSetting("autostart", autostart.value ? "true" : "false"); }; -onMounted(() => { - os.value = platform(); - - keyboard.down("all", (event) => { - const isMacSaveCombo = - os.value === "macos" && - (event.code === "MetaLeft" || event.code === "MetaRight") && - event.key === "Enter"; - - const isOtherOsSaveCombo = - os.value !== "macos" && - (event.code === "ControlLeft" || event.code === "ControlRight") && - event.key === "Enter"; - - if ( - (isMacSaveCombo || isOtherOsSaveCombo) && - !isKeybindInputFocused.value - ) { - event.preventDefault(); - saveKeybind(); +os.value = platform(); + +onMounted(async () => { + keyboard.down([Key.All], (event) => { + if (isKeybindInputFocused.value) { + onKeyDown(event); } }); - keyboard.down("Escape", (event) => { - const now = Date.now(); - if (!isKeybindInputFocused.value && now - lastBlurTime.value > 100) { - event.preventDefault(); + keyboard.down([Key.Escape], (event) => { + if (isKeybindInputFocused.value) { + keybindInput.value?.blur(); + } else { router.push("/"); } }); + + switch (os.value) { + case "macos": + keyboard.down([Key.LeftMeta, Key.Enter], (event) => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }); + + keyboard.down([Key.RightMeta, Key.Enter], (event) => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }); + break; + + case "linux" || "windows": + keyboard.down([Key.LeftControl, Key.Enter], (event) => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }); + + keyboard.down([Key.RightControl, Key.Enter], (event) => { + if (!isKeybindInputFocused.value) { + saveKeybind(); + } + }); + break; + } + + autostart.value = (await $settings.getSetting("autostart")) === "true"; +}); + +onUnmounted(() => { + keyboard.unregisterAll(); }); diff --git a/patches/wrdu-keyboard@3.0.0.patch b/patches/wrdu-keyboard@3.0.0.patch new file mode 100644 index 0000000..bc77583 --- /dev/null +++ b/patches/wrdu-keyboard@3.0.0.patch @@ -0,0 +1,131 @@ +diff --git a/node_modules/wrdu-keyboard/.DS_Store b/.DS_Store +new file mode 100644 +index 0000000000000000000000000000000000000000..4b7e9446f3580fab3e4feaba097bcdaf98c5833c +Binary files /dev/null and b/.DS_Store differ +diff --git a/dist/runtime/keyboard.d.ts b/dist/runtime/keyboard.d.ts +index aeae40f3d2bc3efd459cce04c29c21c43884154d..6131bab4895ebb3048a5225f366430d23c5f1f13 100644 +--- a/dist/runtime/keyboard.d.ts ++++ b/dist/runtime/keyboard.d.ts +@@ -1,15 +1,16 @@ +-import { Key } from './types/keys.js'; +-import { type Plugin } from '#app'; ++import { Key } from "./types/keys.js"; ++import { type Plugin } from "#app"; + type Handler = (event: KeyboardEvent) => void; + type Config = { + once?: boolean; + prevent?: boolean; + }; +-type PublicConfig = Omit; ++type PublicConfig = Omit; + type New = (keys: Key[], handler: Handler, config?: PublicConfig) => void; + export interface Keyboard { + init: () => void; + stop: () => void; ++ unregisterAll: () => void; + down: New; + up: New; + prevent: { +diff --git a/dist/runtime/keyboard.js b/dist/runtime/keyboard.js +index e16f600258cee90d185ffc52777bed95c14bd93e..5ddec447a5dc66ffe063eb9f9dd765c9045bdaf7 100644 +--- a/dist/runtime/keyboard.js ++++ b/dist/runtime/keyboard.js +@@ -1,45 +1,54 @@ + import { Key } from "./types/keys.js"; + import { defineNuxtPlugin } from "#app"; +-const getKeyString = (keys) => keys[0] == Key.All ? keys.sort().join("+") : "All"; ++const getKeyString = (keys) => keys.includes(Key.All) ? "All" : keys.sort().join("+"); + const handlers = { + down: {}, + up: {} + }; + const pressedKeys = /* @__PURE__ */ new Set(); + const onKeydown = (event) => { +- pressedKeys.add(event.code); ++ const key = event.code; ++ pressedKeys.add(key); + const pressedArray = Array.from(pressedKeys); +- const keyString = getKeyString(pressedArray); +- if (handlers.down[keyString]) { +- handlers.down[keyString].forEach((eventHandler) => { +- if (eventHandler.prevent) { +- event.preventDefault(); +- } +- eventHandler.handler(event); +- if (eventHandler.once) { +- handlers.down[keyString] = handlers.down[keyString].filter((h) => h !== eventHandler); +- } +- }); ++ for (const keyString of [getKeyString(pressedArray), "All"]) { ++ if (handlers.down[keyString]) { ++ handlers.down[keyString].forEach((eventHandler) => { ++ if (eventHandler.prevent) { ++ event.preventDefault(); ++ } ++ eventHandler.handler(event); ++ if (eventHandler.once) { ++ handlers.down[keyString] = handlers.down[keyString].filter( ++ (h) => h !== eventHandler ++ ); ++ } ++ }); ++ } + } + }; + const onKeyup = (event) => { +- pressedKeys.delete(event.code); ++ const key = event.code; ++ pressedKeys.delete(key); + const releasedArray = Array.from(pressedKeys); +- const keyString = getKeyString(releasedArray); +- if (handlers.up[keyString]) { +- handlers.up[keyString].forEach((eventHandler) => { +- if (eventHandler.prevent) { +- event.preventDefault(); +- } +- eventHandler.handler(event); +- if (eventHandler.once) { +- handlers.up[keyString] = handlers.up[keyString].filter((h) => h !== eventHandler); +- } +- }); ++ for (const keyString of [getKeyString(releasedArray), "All"]) { ++ if (handlers.up[keyString]) { ++ handlers.up[keyString].forEach((eventHandler) => { ++ if (eventHandler.prevent) { ++ event.preventDefault(); ++ } ++ eventHandler.handler(event); ++ if (eventHandler.once) { ++ handlers.up[keyString] = handlers.up[keyString].filter( ++ (h) => h !== eventHandler ++ ); ++ } ++ }); ++ } + } + }; + const init = () => { + stop(); ++ pressedKeys.clear(); + window.addEventListener("keydown", onKeydown); + window.addEventListener("keyup", onKeyup); + }; +@@ -47,6 +56,10 @@ const stop = () => { + window.removeEventListener("keydown", onKeydown); + window.removeEventListener("keyup", onKeyup); + }; ++const unregisterAll = () => { ++ handlers.down = {}; ++ handlers.up = {}; ++}; + const down = (keys, handler, config = {}) => { + if (keys.includes(Key.All)) { + keys = [Key.All]; +@@ -84,6 +97,7 @@ const keyboard = defineNuxtPlugin((nuxtApp) => { + keyboard: { + init, + stop, ++ unregisterAll, + down: (keys, handler, config = {}) => down(keys, handler, config), + up: (keys, handler, config = {}) => up(keys, handler, config), + prevent: { diff --git a/plugins/settings.ts b/plugins/settings.ts index 601efea..4ca1b52 100644 --- a/plugins/settings.ts +++ b/plugins/settings.ts @@ -12,14 +12,6 @@ export default defineNuxtPlugin(() => { async saveSetting(key: string, value: string): Promise { await invoke("save_setting", { key, value }); }, - - async getKeybind(): Promise { - return await invoke("get_keybind"); - }, - - async saveKeybind(keybind: string[]): Promise { - await invoke("save_keybind", { keybind }); - }, }, }, }; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 31fba10..a5d7869 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4054,7 +4054,7 @@ dependencies = [ [[package]] name = "qopy" -version = "0.3.3" +version = "0.3.4" dependencies = [ "active-win-pos-rs", "applications", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 08142a1..45e9dd7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qopy" -version = "0.3.3" +version = "0.3.4" description = "Qopy" authors = ["pandadev"] edition = "2021" diff --git a/src-tauri/src/api/clipboard.rs b/src-tauri/src/api/clipboard.rs index 59ac7b1..381a7d9 100644 --- a/src-tauri/src/api/clipboard.rs +++ b/src-tauri/src/api/clipboard.rs @@ -1,14 +1,14 @@ use tauri_plugin_aptabase::EventTracker; -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::{ engine::general_purpose::STANDARD, Engine }; // use hyperpolyglot; use lazy_static::lazy_static; -use rdev::{simulate, EventType, Key}; +use rdev::{ simulate, EventType, Key }; use regex::Regex; use sqlx::SqlitePool; use std::fs; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::{thread, time::Duration}; -use tauri::{AppHandle, Emitter, Listener, Manager}; +use std::sync::atomic::{ AtomicBool, Ordering }; +use std::{ thread, time::Duration }; +use tauri::{ AppHandle, Emitter, Listener, Manager }; use tauri_plugin_clipboard::Clipboard; use tokio::runtime::Runtime as TokioRuntime; use url::Url; @@ -17,7 +17,7 @@ use uuid::Uuid; use crate::db; use crate::utils::commands::get_app_info; use crate::utils::favicon::fetch_favicon_as_base64; -use crate::utils::types::{ContentType, HistoryItem}; +use crate::utils::types::{ ContentType, HistoryItem }; lazy_static! { static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false); @@ -27,16 +27,14 @@ lazy_static! { pub async fn write_and_paste( app_handle: AppHandle, content: String, - content_type: String, + content_type: String ) -> Result<(), String> { let clipboard = app_handle.state::(); match content_type.as_str() { "text" => clipboard.write_text(content).map_err(|e| e.to_string())?, "image" => { - clipboard - .write_image_base64(content) - .map_err(|e| e.to_string())?; + clipboard.write_image_base64(content).map_err(|e| e.to_string())?; } "files" => { clipboard @@ -44,11 +42,13 @@ pub async fn write_and_paste( content .split(", ") .map(|file| file.to_string()) - .collect::>(), + .collect::>() ) .map_err(|e| e.to_string())?; } - _ => return Err("Unsupported content type".to_string()), + _ => { + return Err("Unsupported content type".to_string()); + } } IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst); @@ -65,7 +65,7 @@ pub async fn write_and_paste( EventType::KeyPress(modifier_key), EventType::KeyPress(Key::KeyV), EventType::KeyRelease(Key::KeyV), - EventType::KeyRelease(modifier_key), + EventType::KeyRelease(modifier_key) ]; for event in events { @@ -81,9 +81,12 @@ pub async fn write_and_paste( IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst); }); - let _ = app_handle.track_event("clipboard_paste", Some(serde_json::json!({ + let _ = app_handle.track_event( + "clipboard_paste", + Some(serde_json::json!({ "content_type": content_type - }))); + })) + ); Ok(()) } @@ -92,79 +95,92 @@ pub fn setup(app: &AppHandle) { let app_handle = app.clone(); let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime"); - app_handle.clone().listen( - "plugin:clipboard://clipboard-monitor/update", - move |_event| { - let app_handle = app_handle.clone(); - runtime.block_on(async move { - if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) { - return; - } + app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| { + let app_handle = app_handle.clone(); + runtime.block_on(async move { + if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) { + return; + } - let clipboard = app_handle.state::(); - let available_types = clipboard.available_types().unwrap(); + let clipboard = app_handle.state::(); + let available_types = clipboard.available_types().unwrap(); - let (app_name, app_icon) = get_app_info(); + let (app_name, app_icon) = get_app_info(); - match get_pool(&app_handle).await { - Ok(pool) => { - if available_types.image { - println!("Handling image change"); - if let Ok(image_data) = clipboard.read_image_base64() { - let file_path = save_image_to_file(&app_handle, &image_data) - .await - .map_err(|e| e.to_string()) - .unwrap_or_else(|e| e); + match get_pool(&app_handle).await { + Ok(pool) => { + if available_types.image { + println!("Handling image change"); + if let Ok(image_data) = clipboard.read_image_base64() { + let file_path = save_image_to_file(&app_handle, &image_data).await + .map_err(|e| e.to_string()) + .unwrap_or_else(|e| e); + let _ = db::history::add_history_item( + app_handle.clone(), + pool, + HistoryItem::new( + app_name, + ContentType::Image, + file_path, + None, + app_icon, + None + ) + ).await; + } + } else if available_types.files { + println!("Handling files change"); + if let Ok(files) = clipboard.read_files() { + for file in files { let _ = db::history::add_history_item( app_handle.clone(), - pool, - HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon, None) + pool.clone(), + HistoryItem::new( + app_name.clone(), + ContentType::File, + file, + None, + app_icon.clone(), + None + ) ).await; } - } else if available_types.files { - println!("Handling files change"); - if let Ok(files) = clipboard.read_files() { - for file in files { + } + } else if available_types.text { + println!("Handling text change"); + if let Ok(text) = clipboard.read_text() { + let text = text.to_string(); + let url_regex = Regex::new( + r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$" + ).unwrap(); + + if url_regex.is_match(&text) { + if let Ok(url) = Url::parse(&text) { + let favicon = match fetch_favicon_as_base64(url).await { + Ok(Some(f)) => Some(f), + _ => None, + }; + let _ = db::history::add_history_item( app_handle.clone(), - pool.clone(), + pool, HistoryItem::new( - app_name.clone(), - ContentType::File, - file, - None, - app_icon.clone(), + app_name, + ContentType::Link, + text, + favicon, + app_icon, None - ), + ) ).await; } - } - } else if available_types.text { - println!("Handling text change"); - if let Ok(text) = clipboard.read_text() { - let text = text.to_string(); - let url_regex = Regex::new(r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$").unwrap(); - - if url_regex.is_match(&text) { - if let Ok(url) = Url::parse(&text) { - let favicon = match fetch_favicon_as_base64(url).await { - Ok(Some(f)) => Some(f), - _ => None, - }; - - let _ = db::history::add_history_item( - app_handle.clone(), - pool, - HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon, None) - ).await; - } - } else { - if text.is_empty() { - return; - } + } else { + if text.is_empty() { + return; + } - // Temporarily disabled code detection - /*if let Some(detection) = hyperpolyglot::detect_from_text(&text) { + // Temporarily disabled code detection + /*if let Some(detection) = hyperpolyglot::detect_from_text(&text) { let language = match detection { hyperpolyglot::Detection::Heuristics(lang) => lang.to_string(), _ => detection.language().to_string(), @@ -175,43 +191,61 @@ pub fn setup(app: &AppHandle) { HistoryItem::new(app_name, ContentType::Code, text, None, app_icon, Some(language)) ).await; } else*/ if crate::utils::commands::detect_color(&text) { - let _ = db::history::add_history_item( - app_handle.clone(), - pool, - HistoryItem::new(app_name, ContentType::Color, text, None, app_icon, None) - ).await; - } else { - let _ = db::history::add_history_item( - app_handle.clone(), - pool, - HistoryItem::new(app_name, ContentType::Text, text.clone(), None, app_icon, None) - ).await; - } + let _ = db::history::add_history_item( + app_handle.clone(), + pool, + HistoryItem::new( + app_name, + ContentType::Color, + text, + None, + app_icon, + None + ) + ).await; + } else { + let _ = db::history::add_history_item( + app_handle.clone(), + pool, + HistoryItem::new( + app_name, + ContentType::Text, + text.clone(), + None, + app_icon, + None + ) + ).await; } } - } else { - println!("Unknown clipboard content type"); } + } else { + println!("Unknown clipboard content type"); } - Err(e) => { - println!("Failed to get database pool: {}", e); - } } + Err(e) => { + println!("Failed to get database pool: {}", e); + } + } - let _ = app_handle.emit("clipboard-content-updated", ()); - let _ = app_handle.track_event("clipboard_copied", Some(serde_json::json!({ + let _ = app_handle.emit("clipboard-content-updated", ()); + let _ = app_handle.track_event( + "clipboard_copied", + Some( + serde_json::json!({ "content_type": if available_types.image { "image" } else if available_types.files { "files" } else if available_types.text { "text" } else { "unknown" } - }))); - }); - }, - ); + }) + ) + ); + }); + }); } async fn get_pool( - app_handle: &AppHandle, + app_handle: &AppHandle ) -> Result, Box> { Ok(app_handle.state::()) } @@ -219,9 +253,7 @@ async fn get_pool( #[tauri::command] pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> { let clipboard = app_handle.state::(); - clipboard - .start_monitor(app_handle.clone()) - .map_err(|e| e.to_string())?; + clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?; app_handle .emit("plugin:clipboard://clipboard-monitor/status", true) .map_err(|e| e.to_string())?; @@ -230,7 +262,7 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> { async fn save_image_to_file( app_handle: &AppHandle, - base64_data: &str, + base64_data: &str ) -> Result> { let app_data_dir = app_handle.path().app_data_dir().unwrap(); let images_dir = app_data_dir.join("images"); diff --git a/src-tauri/src/api/hotkeys.rs b/src-tauri/src/api/hotkeys.rs index 0470e24..a0084eb 100644 --- a/src-tauri/src/api/hotkeys.rs +++ b/src-tauri/src/api/hotkeys.rs @@ -1,48 +1,60 @@ -use tauri_plugin_aptabase::EventTracker; use crate::utils::commands::center_window_on_current_monitor; +use crate::utils::keys::KeyCode; use global_hotkey::{ - hotkey::{Code, HotKey, Modifiers}, - GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, + hotkey::{ Code, HotKey, Modifiers }, + GlobalHotKeyEvent, + GlobalHotKeyManager, + HotKeyState, }; -use std::cell::RefCell; +use lazy_static::lazy_static; use std::str::FromStr; -use tauri::{AppHandle, Listener, Manager}; +use std::sync::Mutex; +use tauri::{ AppHandle, Listener, Manager }; +use tauri_plugin_aptabase::EventTracker; -thread_local! { - static HOTKEY_MANAGER: RefCell> = RefCell::new(None); +lazy_static! { + static ref HOTKEY_MANAGER: Mutex> = Mutex::new(None); + static ref REGISTERED_HOTKEY: Mutex> = Mutex::new(None); } pub fn setup(app_handle: tauri::AppHandle) { let app_handle_clone = app_handle.clone(); - let manager = GlobalHotKeyManager::new().expect("Failed to initialize hotkey manager"); - HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager)); + let manager = match GlobalHotKeyManager::new() { + Ok(manager) => manager, + Err(err) => { + eprintln!("Failed to initialize hotkey manager: {:?}", err); + return; + } + }; + + { + let mut manager_guard = HOTKEY_MANAGER.lock().unwrap(); + *manager_guard = Some(manager); + } let rt = app_handle.state::(); let initial_keybind = rt .block_on(crate::db::settings::get_keybind(app_handle_clone.clone())) .expect("Failed to get initial keybind"); - let initial_shortcut = initial_keybind.join("+"); - - let initial_shortcut_for_update = initial_shortcut.clone(); - let initial_shortcut_for_save = initial_shortcut.clone(); - if let Err(e) = register_shortcut(&initial_shortcut) { + if let Err(e) = register_shortcut(&initial_keybind) { eprintln!("Error registering initial shortcut: {:?}", e); } app_handle.listen("update-shortcut", move |event| { - let payload_str = event.payload().to_string(); + let payload_str = event.payload(); - if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_update) { - HOTKEY_MANAGER.with(|manager| { - if let Some(manager) = manager.borrow().as_ref() { - let _ = manager.unregister(old_hotkey); - } - }); + if let Some(old_hotkey) = REGISTERED_HOTKEY.lock().unwrap().take() { + let manager_guard = HOTKEY_MANAGER.lock().unwrap(); + if let Some(manager) = manager_guard.as_ref() { + let _ = manager.unregister(old_hotkey); + } } - if let Err(e) = register_shortcut(&payload_str) { + let payload: Vec = serde_json::from_str(payload_str).unwrap_or_default(); + + if let Err(e) = register_shortcut(&payload) { eprintln!("Error re-registering shortcut: {:?}", e); } }); @@ -50,15 +62,15 @@ pub fn setup(app_handle: tauri::AppHandle) { app_handle.listen("save_keybind", move |event| { let payload_str = event.payload().to_string(); - if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_save) { - HOTKEY_MANAGER.with(|manager| { - if let Some(manager) = manager.borrow().as_ref() { - let _ = manager.unregister(old_hotkey); - } - }); + if let Some(old_hotkey) = REGISTERED_HOTKEY.lock().unwrap().take() { + let manager_guard = HOTKEY_MANAGER.lock().unwrap(); + if let Some(manager) = manager_guard.as_ref() { + let _ = manager.unregister(old_hotkey); + } } - if let Err(e) = register_shortcut(&payload_str) { + let payload: Vec = serde_json::from_str(&payload_str).unwrap_or_default(); + if let Err(e) = register_shortcut(&payload) { eprintln!("Error registering saved shortcut: {:?}", e); } }); @@ -81,48 +93,44 @@ pub fn setup(app_handle: tauri::AppHandle) { }); } -fn register_shortcut(shortcut: &str) -> Result<(), Box> { +fn register_shortcut(shortcut: &[String]) -> Result<(), Box> { let hotkey = parse_hotkey(shortcut)?; - HOTKEY_MANAGER.with(|manager| { - if let Some(manager) = manager.borrow().as_ref() { - manager.register(hotkey)?; - } + + let manager_guard = HOTKEY_MANAGER.lock().unwrap(); + if let Some(manager) = manager_guard.as_ref() { + manager.register(hotkey.clone())?; + *REGISTERED_HOTKEY.lock().unwrap() = Some(hotkey); Ok(()) - }) + } else { + Err("Hotkey manager not initialized".into()) + } } -fn parse_hotkey(shortcut: &str) -> Result> { +fn parse_hotkey(shortcut: &[String]) -> Result> { let mut modifiers = Modifiers::empty(); let mut code = None; - let shortcut = shortcut.replace("\"", ""); - - for part in shortcut.split('+') { - let part = part.trim().to_lowercase(); + for part in shortcut { match part.as_str() { - "ctrl" | "control" | "controlleft" => modifiers |= Modifiers::CONTROL, - "alt" | "altleft" | "optionleft" => modifiers |= Modifiers::ALT, - "shift" | "shiftleft" => modifiers |= Modifiers::SHIFT, - "super" | "meta" | "cmd" | "metaleft" => modifiers |= Modifiers::META, + "ControlLeft" => { + modifiers |= Modifiers::CONTROL; + } + "AltLeft" => { + modifiers |= Modifiers::ALT; + } + "ShiftLeft" => { + modifiers |= Modifiers::SHIFT; + } + "MetaLeft" => { + modifiers |= Modifiers::META; + } key => { - let key_code = if key.starts_with("key") { - "Key".to_string() + &key[3..].to_uppercase() - } else if key.len() == 1 && key.chars().next().unwrap().is_alphabetic() { - "Key".to_string() + &key.to_uppercase() - } else { - key.to_string() - }; - - code = Some( - Code::from_str(&key_code) - .map_err(|_| format!("Invalid key code: {}", key_code))?, - ); + code = Some(Code::from(KeyCode::from_str(key)?)); } } } - let key_code = - code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?; + let key_code = code.ok_or_else(|| "No valid key code found".to_string())?; Ok(HotKey::new(Some(modifiers), key_code)) } @@ -144,7 +152,12 @@ fn handle_hotkey_event(app_handle: &AppHandle) { center_window_on_current_monitor(&window); } - let _ = app_handle.track_event("hotkey_triggered", Some(serde_json::json!({ - "action": if window.is_visible().unwrap() { "hide" } else { "show" } - }))); + let _ = app_handle.track_event( + "hotkey_triggered", + Some( + serde_json::json!({ + "action": if window.is_visible().unwrap() { "hide" } else { "show" } + }) + ) + ); } diff --git a/src-tauri/src/api/tray.rs b/src-tauri/src/api/tray.rs index 72d24ec..2ae2bfa 100644 --- a/src-tauri/src/api/tray.rs +++ b/src-tauri/src/api/tray.rs @@ -1,16 +1,15 @@ -use tauri::{ - menu::{MenuBuilder, MenuItemBuilder}, - tray::TrayIconBuilder, - Emitter, Manager, -}; +use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager }; use tauri_plugin_aptabase::EventTracker; pub fn setup(app: &mut tauri::App) -> Result<(), Box> { let window = app.get_webview_window("main").unwrap(); let is_visible = window.is_visible().unwrap(); - let _ = app.track_event("tray_toggle", Some(serde_json::json!({ + let _ = app.track_event( + "tray_toggle", + Some(serde_json::json!({ "action": if is_visible { "hide" } else { "show" } - }))); + })) + ); let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png"); let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap(); @@ -18,45 +17,42 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box> { let _tray = TrayIconBuilder::new() .menu( &MenuBuilder::new(app) - .items(&[&MenuItemBuilder::with_id("app_name", "Qopy") - .enabled(false) - .build(app)?]) + .items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?]) .items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?]) - .items(&[&MenuItemBuilder::with_id("keybind", "Change keybind").build(app)?]) - .items(&[&MenuItemBuilder::with_id("check_updates", "Check for updates").build(app)?]) + .items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?]) .items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?]) - .build()?, + .build()? ) - .on_menu_event(move |_app, event| match event.id().as_ref() { - "quit" => { - let _ = _app.track_event("app_quit", None); - std::process::exit(0); - } - "show" => { - let _ = _app.track_event("tray_toggle", Some(serde_json::json!({ + .on_menu_event(move |_app, event| { + match event.id().as_ref() { + "quit" => { + let _ = _app.track_event("app_quit", None); + std::process::exit(0); + } + "show" => { + let _ = _app.track_event( + "tray_toggle", + Some( + serde_json::json!({ "action": if is_visible { "hide" } else { "show" } - }))); - let is_visible = window.is_visible().unwrap(); - if is_visible { - window.hide().unwrap(); - } else { - window.show().unwrap(); - window.set_focus().unwrap(); + }) + ) + ); + let is_visible = window.is_visible().unwrap(); + if is_visible { + window.hide().unwrap(); + } else { + window.show().unwrap(); + window.set_focus().unwrap(); + } + window.emit("main_route", ()).unwrap(); } - window.emit("main_route", ()).unwrap(); - } - "keybind" => { - let _ = _app.track_event("tray_keybind_change", None); - window.emit("change_keybind", ()).unwrap(); - } - "check_updates" => { - let _ = _app.track_event("tray_check_updates", None); - let app_handle = _app.app_handle().clone(); - tauri::async_runtime::spawn(async move { - crate::api::updater::check_for_updates(app_handle, true).await; - }); + "settings" => { + let _ = _app.track_event("tray_settings", None); + window.emit("settings", ()).unwrap(); + } + _ => (), } - _ => (), }) .icon(icon) .build(app)?; diff --git a/src-tauri/src/api/updater.rs b/src-tauri/src/api/updater.rs index 8c600fa..5b153a6 100644 --- a/src-tauri/src/api/updater.rs +++ b/src-tauri/src/api/updater.rs @@ -1,6 +1,5 @@ -use tauri::Manager; -use tauri::{async_runtime, AppHandle}; -use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; +use tauri::{ async_runtime, AppHandle }; +use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind }; use tauri_plugin_updater::UpdaterExt; pub async fn check_for_updates(app: AppHandle, prompted: bool) { @@ -26,18 +25,35 @@ pub async fn check_for_updates(app: AppHandle, prompted: bool) { app.dialog() .message(msg) .title("Qopy Update Available") - .buttons(MessageDialogButtons::OkCancelCustom(String::from("Install"), String::from("Cancel"))) + .buttons( + MessageDialogButtons::OkCancelCustom( + String::from("Install"), + String::from("Cancel") + ) + ) .show(move |response| { if !response { return; } async_runtime::spawn(async move { - match update.download_and_install(|_, _| {}, || {}).await { + match + update.download_and_install( + |_, _| {}, + || {} + ).await + { Ok(_) => { app.dialog() - .message("Update installed successfully. The application needs to restart to apply the changes.") - .title("Qopy Needs to Restart") - .buttons(MessageDialogButtons::OkCancelCustom(String::from("Restart"), String::from("Cancel"))) + .message( + "Update installed successfully. The application needs to restart to apply the changes." + ) + .title("Qopy Update Installed") + .buttons( + MessageDialogButtons::OkCancelCustom( + String::from("Restart"), + String::from("Cancel") + ) + ) .show(move |response| { if response { app.restart(); @@ -47,7 +63,9 @@ pub async fn check_for_updates(app: AppHandle, prompted: bool) { Err(e) => { println!("Error installing new update: {:?}", e); app.dialog() - .message("Failed to install new update. The new update can be downloaded from Github") + .message( + "Failed to install new update. The new update can be downloaded from Github" + ) .kind(MessageDialogKind::Error) .show(|_| {}); } diff --git a/src-tauri/src/db/database.rs b/src-tauri/src/db/database.rs index 4a3468d..264d308 100644 --- a/src-tauri/src/db/database.rs +++ b/src-tauri/src/db/database.rs @@ -1,5 +1,5 @@ -use include_dir::{include_dir, Dir}; -use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; +use include_dir::{ include_dir, Dir }; +use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions }; use std::fs; use tauri::Manager; use tokio::runtime::Runtime as TokioRuntime; @@ -25,8 +25,7 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box> { let pool = rt.block_on(async { SqlitePoolOptions::new() .max_connections(5) - .connect(&db_url) - .await + .connect(&db_url).await .expect("Failed to create pool") }); @@ -49,29 +48,27 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box> { } async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box> { - sqlx::query( - "CREATE TABLE IF NOT EXISTS schema_version ( + sqlx + ::query( + "CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP - );", - ) - .execute(pool) - .await?; + );" + ) + .execute(pool).await?; - let current_version: Option = - sqlx::query_scalar("SELECT MAX(version) FROM schema_version") - .fetch_one(pool) - .await?; + let current_version: Option = sqlx + ::query_scalar("SELECT MAX(version) FROM schema_version") + .fetch_one(pool).await?; let current_version = current_version.unwrap_or(0); - let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR - .files() + let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files() .filter_map(|file| { let file_name = file.path().file_name()?.to_str()?; - if file_name.ends_with(".sql") && file_name.starts_with("migration") { + if file_name.ends_with(".sql") && file_name.starts_with("v") { let version: i64 = file_name - .trim_start_matches("migration") + .trim_start_matches("v") .trim_end_matches(".sql") .parse() .ok()?; @@ -93,16 +90,16 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box Result<(), Box> { - let id: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(16) - .map(char::from) - .collect(); + let id: String = thread_rng().sample_iter(&Alphanumeric).take(16).map(char::from).collect(); - sqlx::query( - "INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)" - ) - .bind(id) - .bind("System") - .bind("text") - .bind("Welcome to your clipboard history!") - .execute(pool) - .await?; + sqlx + ::query( + "INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)" + ) + .bind(id) + .bind("System") + .bind("text") + .bind("Welcome to your clipboard history!") + .execute(pool).await?; Ok(()) } #[tauri::command] pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result, String> { - let rows = sqlx::query( - "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC", - ) - .fetch_all(&*pool) - .await - .map_err(|e| e.to_string())?; + let rows = sqlx + ::query( + "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC" + ) + .fetch_all(&*pool).await + .map_err(|e| e.to_string())?; let items = rows .iter() @@ -56,50 +52,53 @@ pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result, - item: HistoryItem, + item: HistoryItem ) -> Result<(), String> { let (id, source, source_icon, content_type, content, favicon, timestamp, language) = item.to_row(); - let existing = sqlx::query("SELECT id FROM history WHERE content = ? AND content_type = ?") + let existing = sqlx + ::query("SELECT id FROM history WHERE content = ? AND content_type = ?") .bind(&content) .bind(&content_type) - .fetch_optional(&*pool) - .await + .fetch_optional(&*pool).await .map_err(|e| e.to_string())?; match existing { Some(_) => { - sqlx::query( - "UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?" - ) - .bind(&content) - .bind(&content_type) - .execute(&*pool) - .await - .map_err(|e| e.to_string())?; + sqlx + ::query( + "UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?" + ) + .bind(&content) + .bind(&content_type) + .execute(&*pool).await + .map_err(|e| e.to_string())?; } None => { - sqlx::query( - "INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" - ) - .bind(id) - .bind(source) - .bind(source_icon) - .bind(content_type) - .bind(content) - .bind(favicon) - .bind(timestamp) - .bind(language) - .execute(&*pool) - .await - .map_err(|e| e.to_string())?; + sqlx + ::query( + "INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" + ) + .bind(id) + .bind(source) + .bind(source_icon) + .bind(content_type) + .bind(content) + .bind(favicon) + .bind(timestamp) + .bind(language) + .execute(&*pool).await + .map_err(|e| e.to_string())?; } } - let _ = app_handle.track_event("history_item_added", Some(serde_json::json!({ + let _ = app_handle.track_event( + "history_item_added", + Some(serde_json::json!({ "content_type": item.content_type.to_string() - }))); + })) + ); Ok(()) } @@ -107,16 +106,16 @@ pub async fn add_history_item( #[tauri::command] pub async fn search_history( pool: tauri::State<'_, SqlitePool>, - query: String, + query: String ) -> Result, String> { let query = format!("%{}%", query); - let rows = sqlx::query( - "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history WHERE content LIKE ? ORDER BY timestamp DESC" - ) - .bind(query) - .fetch_all(&*pool) - .await - .map_err(|e| e.to_string())?; + let rows = sqlx + ::query( + "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history WHERE content LIKE ? ORDER BY timestamp DESC" + ) + .bind(query) + .fetch_all(&*pool).await + .map_err(|e| e.to_string())?; let items = rows .iter() @@ -139,16 +138,16 @@ pub async fn search_history( pub async fn load_history_chunk( pool: tauri::State<'_, SqlitePool>, offset: i64, - limit: i64, + limit: i64 ) -> Result, String> { - let rows = sqlx::query( - "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?" - ) - .bind(limit) - .bind(offset) - .fetch_all(&*pool) - .await - .map_err(|e| e.to_string())?; + let rows = sqlx + ::query( + "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?" + ) + .bind(limit) + .bind(offset) + .fetch_all(&*pool).await + .map_err(|e| e.to_string())?; let items = rows .iter() @@ -171,12 +170,12 @@ pub async fn load_history_chunk( pub async fn delete_history_item( app_handle: tauri::AppHandle, pool: tauri::State<'_, SqlitePool>, - id: String, + id: String ) -> Result<(), String> { - sqlx::query("DELETE FROM history WHERE id = ?") + sqlx + ::query("DELETE FROM history WHERE id = ?") .bind(id) - .execute(&*pool) - .await + .execute(&*pool).await .map_err(|e| e.to_string())?; let _ = app_handle.track_event("history_item_deleted", None); @@ -189,9 +188,9 @@ pub async fn clear_history( app_handle: tauri::AppHandle, pool: tauri::State<'_, SqlitePool> ) -> Result<(), String> { - sqlx::query("DELETE FROM history") - .execute(&*pool) - .await + sqlx + ::query("DELETE FROM history") + .execute(&*pool).await .map_err(|e| e.to_string())?; let _ = app_handle.track_event("history_cleared", None); diff --git a/src-tauri/src/db/migrations/migration1.sql b/src-tauri/src/db/migrations/v1.sql similarity index 100% rename from src-tauri/src/db/migrations/migration1.sql rename to src-tauri/src/db/migrations/v1.sql diff --git a/src-tauri/src/db/migrations/migration2.sql b/src-tauri/src/db/migrations/v2.sql similarity index 100% rename from src-tauri/src/db/migrations/migration2.sql rename to src-tauri/src/db/migrations/v2.sql diff --git a/src-tauri/src/db/migrations/v3.sql b/src-tauri/src/db/migrations/v3.sql new file mode 100644 index 0000000..e299cae --- /dev/null +++ b/src-tauri/src/db/migrations/v3.sql @@ -0,0 +1 @@ +INSERT INTO settings (key, value) VALUES ('autostart', 'true'); diff --git a/src-tauri/src/db/settings.rs b/src-tauri/src/db/settings.rs index bdd6f58..caee425 100644 --- a/src-tauri/src/db/settings.rs +++ b/src-tauri/src/db/settings.rs @@ -1,8 +1,8 @@ -use serde::{Deserialize, Serialize}; +use serde::{ Deserialize, Serialize }; use serde_json; use sqlx::Row; use sqlx::SqlitePool; -use tauri::{Emitter, Manager}; +use tauri::{ Emitter, Manager }; use tauri_plugin_aptabase::EventTracker; #[derive(Deserialize, Serialize)] @@ -16,10 +16,10 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box Result<(), Box, - keybind: Vec, + keybind: Vec ) -> Result<(), String> { - let keybind_str = keybind.join("+"); - let keybind_clone = keybind_str.clone(); - - app_handle - .emit("update-shortcut", &keybind_str) - .map_err(|e| e.to_string())?; + app_handle.emit("update-shortcut", &keybind).map_err(|e| e.to_string())?; let json = serde_json::to_string(&keybind).map_err(|e| e.to_string())?; - sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)") + sqlx + ::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)") .bind(json) - .execute(&*pool) - .await + .execute(&*pool).await .map_err(|e| e.to_string())?; - let _ = app_handle.track_event("keybind_saved", Some(serde_json::json!({ - "keybind": keybind_clone - }))); + let _ = app_handle.track_event( + "keybind_saved", + Some(serde_json::json!({ + "keybind": keybind + })) + ); Ok(()) } @@ -55,12 +53,12 @@ pub async fn save_keybind( #[tauri::command] pub async fn get_setting( pool: tauri::State<'_, SqlitePool>, - key: String, + key: String ) -> Result { - let row = sqlx::query("SELECT value FROM settings WHERE key = ?") + let row = sqlx + ::query("SELECT value FROM settings WHERE key = ?") .bind(key) - .fetch_optional(&*pool) - .await + .fetch_optional(&*pool).await .map_err(|e| e.to_string())?; Ok(row.map(|r| r.get("value")).unwrap_or_default()) @@ -71,18 +69,21 @@ pub async fn save_setting( app_handle: tauri::AppHandle, pool: tauri::State<'_, SqlitePool>, key: String, - value: String, + value: String ) -> Result<(), String> { - sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") + sqlx + ::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") .bind(key.clone()) .bind(value) - .execute(&*pool) - .await + .execute(&*pool).await .map_err(|e| e.to_string())?; - let _ = app_handle.track_event("setting_saved", Some(serde_json::json!({ + let _ = app_handle.track_event( + "setting_saved", + Some(serde_json::json!({ "key": key - }))); + })) + ); Ok(()) } @@ -91,15 +92,18 @@ pub async fn save_setting( pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result, String> { let pool = app_handle.state::(); - let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'") - .fetch_optional(&*pool) - .await + let row = sqlx + ::query("SELECT value FROM settings WHERE key = 'keybind'") + .fetch_optional(&*pool).await .map_err(|e| e.to_string())?; - let json = row.map(|r| r.get::("value")).unwrap_or_else(|| { - serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()]) - .expect("Failed to serialize default keybind") - }); + let json = row + .map(|r| r.get::("value")) + .unwrap_or_else(|| { + serde_json + ::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()]) + .expect("Failed to serialize default keybind") + }); serde_json::from_str::>(&json).map_err(|e| e.to_string()) } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0f2ae6a..0291cbc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,7 +1,4 @@ -#![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" -)] +#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")] mod api; mod db; @@ -10,7 +7,7 @@ mod utils; use sqlx::sqlite::SqlitePoolOptions; use std::fs; use tauri::Manager; -use tauri_plugin_aptabase::{EventTracker, InitOptions}; +use tauri_plugin_aptabase::{ EventTracker, InitOptions }; use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_prevent_default::Flags; @@ -18,7 +15,8 @@ fn main() { let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); let _guard = runtime.enter(); - tauri::Builder::default() + tauri::Builder + ::default() .plugin(tauri_plugin_clipboard::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_sql::Builder::default().build()) @@ -26,34 +24,37 @@ fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_updater::Builder::default().build()) .plugin( - tauri_plugin_aptabase::Builder::new("A-SH-8937252746") + tauri_plugin_aptabase::Builder + ::new("A-SH-8937252746") .with_options(InitOptions { host: Some("https://aptabase.pandadev.net".to_string()), flush_interval: None, }) - .with_panic_hook(Box::new(|client, info, msg| { - let location = info - .location() - .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) - .unwrap_or_else(|| "".to_string()); - - let _ = client.track_event( - "panic", - Some(serde_json::json!({ + .with_panic_hook( + Box::new(|client, info, msg| { + let location = info + .location() + .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) + .unwrap_or_else(|| "".to_string()); + + let _ = client.track_event( + "panic", + Some( + serde_json::json!({ "info": format!("{} ({})", msg, location), - })), - ); - })) - .build(), + }) + ) + ); + }) + ) + .build() ) - .plugin(tauri_plugin_autostart::init( - MacosLauncher::LaunchAgent, - Some(vec![]), - )) + .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![]))) .plugin( - tauri_plugin_prevent_default::Builder::new() + tauri_plugin_prevent_default::Builder + ::new() .with_flags(Flags::all().difference(Flags::CONTEXT_MENU)) - .build(), + .build() ) .setup(|app| { let app_data_dir = app.path().app_data_dir().unwrap(); @@ -75,8 +76,7 @@ fn main() { tauri::async_runtime::spawn(async move { let pool = SqlitePoolOptions::new() .max_connections(5) - .connect(&db_url) - .await + .connect(&db_url).await .expect("Failed to create pool"); app_handle_clone.manage(pool); @@ -91,7 +91,10 @@ fn main() { let _ = api::clipboard::start_monitor(app_handle.clone()); utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap()); - main_window.as_ref().map(|w| w.hide()).unwrap_or(Ok(()))?; + main_window + .as_ref() + .map(|w| w.hide()) + .unwrap_or(Ok(()))?; let _ = app.track_event("app_started", None); @@ -109,21 +112,23 @@ fn main() { } } }) - .invoke_handler(tauri::generate_handler![ - api::clipboard::write_and_paste, - db::history::get_history, - db::history::add_history_item, - db::history::search_history, - db::history::load_history_chunk, - db::history::delete_history_item, - db::history::clear_history, - db::history::read_image, - db::settings::get_setting, - db::settings::save_setting, - db::settings::save_keybind, - db::settings::get_keybind, - utils::commands::fetch_page_meta, - ]) + .invoke_handler( + tauri::generate_handler![ + api::clipboard::write_and_paste, + db::history::get_history, + db::history::add_history_item, + db::history::search_history, + db::history::load_history_chunk, + db::history::delete_history_item, + db::history::clear_history, + db::history::read_image, + db::settings::get_setting, + db::settings::save_setting, + db::settings::save_keybind, + db::settings::get_keybind, + utils::commands::fetch_page_meta + ] + ) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/utils/commands.rs b/src-tauri/src/utils/commands.rs index f87a866..48c84e5 100644 --- a/src-tauri/src/utils/commands.rs +++ b/src-tauri/src/utils/commands.rs @@ -1,34 +1,37 @@ use active_win_pos_rs::get_active_window; -use base64::{engine::general_purpose::STANDARD, Engine}; +use base64::{ engine::general_purpose::STANDARD, Engine }; use image::codecs::png::PngEncoder; use tauri::PhysicalPosition; use meta_fetcher; pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) { - if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| { - let primary_monitor = window - .primary_monitor() + if + let Some(monitor) = window + .available_monitors() .unwrap() - .expect("Failed to get primary monitor"); - let mouse_position = primary_monitor.position(); - let monitor_position = m.position(); - let monitor_size = m.size(); - mouse_position.x >= monitor_position.x - && mouse_position.x < monitor_position.x + monitor_size.width as i32 - && mouse_position.y >= monitor_position.y - && mouse_position.y < monitor_position.y + monitor_size.height as i32 - }) { + .iter() + .find(|m| { + let primary_monitor = window + .primary_monitor() + .unwrap() + .expect("Failed to get primary monitor"); + let mouse_position = primary_monitor.position(); + let monitor_position = m.position(); + let monitor_size = m.size(); + mouse_position.x >= monitor_position.x && + mouse_position.x < monitor_position.x + (monitor_size.width as i32) && + mouse_position.y >= monitor_position.y && + mouse_position.y < monitor_position.y + (monitor_size.height as i32) + }) + { let monitor_size = monitor.size(); let window_size = window.outer_size().unwrap(); - let x = (monitor_size.width as i32 - window_size.width as i32) / 2; - let y = (monitor_size.height as i32 - window_size.height as i32) / 2; + let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2; + let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2; window - .set_position(PhysicalPosition::new( - monitor.position().x + x, - monitor.position().y + y, - )) + .set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y)) .unwrap(); } } @@ -51,59 +54,64 @@ fn _process_icon_to_base64(path: &str) -> Result bool { let color = color.trim().to_lowercase(); - + // hex if color.starts_with('#') && color.len() == color.trim_end_matches(char::is_whitespace).len() { let hex = &color[1..]; return match hex.len() { 3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()), - _ => false + _ => false, }; } - + // rgb/rgba - if (color.starts_with("rgb(") || color.starts_with("rgba(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") { + if + (color.starts_with("rgb(") || color.starts_with("rgba(")) && + color.ends_with(")") && + !color[..color.len() - 1].contains(")") + { let values = color .trim_start_matches("rgba(") .trim_start_matches("rgb(") .trim_end_matches(')') .split(',') .collect::>(); - + return match values.len() { 3 | 4 => values.iter().all(|v| v.trim().parse::().is_ok()), - _ => false + _ => false, }; } - - // hsl/hsla - if (color.starts_with("hsl(") || color.starts_with("hsla(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") { + + // hsl/hsla + if + (color.starts_with("hsl(") || color.starts_with("hsla(")) && + color.ends_with(")") && + !color[..color.len() - 1].contains(")") + { let values = color .trim_start_matches("hsla(") .trim_start_matches("hsl(") .trim_end_matches(')') .split(',') .collect::>(); - + return match values.len() { 3 | 4 => values.iter().all(|v| v.trim().parse::().is_ok()), - _ => false + _ => false, }; } - + false } #[tauri::command] pub async fn fetch_page_meta(url: String) -> Result<(String, Option), String> { - let metadata = meta_fetcher::fetch_metadata(&url) + let metadata = meta_fetcher + ::fetch_metadata(&url) .map_err(|e| format!("Failed to fetch metadata: {}", e))?; - - Ok(( - metadata.title.unwrap_or_else(|| "No title found".to_string()), - metadata.image - )) -} \ No newline at end of file + + Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image)) +} diff --git a/src-tauri/src/utils/favicon.rs b/src-tauri/src/utils/favicon.rs index 38321f3..45568dc 100644 --- a/src-tauri/src/utils/favicon.rs +++ b/src-tauri/src/utils/favicon.rs @@ -5,7 +5,7 @@ use reqwest; use url::Url; pub async fn fetch_favicon_as_base64( - url: Url, + url: Url ) -> Result, Box> { let client = reqwest::Client::new(); let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap()); diff --git a/src-tauri/src/utils/keys.rs b/src-tauri/src/utils/keys.rs new file mode 100644 index 0000000..01b6836 --- /dev/null +++ b/src-tauri/src/utils/keys.rs @@ -0,0 +1,120 @@ +use global_hotkey::hotkey::Code; +use std::str::FromStr; + +pub struct KeyCode(Code); + +impl FromStr for KeyCode { + type Err = String; + + fn from_str(s: &str) -> Result { + let code = match s { + "Backquote" => Code::Backquote, + "Backslash" => Code::Backslash, + "BracketLeft" => Code::BracketLeft, + "BracketRight" => Code::BracketRight, + "Comma" => Code::Comma, + "Digit0" => Code::Digit0, + "Digit1" => Code::Digit1, + "Digit2" => Code::Digit2, + "Digit3" => Code::Digit3, + "Digit4" => Code::Digit4, + "Digit5" => Code::Digit5, + "Digit6" => Code::Digit6, + "Digit7" => Code::Digit7, + "Digit8" => Code::Digit8, + "Digit9" => Code::Digit9, + "Equal" => Code::Equal, + "KeyA" => Code::KeyA, + "KeyB" => Code::KeyB, + "KeyC" => Code::KeyC, + "KeyD" => Code::KeyD, + "KeyE" => Code::KeyE, + "KeyF" => Code::KeyF, + "KeyG" => Code::KeyG, + "KeyH" => Code::KeyH, + "KeyI" => Code::KeyI, + "KeyJ" => Code::KeyJ, + "KeyK" => Code::KeyK, + "KeyL" => Code::KeyL, + "KeyM" => Code::KeyM, + "KeyN" => Code::KeyN, + "KeyO" => Code::KeyO, + "KeyP" => Code::KeyP, + "KeyQ" => Code::KeyQ, + "KeyR" => Code::KeyR, + "KeyS" => Code::KeyS, + "KeyT" => Code::KeyT, + "KeyU" => Code::KeyU, + "KeyV" => Code::KeyV, + "KeyW" => Code::KeyW, + "KeyX" => Code::KeyX, + "KeyY" => Code::KeyY, + "KeyZ" => Code::KeyZ, + "Minus" => Code::Minus, + "Period" => Code::Period, + "Quote" => Code::Quote, + "Semicolon" => Code::Semicolon, + "Slash" => Code::Slash, + "Backspace" => Code::Backspace, + "CapsLock" => Code::CapsLock, + "Delete" => Code::Delete, + "Enter" => Code::Enter, + "Space" => Code::Space, + "Tab" => Code::Tab, + "End" => Code::End, + "Home" => Code::Home, + "Insert" => Code::Insert, + "PageDown" => Code::PageDown, + "PageUp" => Code::PageUp, + "ArrowDown" => Code::ArrowDown, + "ArrowLeft" => Code::ArrowLeft, + "ArrowRight" => Code::ArrowRight, + "ArrowUp" => Code::ArrowUp, + "NumLock" => Code::NumLock, + "Numpad0" => Code::Numpad0, + "Numpad1" => Code::Numpad1, + "Numpad2" => Code::Numpad2, + "Numpad3" => Code::Numpad3, + "Numpad4" => Code::Numpad4, + "Numpad5" => Code::Numpad5, + "Numpad6" => Code::Numpad6, + "Numpad7" => Code::Numpad7, + "Numpad8" => Code::Numpad8, + "Numpad9" => Code::Numpad9, + "NumpadAdd" => Code::NumpadAdd, + "NumpadDecimal" => Code::NumpadDecimal, + "NumpadDivide" => Code::NumpadDivide, + "NumpadMultiply" => Code::NumpadMultiply, + "NumpadSubtract" => Code::NumpadSubtract, + "Escape" => Code::Escape, + "PrintScreen" => Code::PrintScreen, + "ScrollLock" => Code::ScrollLock, + "Pause" => Code::Pause, + "AudioVolumeDown" => Code::AudioVolumeDown, + "AudioVolumeMute" => Code::AudioVolumeMute, + "AudioVolumeUp" => Code::AudioVolumeUp, + "F1" => Code::F1, + "F2" => Code::F2, + "F3" => Code::F3, + "F4" => Code::F4, + "F5" => Code::F5, + "F6" => Code::F6, + "F7" => Code::F7, + "F8" => Code::F8, + "F9" => Code::F9, + "F10" => Code::F10, + "F11" => Code::F11, + "F12" => Code::F12, + _ => { + return Err(format!("Unknown key code: {}", s)); + } + }; + Ok(KeyCode(code)) + } +} + +impl From for Code { + fn from(key_code: KeyCode) -> Self { + key_code.0 + } +} diff --git a/src-tauri/src/utils/logger.rs b/src-tauri/src/utils/logger.rs index 114b2c6..bd39108 100644 --- a/src-tauri/src/utils/logger.rs +++ b/src-tauri/src/utils/logger.rs @@ -1,6 +1,6 @@ use chrono; -use log::{LevelFilter, SetLoggerError}; -use std::fs::{File, OpenOptions}; +use log::{ LevelFilter, SetLoggerError }; +use std::fs::{ File, OpenOptions }; use std::io::Write; use std::panic; @@ -16,7 +16,7 @@ impl log::Log for FileLogger { fn log(&self, record: &log::Record) { if self.enabled(record.metadata()) { let mut file = self.file.try_clone().expect("Failed to clone file handle"); - + // Format: timestamp [LEVEL] target: message (file:line) writeln!( file, @@ -50,32 +50,38 @@ pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError> // Set up panic hook let panic_file = file.try_clone().expect("Failed to clone file handle"); - panic::set_hook(Box::new(move |panic_info| { - let mut file = panic_file.try_clone().expect("Failed to clone file handle"); - - let location = panic_info.location() - .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) - .unwrap_or_else(|| "unknown location".to_string()); + panic::set_hook( + Box::new(move |panic_info| { + let mut file = panic_file.try_clone().expect("Failed to clone file handle"); + + let location = panic_info + .location() + .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) + .unwrap_or_else(|| "unknown location".to_string()); - let message = match panic_info.payload().downcast_ref::<&str>() { - Some(s) => *s, - None => match panic_info.payload().downcast_ref::() { - Some(s) => s.as_str(), - None => "Unknown panic message", - }, - }; + let message = match panic_info.payload().downcast_ref::<&str>() { + Some(s) => *s, + None => + match panic_info.payload().downcast_ref::() { + Some(s) => s.as_str(), + None => "Unknown panic message", + } + }; - let _ = writeln!( - file, - "{} [PANIC] rust_panic: {} ({})", - chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), - message, - location - ); - })); + let _ = writeln!( + file, + "{} [PANIC] rust_panic: {} ({})", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + message, + location + ); + }) + ); let logger = Box::new(FileLogger { file }); - unsafe { log::set_logger_racy(Box::leak(logger))? }; + unsafe { + log::set_logger_racy(Box::leak(logger))?; + } log::set_max_level(LevelFilter::Debug); Ok(()) -} \ No newline at end of file +} diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index b888b1f..78e50cb 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -2,3 +2,4 @@ pub mod commands; pub mod favicon; pub mod types; pub mod logger; +pub mod keys; diff --git a/src-tauri/src/utils/types.rs b/src-tauri/src/utils/types.rs index 76c846e..4e6886f 100644 --- a/src-tauri/src/utils/types.rs +++ b/src-tauri/src/utils/types.rs @@ -1,5 +1,5 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use chrono::{ DateTime, Utc }; +use serde::{ Deserialize, Serialize }; use std::fmt; use uuid::Uuid; @@ -115,7 +115,7 @@ impl HistoryItem { content: String, favicon: Option, source_icon: Option, - language: Option, + language: Option ) -> Self { Self { id: Uuid::new_v4().to_string(), @@ -130,7 +130,7 @@ impl HistoryItem { } pub fn to_row( - &self, + &self ) -> ( String, String, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7be281f..9ce7712 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Qopy", - "version": "0.3.3", + "version": "0.3.4", "identifier": "net.pandadev.qopy", "build": { "frontendDist": "../dist", diff --git a/types/keys.ts b/types/keys.ts new file mode 100644 index 0000000..d81454f --- /dev/null +++ b/types/keys.ts @@ -0,0 +1,217 @@ +export enum KeyValues { + Backquote = 'Backquote', + Backslash = 'Backslash', + BracketLeft = 'BracketLeft', + BracketRight = 'BracketRight', + Comma = 'Comma', + Digit0 = 'Digit0', + Digit1 = 'Digit1', + Digit2 = 'Digit2', + Digit3 = 'Digit3', + Digit4 = 'Digit4', + Digit5 = 'Digit5', + Digit6 = 'Digit6', + Digit7 = 'Digit7', + Digit8 = 'Digit8', + Digit9 = 'Digit9', + Equal = 'Equal', + KeyA = 'KeyA', + KeyB = 'KeyB', + KeyC = 'KeyC', + KeyD = 'KeyD', + KeyE = 'KeyE', + KeyF = 'KeyF', + KeyG = 'KeyG', + KeyH = 'KeyH', + KeyI = 'KeyI', + KeyJ = 'KeyJ', + KeyK = 'KeyK', + KeyL = 'KeyL', + KeyM = 'KeyM', + KeyN = 'KeyN', + KeyO = 'KeyO', + KeyP = 'KeyP', + KeyQ = 'KeyQ', + KeyR = 'KeyR', + KeyS = 'KeyS', + KeyT = 'KeyT', + KeyU = 'KeyU', + KeyV = 'KeyV', + KeyW = 'KeyW', + KeyX = 'KeyX', + KeyY = 'KeyY', + KeyZ = 'KeyZ', + Minus = 'Minus', + Period = 'Period', + Quote = 'Quote', + Semicolon = 'Semicolon', + Slash = 'Slash', + AltLeft = 'AltLeft', + AltRight = 'AltRight', + Backspace = 'Backspace', + CapsLock = 'CapsLock', + ContextMenu = 'ContextMenu', + ControlLeft = 'ControlLeft', + ControlRight = 'ControlRight', + Enter = 'Enter', + MetaLeft = 'MetaLeft', + MetaRight = 'MetaRight', + ShiftLeft = 'ShiftLeft', + ShiftRight = 'ShiftRight', + Space = 'Space', + Tab = 'Tab', + Delete = 'Delete', + End = 'End', + Home = 'Home', + Insert = 'Insert', + PageDown = 'PageDown', + PageUp = 'PageUp', + ArrowDown = 'ArrowDown', + ArrowLeft = 'ArrowLeft', + ArrowRight = 'ArrowRight', + ArrowUp = 'ArrowUp', + NumLock = 'NumLock', + Numpad0 = 'Numpad0', + Numpad1 = 'Numpad1', + Numpad2 = 'Numpad2', + Numpad3 = 'Numpad3', + Numpad4 = 'Numpad4', + Numpad5 = 'Numpad5', + Numpad6 = 'Numpad6', + Numpad7 = 'Numpad7', + Numpad8 = 'Numpad8', + Numpad9 = 'Numpad9', + NumpadAdd = 'NumpadAdd', + NumpadDecimal = 'NumpadDecimal', + NumpadDivide = 'NumpadDivide', + NumpadMultiply = 'NumpadMultiply', + NumpadSubtract = 'NumpadSubtract', + Escape = 'Escape', + PrintScreen = 'PrintScreen', + ScrollLock = 'ScrollLock', + Pause = 'Pause', + AudioVolumeDown = 'AudioVolumeDown', + AudioVolumeMute = 'AudioVolumeMute', + AudioVolumeUp = 'AudioVolumeUp', + F1 = 'F1', + F2 = 'F2', + F3 = 'F3', + F4 = 'F4', + F5 = 'F5', + F6 = 'F6', + F7 = 'F7', + F8 = 'F8', + F9 = 'F9', + F10 = 'F10', + F11 = 'F11', + F12 = 'F12', +} + +export enum KeyLabels { + Backquote = '`', + Backslash = '\\', + BracketLeft = '[', + BracketRight = ']', + Comma = ',', + Digit0 = '0', + Digit1 = '1', + Digit2 = '2', + Digit3 = '3', + Digit4 = '4', + Digit5 = '5', + Digit6 = '6', + Digit7 = '7', + Digit8 = '8', + Digit9 = '9', + Equal = '=', + KeyA = 'A', + KeyB = 'B', + KeyC = 'C', + KeyD = 'D', + KeyE = 'E', + KeyF = 'F', + KeyG = 'G', + KeyH = 'H', + KeyI = 'I', + KeyJ = 'J', + KeyK = 'K', + KeyL = 'L', + KeyM = 'M', + KeyN = 'N', + KeyO = 'O', + KeyP = 'P', + KeyQ = 'Q', + KeyR = 'R', + KeyS = 'S', + KeyT = 'T', + KeyU = 'U', + KeyV = 'V', + KeyW = 'W', + KeyX = 'X', + KeyY = 'Y', + KeyZ = 'Z', + Minus = '-', + Period = '.', + Quote = "'", + Semicolon = ';', + Slash = '/', + AltLeft = 'Alt', + AltRight = 'Alt (Right)', + Backspace = 'Backspace', + CapsLock = 'Caps Lock', + ContextMenu = 'Context Menu', + ControlLeft = 'Ctrl', + ControlRight = 'Ctrl (Right)', + Enter = 'Enter', + MetaLeft = 'Meta', + MetaRight = 'Meta (Right)', + ShiftLeft = 'Shift', + ShiftRight = 'Shift (Right)', + Space = 'Space', + Tab = 'Tab', + Delete = 'Delete', + End = 'End', + Home = 'Home', + Insert = 'Insert', + PageDown = 'Page Down', + PageUp = 'Page Up', + ArrowDown = '↓', + ArrowLeft = '←', + ArrowRight = '→', + ArrowUp = '↑', + NumLock = 'Num Lock', + Numpad0 = 'Numpad 0', + Numpad1 = 'Numpad 1', + Numpad2 = 'Numpad 2', + Numpad3 = 'Numpad 3', + Numpad4 = 'Numpad 4', + Numpad5 = 'Numpad 5', + Numpad6 = 'Numpad 6', + Numpad7 = 'Numpad 7', + Numpad8 = 'Numpad 8', + Numpad9 = 'Numpad 9', + NumpadAdd = 'Numpad +', + NumpadDecimal = 'Numpad .', + NumpadDivide = 'Numpad /', + NumpadMultiply = 'Numpad *', + NumpadSubtract = 'Numpad -', + Escape = 'Esc', + PrintScreen = 'Print Screen', + ScrollLock = 'Scroll Lock', + Pause = 'Pause', + AudioVolumeDown = 'Volume Down', + AudioVolumeMute = 'Volume Mute', + AudioVolumeUp = 'Volume Up', + F1 = 'F1', + F2 = 'F2', + F3 = 'F3', + F4 = 'F4', + F5 = 'F5', + F6 = 'F6', + F7 = 'F7', + F8 = 'F8', + F9 = 'F9', + F10 = 'F10', + F11 = 'F11', + F12 = 'F12', +} \ No newline at end of file
{{ row.label }}
Back
Qopy
Save
Startup
Qopy Hotkey
Launch Qopy at login