Merge pull request #29 from 0PandaDEV/issue/settings

This commit is contained in:
PandaDEV 2025-01-10 22:21:54 +10:00 committed by GitHub
commit c0b50fcc80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1446 additions and 704 deletions

View file

@ -234,30 +234,41 @@ jobs:
with: with:
fetch-depth: 0 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 - name: Download all artifacts
if: env.SKIP_RELEASE == 'false'
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
path: artifacts 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 - name: Generate Release Body
if: env.SKIP_RELEASE == 'false'
id: release_body id: release_body
run: | run: |
VERSION="${{ needs.prepare.outputs.version }}" 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 # Calculate hashes with corrected paths
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }') 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 }') WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }')
@ -278,9 +289,8 @@ jobs:
echo "Red Hat: $REDHAT_HASH" echo "Red Hat: $REDHAT_HASH"
RELEASE_BODY=$(cat <<-EOF RELEASE_BODY=$(cat <<-EOF
## ♻️ Changelog
$CHANGES ${{ needs.create-release.outputs.changelog }}
## ⬇️ Downloads ## ⬇️ Downloads
@ -299,6 +309,7 @@ jobs:
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
- name: Create Release - name: Create Release
if: env.SKIP_RELEASE == 'false'
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -12,7 +12,7 @@ All the data of Qopy is stored inside of a SQLite database.
## Disable Windows+V for default clipboard manager ## Disable Windows+V for default clipboard manager
https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620 <video src="https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620" controls title="Disable Windows+V for default clipboard manager"></video>
To disable the default clipboard manager popup from windows open Command prompt and run this command To disable the default clipboard manager popup from windows open Command prompt and run this command

42
app.vue
View file

@ -1,27 +1,36 @@
<template> <template>
<div style="pointer-events: auto;"> <div style="pointer-events: auto">
<NuxtPage /> <NuxtPage />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { listen } from '@tauri-apps/api/event' import { listen } from "@tauri-apps/api/event";
import { app, window } from '@tauri-apps/api'; import { app, window } from "@tauri-apps/api";
import { onMounted } from 'vue' import { disable, enable } from "@tauri-apps/plugin-autostart";
import { onMounted } from "vue";
const keyboard = useKeyboard();
const { $settings } = useNuxtApp();
onMounted(async () => { onMounted(async () => {
await listen('change_keybind', async () => { await listen("settings", async () => {
console.log("change_keybind"); keyboard.unregisterAll();
await navigateTo('/settings') await navigateTo("/settings");
await app.show(); await app.show();
await window.getCurrentWindow().show(); await window.getCurrentWindow().show();
}) });
await listen('main_route', async () => { if ((await $settings.getSetting("autostart")) === "true") {
console.log("main_route"); await enable();
await navigateTo('/') } else {
}) await disable();
}) }
await listen("main_route", async () => {
await navigateTo("/");
});
});
</script> </script>
<style lang="scss"> <style lang="scss">
@ -53,7 +62,6 @@ onMounted(async () => {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
color: #E5DFD5;
text-decoration: none; text-decoration: none;
font-family: SFRoundedRegular; font-family: SFRoundedRegular;
scroll-behavior: smooth; scroll-behavior: smooth;
@ -62,9 +70,9 @@ onMounted(async () => {
position: relative; position: relative;
z-index: 1; z-index: 1;
--os-handle-bg: #ADA9A1; --os-handle-bg: #ada9a1;
--os-handle-bg-hover: #78756F; --os-handle-bg-hover: #78756f;
--os-handle-bg-active: #78756F; --os-handle-bg-active: #78756f;
} }
html, html,

View file

@ -22,7 +22,7 @@ $mutedtext: #78756f;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
height: 54px; height: 56px;
background-color: transparent; background-color: transparent;
outline: none; outline: none;
border: none; border: none;
@ -35,10 +35,10 @@ $mutedtext: #78756f;
.results { .results {
position: absolute; position: absolute;
width: 284px; width: 286px;
top: 53px; top: 55px;
left: 0; left: 0;
height: calc(100vh - 95px); height: 417px;
border-right: 1px solid $divider; border-right: 1px solid $divider;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -46,6 +46,7 @@ $mutedtext: #78756f;
padding-bottom: 8px; padding-bottom: 8px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
z-index: 3;
.result { .result {
height: 40px; height: 40px;
@ -59,6 +60,7 @@ $mutedtext: #78756f;
overflow: hidden; overflow: hidden;
text-overflow: clip; text-overflow: clip;
white-space: nowrap; white-space: nowrap;
color: $text;
} }
.result { .result {
@ -96,20 +98,22 @@ $mutedtext: #78756f;
.content { .content {
position: absolute; position: absolute;
top: 53px; top: 55px;
left: 284px; left: 285px;
height: calc(100vh - 254px); height: 220px;
font-family: CommitMono !important; font-family: CommitMono !important;
font-size: 12px; font-size: 12px;
letter-spacing: 1; letter-spacing: 1;
border-radius: 10px; border-radius: 10px;
width: calc(100vw - 286px); width: 465px;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
z-index: 2;
color: $text;
&:not(:has(.image)) { &:not(:has(.image)) {
padding: 8px; padding: 8px;
@ -128,7 +132,7 @@ $mutedtext: #78756f;
} }
.bottom-bar { .bottom-bar {
height: 40px; height: 39px;
width: calc(100vw - 2px); width: calc(100vw - 2px);
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
background-color: hsla(40, 3%, 16%, 0.8); background-color: hsla(40, 3%, 16%, 0.8);
@ -215,18 +219,20 @@ $mutedtext: #78756f;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
bottom: 40px; bottom: 39px;
left: 284px; left: 285px;
height: 160px; height: 160px;
width: calc(100vw - 286px); width: 465px;
border-top: 1px solid $divider; border-top: 1px solid $divider;
background-color: $primary; background-color: $primary;
padding: 14px; padding: 14px;
z-index: 1;
.title { .title {
font-family: SFRoundedSemiBold; font-family: SFRoundedSemiBold;
font-size: 12px; font-size: 12px;
letter-spacing: 0.6px; letter-spacing: 0.6px;
color: $text;
} }
.info-content { .info-content {

View file

@ -36,40 +36,116 @@ $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; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; gap: 16px;
justify-content: center;
height: 100vh;
gap: 6px;
.title { p {
font-size: 20px; font-family: SFRoundedSemiBold;
font-weight: 800; color: $text2;
display: flex;
justify-content: right;
}
} }
.keybind-input { .actions {
display: flex;
flex-direction: column;
gap: 16px;
color: $mutedtext;
}
}
}
.launch {
display: flex;
align-items: center;
gap: 6px;
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;
}
}
}
.checkmark {
height: 14px;
width: 14px;
position: absolute;
opacity: 0;
transition: opacity 0.2s;
}
p {
color: $text2;
}
}
.keybind-input {
width: min-content;
white-space: nowrap;
padding: 6px; padding: 6px;
border: 1px solid $divider; border: 1px solid $divider;
color: $text2; color: $text2;
display: flex; display: flex;
border-radius: 13px; border-radius: 10px;
outline: none; outline: none;
gap: 6px; gap: 4px;
.key { .key {
color: $text2; color: $text2;
font-family: SFRoundedMedium; font-family: SFRoundedMedium;
background-color: $divider; background-color: $divider;
padding: 6px 8px; padding: 2px 6px;
border-radius: 8px; border-radius: 6px;
} font-size: 14px;
} }
}
.keybind-input:focus { .keybind-input:focus {
border: 1px solid rgba(255, 255, 255, 0.2); 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 { .bottom-bar {
@ -136,6 +212,15 @@ $mutedtext: #78756f;
background-color: transparent; background-color: transparent;
transition: all 0.2s; transition: all 0.2s;
cursor: pointer; cursor: pointer;
p {
color: $text;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
} }
.actions:hover { .actions:hover {

View file

@ -20,9 +20,12 @@
"sass-embedded": "1.83.0", "sass-embedded": "1.83.0",
"uuid": "11.0.3", "uuid": "11.0.3",
"vue": "3.5.13", "vue": "3.5.13",
"wrdu-keyboard": "1.1.1" "wrdu-keyboard": "3.0.0"
}, },
"overrides": { "overrides": {
"chokidar": "^3.6.0" "chokidar": "^3.6.0"
},
"patchedDependencies": {
"wrdu-keyboard@3.0.0": "patches/wrdu-keyboard@3.0.0.patch"
} }
} }

View file

@ -65,10 +65,17 @@
</template> </template>
<template v-else-if="hasFavicon(item.favicon ?? '')"> <template v-else-if="hasFavicon(item.favicon ?? '')">
<img <img
:src="item.favicon ? getFaviconFromDb(item.favicon) : '../public/icons/Link.svg'" :src="
item.favicon
? getFaviconFromDb(item.favicon)
: '../public/icons/Link.svg'
"
alt="Favicon" alt="Favicon"
class="favicon" class="favicon"
@error="($event.target as HTMLImageElement).src = '../public/icons/Link.svg'" /> @error="
($event.target as HTMLImageElement).src =
'../public/icons/Link.svg'
" />
</template> </template>
<img <img
src="../public/icons/File.svg" src="../public/icons/File.svg"
@ -121,8 +128,12 @@
:src="getYoutubeThumbnail(selectedItem.content)" :src="getYoutubeThumbnail(selectedItem.content)"
alt="YouTube Thumbnail" /> alt="YouTube Thumbnail" />
</div> </div>
<div class="content" v-else-if="selectedItem?.content_type === ContentType.Link && pageOgImage"> <div
<img :src="pageOgImage" alt="Image" class="image"> class="content"
v-else-if="
selectedItem?.content_type === ContentType.Link && pageOgImage
">
<img :src="pageOgImage" alt="Image" class="image" />
</div> </div>
<OverlayScrollbarsComponent v-else class="content"> <OverlayScrollbarsComponent v-else class="content">
<span>{{ selectedItem?.content || "" }}</span> <span>{{ selectedItem?.content || "" }}</span>
@ -135,9 +146,7 @@
<div class="info-content" v-if="selectedItem && getInfo"> <div class="info-content" v-if="selectedItem && getInfo">
<div class="info-row" v-for="(row, index) in infoRows" :key="index"> <div class="info-row" v-for="(row, index) in infoRows" :key="index">
<p class="label">{{ row.label }}</p> <p class="label">{{ row.label }}</p>
<span <span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value">
:class="{ 'url-truncate': row.isUrl }"
:data-text="row.value">
{{ row.value }} {{ row.value }}
</span> </span>
</div> </div>
@ -153,12 +162,19 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import "overlayscrollbars/overlayscrollbars.css"; import "overlayscrollbars/overlayscrollbars.css";
import { app, window } from "@tauri-apps/api"; import { app, window } from "@tauri-apps/api";
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { useNuxtApp } from "#app"; import { useNuxtApp } from "#app";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { HistoryItem, ContentType } from "~/types/types"; 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 { interface GroupedHistory {
label: string; label: string;
@ -188,8 +204,8 @@ const imageSizes = shallowRef<Record<string, string>>({});
const lastUpdateTime = ref<number>(Date.now()); const lastUpdateTime = ref<number>(Date.now());
const imageLoadError = ref<boolean>(false); const imageLoadError = ref<boolean>(false);
const imageLoading = ref<boolean>(false); const imageLoading = ref<boolean>(false);
const pageTitle = ref<string>(''); const pageTitle = ref<string>("");
const pageOgImage = ref<string>(''); const pageOgImage = ref<string>("");
const keyboard = useKeyboard(); const keyboard = useKeyboard();
@ -583,41 +599,35 @@ const setupEventListeners = async (): Promise<void> => {
searchInput.value?.blur(); searchInput.value?.blur();
}); });
keyboard.down("ArrowDown", (event) => { keyboard.prevent.down([Key.DownArrow], (event) => {
event.preventDefault();
selectNext(); selectNext();
}); });
keyboard.down("ArrowUp", (event) => { keyboard.prevent.down([Key.UpArrow], (event) => {
event.preventDefault();
selectPrevious(); selectPrevious();
}); });
keyboard.down("Enter", (event) => { keyboard.prevent.down([Key.Enter], (event) => {
event.preventDefault();
pasteSelectedItem(); pasteSelectedItem();
}); });
keyboard.down("Escape", (event) => { keyboard.prevent.down([Key.Escape], (event) => {
event.preventDefault();
hideApp(); hideApp();
}); });
keyboard.down("all", (event) => { switch (os.value) {
const isMacActionCombo = case "macos":
os.value === "macos" && keyboard.prevent.down([Key.LeftMeta, Key.K], (event) => {});
(event.code === "MetaLeft" || event.code === "MetaRight") &&
event.key === "k";
const isOtherOsActionCombo = keyboard.prevent.down([Key.RightMeta, Key.K], (event) => {});
os.value !== "macos" && break;
(event.code === "ControlLeft" || event.code === "ControlRight") &&
event.key === "k";
if (isMacActionCombo || isOtherOsActionCombo) { case "linux" || "windows":
event.preventDefault(); keyboard.prevent.down([Key.LeftControl, Key.K], (event) => {});
keyboard.prevent.down([Key.RightControl, Key.K], (event) => {});
break;
} }
});
}; };
const hideApp = async (): Promise<void> => { const hideApp = async (): Promise<void> => {
@ -646,7 +656,7 @@ watch(searchQuery, () => {
onMounted(async () => { onMounted(async () => {
try { try {
os.value = await platform(); os.value = platform();
await loadHistoryChunk(); await loadHistoryChunk();
resultsContainer.value resultsContainer.value
@ -655,10 +665,6 @@ onMounted(async () => {
?.viewport?.addEventListener("scroll", handleScroll); ?.viewport?.addEventListener("scroll", handleScroll);
await setupEventListeners(); await setupEventListeners();
if (!(await isEnabled())) {
await enable();
}
} catch (error) { } catch (error) {
console.error("Error during onMounted:", error); console.error("Error during onMounted:", error);
} }
@ -686,27 +692,33 @@ const formatFileSize = (bytes: number): string => {
const fetchPageMeta = async (url: string) => { const fetchPageMeta = async (url: string) => {
try { 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; pageTitle.value = title;
if (ogImage) { if (ogImage) {
pageOgImage.value = ogImage; pageOgImage.value = ogImage;
} }
} catch (error) { } catch (error) {
console.error('Error fetching page meta:', error); console.error("Error fetching page meta:", error);
pageTitle.value = 'Error loading title'; pageTitle.value = "Error loading title";
} }
}; };
watch(() => selectedItem.value, (newItem) => { watch(
() => selectedItem.value,
(newItem) => {
if (newItem?.content_type === ContentType.Link) { if (newItem?.content_type === ContentType.Link) {
pageTitle.value = 'Loading...'; pageTitle.value = "Loading...";
pageOgImage.value = ''; pageOgImage.value = "";
fetchPageMeta(newItem.content); fetchPageMeta(newItem.content);
} else { } else {
pageTitle.value = ''; pageTitle.value = "";
pageOgImage.value = ''; pageOgImage.value = "";
} }
}); }
);
const getInfo = computed(() => { const getInfo = computed(() => {
if (!selectedItem.value) return null; if (!selectedItem.value) return null;
@ -716,7 +728,10 @@ const getInfo = computed(() => {
copied: selectedItem.value.timestamp, copied: selectedItem.value.timestamp,
}; };
const infoMap: Record<ContentType, () => InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode> = { const infoMap: Record<
ContentType,
() => InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode
> = {
[ContentType.Text]: () => ({ [ContentType.Text]: () => ({
...baseInfo, ...baseInfo,
content_type: ContentType.Text, content_type: ContentType.Text,
@ -754,7 +769,8 @@ const getInfo = computed(() => {
const max = Math.max(rNorm, gNorm, bNorm); const max = Math.max(rNorm, gNorm, bNorm);
const min = Math.min(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; const l = (max + min) / 2;
if (max !== min) { if (max !== min) {
@ -780,14 +796,16 @@ const getInfo = computed(() => {
content_type: ContentType.Color, content_type: ContentType.Color,
hex: hex, hex: hex,
rgb: `rgb(${r}, ${g}, ${b})`, 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]: () => ({ [ContentType.Code]: () => ({
...baseInfo, ...baseInfo,
content_type: ContentType.Code, content_type: ContentType.Code,
language: selectedItem.value!.language ?? "Unknown", 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 = [ const commonRows = [
{ label: "Source", value: getInfo.value.source, isUrl: false }, { 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<ContentType, Array<{ label: string; value: string | number; isUrl?: boolean }>> = { const typeSpecificRows: Record<
ContentType,
Array<{ label: string; value: string | number; isUrl?: boolean }>
> = {
[ContentType.Text]: [ [ContentType.Text]: [
{ label: "Characters", value: (getInfo.value as InfoText).characters }, { label: "Characters", value: (getInfo.value as InfoText).characters },
{ label: "Words", value: (getInfo.value as InfoText).words }, { label: "Words", value: (getInfo.value as InfoText).words },
], ],
[ContentType.Image]: [ [ContentType.Image]: [
{ label: "Dimensions", value: (getInfo.value as InfoImage).dimensions }, { 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]: [ [ContentType.File]: [
{ label: "Path", value: (getInfo.value as InfoFile).path }, { label: "Path", value: (getInfo.value as InfoFile).path },
], ],
[ContentType.Link]: [ [ContentType.Link]: [
...((getInfo.value as InfoLink).title && (getInfo.value as InfoLink).title !== 'Loading...' ...((getInfo.value as InfoLink).title &&
? [{ label: "Title", value: (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: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },
{ label: "Characters", value: (getInfo.value as InfoLink).characters }, { label: "Characters", value: (getInfo.value as InfoLink).characters },
@ -832,8 +863,9 @@ const infoRows = computed(() => {
], ],
}; };
const specificRows = typeSpecificRows[getInfo.value.content_type] const specificRows = typeSpecificRows[getInfo.value.content_type].filter(
.filter(row => row.value !== ""); (row) => row.value !== ""
);
return [ return [
...commonRows, ...commonRows,

View file

@ -1,8 +1,10 @@
<template> <template>
<div class="bg"> <div class="bg">
<div class="back"> <div class="top-bar">
<img @click="router.push('/')" src="../public/back_arrow.svg" /> <NuxtLink to="/" class="back">
<img src="../public/back_arrow.svg" />
<p>Back</p> <p>Back</p>
</NuxtLink>
</div> </div>
<div class="bottom-bar"> <div class="bottom-bar">
<div class="left"> <div class="left">
@ -10,7 +12,10 @@
<p>Qopy</p> <p>Qopy</p>
</div> </div>
<div class="right"> <div class="right">
<div @click="saveKeybind" class="actions"> <div
@click="saveKeybind"
class="actions"
:class="{ disabled: keybind.length === 0 }">
<p>Save</p> <p>Save</p>
<div> <div>
<img alt="" src="../public/cmd.svg" v-if="os === 'macos'" /> <img alt="" src="../public/cmd.svg" v-if="os === 'macos'" />
@ -23,15 +28,49 @@
</div> </div>
</div> </div>
</div> </div>
<div class="keybind-container"> <div class="settings-container">
<h2 class="title">Record a new Hotkey</h2> <div class="settings">
<div class="names">
<p style="line-height: 14px">Startup</p>
<p style="line-height: 34px">Qopy Hotkey</p>
</div>
<div class="actions">
<div class="launch">
<input
type="checkbox"
id="launch"
v-model="autostart"
@change="toggleAutostart" />
<label for="launch" class="checkmark">
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g>
<rect width="14" height="14" />
<path
id="Path"
d="M0 2.00696L2.25015 4.25L6 0"
fill="none"
stroke-width="1.5"
stroke="#E5DFD5"
stroke-linecap="round"
stroke-linejoin="round"
transform="translate(4 5)" />
</g>
</svg>
</label>
<p for="launch">Launch Qopy at login</p>
</div>
<div <div
@blur="onBlur" @blur="onBlur"
@focus="onFocus" @focus="onFocus"
@keydown="onKeyDown"
class="keybind-input" class="keybind-input"
ref="keybindInput" ref="keybindInput"
tabindex="0"> tabindex="0"
:class="{ 'empty-keybind': showEmptyKeybindError }">
<span class="key" v-if="keybind.length === 0">Click here</span> <span class="key" v-if="keybind.length === 0">Click here</span>
<template v-else> <template v-else>
<span <span
@ -39,12 +78,14 @@
class="key" class="key"
:class="{ modifier: isModifier(key) }" :class="{ modifier: isModifier(key) }"
v-for="(key, index) in keybind"> v-for="(key, index) in keybind">
{{ keyToDisplay(key) }} {{ keyToLabel(key) }}
</span> </span>
</template> </template>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -52,62 +93,43 @@ import { invoke } from "@tauri-apps/api/core";
import { onMounted, onUnmounted, reactive, ref } from "vue"; import { onMounted, onUnmounted, reactive, ref } from "vue";
import { platform } from "@tauri-apps/plugin-os"; import { platform } from "@tauri-apps/plugin-os";
import { useRouter } from "vue-router"; 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<Set<string>>(new Set()); const activeModifiers = reactive<Set<KeyValues>>(new Set());
const isKeybindInputFocused = ref(false); const isKeybindInputFocused = ref(false);
const keybind = ref<string[]>([]); const keybind = ref<KeyValues[]>([]);
const keybindInput = ref<HTMLElement | null>(null); const keybindInput = ref<HTMLElement | null>(null);
const lastBlurTime = ref(0); const lastBlurTime = ref(0);
const os = ref(""); const os = ref("");
const router = useRouter(); const router = useRouter();
const keyboard = useKeyboard(); const keyboard = useKeyboard();
const showEmptyKeybindError = ref(false);
const keyToDisplayMap: Record<string, string> = { const autostart = ref(false);
" ": "Space", const { $settings } = useNuxtApp();
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 modifierKeySet = new Set([ const modifierKeySet = new Set([
"Alt", KeyValues.AltLeft,
"AltLeft", KeyValues.AltRight,
"AltRight", KeyValues.ControlLeft,
"Control", KeyValues.ControlRight,
"ControlLeft", KeyValues.MetaLeft,
"ControlRight", KeyValues.MetaRight,
"Meta", KeyValues.ShiftLeft,
"MetaLeft", KeyValues.ShiftRight,
"MetaRight",
"Shift",
"ShiftLeft",
"ShiftRight",
]); ]);
const isModifier = (key: string): boolean => { const isModifier = (key: KeyValues): boolean => {
return modifierKeySet.has(key); return modifierKeySet.has(key);
}; };
const keyToDisplay = (key: string): string => { const keyToLabel = (key: KeyValues): string => {
return keyToDisplayMap[key] || key; return KeyLabels[key] || key;
}; };
const updateKeybind = () => { const updateKeybind = () => {
const modifiers = Array.from(activeModifiers).sort(); const modifiers = Array.from(activeModifiers);
const nonModifiers = keybind.value.filter((key) => !isModifier(key)); const nonModifiers = keybind.value.filter((key) => !isModifier(key));
keybind.value = [...modifiers, ...nonModifiers]; keybind.value = [...modifiers, ...nonModifiers];
}; };
@ -115,19 +137,20 @@ const updateKeybind = () => {
const onBlur = () => { const onBlur = () => {
isKeybindInputFocused.value = false; isKeybindInputFocused.value = false;
lastBlurTime.value = Date.now(); lastBlurTime.value = Date.now();
showEmptyKeybindError.value = false;
}; };
const onFocus = () => { const onFocus = () => {
isKeybindInputFocused.value = true; isKeybindInputFocused.value = true;
activeModifiers.clear(); activeModifiers.clear();
keybind.value = []; keybind.value = [];
showEmptyKeybindError.value = false;
}; };
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
event.preventDefault(); const key = event.code as KeyValues;
const key = event.code;
if (key === "Escape") { if (key === KeyValues.Escape) {
if (keybindInput.value) { if (keybindInput.value) {
keybindInput.value.blur(); keybindInput.value.blur();
} }
@ -142,45 +165,79 @@ const onKeyDown = (event: KeyboardEvent) => {
} }
updateKeybind(); updateKeybind();
showEmptyKeybindError.value = false;
}; };
const saveKeybind = async () => { const saveKeybind = async () => {
console.log("New:", keybind.value); if (keybind.value.length > 0) {
const oldKeybind = await invoke<string[]>("get_keybind"); await $settings.saveSetting("keybind", JSON.stringify(keybind.value));
console.log("Old:", oldKeybind); router.push("/");
await invoke("save_keybind", { keybind: keybind.value }); } else {
showEmptyKeybindError.value = true;
}
}; };
onMounted(() => { const toggleAutostart = async () => {
os.value = platform(); if (autostart.value === true) {
await enable();
} else {
await disable();
}
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
};
keyboard.down("all", (event) => { os.value = platform();
const isMacSaveCombo =
os.value === "macos" &&
(event.code === "MetaLeft" || event.code === "MetaRight") &&
event.key === "Enter";
const isOtherOsSaveCombo = onMounted(async () => {
os.value !== "macos" && keyboard.down([Key.All], (event) => {
(event.code === "ControlLeft" || event.code === "ControlRight") && if (isKeybindInputFocused.value) {
event.key === "Enter"; onKeyDown(event);
}
});
if ( keyboard.down([Key.Escape], (event) => {
(isMacSaveCombo || isOtherOsSaveCombo) && if (isKeybindInputFocused.value) {
!isKeybindInputFocused.value keybindInput.value?.blur();
) { } else {
event.preventDefault(); router.push("/");
}
});
switch (os.value) {
case "macos":
keyboard.down([Key.LeftMeta, Key.Enter], (event) => {
if (!isKeybindInputFocused.value) {
saveKeybind(); saveKeybind();
} }
}); });
keyboard.down("Escape", (event) => { keyboard.down([Key.RightMeta, Key.Enter], (event) => {
const now = Date.now(); if (!isKeybindInputFocused.value) {
if (!isKeybindInputFocused.value && now - lastBlurTime.value > 100) { saveKeybind();
event.preventDefault();
router.push("/");
} }
}); });
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();
}); });
</script> </script>

View file

@ -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<Config, 'prevent'>;
+type PublicConfig = Omit<Config, "prevent">;
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: {

View file

@ -12,14 +12,6 @@ export default defineNuxtPlugin(() => {
async saveSetting(key: string, value: string): Promise<void> { async saveSetting(key: string, value: string): Promise<void> {
await invoke<void>("save_setting", { key, value }); await invoke<void>("save_setting", { key, value });
}, },
async getKeybind(): Promise<string[]> {
return await invoke<string[]>("get_keybind");
},
async saveKeybind(keybind: string[]): Promise<void> {
await invoke<void>("save_keybind", { keybind });
},
}, },
}, },
}; };

2
src-tauri/Cargo.lock generated
View file

@ -4054,7 +4054,7 @@ dependencies = [
[[package]] [[package]]
name = "qopy" name = "qopy"
version = "0.3.3" version = "0.3.4"
dependencies = [ dependencies = [
"active-win-pos-rs", "active-win-pos-rs",
"applications", "applications",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "qopy" name = "qopy"
version = "0.3.3" version = "0.3.4"
description = "Qopy" description = "Qopy"
authors = ["pandadev"] authors = ["pandadev"]
edition = "2021" edition = "2021"

View file

@ -1,14 +1,14 @@
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
use base64::{engine::general_purpose::STANDARD, Engine}; use base64::{ engine::general_purpose::STANDARD, Engine };
// use hyperpolyglot; // use hyperpolyglot;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rdev::{simulate, EventType, Key}; use rdev::{ simulate, EventType, Key };
use regex::Regex; use regex::Regex;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::fs; use std::fs;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{ AtomicBool, Ordering };
use std::{thread, time::Duration}; use std::{ thread, time::Duration };
use tauri::{AppHandle, Emitter, Listener, Manager}; use tauri::{ AppHandle, Emitter, Listener, Manager };
use tauri_plugin_clipboard::Clipboard; use tauri_plugin_clipboard::Clipboard;
use tokio::runtime::Runtime as TokioRuntime; use tokio::runtime::Runtime as TokioRuntime;
use url::Url; use url::Url;
@ -17,7 +17,7 @@ use uuid::Uuid;
use crate::db; use crate::db;
use crate::utils::commands::get_app_info; use crate::utils::commands::get_app_info;
use crate::utils::favicon::fetch_favicon_as_base64; use crate::utils::favicon::fetch_favicon_as_base64;
use crate::utils::types::{ContentType, HistoryItem}; use crate::utils::types::{ ContentType, HistoryItem };
lazy_static! { lazy_static! {
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false); static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
@ -27,16 +27,14 @@ lazy_static! {
pub async fn write_and_paste( pub async fn write_and_paste(
app_handle: AppHandle, app_handle: AppHandle,
content: String, content: String,
content_type: String, content_type: String
) -> Result<(), String> { ) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>(); let clipboard = app_handle.state::<Clipboard>();
match content_type.as_str() { match content_type.as_str() {
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?, "text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
"image" => { "image" => {
clipboard clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
.write_image_base64(content)
.map_err(|e| e.to_string())?;
} }
"files" => { "files" => {
clipboard clipboard
@ -44,11 +42,13 @@ pub async fn write_and_paste(
content content
.split(", ") .split(", ")
.map(|file| file.to_string()) .map(|file| file.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>()
) )
.map_err(|e| e.to_string())?; .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); IS_PROGRAMMATIC_PASTE.store(true, Ordering::SeqCst);
@ -65,7 +65,7 @@ pub async fn write_and_paste(
EventType::KeyPress(modifier_key), EventType::KeyPress(modifier_key),
EventType::KeyPress(Key::KeyV), EventType::KeyPress(Key::KeyV),
EventType::KeyRelease(Key::KeyV), EventType::KeyRelease(Key::KeyV),
EventType::KeyRelease(modifier_key), EventType::KeyRelease(modifier_key)
]; ];
for event in events { for event in events {
@ -81,9 +81,12 @@ pub async fn write_and_paste(
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst); 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 "content_type": content_type
}))); }))
);
Ok(()) Ok(())
} }
@ -92,9 +95,7 @@ pub fn setup(app: &AppHandle) {
let app_handle = app.clone(); let app_handle = app.clone();
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime"); let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
app_handle.clone().listen( app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
"plugin:clipboard://clipboard-monitor/update",
move |_event| {
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
runtime.block_on(async move { runtime.block_on(async move {
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) { if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
@ -111,14 +112,20 @@ pub fn setup(app: &AppHandle) {
if available_types.image { if available_types.image {
println!("Handling image change"); println!("Handling image change");
if let Ok(image_data) = clipboard.read_image_base64() { if let Ok(image_data) = clipboard.read_image_base64() {
let file_path = save_image_to_file(&app_handle, &image_data) let file_path = save_image_to_file(&app_handle, &image_data).await
.await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.unwrap_or_else(|e| e); .unwrap_or_else(|e| e);
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon, None) HistoryItem::new(
app_name,
ContentType::Image,
file_path,
None,
app_icon,
None
)
).await; ).await;
} }
} else if available_types.files { } else if available_types.files {
@ -135,7 +142,7 @@ pub fn setup(app: &AppHandle) {
None, None,
app_icon.clone(), app_icon.clone(),
None None
), )
).await; ).await;
} }
} }
@ -143,7 +150,9 @@ pub fn setup(app: &AppHandle) {
println!("Handling text change"); println!("Handling text change");
if let Ok(text) = clipboard.read_text() { if let Ok(text) = clipboard.read_text() {
let text = text.to_string(); 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(); 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 url_regex.is_match(&text) {
if let Ok(url) = Url::parse(&text) { if let Ok(url) = Url::parse(&text) {
@ -155,7 +164,14 @@ pub fn setup(app: &AppHandle) {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon, None) HistoryItem::new(
app_name,
ContentType::Link,
text,
favicon,
app_icon,
None
)
).await; ).await;
} }
} else { } else {
@ -178,13 +194,27 @@ pub fn setup(app: &AppHandle) {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new(app_name, ContentType::Color, text, None, app_icon, None) HistoryItem::new(
app_name,
ContentType::Color,
text,
None,
app_icon,
None
)
).await; ).await;
} else { } else {
let _ = db::history::add_history_item( let _ = db::history::add_history_item(
app_handle.clone(), app_handle.clone(),
pool, pool,
HistoryItem::new(app_name, ContentType::Text, text.clone(), None, app_icon, None) HistoryItem::new(
app_name,
ContentType::Text,
text.clone(),
None,
app_icon,
None
)
).await; ).await;
} }
} }
@ -199,19 +229,23 @@ pub fn setup(app: &AppHandle) {
} }
let _ = app_handle.emit("clipboard-content-updated", ()); let _ = app_handle.emit("clipboard-content-updated", ());
let _ = app_handle.track_event("clipboard_copied", Some(serde_json::json!({ let _ = app_handle.track_event(
"clipboard_copied",
Some(
serde_json::json!({
"content_type": if available_types.image { "image" } "content_type": if available_types.image { "image" }
else if available_types.files { "files" } else if available_types.files { "files" }
else if available_types.text { "text" } else if available_types.text { "text" }
else { "unknown" } else { "unknown" }
}))); })
}); )
},
); );
});
});
} }
async fn get_pool( async fn get_pool(
app_handle: &AppHandle, app_handle: &AppHandle
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
Ok(app_handle.state::<SqlitePool>()) Ok(app_handle.state::<SqlitePool>())
} }
@ -219,9 +253,7 @@ async fn get_pool(
#[tauri::command] #[tauri::command]
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> { pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
let clipboard = app_handle.state::<Clipboard>(); let clipboard = app_handle.state::<Clipboard>();
clipboard clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
.start_monitor(app_handle.clone())
.map_err(|e| e.to_string())?;
app_handle app_handle
.emit("plugin:clipboard://clipboard-monitor/status", true) .emit("plugin:clipboard://clipboard-monitor/status", true)
.map_err(|e| e.to_string())?; .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( async fn save_image_to_file(
app_handle: &AppHandle, app_handle: &AppHandle,
base64_data: &str, base64_data: &str
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
let app_data_dir = app_handle.path().app_data_dir().unwrap(); let app_data_dir = app_handle.path().app_data_dir().unwrap();
let images_dir = app_data_dir.join("images"); let images_dir = app_data_dir.join("images");

View file

@ -1,48 +1,60 @@
use tauri_plugin_aptabase::EventTracker;
use crate::utils::commands::center_window_on_current_monitor; use crate::utils::commands::center_window_on_current_monitor;
use crate::utils::keys::KeyCode;
use global_hotkey::{ use global_hotkey::{
hotkey::{Code, HotKey, Modifiers}, hotkey::{ Code, HotKey, Modifiers },
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState, GlobalHotKeyEvent,
GlobalHotKeyManager,
HotKeyState,
}; };
use std::cell::RefCell; use lazy_static::lazy_static;
use std::str::FromStr; 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! { lazy_static! {
static HOTKEY_MANAGER: RefCell<Option<GlobalHotKeyManager>> = RefCell::new(None); static ref HOTKEY_MANAGER: Mutex<Option<GlobalHotKeyManager>> = Mutex::new(None);
static ref REGISTERED_HOTKEY: Mutex<Option<HotKey>> = Mutex::new(None);
} }
pub fn setup(app_handle: tauri::AppHandle) { pub fn setup(app_handle: tauri::AppHandle) {
let app_handle_clone = app_handle.clone(); let app_handle_clone = app_handle.clone();
let manager = GlobalHotKeyManager::new().expect("Failed to initialize hotkey manager"); let manager = match GlobalHotKeyManager::new() {
HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager)); 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::<tokio::runtime::Runtime>(); let rt = app_handle.state::<tokio::runtime::Runtime>();
let initial_keybind = rt let initial_keybind = rt
.block_on(crate::db::settings::get_keybind(app_handle_clone.clone())) .block_on(crate::db::settings::get_keybind(app_handle_clone.clone()))
.expect("Failed to get initial keybind"); .expect("Failed to get initial keybind");
let initial_shortcut = initial_keybind.join("+");
let initial_shortcut_for_update = initial_shortcut.clone(); if let Err(e) = register_shortcut(&initial_keybind) {
let initial_shortcut_for_save = initial_shortcut.clone();
if let Err(e) = register_shortcut(&initial_shortcut) {
eprintln!("Error registering initial shortcut: {:?}", e); eprintln!("Error registering initial shortcut: {:?}", e);
} }
app_handle.listen("update-shortcut", move |event| { 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) { if let Some(old_hotkey) = REGISTERED_HOTKEY.lock().unwrap().take() {
HOTKEY_MANAGER.with(|manager| { let manager_guard = HOTKEY_MANAGER.lock().unwrap();
if let Some(manager) = manager.borrow().as_ref() { if let Some(manager) = manager_guard.as_ref() {
let _ = manager.unregister(old_hotkey); let _ = manager.unregister(old_hotkey);
} }
});
} }
if let Err(e) = register_shortcut(&payload_str) { let payload: Vec<String> = serde_json::from_str(payload_str).unwrap_or_default();
if let Err(e) = register_shortcut(&payload) {
eprintln!("Error re-registering shortcut: {:?}", e); eprintln!("Error re-registering shortcut: {:?}", e);
} }
}); });
@ -50,15 +62,15 @@ pub fn setup(app_handle: tauri::AppHandle) {
app_handle.listen("save_keybind", move |event| { app_handle.listen("save_keybind", move |event| {
let payload_str = event.payload().to_string(); let payload_str = event.payload().to_string();
if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_save) { if let Some(old_hotkey) = REGISTERED_HOTKEY.lock().unwrap().take() {
HOTKEY_MANAGER.with(|manager| { let manager_guard = HOTKEY_MANAGER.lock().unwrap();
if let Some(manager) = manager.borrow().as_ref() { if let Some(manager) = manager_guard.as_ref() {
let _ = manager.unregister(old_hotkey); let _ = manager.unregister(old_hotkey);
} }
});
} }
if let Err(e) = register_shortcut(&payload_str) { let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
if let Err(e) = register_shortcut(&payload) {
eprintln!("Error registering saved shortcut: {:?}", e); eprintln!("Error registering saved shortcut: {:?}", e);
} }
}); });
@ -81,48 +93,44 @@ pub fn setup(app_handle: tauri::AppHandle) {
}); });
} }
fn register_shortcut(shortcut: &str) -> Result<(), Box<dyn std::error::Error>> { fn register_shortcut(shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let hotkey = parse_hotkey(shortcut)?; let hotkey = parse_hotkey(shortcut)?;
HOTKEY_MANAGER.with(|manager| {
if let Some(manager) = manager.borrow().as_ref() { let manager_guard = HOTKEY_MANAGER.lock().unwrap();
manager.register(hotkey)?; if let Some(manager) = manager_guard.as_ref() {
} manager.register(hotkey.clone())?;
*REGISTERED_HOTKEY.lock().unwrap() = Some(hotkey);
Ok(()) Ok(())
}) } else {
Err("Hotkey manager not initialized".into())
}
} }
fn parse_hotkey(shortcut: &str) -> Result<HotKey, Box<dyn std::error::Error>> { fn parse_hotkey(shortcut: &[String]) -> Result<HotKey, Box<dyn std::error::Error>> {
let mut modifiers = Modifiers::empty(); let mut modifiers = Modifiers::empty();
let mut code = None; let mut code = None;
let shortcut = shortcut.replace("\"", ""); for part in shortcut {
for part in shortcut.split('+') {
let part = part.trim().to_lowercase();
match part.as_str() { match part.as_str() {
"ctrl" | "control" | "controlleft" => modifiers |= Modifiers::CONTROL, "ControlLeft" => {
"alt" | "altleft" | "optionleft" => modifiers |= Modifiers::ALT, modifiers |= Modifiers::CONTROL;
"shift" | "shiftleft" => modifiers |= Modifiers::SHIFT, }
"super" | "meta" | "cmd" | "metaleft" => modifiers |= Modifiers::META, "AltLeft" => {
modifiers |= Modifiers::ALT;
}
"ShiftLeft" => {
modifiers |= Modifiers::SHIFT;
}
"MetaLeft" => {
modifiers |= Modifiers::META;
}
key => { key => {
let key_code = if key.starts_with("key") { code = Some(Code::from(KeyCode::from_str(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))?,
);
} }
} }
} }
let key_code = let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?;
Ok(HotKey::new(Some(modifiers), key_code)) Ok(HotKey::new(Some(modifiers), key_code))
} }
@ -144,7 +152,12 @@ fn handle_hotkey_event(app_handle: &AppHandle) {
center_window_on_current_monitor(&window); center_window_on_current_monitor(&window);
} }
let _ = app_handle.track_event("hotkey_triggered", Some(serde_json::json!({ let _ = app_handle.track_event(
"hotkey_triggered",
Some(
serde_json::json!({
"action": if window.is_visible().unwrap() { "hide" } else { "show" } "action": if window.is_visible().unwrap() { "hide" } else { "show" }
}))); })
)
);
} }

View file

@ -1,16 +1,15 @@
use tauri::{ use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
menu::{MenuBuilder, MenuItemBuilder},
tray::TrayIconBuilder,
Emitter, Manager,
};
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let window = app.get_webview_window("main").unwrap(); let window = app.get_webview_window("main").unwrap();
let is_visible = window.is_visible().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" } "action": if is_visible { "hide" } else { "show" }
}))); }))
);
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png"); let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap(); let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
@ -18,24 +17,27 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let _tray = TrayIconBuilder::new() let _tray = TrayIconBuilder::new()
.menu( .menu(
&MenuBuilder::new(app) &MenuBuilder::new(app)
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy") .items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?])
.enabled(false)
.build(app)?])
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?]) .items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
.items(&[&MenuItemBuilder::with_id("keybind", "Change keybind").build(app)?]) .items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
.items(&[&MenuItemBuilder::with_id("check_updates", "Check for updates").build(app)?])
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?]) .items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
.build()?, .build()?
) )
.on_menu_event(move |_app, event| match event.id().as_ref() { .on_menu_event(move |_app, event| {
match event.id().as_ref() {
"quit" => { "quit" => {
let _ = _app.track_event("app_quit", None); let _ = _app.track_event("app_quit", None);
std::process::exit(0); std::process::exit(0);
} }
"show" => { "show" => {
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" } "action": if is_visible { "hide" } else { "show" }
}))); })
)
);
let is_visible = window.is_visible().unwrap(); let is_visible = window.is_visible().unwrap();
if is_visible { if is_visible {
window.hide().unwrap(); window.hide().unwrap();
@ -45,18 +47,12 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
} }
window.emit("main_route", ()).unwrap(); window.emit("main_route", ()).unwrap();
} }
"keybind" => { "settings" => {
let _ = _app.track_event("tray_keybind_change", None); let _ = _app.track_event("tray_settings", None);
window.emit("change_keybind", ()).unwrap(); window.emit("settings", ()).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;
});
} }
_ => (), _ => (),
}
}) })
.icon(icon) .icon(icon)
.build(app)?; .build(app)?;

View file

@ -1,6 +1,5 @@
use tauri::Manager; use tauri::{ async_runtime, AppHandle };
use tauri::{async_runtime, AppHandle}; use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
use tauri_plugin_updater::UpdaterExt; use tauri_plugin_updater::UpdaterExt;
pub async fn check_for_updates(app: AppHandle, prompted: bool) { 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() app.dialog()
.message(msg) .message(msg)
.title("Qopy Update Available") .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| { .show(move |response| {
if !response { if !response {
return; return;
} }
async_runtime::spawn(async move { async_runtime::spawn(async move {
match update.download_and_install(|_, _| {}, || {}).await { match
update.download_and_install(
|_, _| {},
|| {}
).await
{
Ok(_) => { Ok(_) => {
app.dialog() app.dialog()
.message("Update installed successfully. The application needs to restart to apply the changes.") .message(
.title("Qopy Needs to Restart") "Update installed successfully. The application needs to restart to apply the changes."
.buttons(MessageDialogButtons::OkCancelCustom(String::from("Restart"), String::from("Cancel"))) )
.title("Qopy Update Installed")
.buttons(
MessageDialogButtons::OkCancelCustom(
String::from("Restart"),
String::from("Cancel")
)
)
.show(move |response| { .show(move |response| {
if response { if response {
app.restart(); app.restart();
@ -47,7 +63,9 @@ pub async fn check_for_updates(app: AppHandle, prompted: bool) {
Err(e) => { Err(e) => {
println!("Error installing new update: {:?}", e); println!("Error installing new update: {:?}", e);
app.dialog() 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) .kind(MessageDialogKind::Error)
.show(|_| {}); .show(|_| {});
} }

View file

@ -1,5 +1,5 @@
use include_dir::{include_dir, Dir}; use include_dir::{ include_dir, Dir };
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
use std::fs; use std::fs;
use tauri::Manager; use tauri::Manager;
use tokio::runtime::Runtime as TokioRuntime; use tokio::runtime::Runtime as TokioRuntime;
@ -25,8 +25,7 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let pool = rt.block_on(async { let pool = rt.block_on(async {
SqlitePoolOptions::new() SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&db_url) .connect(&db_url).await
.await
.expect("Failed to create pool") .expect("Failed to create pool")
}); });
@ -49,29 +48,27 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
} }
async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> { async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query( sqlx
::query(
"CREATE TABLE IF NOT EXISTS schema_version ( "CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY, version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);", );"
) )
.execute(pool) .execute(pool).await?;
.await?;
let current_version: Option<i64> = let current_version: Option<i64> = sqlx
sqlx::query_scalar("SELECT MAX(version) FROM schema_version") ::query_scalar("SELECT MAX(version) FROM schema_version")
.fetch_one(pool) .fetch_one(pool).await?;
.await?;
let current_version = current_version.unwrap_or(0); let current_version = current_version.unwrap_or(0);
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
.files()
.filter_map(|file| { .filter_map(|file| {
let file_name = file.path().file_name()?.to_str()?; 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 let version: i64 = file_name
.trim_start_matches("migration") .trim_start_matches("v")
.trim_end_matches(".sql") .trim_end_matches(".sql")
.parse() .parse()
.ok()?; .ok()?;
@ -93,16 +90,16 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
.collect(); .collect();
for statement in statements { for statement in statements {
sqlx::query(statement) sqlx
.execute(pool) ::query(statement)
.await .execute(pool).await
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?; .map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
} }
sqlx::query("INSERT INTO schema_version (version) VALUES (?)") sqlx
::query("INSERT INTO schema_version (version) VALUES (?)")
.bind(version) .bind(version)
.execute(pool) .execute(pool).await?;
.await?;
} }
} }

View file

@ -1,38 +1,34 @@
use crate::utils::types::{ContentType, HistoryItem}; use crate::utils::types::{ ContentType, HistoryItem };
use base64::{engine::general_purpose::STANDARD, Engine}; use base64::{ engine::general_purpose::STANDARD, Engine };
use rand::distributions::Alphanumeric; use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng}; use rand::{ thread_rng, Rng };
use sqlx::{Row, SqlitePool}; use sqlx::{ Row, SqlitePool };
use std::fs; use std::fs;
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> { pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let id: String = thread_rng() let id: String = thread_rng().sample_iter(&Alphanumeric).take(16).map(char::from).collect();
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
sqlx::query( sqlx
::query(
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)" "INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
) )
.bind(id) .bind(id)
.bind("System") .bind("System")
.bind("text") .bind("text")
.bind("Welcome to your clipboard history!") .bind("Welcome to your clipboard history!")
.execute(pool) .execute(pool).await?;
.await?;
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> { pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
let rows = sqlx::query( let rows = sqlx
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC", ::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
) )
.fetch_all(&*pool) .fetch_all(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let items = rows let items = rows
@ -56,31 +52,32 @@ pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<Histo
pub async fn add_history_item( pub async fn add_history_item(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
item: HistoryItem, item: HistoryItem
) -> Result<(), String> { ) -> Result<(), String> {
let (id, source, source_icon, content_type, content, favicon, timestamp, language) = let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
item.to_row(); 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)
.bind(&content_type) .bind(&content_type)
.fetch_optional(&*pool) .fetch_optional(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
match existing { match existing {
Some(_) => { Some(_) => {
sqlx::query( sqlx
::query(
"UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?" "UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?"
) )
.bind(&content) .bind(&content)
.bind(&content_type) .bind(&content_type)
.execute(&*pool) .execute(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
} }
None => { None => {
sqlx::query( sqlx
::query(
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" "INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
) )
.bind(id) .bind(id)
@ -91,15 +88,17 @@ pub async fn add_history_item(
.bind(favicon) .bind(favicon)
.bind(timestamp) .bind(timestamp)
.bind(language) .bind(language)
.execute(&*pool) .execute(&*pool).await
.await
.map_err(|e| e.to_string())?; .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() "content_type": item.content_type.to_string()
}))); }))
);
Ok(()) Ok(())
} }
@ -107,15 +106,15 @@ pub async fn add_history_item(
#[tauri::command] #[tauri::command]
pub async fn search_history( pub async fn search_history(
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
query: String, query: String
) -> Result<Vec<HistoryItem>, String> { ) -> Result<Vec<HistoryItem>, String> {
let query = format!("%{}%", query); let query = format!("%{}%", query);
let rows = sqlx::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" "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history WHERE content LIKE ? ORDER BY timestamp DESC"
) )
.bind(query) .bind(query)
.fetch_all(&*pool) .fetch_all(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let items = rows let items = rows
@ -139,15 +138,15 @@ pub async fn search_history(
pub async fn load_history_chunk( pub async fn load_history_chunk(
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
offset: i64, offset: i64,
limit: i64, limit: i64
) -> Result<Vec<HistoryItem>, String> { ) -> Result<Vec<HistoryItem>, String> {
let rows = sqlx::query( let rows = sqlx
::query(
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?" "SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC LIMIT ? OFFSET ?"
) )
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(&*pool) .fetch_all(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let items = rows let items = rows
@ -171,12 +170,12 @@ pub async fn load_history_chunk(
pub async fn delete_history_item( pub async fn delete_history_item(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
id: String, id: String
) -> Result<(), String> { ) -> Result<(), String> {
sqlx::query("DELETE FROM history WHERE id = ?") sqlx
::query("DELETE FROM history WHERE id = ?")
.bind(id) .bind(id)
.execute(&*pool) .execute(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_item_deleted", None); let _ = app_handle.track_event("history_item_deleted", None);
@ -189,9 +188,9 @@ pub async fn clear_history(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool> pool: tauri::State<'_, SqlitePool>
) -> Result<(), String> { ) -> Result<(), String> {
sqlx::query("DELETE FROM history") sqlx
.execute(&*pool) ::query("DELETE FROM history")
.await .execute(&*pool).await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let _ = app_handle.track_event("history_cleared", None); let _ = app_handle.track_event("history_cleared", None);

View file

@ -0,0 +1 @@
INSERT INTO settings (key, value) VALUES ('autostart', 'true');

View file

@ -1,8 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{ Deserialize, Serialize };
use serde_json; use serde_json;
use sqlx::Row; use sqlx::Row;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tauri::{Emitter, Manager}; use tauri::{ Emitter, Manager };
use tauri_plugin_aptabase::EventTracker; use tauri_plugin_aptabase::EventTracker;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -16,10 +16,10 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
}; };
let json = serde_json::to_string(&default_keybind)?; let json = serde_json::to_string(&default_keybind)?;
sqlx::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)") sqlx
::query("INSERT INTO settings (key, value) VALUES ('keybind', ?)")
.bind(json) .bind(json)
.execute(pool) .execute(pool).await?;
.await?;
Ok(()) Ok(())
} }
@ -28,26 +28,24 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
pub async fn save_keybind( pub async fn save_keybind(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
keybind: Vec<String>, keybind: Vec<String>
) -> Result<(), String> { ) -> Result<(), String> {
let keybind_str = keybind.join("+"); app_handle.emit("update-shortcut", &keybind).map_err(|e| e.to_string())?;
let keybind_clone = keybind_str.clone();
app_handle
.emit("update-shortcut", &keybind_str)
.map_err(|e| e.to_string())?;
let json = serde_json::to_string(&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) .bind(json)
.execute(&*pool) .execute(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let _ = app_handle.track_event("keybind_saved", Some(serde_json::json!({ let _ = app_handle.track_event(
"keybind": keybind_clone "keybind_saved",
}))); Some(serde_json::json!({
"keybind": keybind
}))
);
Ok(()) Ok(())
} }
@ -55,12 +53,12 @@ pub async fn save_keybind(
#[tauri::command] #[tauri::command]
pub async fn get_setting( pub async fn get_setting(
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
key: String, key: String
) -> Result<String, String> { ) -> Result<String, String> {
let row = sqlx::query("SELECT value FROM settings WHERE key = ?") let row = sqlx
::query("SELECT value FROM settings WHERE key = ?")
.bind(key) .bind(key)
.fetch_optional(&*pool) .fetch_optional(&*pool).await
.await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(row.map(|r| r.get("value")).unwrap_or_default()) Ok(row.map(|r| r.get("value")).unwrap_or_default())
@ -71,18 +69,21 @@ pub async fn save_setting(
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
pool: tauri::State<'_, SqlitePool>, pool: tauri::State<'_, SqlitePool>,
key: String, key: String,
value: String, value: String
) -> Result<(), 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(key.clone())
.bind(value) .bind(value)
.execute(&*pool) .execute(&*pool).await
.await
.map_err(|e| e.to_string())?; .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 "key": key
}))); }))
);
Ok(()) Ok(())
} }
@ -91,13 +92,16 @@ pub async fn save_setting(
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> { pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
let pool = app_handle.state::<SqlitePool>(); let pool = app_handle.state::<SqlitePool>();
let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'") let row = sqlx
.fetch_optional(&*pool) ::query("SELECT value FROM settings WHERE key = 'keybind'")
.await .fetch_optional(&*pool).await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let json = row.map(|r| r.get::<String, _>("value")).unwrap_or_else(|| { let json = row
serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()]) .map(|r| r.get::<String, _>("value"))
.unwrap_or_else(|| {
serde_json
::to_string(&vec!["MetaLeft".to_string(), "KeyV".to_string()])
.expect("Failed to serialize default keybind") .expect("Failed to serialize default keybind")
}); });

View file

@ -1,7 +1,4 @@
#![cfg_attr( #![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod api; mod api;
mod db; mod db;
@ -10,7 +7,7 @@ mod utils;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use std::fs; use std::fs;
use tauri::Manager; use tauri::Manager;
use tauri_plugin_aptabase::{EventTracker, InitOptions}; use tauri_plugin_aptabase::{ EventTracker, InitOptions };
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_prevent_default::Flags; 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 runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
let _guard = runtime.enter(); let _guard = runtime.enter();
tauri::Builder::default() tauri::Builder
::default()
.plugin(tauri_plugin_clipboard::init()) .plugin(tauri_plugin_clipboard::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_sql::Builder::default().build()) .plugin(tauri_plugin_sql::Builder::default().build())
@ -26,12 +24,14 @@ fn main() {
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_updater::Builder::default().build()) .plugin(tauri_plugin_updater::Builder::default().build())
.plugin( .plugin(
tauri_plugin_aptabase::Builder::new("A-SH-8937252746") tauri_plugin_aptabase::Builder
::new("A-SH-8937252746")
.with_options(InitOptions { .with_options(InitOptions {
host: Some("https://aptabase.pandadev.net".to_string()), host: Some("https://aptabase.pandadev.net".to_string()),
flush_interval: None, flush_interval: None,
}) })
.with_panic_hook(Box::new(|client, info, msg| { .with_panic_hook(
Box::new(|client, info, msg| {
let location = info let location = info
.location() .location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
@ -39,21 +39,22 @@ fn main() {
let _ = client.track_event( let _ = client.track_event(
"panic", "panic",
Some(serde_json::json!({ Some(
serde_json::json!({
"info": format!("{} ({})", msg, location), "info": format!("{} ({})", msg, location),
})), })
);
}))
.build(),
) )
.plugin(tauri_plugin_autostart::init( );
MacosLauncher::LaunchAgent, })
Some(vec![]), )
)) .build()
)
.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])))
.plugin( .plugin(
tauri_plugin_prevent_default::Builder::new() tauri_plugin_prevent_default::Builder
::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU)) .with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build(), .build()
) )
.setup(|app| { .setup(|app| {
let app_data_dir = app.path().app_data_dir().unwrap(); let app_data_dir = app.path().app_data_dir().unwrap();
@ -75,8 +76,7 @@ fn main() {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&db_url) .connect(&db_url).await
.await
.expect("Failed to create pool"); .expect("Failed to create pool");
app_handle_clone.manage(pool); app_handle_clone.manage(pool);
@ -91,7 +91,10 @@ fn main() {
let _ = api::clipboard::start_monitor(app_handle.clone()); let _ = api::clipboard::start_monitor(app_handle.clone());
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap()); 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); let _ = app.track_event("app_started", None);
@ -109,7 +112,8 @@ fn main() {
} }
} }
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(
tauri::generate_handler![
api::clipboard::write_and_paste, api::clipboard::write_and_paste,
db::history::get_history, db::history::get_history,
db::history::add_history_item, db::history::add_history_item,
@ -122,8 +126,9 @@ fn main() {
db::settings::save_setting, db::settings::save_setting,
db::settings::save_keybind, db::settings::save_keybind,
db::settings::get_keybind, db::settings::get_keybind,
utils::commands::fetch_page_meta, utils::commands::fetch_page_meta
]) ]
)
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View file

@ -1,11 +1,16 @@
use active_win_pos_rs::get_active_window; 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 image::codecs::png::PngEncoder;
use tauri::PhysicalPosition; use tauri::PhysicalPosition;
use meta_fetcher; use meta_fetcher;
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) { pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| { if
let Some(monitor) = window
.available_monitors()
.unwrap()
.iter()
.find(|m| {
let primary_monitor = window let primary_monitor = window
.primary_monitor() .primary_monitor()
.unwrap() .unwrap()
@ -13,22 +18,20 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
let mouse_position = primary_monitor.position(); let mouse_position = primary_monitor.position();
let monitor_position = m.position(); let monitor_position = m.position();
let monitor_size = m.size(); let monitor_size = m.size();
mouse_position.x >= monitor_position.x mouse_position.x >= monitor_position.x &&
&& mouse_position.x < monitor_position.x + monitor_size.width as i32 mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
&& mouse_position.y >= monitor_position.y mouse_position.y >= monitor_position.y &&
&& mouse_position.y < monitor_position.y + monitor_size.height as i32 mouse_position.y < monitor_position.y + (monitor_size.height as i32)
}) { })
{
let monitor_size = monitor.size(); let monitor_size = monitor.size();
let window_size = window.outer_size().unwrap(); let window_size = window.outer_size().unwrap();
let x = (monitor_size.width as i32 - window_size.width 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; let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
window window
.set_position(PhysicalPosition::new( .set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
monitor.position().x + x,
monitor.position().y + y,
))
.unwrap(); .unwrap();
} }
} }
@ -51,7 +54,6 @@ fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Err
Ok(STANDARD.encode(png_buffer)) Ok(STANDARD.encode(png_buffer))
} }
pub fn detect_color(color: &str) -> bool { pub fn detect_color(color: &str) -> bool {
let color = color.trim().to_lowercase(); let color = color.trim().to_lowercase();
@ -60,12 +62,16 @@ pub fn detect_color(color: &str) -> bool {
let hex = &color[1..]; let hex = &color[1..];
return match hex.len() { return match hex.len() {
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()), 3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
_ => false _ => false,
}; };
} }
// rgb/rgba // 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 let values = color
.trim_start_matches("rgba(") .trim_start_matches("rgba(")
.trim_start_matches("rgb(") .trim_start_matches("rgb(")
@ -75,12 +81,16 @@ pub fn detect_color(color: &str) -> bool {
return match values.len() { return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()), 3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false _ => false,
}; };
} }
// hsl/hsla // hsl/hsla
if (color.starts_with("hsl(") || color.starts_with("hsla(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") { if
(color.starts_with("hsl(") || color.starts_with("hsla(")) &&
color.ends_with(")") &&
!color[..color.len() - 1].contains(")")
{
let values = color let values = color
.trim_start_matches("hsla(") .trim_start_matches("hsla(")
.trim_start_matches("hsl(") .trim_start_matches("hsl(")
@ -90,7 +100,7 @@ pub fn detect_color(color: &str) -> bool {
return match values.len() { return match values.len() {
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()), 3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
_ => false _ => false,
}; };
} }
@ -99,11 +109,9 @@ pub fn detect_color(color: &str) -> bool {
#[tauri::command] #[tauri::command]
pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> { pub async fn fetch_page_meta(url: String) -> Result<(String, Option<String>), String> {
let metadata = meta_fetcher::fetch_metadata(&url) let metadata = meta_fetcher
::fetch_metadata(&url)
.map_err(|e| format!("Failed to fetch metadata: {}", e))?; .map_err(|e| format!("Failed to fetch metadata: {}", e))?;
Ok(( Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
metadata.title.unwrap_or_else(|| "No title found".to_string()),
metadata.image
))
} }

View file

@ -5,7 +5,7 @@ use reqwest;
use url::Url; use url::Url;
pub async fn fetch_favicon_as_base64( pub async fn fetch_favicon_as_base64(
url: Url, url: Url
) -> Result<Option<String>, Box<dyn std::error::Error>> { ) -> Result<Option<String>, Box<dyn std::error::Error>> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap()); let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());

120
src-tauri/src/utils/keys.rs Normal file
View file

@ -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<Self, Self::Err> {
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<KeyCode> for Code {
fn from(key_code: KeyCode) -> Self {
key_code.0
}
}

View file

@ -1,6 +1,6 @@
use chrono; use chrono;
use log::{LevelFilter, SetLoggerError}; use log::{ LevelFilter, SetLoggerError };
use std::fs::{File, OpenOptions}; use std::fs::{ File, OpenOptions };
use std::io::Write; use std::io::Write;
use std::panic; use std::panic;
@ -50,19 +50,22 @@ pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError>
// Set up panic hook // Set up panic hook
let panic_file = file.try_clone().expect("Failed to clone file handle"); let panic_file = file.try_clone().expect("Failed to clone file handle");
panic::set_hook(Box::new(move |panic_info| { panic::set_hook(
Box::new(move |panic_info| {
let mut file = panic_file.try_clone().expect("Failed to clone file handle"); let mut file = panic_file.try_clone().expect("Failed to clone file handle");
let location = panic_info.location() let location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column())) .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string()); .unwrap_or_else(|| "unknown location".to_string());
let message = match panic_info.payload().downcast_ref::<&str>() { let message = match panic_info.payload().downcast_ref::<&str>() {
Some(s) => *s, Some(s) => *s,
None => match panic_info.payload().downcast_ref::<String>() { None =>
match panic_info.payload().downcast_ref::<String>() {
Some(s) => s.as_str(), Some(s) => s.as_str(),
None => "Unknown panic message", None => "Unknown panic message",
}, }
}; };
let _ = writeln!( let _ = writeln!(
@ -72,10 +75,13 @@ pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError>
message, message,
location location
); );
})); })
);
let logger = Box::new(FileLogger { file }); 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); log::set_max_level(LevelFilter::Debug);
Ok(()) Ok(())
} }

View file

@ -2,3 +2,4 @@ pub mod commands;
pub mod favicon; pub mod favicon;
pub mod types; pub mod types;
pub mod logger; pub mod logger;
pub mod keys;

View file

@ -1,5 +1,5 @@
use chrono::{DateTime, Utc}; use chrono::{ DateTime, Utc };
use serde::{Deserialize, Serialize}; use serde::{ Deserialize, Serialize };
use std::fmt; use std::fmt;
use uuid::Uuid; use uuid::Uuid;
@ -115,7 +115,7 @@ impl HistoryItem {
content: String, content: String,
favicon: Option<String>, favicon: Option<String>,
source_icon: Option<String>, source_icon: Option<String>,
language: Option<String>, language: Option<String>
) -> Self { ) -> Self {
Self { Self {
id: Uuid::new_v4().to_string(), id: Uuid::new_v4().to_string(),
@ -130,7 +130,7 @@ impl HistoryItem {
} }
pub fn to_row( pub fn to_row(
&self, &self
) -> ( ) -> (
String, String,
String, String,

View file

@ -1,6 +1,6 @@
{ {
"productName": "Qopy", "productName": "Qopy",
"version": "0.3.3", "version": "0.3.4",
"identifier": "net.pandadev.qopy", "identifier": "net.pandadev.qopy",
"build": { "build": {
"frontendDist": "../dist", "frontendDist": "../dist",

217
types/keys.ts Normal file
View file

@ -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',
}