mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-21 21:24:05 +02:00
Merge pull request #29 from 0PandaDEV/issue/settings
This commit is contained in:
commit
c0b50fcc80
31 changed files with 1446 additions and 704 deletions
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
|
@ -234,30 +234,41 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if release already exists
|
||||
id: check_release
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
RELEASE_EXISTS=$(gh release view v$VERSION --json id --jq '.id' 2>/dev/null || echo "")
|
||||
if [ -n "$RELEASE_EXISTS" ]; then
|
||||
echo "Release v$VERSION already exists. Skipping release creation."
|
||||
echo "SKIP_RELEASE=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Release v$VERSION does not exist. Proceeding with release creation."
|
||||
echo "SKIP_RELEASE=false" >> $GITHUB_ENV
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download all artifacts
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Update CHANGELOG
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: changelog
|
||||
uses: requarks/changelog-action@v1
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
tag: ${{ github.ref_name }}
|
||||
|
||||
- name: Generate Release Body
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
id: release_body
|
||||
run: |
|
||||
VERSION="${{ needs.prepare.outputs.version }}"
|
||||
|
||||
# Get the most recent release tag (v* tags only)
|
||||
LAST_TAG=$(git describe --match "v*" --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1` 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$LAST_TAG" ]; then
|
||||
echo "Debug: Found last release tag: $LAST_TAG"
|
||||
CHANGES=$(git log ${LAST_TAG}..HEAD --pretty=format:"- %s")
|
||||
else
|
||||
echo "Debug: No previous release tag found, using first commit"
|
||||
CHANGES=$(git log --pretty=format:"- %s")
|
||||
fi
|
||||
|
||||
echo "Debug: Changelog content:"
|
||||
echo "$CHANGES"
|
||||
|
||||
# Calculate hashes with corrected paths
|
||||
WINDOWS_ARM_HASH=$(sha256sum "artifacts/windows-arm64-binaries/Qopy-${VERSION}_arm64.msi" | awk '{ print $1 }')
|
||||
WINDOWS_64_HASH=$(sha256sum "artifacts/windows-x64-binaries/Qopy-${VERSION}_x64.msi" | awk '{ print $1 }')
|
||||
|
@ -278,9 +289,8 @@ jobs:
|
|||
echo "Red Hat: $REDHAT_HASH"
|
||||
|
||||
RELEASE_BODY=$(cat <<-EOF
|
||||
## ♻️ Changelog
|
||||
|
||||
$CHANGES
|
||||
${{ needs.create-release.outputs.changelog }}
|
||||
|
||||
## ⬇️ Downloads
|
||||
|
||||
|
@ -299,6 +309,7 @@ jobs:
|
|||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: env.SKIP_RELEASE == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -12,7 +12,7 @@ All the data of Qopy is stored inside of a SQLite database.
|
|||
|
||||
## Disable Windows+V for default clipboard manager
|
||||
|
||||
https://github.com/user-attachments/assets/723f9e07-3190-46ec-9bb7-15dfc112f620
|
||||
<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
|
||||
|
||||
|
|
42
app.vue
42
app.vue
|
@ -1,27 +1,36 @@
|
|||
<template>
|
||||
<div style="pointer-events: auto;">
|
||||
<div style="pointer-events: auto">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { app, window } from '@tauri-apps/api';
|
||||
import { onMounted } from 'vue'
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { app, window } from "@tauri-apps/api";
|
||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
const keyboard = useKeyboard();
|
||||
const { $settings } = useNuxtApp();
|
||||
|
||||
onMounted(async () => {
|
||||
await listen('change_keybind', async () => {
|
||||
console.log("change_keybind");
|
||||
await navigateTo('/settings')
|
||||
await listen("settings", async () => {
|
||||
keyboard.unregisterAll();
|
||||
await navigateTo("/settings");
|
||||
await app.show();
|
||||
await window.getCurrentWindow().show();
|
||||
})
|
||||
});
|
||||
|
||||
await listen('main_route', async () => {
|
||||
console.log("main_route");
|
||||
await navigateTo('/')
|
||||
})
|
||||
})
|
||||
if ((await $settings.getSetting("autostart")) === "true") {
|
||||
await enable();
|
||||
} else {
|
||||
await disable();
|
||||
}
|
||||
|
||||
await listen("main_route", async () => {
|
||||
await navigateTo("/");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -53,7 +62,6 @@ onMounted(async () => {
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
color: #E5DFD5;
|
||||
text-decoration: none;
|
||||
font-family: SFRoundedRegular;
|
||||
scroll-behavior: smooth;
|
||||
|
@ -62,9 +70,9 @@ onMounted(async () => {
|
|||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
--os-handle-bg: #ADA9A1;
|
||||
--os-handle-bg-hover: #78756F;
|
||||
--os-handle-bg-active: #78756F;
|
||||
--os-handle-bg: #ada9a1;
|
||||
--os-handle-bg-hover: #78756f;
|
||||
--os-handle-bg-active: #78756f;
|
||||
}
|
||||
|
||||
html,
|
||||
|
|
|
@ -22,7 +22,7 @@ $mutedtext: #78756f;
|
|||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 54px;
|
||||
height: 56px;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
|
@ -35,10 +35,10 @@ $mutedtext: #78756f;
|
|||
|
||||
.results {
|
||||
position: absolute;
|
||||
width: 284px;
|
||||
top: 53px;
|
||||
width: 286px;
|
||||
top: 55px;
|
||||
left: 0;
|
||||
height: calc(100vh - 95px);
|
||||
height: 417px;
|
||||
border-right: 1px solid $divider;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -46,6 +46,7 @@ $mutedtext: #78756f;
|
|||
padding-bottom: 8px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 3;
|
||||
|
||||
.result {
|
||||
height: 40px;
|
||||
|
@ -59,6 +60,7 @@ $mutedtext: #78756f;
|
|||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
white-space: nowrap;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.result {
|
||||
|
@ -96,20 +98,22 @@ $mutedtext: #78756f;
|
|||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 53px;
|
||||
left: 284px;
|
||||
height: calc(100vh - 254px);
|
||||
top: 55px;
|
||||
left: 285px;
|
||||
height: 220px;
|
||||
font-family: CommitMono !important;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1;
|
||||
border-radius: 10px;
|
||||
width: calc(100vw - 286px);
|
||||
width: 465px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
color: $text;
|
||||
|
||||
&:not(:has(.image)) {
|
||||
padding: 8px;
|
||||
|
@ -128,7 +132,7 @@ $mutedtext: #78756f;
|
|||
}
|
||||
|
||||
.bottom-bar {
|
||||
height: 40px;
|
||||
height: 39px;
|
||||
width: calc(100vw - 2px);
|
||||
backdrop-filter: blur(18px);
|
||||
background-color: hsla(40, 3%, 16%, 0.8);
|
||||
|
@ -215,18 +219,20 @@ $mutedtext: #78756f;
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
bottom: 40px;
|
||||
left: 284px;
|
||||
bottom: 39px;
|
||||
left: 285px;
|
||||
height: 160px;
|
||||
width: calc(100vw - 286px);
|
||||
width: 465px;
|
||||
border-top: 1px solid $divider;
|
||||
background-color: $primary;
|
||||
padding: 14px;
|
||||
z-index: 1;
|
||||
|
||||
.title {
|
||||
font-family: SFRoundedSemiBold;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.6px;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
|
|
|
@ -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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
gap: 6px;
|
||||
gap: 16px;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
p {
|
||||
font-family: SFRoundedSemiBold;
|
||||
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;
|
||||
border: 1px solid $divider;
|
||||
color: $text2;
|
||||
display: flex;
|
||||
border-radius: 13px;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
|
||||
.key {
|
||||
color: $text2;
|
||||
font-family: SFRoundedMedium;
|
||||
background-color: $divider;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
padding: 2px 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-input:focus {
|
||||
.keybind-input:focus {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-keybind {
|
||||
border-color: rgba(255, 82, 82, 0.298);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
border-bottom: 1px solid $divider;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
|
@ -136,6 +212,15 @@ $mutedtext: #78756f;
|
|||
background-color: transparent;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions:hover {
|
||||
|
|
|
@ -20,9 +20,12 @@
|
|||
"sass-embedded": "1.83.0",
|
||||
"uuid": "11.0.3",
|
||||
"vue": "3.5.13",
|
||||
"wrdu-keyboard": "1.1.1"
|
||||
"wrdu-keyboard": "3.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"chokidar": "^3.6.0"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"wrdu-keyboard@3.0.0": "patches/wrdu-keyboard@3.0.0.patch"
|
||||
}
|
||||
}
|
144
pages/index.vue
144
pages/index.vue
|
@ -65,10 +65,17 @@
|
|||
</template>
|
||||
<template v-else-if="hasFavicon(item.favicon ?? '')">
|
||||
<img
|
||||
:src="item.favicon ? getFaviconFromDb(item.favicon) : '../public/icons/Link.svg'"
|
||||
:src="
|
||||
item.favicon
|
||||
? getFaviconFromDb(item.favicon)
|
||||
: '../public/icons/Link.svg'
|
||||
"
|
||||
alt="Favicon"
|
||||
class="favicon"
|
||||
@error="($event.target as HTMLImageElement).src = '../public/icons/Link.svg'" />
|
||||
@error="
|
||||
($event.target as HTMLImageElement).src =
|
||||
'../public/icons/Link.svg'
|
||||
" />
|
||||
</template>
|
||||
<img
|
||||
src="../public/icons/File.svg"
|
||||
|
@ -121,8 +128,12 @@
|
|||
:src="getYoutubeThumbnail(selectedItem.content)"
|
||||
alt="YouTube Thumbnail" />
|
||||
</div>
|
||||
<div class="content" v-else-if="selectedItem?.content_type === ContentType.Link && pageOgImage">
|
||||
<img :src="pageOgImage" alt="Image" class="image">
|
||||
<div
|
||||
class="content"
|
||||
v-else-if="
|
||||
selectedItem?.content_type === ContentType.Link && pageOgImage
|
||||
">
|
||||
<img :src="pageOgImage" alt="Image" class="image" />
|
||||
</div>
|
||||
<OverlayScrollbarsComponent v-else class="content">
|
||||
<span>{{ selectedItem?.content || "" }}</span>
|
||||
|
@ -135,9 +146,7 @@
|
|||
<div class="info-content" v-if="selectedItem && getInfo">
|
||||
<div class="info-row" v-for="(row, index) in infoRows" :key="index">
|
||||
<p class="label">{{ row.label }}</p>
|
||||
<span
|
||||
:class="{ 'url-truncate': row.isUrl }"
|
||||
:data-text="row.value">
|
||||
<span :class="{ 'url-truncate': row.isUrl }" :data-text="row.value">
|
||||
{{ row.value }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -153,12 +162,19 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
|||
import "overlayscrollbars/overlayscrollbars.css";
|
||||
import { app, window } from "@tauri-apps/api";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useNuxtApp } from "#app";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { HistoryItem, ContentType } from "~/types/types";
|
||||
import type { InfoText, InfoImage, InfoFile, InfoLink, InfoColor, InfoCode } from "~/types/types";
|
||||
import type {
|
||||
InfoText,
|
||||
InfoImage,
|
||||
InfoFile,
|
||||
InfoLink,
|
||||
InfoColor,
|
||||
InfoCode,
|
||||
} from "~/types/types";
|
||||
import { Key } from "wrdu-keyboard/key";
|
||||
|
||||
interface GroupedHistory {
|
||||
label: string;
|
||||
|
@ -188,8 +204,8 @@ const imageSizes = shallowRef<Record<string, string>>({});
|
|||
const lastUpdateTime = ref<number>(Date.now());
|
||||
const imageLoadError = ref<boolean>(false);
|
||||
const imageLoading = ref<boolean>(false);
|
||||
const pageTitle = ref<string>('');
|
||||
const pageOgImage = ref<string>('');
|
||||
const pageTitle = ref<string>("");
|
||||
const pageOgImage = ref<string>("");
|
||||
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
|
@ -583,41 +599,35 @@ const setupEventListeners = async (): Promise<void> => {
|
|||
searchInput.value?.blur();
|
||||
});
|
||||
|
||||
keyboard.down("ArrowDown", (event) => {
|
||||
event.preventDefault();
|
||||
keyboard.prevent.down([Key.DownArrow], (event) => {
|
||||
selectNext();
|
||||
});
|
||||
|
||||
keyboard.down("ArrowUp", (event) => {
|
||||
event.preventDefault();
|
||||
keyboard.prevent.down([Key.UpArrow], (event) => {
|
||||
selectPrevious();
|
||||
});
|
||||
|
||||
keyboard.down("Enter", (event) => {
|
||||
event.preventDefault();
|
||||
keyboard.prevent.down([Key.Enter], (event) => {
|
||||
pasteSelectedItem();
|
||||
});
|
||||
|
||||
keyboard.down("Escape", (event) => {
|
||||
event.preventDefault();
|
||||
keyboard.prevent.down([Key.Escape], (event) => {
|
||||
hideApp();
|
||||
});
|
||||
|
||||
keyboard.down("all", (event) => {
|
||||
const isMacActionCombo =
|
||||
os.value === "macos" &&
|
||||
(event.code === "MetaLeft" || event.code === "MetaRight") &&
|
||||
event.key === "k";
|
||||
switch (os.value) {
|
||||
case "macos":
|
||||
keyboard.prevent.down([Key.LeftMeta, Key.K], (event) => {});
|
||||
|
||||
const isOtherOsActionCombo =
|
||||
os.value !== "macos" &&
|
||||
(event.code === "ControlLeft" || event.code === "ControlRight") &&
|
||||
event.key === "k";
|
||||
keyboard.prevent.down([Key.RightMeta, Key.K], (event) => {});
|
||||
break;
|
||||
|
||||
if (isMacActionCombo || isOtherOsActionCombo) {
|
||||
event.preventDefault();
|
||||
case "linux" || "windows":
|
||||
keyboard.prevent.down([Key.LeftControl, Key.K], (event) => {});
|
||||
|
||||
keyboard.prevent.down([Key.RightControl, Key.K], (event) => {});
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const hideApp = async (): Promise<void> => {
|
||||
|
@ -646,7 +656,7 @@ watch(searchQuery, () => {
|
|||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
os.value = await platform();
|
||||
os.value = platform();
|
||||
await loadHistoryChunk();
|
||||
|
||||
resultsContainer.value
|
||||
|
@ -655,10 +665,6 @@ onMounted(async () => {
|
|||
?.viewport?.addEventListener("scroll", handleScroll);
|
||||
|
||||
await setupEventListeners();
|
||||
|
||||
if (!(await isEnabled())) {
|
||||
await enable();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during onMounted:", error);
|
||||
}
|
||||
|
@ -686,27 +692,33 @@ const formatFileSize = (bytes: number): string => {
|
|||
|
||||
const fetchPageMeta = async (url: string) => {
|
||||
try {
|
||||
const [title, ogImage] = await invoke('fetch_page_meta', { url }) as [string, string | null];
|
||||
const [title, ogImage] = (await invoke("fetch_page_meta", { url })) as [
|
||||
string,
|
||||
string | null
|
||||
];
|
||||
pageTitle.value = title;
|
||||
if (ogImage) {
|
||||
pageOgImage.value = ogImage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching page meta:', error);
|
||||
pageTitle.value = 'Error loading title';
|
||||
console.error("Error fetching page meta:", error);
|
||||
pageTitle.value = "Error loading title";
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => selectedItem.value, (newItem) => {
|
||||
watch(
|
||||
() => selectedItem.value,
|
||||
(newItem) => {
|
||||
if (newItem?.content_type === ContentType.Link) {
|
||||
pageTitle.value = 'Loading...';
|
||||
pageOgImage.value = '';
|
||||
pageTitle.value = "Loading...";
|
||||
pageOgImage.value = "";
|
||||
fetchPageMeta(newItem.content);
|
||||
} else {
|
||||
pageTitle.value = '';
|
||||
pageOgImage.value = '';
|
||||
pageTitle.value = "";
|
||||
pageOgImage.value = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const getInfo = computed(() => {
|
||||
if (!selectedItem.value) return null;
|
||||
|
@ -716,7 +728,10 @@ const getInfo = computed(() => {
|
|||
copied: selectedItem.value.timestamp,
|
||||
};
|
||||
|
||||
const infoMap: Record<ContentType, () => InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode> = {
|
||||
const infoMap: Record<
|
||||
ContentType,
|
||||
() => InfoText | InfoImage | InfoFile | InfoLink | InfoColor | InfoCode
|
||||
> = {
|
||||
[ContentType.Text]: () => ({
|
||||
...baseInfo,
|
||||
content_type: ContentType.Text,
|
||||
|
@ -754,7 +769,8 @@ const getInfo = computed(() => {
|
|||
|
||||
const max = Math.max(rNorm, gNorm, bNorm);
|
||||
const min = Math.min(rNorm, gNorm, bNorm);
|
||||
let h = 0, s = 0;
|
||||
let h = 0,
|
||||
s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
|
@ -780,14 +796,16 @@ const getInfo = computed(() => {
|
|||
content_type: ContentType.Color,
|
||||
hex: hex,
|
||||
rgb: `rgb(${r}, ${g}, ${b})`,
|
||||
hsl: `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`,
|
||||
hsl: `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(
|
||||
l * 100
|
||||
)}%)`,
|
||||
};
|
||||
},
|
||||
[ContentType.Code]: () => ({
|
||||
...baseInfo,
|
||||
content_type: ContentType.Code,
|
||||
language: selectedItem.value!.language ?? "Unknown",
|
||||
lines: selectedItem.value!.content.split('\n').length,
|
||||
lines: selectedItem.value!.content.split("\n").length,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -799,24 +817,37 @@ const infoRows = computed(() => {
|
|||
|
||||
const commonRows = [
|
||||
{ label: "Source", value: getInfo.value.source, isUrl: false },
|
||||
{ label: "Content Type", value: getInfo.value.content_type.charAt(0).toUpperCase() + getInfo.value.content_type.slice(1), isUrl: false },
|
||||
{
|
||||
label: "Content Type",
|
||||
value:
|
||||
getInfo.value.content_type.charAt(0).toUpperCase() +
|
||||
getInfo.value.content_type.slice(1),
|
||||
isUrl: false,
|
||||
},
|
||||
];
|
||||
|
||||
const typeSpecificRows: Record<ContentType, Array<{ label: string; value: string | number; isUrl?: boolean }>> = {
|
||||
const typeSpecificRows: Record<
|
||||
ContentType,
|
||||
Array<{ label: string; value: string | number; isUrl?: boolean }>
|
||||
> = {
|
||||
[ContentType.Text]: [
|
||||
{ label: "Characters", value: (getInfo.value as InfoText).characters },
|
||||
{ label: "Words", value: (getInfo.value as InfoText).words },
|
||||
],
|
||||
[ContentType.Image]: [
|
||||
{ label: "Dimensions", value: (getInfo.value as InfoImage).dimensions },
|
||||
{ label: "Image size", value: formatFileSize((getInfo.value as InfoImage).size) },
|
||||
{
|
||||
label: "Image size",
|
||||
value: formatFileSize((getInfo.value as InfoImage).size),
|
||||
},
|
||||
],
|
||||
[ContentType.File]: [
|
||||
{ label: "Path", value: (getInfo.value as InfoFile).path },
|
||||
],
|
||||
[ContentType.Link]: [
|
||||
...((getInfo.value as InfoLink).title && (getInfo.value as InfoLink).title !== 'Loading...'
|
||||
? [{ label: "Title", value: (getInfo.value as InfoLink).title || '' }]
|
||||
...((getInfo.value as InfoLink).title &&
|
||||
(getInfo.value as InfoLink).title !== "Loading..."
|
||||
? [{ label: "Title", value: (getInfo.value as InfoLink).title || "" }]
|
||||
: []),
|
||||
{ label: "URL", value: (getInfo.value as InfoLink).url, isUrl: true },
|
||||
{ label: "Characters", value: (getInfo.value as InfoLink).characters },
|
||||
|
@ -832,8 +863,9 @@ const infoRows = computed(() => {
|
|||
],
|
||||
};
|
||||
|
||||
const specificRows = typeSpecificRows[getInfo.value.content_type]
|
||||
.filter(row => row.value !== "");
|
||||
const specificRows = typeSpecificRows[getInfo.value.content_type].filter(
|
||||
(row) => row.value !== ""
|
||||
);
|
||||
|
||||
return [
|
||||
...commonRows,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div class="bg">
|
||||
<div class="back">
|
||||
<img @click="router.push('/')" src="../public/back_arrow.svg" />
|
||||
<div class="top-bar">
|
||||
<NuxtLink to="/" class="back">
|
||||
<img src="../public/back_arrow.svg" />
|
||||
<p>Back</p>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="bottom-bar">
|
||||
<div class="left">
|
||||
|
@ -10,7 +12,10 @@
|
|||
<p>Qopy</p>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div @click="saveKeybind" class="actions">
|
||||
<div
|
||||
@click="saveKeybind"
|
||||
class="actions"
|
||||
:class="{ disabled: keybind.length === 0 }">
|
||||
<p>Save</p>
|
||||
<div>
|
||||
<img alt="" src="../public/cmd.svg" v-if="os === 'macos'" />
|
||||
|
@ -23,15 +28,49 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="keybind-container">
|
||||
<h2 class="title">Record a new Hotkey</h2>
|
||||
<div class="settings-container">
|
||||
<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
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
@keydown="onKeyDown"
|
||||
class="keybind-input"
|
||||
ref="keybindInput"
|
||||
tabindex="0">
|
||||
tabindex="0"
|
||||
:class="{ 'empty-keybind': showEmptyKeybindError }">
|
||||
<span class="key" v-if="keybind.length === 0">Click here</span>
|
||||
<template v-else>
|
||||
<span
|
||||
|
@ -39,12 +78,14 @@
|
|||
class="key"
|
||||
:class="{ modifier: isModifier(key) }"
|
||||
v-for="(key, index) in keybind">
|
||||
{{ keyToDisplay(key) }}
|
||||
{{ keyToLabel(key) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -52,62 +93,43 @@ import { invoke } from "@tauri-apps/api/core";
|
|||
import { onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Key } from "wrdu-keyboard/key";
|
||||
import { KeyValues, KeyLabels } from "../types/keys";
|
||||
import { disable, enable } from "@tauri-apps/plugin-autostart";
|
||||
|
||||
const activeModifiers = reactive<Set<string>>(new Set());
|
||||
const activeModifiers = reactive<Set<KeyValues>>(new Set());
|
||||
const isKeybindInputFocused = ref(false);
|
||||
const keybind = ref<string[]>([]);
|
||||
const keybind = ref<KeyValues[]>([]);
|
||||
const keybindInput = ref<HTMLElement | null>(null);
|
||||
const lastBlurTime = ref(0);
|
||||
const os = ref("");
|
||||
const router = useRouter();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const keyToDisplayMap: Record<string, string> = {
|
||||
" ": "Space",
|
||||
Alt: "Alt",
|
||||
AltLeft: "Alt L",
|
||||
AltRight: "Alt R",
|
||||
ArrowDown: "↓",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
ArrowUp: "↑",
|
||||
Control: "Ctrl",
|
||||
ControlLeft: "Ctrl L",
|
||||
ControlRight: "Ctrl R",
|
||||
Enter: "↵",
|
||||
Meta: "Meta",
|
||||
MetaLeft: "Meta L",
|
||||
MetaRight: "Meta R",
|
||||
Shift: "⇧",
|
||||
ShiftLeft: "⇧ L",
|
||||
ShiftRight: "⇧ R",
|
||||
};
|
||||
const showEmptyKeybindError = ref(false);
|
||||
const autostart = ref(false);
|
||||
const { $settings } = useNuxtApp();
|
||||
|
||||
const modifierKeySet = new Set([
|
||||
"Alt",
|
||||
"AltLeft",
|
||||
"AltRight",
|
||||
"Control",
|
||||
"ControlLeft",
|
||||
"ControlRight",
|
||||
"Meta",
|
||||
"MetaLeft",
|
||||
"MetaRight",
|
||||
"Shift",
|
||||
"ShiftLeft",
|
||||
"ShiftRight",
|
||||
KeyValues.AltLeft,
|
||||
KeyValues.AltRight,
|
||||
KeyValues.ControlLeft,
|
||||
KeyValues.ControlRight,
|
||||
KeyValues.MetaLeft,
|
||||
KeyValues.MetaRight,
|
||||
KeyValues.ShiftLeft,
|
||||
KeyValues.ShiftRight,
|
||||
]);
|
||||
|
||||
const isModifier = (key: string): boolean => {
|
||||
const isModifier = (key: KeyValues): boolean => {
|
||||
return modifierKeySet.has(key);
|
||||
};
|
||||
|
||||
const keyToDisplay = (key: string): string => {
|
||||
return keyToDisplayMap[key] || key;
|
||||
const keyToLabel = (key: KeyValues): string => {
|
||||
return KeyLabels[key] || key;
|
||||
};
|
||||
|
||||
const updateKeybind = () => {
|
||||
const modifiers = Array.from(activeModifiers).sort();
|
||||
const modifiers = Array.from(activeModifiers);
|
||||
const nonModifiers = keybind.value.filter((key) => !isModifier(key));
|
||||
keybind.value = [...modifiers, ...nonModifiers];
|
||||
};
|
||||
|
@ -115,19 +137,20 @@ const updateKeybind = () => {
|
|||
const onBlur = () => {
|
||||
isKeybindInputFocused.value = false;
|
||||
lastBlurTime.value = Date.now();
|
||||
showEmptyKeybindError.value = false;
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
isKeybindInputFocused.value = true;
|
||||
activeModifiers.clear();
|
||||
keybind.value = [];
|
||||
showEmptyKeybindError.value = false;
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
const key = event.code;
|
||||
const key = event.code as KeyValues;
|
||||
|
||||
if (key === "Escape") {
|
||||
if (key === KeyValues.Escape) {
|
||||
if (keybindInput.value) {
|
||||
keybindInput.value.blur();
|
||||
}
|
||||
|
@ -142,45 +165,79 @@ const onKeyDown = (event: KeyboardEvent) => {
|
|||
}
|
||||
|
||||
updateKeybind();
|
||||
showEmptyKeybindError.value = false;
|
||||
};
|
||||
|
||||
const saveKeybind = async () => {
|
||||
console.log("New:", keybind.value);
|
||||
const oldKeybind = await invoke<string[]>("get_keybind");
|
||||
console.log("Old:", oldKeybind);
|
||||
await invoke("save_keybind", { keybind: keybind.value });
|
||||
if (keybind.value.length > 0) {
|
||||
await $settings.saveSetting("keybind", JSON.stringify(keybind.value));
|
||||
router.push("/");
|
||||
} else {
|
||||
showEmptyKeybindError.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
os.value = platform();
|
||||
const toggleAutostart = async () => {
|
||||
if (autostart.value === true) {
|
||||
await enable();
|
||||
} else {
|
||||
await disable();
|
||||
}
|
||||
await $settings.saveSetting("autostart", autostart.value ? "true" : "false");
|
||||
};
|
||||
|
||||
keyboard.down("all", (event) => {
|
||||
const isMacSaveCombo =
|
||||
os.value === "macos" &&
|
||||
(event.code === "MetaLeft" || event.code === "MetaRight") &&
|
||||
event.key === "Enter";
|
||||
os.value = platform();
|
||||
|
||||
const isOtherOsSaveCombo =
|
||||
os.value !== "macos" &&
|
||||
(event.code === "ControlLeft" || event.code === "ControlRight") &&
|
||||
event.key === "Enter";
|
||||
onMounted(async () => {
|
||||
keyboard.down([Key.All], (event) => {
|
||||
if (isKeybindInputFocused.value) {
|
||||
onKeyDown(event);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
(isMacSaveCombo || isOtherOsSaveCombo) &&
|
||||
!isKeybindInputFocused.value
|
||||
) {
|
||||
event.preventDefault();
|
||||
keyboard.down([Key.Escape], (event) => {
|
||||
if (isKeybindInputFocused.value) {
|
||||
keybindInput.value?.blur();
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
|
||||
switch (os.value) {
|
||||
case "macos":
|
||||
keyboard.down([Key.LeftMeta, Key.Enter], (event) => {
|
||||
if (!isKeybindInputFocused.value) {
|
||||
saveKeybind();
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.down("Escape", (event) => {
|
||||
const now = Date.now();
|
||||
if (!isKeybindInputFocused.value && now - lastBlurTime.value > 100) {
|
||||
event.preventDefault();
|
||||
router.push("/");
|
||||
keyboard.down([Key.RightMeta, Key.Enter], (event) => {
|
||||
if (!isKeybindInputFocused.value) {
|
||||
saveKeybind();
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case "linux" || "windows":
|
||||
keyboard.down([Key.LeftControl, Key.Enter], (event) => {
|
||||
if (!isKeybindInputFocused.value) {
|
||||
saveKeybind();
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.down([Key.RightControl, Key.Enter], (event) => {
|
||||
if (!isKeybindInputFocused.value) {
|
||||
saveKeybind();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
autostart.value = (await $settings.getSetting("autostart")) === "true";
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
keyboard.unregisterAll();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
131
patches/wrdu-keyboard@3.0.0.patch
Normal file
131
patches/wrdu-keyboard@3.0.0.patch
Normal 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: {
|
|
@ -12,14 +12,6 @@ export default defineNuxtPlugin(() => {
|
|||
async saveSetting(key: string, value: string): Promise<void> {
|
||||
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
2
src-tauri/Cargo.lock
generated
|
@ -4054,7 +4054,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "qopy"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"active-win-pos-rs",
|
||||
"applications",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "qopy"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
description = "Qopy"
|
||||
authors = ["pandadev"]
|
||||
edition = "2021"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use tauri_plugin_aptabase::EventTracker;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
// use hyperpolyglot;
|
||||
use lazy_static::lazy_static;
|
||||
use rdev::{simulate, EventType, Key};
|
||||
use rdev::{ simulate, EventType, Key };
|
||||
use regex::Regex;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fs;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{thread, time::Duration};
|
||||
use tauri::{AppHandle, Emitter, Listener, Manager};
|
||||
use std::sync::atomic::{ AtomicBool, Ordering };
|
||||
use std::{ thread, time::Duration };
|
||||
use tauri::{ AppHandle, Emitter, Listener, Manager };
|
||||
use tauri_plugin_clipboard::Clipboard;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
use url::Url;
|
||||
|
@ -17,7 +17,7 @@ use uuid::Uuid;
|
|||
use crate::db;
|
||||
use crate::utils::commands::get_app_info;
|
||||
use crate::utils::favicon::fetch_favicon_as_base64;
|
||||
use crate::utils::types::{ContentType, HistoryItem};
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
|
||||
lazy_static! {
|
||||
static ref IS_PROGRAMMATIC_PASTE: AtomicBool = AtomicBool::new(false);
|
||||
|
@ -27,16 +27,14 @@ lazy_static! {
|
|||
pub async fn write_and_paste(
|
||||
app_handle: AppHandle,
|
||||
content: String,
|
||||
content_type: String,
|
||||
content_type: String
|
||||
) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
|
||||
match content_type.as_str() {
|
||||
"text" => clipboard.write_text(content).map_err(|e| e.to_string())?,
|
||||
"image" => {
|
||||
clipboard
|
||||
.write_image_base64(content)
|
||||
.map_err(|e| e.to_string())?;
|
||||
clipboard.write_image_base64(content).map_err(|e| e.to_string())?;
|
||||
}
|
||||
"files" => {
|
||||
clipboard
|
||||
|
@ -44,11 +42,13 @@ pub async fn write_and_paste(
|
|||
content
|
||||
.split(", ")
|
||||
.map(|file| file.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
.collect::<Vec<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);
|
||||
|
@ -65,7 +65,7 @@ pub async fn write_and_paste(
|
|||
EventType::KeyPress(modifier_key),
|
||||
EventType::KeyPress(Key::KeyV),
|
||||
EventType::KeyRelease(Key::KeyV),
|
||||
EventType::KeyRelease(modifier_key),
|
||||
EventType::KeyRelease(modifier_key)
|
||||
];
|
||||
|
||||
for event in events {
|
||||
|
@ -81,9 +81,12 @@ pub async fn write_and_paste(
|
|||
IS_PROGRAMMATIC_PASTE.store(false, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
let _ = app_handle.track_event("clipboard_paste", Some(serde_json::json!({
|
||||
let _ = app_handle.track_event(
|
||||
"clipboard_paste",
|
||||
Some(serde_json::json!({
|
||||
"content_type": content_type
|
||||
})));
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -92,9 +95,7 @@ pub fn setup(app: &AppHandle) {
|
|||
let app_handle = app.clone();
|
||||
let runtime = TokioRuntime::new().expect("Failed to create Tokio runtime");
|
||||
|
||||
app_handle.clone().listen(
|
||||
"plugin:clipboard://clipboard-monitor/update",
|
||||
move |_event| {
|
||||
app_handle.clone().listen("plugin:clipboard://clipboard-monitor/update", move |_event| {
|
||||
let app_handle = app_handle.clone();
|
||||
runtime.block_on(async move {
|
||||
if IS_PROGRAMMATIC_PASTE.load(Ordering::SeqCst) {
|
||||
|
@ -111,14 +112,20 @@ pub fn setup(app: &AppHandle) {
|
|||
if available_types.image {
|
||||
println!("Handling image change");
|
||||
if let Ok(image_data) = clipboard.read_image_base64() {
|
||||
let file_path = save_image_to_file(&app_handle, &image_data)
|
||||
.await
|
||||
let file_path = save_image_to_file(&app_handle, &image_data).await
|
||||
.map_err(|e| e.to_string())
|
||||
.unwrap_or_else(|e| e);
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Image, file_path, None, app_icon, None)
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Image,
|
||||
file_path,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else if available_types.files {
|
||||
|
@ -135,7 +142,7 @@ pub fn setup(app: &AppHandle) {
|
|||
None,
|
||||
app_icon.clone(),
|
||||
None
|
||||
),
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +150,9 @@ pub fn setup(app: &AppHandle) {
|
|||
println!("Handling text change");
|
||||
if let Ok(text) = clipboard.read_text() {
|
||||
let text = text.to_string();
|
||||
let url_regex = Regex::new(r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$").unwrap();
|
||||
let url_regex = Regex::new(
|
||||
r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)$"
|
||||
).unwrap();
|
||||
|
||||
if url_regex.is_match(&text) {
|
||||
if let Ok(url) = Url::parse(&text) {
|
||||
|
@ -155,7 +164,14 @@ pub fn setup(app: &AppHandle) {
|
|||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Link, text, favicon, app_icon, None)
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Link,
|
||||
text,
|
||||
favicon,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
} else {
|
||||
|
@ -178,13 +194,27 @@ pub fn setup(app: &AppHandle) {
|
|||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Color, text, None, app_icon, None)
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Color,
|
||||
text,
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
} else {
|
||||
let _ = db::history::add_history_item(
|
||||
app_handle.clone(),
|
||||
pool,
|
||||
HistoryItem::new(app_name, ContentType::Text, text.clone(), None, app_icon, None)
|
||||
HistoryItem::new(
|
||||
app_name,
|
||||
ContentType::Text,
|
||||
text.clone(),
|
||||
None,
|
||||
app_icon,
|
||||
None
|
||||
)
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
@ -199,19 +229,23 @@ pub fn setup(app: &AppHandle) {
|
|||
}
|
||||
|
||||
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" }
|
||||
else if available_types.files { "files" }
|
||||
else if available_types.text { "text" }
|
||||
else { "unknown" }
|
||||
})));
|
||||
});
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn get_pool(
|
||||
app_handle: &AppHandle,
|
||||
app_handle: &AppHandle
|
||||
) -> Result<tauri::State<'_, SqlitePool>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(app_handle.state::<SqlitePool>())
|
||||
}
|
||||
|
@ -219,9 +253,7 @@ async fn get_pool(
|
|||
#[tauri::command]
|
||||
pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
|
||||
let clipboard = app_handle.state::<Clipboard>();
|
||||
clipboard
|
||||
.start_monitor(app_handle.clone())
|
||||
.map_err(|e| e.to_string())?;
|
||||
clipboard.start_monitor(app_handle.clone()).map_err(|e| e.to_string())?;
|
||||
app_handle
|
||||
.emit("plugin:clipboard://clipboard-monitor/status", true)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
@ -230,7 +262,7 @@ pub fn start_monitor(app_handle: AppHandle) -> Result<(), String> {
|
|||
|
||||
async fn save_image_to_file(
|
||||
app_handle: &AppHandle,
|
||||
base64_data: &str,
|
||||
base64_data: &str
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let app_data_dir = app_handle.path().app_data_dir().unwrap();
|
||||
let images_dir = app_data_dir.join("images");
|
||||
|
|
|
@ -1,48 +1,60 @@
|
|||
use tauri_plugin_aptabase::EventTracker;
|
||||
use crate::utils::commands::center_window_on_current_monitor;
|
||||
use crate::utils::keys::KeyCode;
|
||||
use global_hotkey::{
|
||||
hotkey::{Code, HotKey, Modifiers},
|
||||
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
|
||||
hotkey::{ Code, HotKey, Modifiers },
|
||||
GlobalHotKeyEvent,
|
||||
GlobalHotKeyManager,
|
||||
HotKeyState,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use lazy_static::lazy_static;
|
||||
use std::str::FromStr;
|
||||
use tauri::{AppHandle, Listener, Manager};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{ AppHandle, Listener, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
thread_local! {
|
||||
static HOTKEY_MANAGER: RefCell<Option<GlobalHotKeyManager>> = RefCell::new(None);
|
||||
lazy_static! {
|
||||
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) {
|
||||
let app_handle_clone = app_handle.clone();
|
||||
|
||||
let manager = GlobalHotKeyManager::new().expect("Failed to initialize hotkey manager");
|
||||
HOTKEY_MANAGER.with(|m| *m.borrow_mut() = Some(manager));
|
||||
let manager = match GlobalHotKeyManager::new() {
|
||||
Ok(manager) => manager,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to initialize hotkey manager: {:?}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
let mut manager_guard = HOTKEY_MANAGER.lock().unwrap();
|
||||
*manager_guard = Some(manager);
|
||||
}
|
||||
|
||||
let rt = app_handle.state::<tokio::runtime::Runtime>();
|
||||
let initial_keybind = rt
|
||||
.block_on(crate::db::settings::get_keybind(app_handle_clone.clone()))
|
||||
.expect("Failed to get initial keybind");
|
||||
let initial_shortcut = initial_keybind.join("+");
|
||||
|
||||
let initial_shortcut_for_update = initial_shortcut.clone();
|
||||
let initial_shortcut_for_save = initial_shortcut.clone();
|
||||
|
||||
if let Err(e) = register_shortcut(&initial_shortcut) {
|
||||
if let Err(e) = register_shortcut(&initial_keybind) {
|
||||
eprintln!("Error registering initial shortcut: {:?}", e);
|
||||
}
|
||||
|
||||
app_handle.listen("update-shortcut", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
let payload_str = event.payload();
|
||||
|
||||
if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_update) {
|
||||
HOTKEY_MANAGER.with(|manager| {
|
||||
if let Some(manager) = manager.borrow().as_ref() {
|
||||
if let Some(old_hotkey) = REGISTERED_HOTKEY.lock().unwrap().take() {
|
||||
let manager_guard = HOTKEY_MANAGER.lock().unwrap();
|
||||
if let Some(manager) = manager_guard.as_ref() {
|
||||
let _ = manager.unregister(old_hotkey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(e) = register_shortcut(&payload_str) {
|
||||
let payload: Vec<String> = serde_json::from_str(payload_str).unwrap_or_default();
|
||||
|
||||
if let Err(e) = register_shortcut(&payload) {
|
||||
eprintln!("Error re-registering shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
@ -50,15 +62,15 @@ pub fn setup(app_handle: tauri::AppHandle) {
|
|||
app_handle.listen("save_keybind", move |event| {
|
||||
let payload_str = event.payload().to_string();
|
||||
|
||||
if let Ok(old_hotkey) = parse_hotkey(&initial_shortcut_for_save) {
|
||||
HOTKEY_MANAGER.with(|manager| {
|
||||
if let Some(manager) = manager.borrow().as_ref() {
|
||||
if let Some(old_hotkey) = REGISTERED_HOTKEY.lock().unwrap().take() {
|
||||
let manager_guard = HOTKEY_MANAGER.lock().unwrap();
|
||||
if let Some(manager) = manager_guard.as_ref() {
|
||||
let _ = manager.unregister(old_hotkey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(e) = register_shortcut(&payload_str) {
|
||||
let payload: Vec<String> = serde_json::from_str(&payload_str).unwrap_or_default();
|
||||
if let Err(e) = register_shortcut(&payload) {
|
||||
eprintln!("Error registering saved shortcut: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
@ -81,48 +93,44 @@ pub fn setup(app_handle: tauri::AppHandle) {
|
|||
});
|
||||
}
|
||||
|
||||
fn register_shortcut(shortcut: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn register_shortcut(shortcut: &[String]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let hotkey = parse_hotkey(shortcut)?;
|
||||
HOTKEY_MANAGER.with(|manager| {
|
||||
if let Some(manager) = manager.borrow().as_ref() {
|
||||
manager.register(hotkey)?;
|
||||
}
|
||||
|
||||
let manager_guard = HOTKEY_MANAGER.lock().unwrap();
|
||||
if let Some(manager) = manager_guard.as_ref() {
|
||||
manager.register(hotkey.clone())?;
|
||||
*REGISTERED_HOTKEY.lock().unwrap() = Some(hotkey);
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Err("Hotkey manager not initialized".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hotkey(shortcut: &str) -> Result<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 code = None;
|
||||
|
||||
let shortcut = shortcut.replace("\"", "");
|
||||
|
||||
for part in shortcut.split('+') {
|
||||
let part = part.trim().to_lowercase();
|
||||
for part in shortcut {
|
||||
match part.as_str() {
|
||||
"ctrl" | "control" | "controlleft" => modifiers |= Modifiers::CONTROL,
|
||||
"alt" | "altleft" | "optionleft" => modifiers |= Modifiers::ALT,
|
||||
"shift" | "shiftleft" => modifiers |= Modifiers::SHIFT,
|
||||
"super" | "meta" | "cmd" | "metaleft" => modifiers |= Modifiers::META,
|
||||
"ControlLeft" => {
|
||||
modifiers |= Modifiers::CONTROL;
|
||||
}
|
||||
"AltLeft" => {
|
||||
modifiers |= Modifiers::ALT;
|
||||
}
|
||||
"ShiftLeft" => {
|
||||
modifiers |= Modifiers::SHIFT;
|
||||
}
|
||||
"MetaLeft" => {
|
||||
modifiers |= Modifiers::META;
|
||||
}
|
||||
key => {
|
||||
let key_code = if key.starts_with("key") {
|
||||
"Key".to_string() + &key[3..].to_uppercase()
|
||||
} else if key.len() == 1 && key.chars().next().unwrap().is_alphabetic() {
|
||||
"Key".to_string() + &key.to_uppercase()
|
||||
} else {
|
||||
key.to_string()
|
||||
};
|
||||
|
||||
code = Some(
|
||||
Code::from_str(&key_code)
|
||||
.map_err(|_| format!("Invalid key code: {}", key_code))?,
|
||||
);
|
||||
code = Some(Code::from(KeyCode::from_str(key)?));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key_code =
|
||||
code.ok_or_else(|| format!("No valid key code found in shortcut: {}", shortcut))?;
|
||||
let key_code = code.ok_or_else(|| "No valid key code found".to_string())?;
|
||||
Ok(HotKey::new(Some(modifiers), key_code))
|
||||
}
|
||||
|
||||
|
@ -144,7 +152,12 @@ fn handle_hotkey_event(app_handle: &AppHandle) {
|
|||
center_window_on_current_monitor(&window);
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event("hotkey_triggered", Some(serde_json::json!({
|
||||
let _ = app_handle.track_event(
|
||||
"hotkey_triggered",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if window.is_visible().unwrap() { "hide" } else { "show" }
|
||||
})));
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
use tauri::{
|
||||
menu::{MenuBuilder, MenuItemBuilder},
|
||||
tray::TrayIconBuilder,
|
||||
Emitter, Manager,
|
||||
};
|
||||
use tauri::{ menu::{ MenuBuilder, MenuItemBuilder }, tray::TrayIconBuilder, Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
let _ = app.track_event("tray_toggle", Some(serde_json::json!({
|
||||
let _ = app.track_event(
|
||||
"tray_toggle",
|
||||
Some(serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
})));
|
||||
}))
|
||||
);
|
||||
|
||||
let icon_bytes = include_bytes!("../../icons/Square71x71Logo.png");
|
||||
let icon = tauri::image::Image::from_bytes(icon_bytes).unwrap();
|
||||
|
@ -18,24 +17,27 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
let _tray = TrayIconBuilder::new()
|
||||
.menu(
|
||||
&MenuBuilder::new(app)
|
||||
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy")
|
||||
.enabled(false)
|
||||
.build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("app_name", "Qopy").enabled(false).build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("show", "Show/Hide").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("keybind", "Change keybind").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("check_updates", "Check for updates").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("settings", "Settings").build(app)?])
|
||||
.items(&[&MenuItemBuilder::with_id("quit", "Quit").build(app)?])
|
||||
.build()?,
|
||||
.build()?
|
||||
)
|
||||
.on_menu_event(move |_app, event| match event.id().as_ref() {
|
||||
.on_menu_event(move |_app, event| {
|
||||
match event.id().as_ref() {
|
||||
"quit" => {
|
||||
let _ = _app.track_event("app_quit", None);
|
||||
std::process::exit(0);
|
||||
}
|
||||
"show" => {
|
||||
let _ = _app.track_event("tray_toggle", Some(serde_json::json!({
|
||||
let _ = _app.track_event(
|
||||
"tray_toggle",
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"action": if is_visible { "hide" } else { "show" }
|
||||
})));
|
||||
})
|
||||
)
|
||||
);
|
||||
let is_visible = window.is_visible().unwrap();
|
||||
if is_visible {
|
||||
window.hide().unwrap();
|
||||
|
@ -45,18 +47,12 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
window.emit("main_route", ()).unwrap();
|
||||
}
|
||||
"keybind" => {
|
||||
let _ = _app.track_event("tray_keybind_change", None);
|
||||
window.emit("change_keybind", ()).unwrap();
|
||||
}
|
||||
"check_updates" => {
|
||||
let _ = _app.track_event("tray_check_updates", None);
|
||||
let app_handle = _app.app_handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
crate::api::updater::check_for_updates(app_handle, true).await;
|
||||
});
|
||||
"settings" => {
|
||||
let _ = _app.track_event("tray_settings", None);
|
||||
window.emit("settings", ()).unwrap();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
})
|
||||
.icon(icon)
|
||||
.build(app)?;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use tauri::Manager;
|
||||
use tauri::{async_runtime, AppHandle};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||
use tauri::{ async_runtime, AppHandle };
|
||||
use tauri_plugin_dialog::{ DialogExt, MessageDialogButtons, MessageDialogKind };
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
||||
|
@ -26,18 +25,35 @@ pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
|||
app.dialog()
|
||||
.message(msg)
|
||||
.title("Qopy Update Available")
|
||||
.buttons(MessageDialogButtons::OkCancelCustom(String::from("Install"), String::from("Cancel")))
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Install"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if !response {
|
||||
return;
|
||||
}
|
||||
async_runtime::spawn(async move {
|
||||
match update.download_and_install(|_, _| {}, || {}).await {
|
||||
match
|
||||
update.download_and_install(
|
||||
|_, _| {},
|
||||
|| {}
|
||||
).await
|
||||
{
|
||||
Ok(_) => {
|
||||
app.dialog()
|
||||
.message("Update installed successfully. The application needs to restart to apply the changes.")
|
||||
.title("Qopy Needs to Restart")
|
||||
.buttons(MessageDialogButtons::OkCancelCustom(String::from("Restart"), String::from("Cancel")))
|
||||
.message(
|
||||
"Update installed successfully. The application needs to restart to apply the changes."
|
||||
)
|
||||
.title("Qopy Update Installed")
|
||||
.buttons(
|
||||
MessageDialogButtons::OkCancelCustom(
|
||||
String::from("Restart"),
|
||||
String::from("Cancel")
|
||||
)
|
||||
)
|
||||
.show(move |response| {
|
||||
if response {
|
||||
app.restart();
|
||||
|
@ -47,7 +63,9 @@ pub async fn check_for_updates(app: AppHandle, prompted: bool) {
|
|||
Err(e) => {
|
||||
println!("Error installing new update: {:?}", e);
|
||||
app.dialog()
|
||||
.message("Failed to install new update. The new update can be downloaded from Github")
|
||||
.message(
|
||||
"Failed to install new update. The new update can be downloaded from Github"
|
||||
)
|
||||
.kind(MessageDialogKind::Error)
|
||||
.show(|_| {});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use include_dir::{include_dir, Dir};
|
||||
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
|
||||
use include_dir::{ include_dir, Dir };
|
||||
use sqlx::sqlite::{ SqlitePool, SqlitePoolOptions };
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tokio::runtime::Runtime as TokioRuntime;
|
||||
|
@ -25,8 +25,7 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
let pool = rt.block_on(async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool")
|
||||
});
|
||||
|
||||
|
@ -49,29 +48,27 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<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 (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);",
|
||||
);"
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
.execute(pool).await?;
|
||||
|
||||
let current_version: Option<i64> =
|
||||
sqlx::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
let current_version: Option<i64> = sqlx
|
||||
::query_scalar("SELECT MAX(version) FROM schema_version")
|
||||
.fetch_one(pool).await?;
|
||||
|
||||
let current_version = current_version.unwrap_or(0);
|
||||
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR
|
||||
.files()
|
||||
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR.files()
|
||||
.filter_map(|file| {
|
||||
let file_name = file.path().file_name()?.to_str()?;
|
||||
if file_name.ends_with(".sql") && file_name.starts_with("migration") {
|
||||
if file_name.ends_with(".sql") && file_name.starts_with("v") {
|
||||
let version: i64 = file_name
|
||||
.trim_start_matches("migration")
|
||||
.trim_start_matches("v")
|
||||
.trim_end_matches(".sql")
|
||||
.parse()
|
||||
.ok()?;
|
||||
|
@ -93,16 +90,16 @@ async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::E
|
|||
.collect();
|
||||
|
||||
for statement in statements {
|
||||
sqlx::query(statement)
|
||||
.execute(pool)
|
||||
.await
|
||||
sqlx
|
||||
::query(statement)
|
||||
.execute(pool).await
|
||||
.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)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
.execute(pool).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +1,34 @@
|
|||
use crate::utils::types::{ContentType, HistoryItem};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use crate::utils::types::{ ContentType, HistoryItem };
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use sqlx::{Row, SqlitePool};
|
||||
use rand::{ thread_rng, Rng };
|
||||
use sqlx::{ Row, SqlitePool };
|
||||
use std::fs;
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
pub async fn initialize_history(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let id: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let id: String = thread_rng().sample_iter(&Alphanumeric).take(16).map(char::from).collect();
|
||||
|
||||
sqlx::query(
|
||||
sqlx
|
||||
::query(
|
||||
"INSERT INTO history (id, source, content_type, content, timestamp) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)"
|
||||
)
|
||||
.bind(id)
|
||||
.bind("System")
|
||||
.bind("text")
|
||||
.bind("Welcome to your clipboard history!")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(pool: tauri::State<'_, SqlitePool>) -> Result<Vec<HistoryItem>, String> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC",
|
||||
let rows = sqlx
|
||||
::query(
|
||||
"SELECT id, source, source_icon, content_type, content, favicon, timestamp, language FROM history ORDER BY timestamp DESC"
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
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(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
item: HistoryItem,
|
||||
item: HistoryItem
|
||||
) -> Result<(), String> {
|
||||
let (id, source, source_icon, content_type, content, favicon, timestamp, language) =
|
||||
item.to_row();
|
||||
|
||||
let existing = sqlx::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
let existing = sqlx
|
||||
::query("SELECT id FROM history WHERE content = ? AND content_type = ?")
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match existing {
|
||||
Some(_) => {
|
||||
sqlx::query(
|
||||
sqlx
|
||||
::query(
|
||||
"UPDATE history SET timestamp = strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now') WHERE content = ? AND content_type = ?"
|
||||
)
|
||||
.bind(&content)
|
||||
.bind(&content_type)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
None => {
|
||||
sqlx::query(
|
||||
sqlx
|
||||
::query(
|
||||
"INSERT INTO history (id, source, source_icon, content_type, content, favicon, timestamp, language) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
)
|
||||
.bind(id)
|
||||
|
@ -91,15 +88,17 @@ pub async fn add_history_item(
|
|||
.bind(favicon)
|
||||
.bind(timestamp)
|
||||
.bind(language)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app_handle.track_event("history_item_added", Some(serde_json::json!({
|
||||
let _ = app_handle.track_event(
|
||||
"history_item_added",
|
||||
Some(serde_json::json!({
|
||||
"content_type": item.content_type.to_string()
|
||||
})));
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -107,15 +106,15 @@ pub async fn add_history_item(
|
|||
#[tauri::command]
|
||||
pub async fn search_history(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
query: String,
|
||||
query: String
|
||||
) -> Result<Vec<HistoryItem>, String> {
|
||||
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"
|
||||
)
|
||||
.bind(query)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
|
@ -139,15 +138,15 @@ pub async fn search_history(
|
|||
pub async fn load_history_chunk(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
limit: i64
|
||||
) -> 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 ?"
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.fetch_all(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let items = rows
|
||||
|
@ -171,12 +170,12 @@ pub async fn load_history_chunk(
|
|||
pub async fn delete_history_item(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
id: String,
|
||||
id: String
|
||||
) -> Result<(), String> {
|
||||
sqlx::query("DELETE FROM history WHERE id = ?")
|
||||
sqlx
|
||||
::query("DELETE FROM history WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_item_deleted", None);
|
||||
|
@ -189,9 +188,9 @@ pub async fn clear_history(
|
|||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>
|
||||
) -> Result<(), String> {
|
||||
sqlx::query("DELETE FROM history")
|
||||
.execute(&*pool)
|
||||
.await
|
||||
sqlx
|
||||
::query("DELETE FROM history")
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("history_cleared", None);
|
||||
|
|
1
src-tauri/src/db/migrations/v3.sql
Normal file
1
src-tauri/src/db/migrations/v3.sql
Normal file
|
@ -0,0 +1 @@
|
|||
INSERT INTO settings (key, value) VALUES ('autostart', 'true');
|
|
@ -1,8 +1,8 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use serde_json;
|
||||
use sqlx::Row;
|
||||
use sqlx::SqlitePool;
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri::{ Emitter, Manager };
|
||||
use tauri_plugin_aptabase::EventTracker;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
@ -16,10 +16,10 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
|
|||
};
|
||||
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)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
.execute(pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -28,26 +28,24 @@ pub async fn initialize_settings(pool: &SqlitePool) -> Result<(), Box<dyn std::e
|
|||
pub async fn save_keybind(
|
||||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
keybind: Vec<String>,
|
||||
keybind: Vec<String>
|
||||
) -> Result<(), String> {
|
||||
let keybind_str = keybind.join("+");
|
||||
let keybind_clone = keybind_str.clone();
|
||||
|
||||
app_handle
|
||||
.emit("update-shortcut", &keybind_str)
|
||||
.map_err(|e| e.to_string())?;
|
||||
app_handle.emit("update-shortcut", &keybind).map_err(|e| e.to_string())?;
|
||||
|
||||
let json = serde_json::to_string(&keybind).map_err(|e| e.to_string())?;
|
||||
|
||||
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
sqlx
|
||||
::query("INSERT OR REPLACE INTO settings (key, value) VALUES ('keybind', ?)")
|
||||
.bind(json)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("keybind_saved", Some(serde_json::json!({
|
||||
"keybind": keybind_clone
|
||||
})));
|
||||
let _ = app_handle.track_event(
|
||||
"keybind_saved",
|
||||
Some(serde_json::json!({
|
||||
"keybind": keybind
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -55,12 +53,12 @@ pub async fn save_keybind(
|
|||
#[tauri::command]
|
||||
pub async fn get_setting(
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
key: String
|
||||
) -> Result<String, String> {
|
||||
let row = sqlx::query("SELECT value FROM settings WHERE key = ?")
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = ?")
|
||||
.bind(key)
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(row.map(|r| r.get("value")).unwrap_or_default())
|
||||
|
@ -71,18 +69,21 @@ pub async fn save_setting(
|
|||
app_handle: tauri::AppHandle,
|
||||
pool: tauri::State<'_, SqlitePool>,
|
||||
key: String,
|
||||
value: String,
|
||||
value: String
|
||||
) -> Result<(), String> {
|
||||
sqlx::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
sqlx
|
||||
::query("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
|
||||
.bind(key.clone())
|
||||
.bind(value)
|
||||
.execute(&*pool)
|
||||
.await
|
||||
.execute(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let _ = app_handle.track_event("setting_saved", Some(serde_json::json!({
|
||||
let _ = app_handle.track_event(
|
||||
"setting_saved",
|
||||
Some(serde_json::json!({
|
||||
"key": key
|
||||
})));
|
||||
}))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -91,13 +92,16 @@ pub async fn save_setting(
|
|||
pub async fn get_keybind(app_handle: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let pool = app_handle.state::<SqlitePool>();
|
||||
|
||||
let row = sqlx::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool)
|
||||
.await
|
||||
let row = sqlx
|
||||
::query("SELECT value FROM settings WHERE key = 'keybind'")
|
||||
.fetch_optional(&*pool).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let json = row.map(|r| r.get::<String, _>("value")).unwrap_or_else(|| {
|
||||
serde_json::to_string(&vec!["Meta".to_string(), "V".to_string()])
|
||||
let json = row
|
||||
.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")
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
||||
|
||||
mod api;
|
||||
mod db;
|
||||
|
@ -10,7 +7,7 @@ mod utils;
|
|||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::fs;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_aptabase::{EventTracker, InitOptions};
|
||||
use tauri_plugin_aptabase::{ EventTracker, InitOptions };
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
|
@ -18,7 +15,8 @@ fn main() {
|
|||
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||
let _guard = runtime.enter();
|
||||
|
||||
tauri::Builder::default()
|
||||
tauri::Builder
|
||||
::default()
|
||||
.plugin(tauri_plugin_clipboard::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
|
@ -26,12 +24,14 @@ fn main() {
|
|||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_updater::Builder::default().build())
|
||||
.plugin(
|
||||
tauri_plugin_aptabase::Builder::new("A-SH-8937252746")
|
||||
tauri_plugin_aptabase::Builder
|
||||
::new("A-SH-8937252746")
|
||||
.with_options(InitOptions {
|
||||
host: Some("https://aptabase.pandadev.net".to_string()),
|
||||
flush_interval: None,
|
||||
})
|
||||
.with_panic_hook(Box::new(|client, info, msg| {
|
||||
.with_panic_hook(
|
||||
Box::new(|client, info, msg| {
|
||||
let location = info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
|
@ -39,21 +39,22 @@ fn main() {
|
|||
|
||||
let _ = client.track_event(
|
||||
"panic",
|
||||
Some(serde_json::json!({
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"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(
|
||||
tauri_plugin_prevent_default::Builder::new()
|
||||
tauri_plugin_prevent_default::Builder
|
||||
::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build(),
|
||||
.build()
|
||||
)
|
||||
.setup(|app| {
|
||||
let app_data_dir = app.path().app_data_dir().unwrap();
|
||||
|
@ -75,8 +76,7 @@ fn main() {
|
|||
tauri::async_runtime::spawn(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.connect(&db_url).await
|
||||
.expect("Failed to create pool");
|
||||
|
||||
app_handle_clone.manage(pool);
|
||||
|
@ -91,7 +91,10 @@ fn main() {
|
|||
let _ = api::clipboard::start_monitor(app_handle.clone());
|
||||
|
||||
utils::commands::center_window_on_current_monitor(main_window.as_ref().unwrap());
|
||||
main_window.as_ref().map(|w| w.hide()).unwrap_or(Ok(()))?;
|
||||
main_window
|
||||
.as_ref()
|
||||
.map(|w| w.hide())
|
||||
.unwrap_or(Ok(()))?;
|
||||
|
||||
let _ = app.track_event("app_started", None);
|
||||
|
||||
|
@ -109,7 +112,8 @@ fn main() {
|
|||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
.invoke_handler(
|
||||
tauri::generate_handler![
|
||||
api::clipboard::write_and_paste,
|
||||
db::history::get_history,
|
||||
db::history::add_history_item,
|
||||
|
@ -122,8 +126,9 @@ fn main() {
|
|||
db::settings::save_setting,
|
||||
db::settings::save_keybind,
|
||||
db::settings::get_keybind,
|
||||
utils::commands::fetch_page_meta,
|
||||
])
|
||||
utils::commands::fetch_page_meta
|
||||
]
|
||||
)
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
use active_win_pos_rs::get_active_window;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use base64::{ engine::general_purpose::STANDARD, Engine };
|
||||
use image::codecs::png::PngEncoder;
|
||||
use tauri::PhysicalPosition;
|
||||
use meta_fetcher;
|
||||
|
||||
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||
if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| {
|
||||
if
|
||||
let Some(monitor) = window
|
||||
.available_monitors()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| {
|
||||
let primary_monitor = window
|
||||
.primary_monitor()
|
||||
.unwrap()
|
||||
|
@ -13,22 +18,20 @@ pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
|||
let mouse_position = primary_monitor.position();
|
||||
let monitor_position = m.position();
|
||||
let monitor_size = m.size();
|
||||
mouse_position.x >= monitor_position.x
|
||||
&& mouse_position.x < monitor_position.x + monitor_size.width as i32
|
||||
&& mouse_position.y >= monitor_position.y
|
||||
&& mouse_position.y < monitor_position.y + monitor_size.height as i32
|
||||
}) {
|
||||
mouse_position.x >= monitor_position.x &&
|
||||
mouse_position.x < monitor_position.x + (monitor_size.width as i32) &&
|
||||
mouse_position.y >= monitor_position.y &&
|
||||
mouse_position.y < monitor_position.y + (monitor_size.height as i32)
|
||||
})
|
||||
{
|
||||
let monitor_size = monitor.size();
|
||||
let window_size = window.outer_size().unwrap();
|
||||
|
||||
let x = (monitor_size.width as i32 - window_size.width as i32) / 2;
|
||||
let y = (monitor_size.height as i32 - window_size.height as i32) / 2;
|
||||
let x = ((monitor_size.width as i32) - (window_size.width as i32)) / 2;
|
||||
let y = ((monitor_size.height as i32) - (window_size.height as i32)) / 2;
|
||||
|
||||
window
|
||||
.set_position(PhysicalPosition::new(
|
||||
monitor.position().x + x,
|
||||
monitor.position().y + y,
|
||||
))
|
||||
.set_position(PhysicalPosition::new(monitor.position().x + x, monitor.position().y + y))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +54,6 @@ fn _process_icon_to_base64(path: &str) -> Result<String, Box<dyn std::error::Err
|
|||
Ok(STANDARD.encode(png_buffer))
|
||||
}
|
||||
|
||||
|
||||
pub fn detect_color(color: &str) -> bool {
|
||||
let color = color.trim().to_lowercase();
|
||||
|
||||
|
@ -60,12 +62,16 @@ pub fn detect_color(color: &str) -> bool {
|
|||
let hex = &color[1..];
|
||||
return match hex.len() {
|
||||
3 | 6 | 8 => hex.chars().all(|c| c.is_ascii_hexdigit()),
|
||||
_ => false
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// rgb/rgba
|
||||
if (color.starts_with("rgb(") || color.starts_with("rgba(")) && color.ends_with(")") && !color[..color.len()-1].contains(")") {
|
||||
if
|
||||
(color.starts_with("rgb(") || color.starts_with("rgba(")) &&
|
||||
color.ends_with(")") &&
|
||||
!color[..color.len() - 1].contains(")")
|
||||
{
|
||||
let values = color
|
||||
.trim_start_matches("rgba(")
|
||||
.trim_start_matches("rgb(")
|
||||
|
@ -75,12 +81,16 @@ pub fn detect_color(color: &str) -> bool {
|
|||
|
||||
return match values.len() {
|
||||
3 | 4 => values.iter().all(|v| v.trim().parse::<f32>().is_ok()),
|
||||
_ => false
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
.trim_start_matches("hsla(")
|
||||
.trim_start_matches("hsl(")
|
||||
|
@ -90,7 +100,7 @@ pub fn detect_color(color: &str) -> bool {
|
|||
|
||||
return match values.len() {
|
||||
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]
|
||||
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))?;
|
||||
|
||||
Ok((
|
||||
metadata.title.unwrap_or_else(|| "No title found".to_string()),
|
||||
metadata.image
|
||||
))
|
||||
Ok((metadata.title.unwrap_or_else(|| "No title found".to_string()), metadata.image))
|
||||
}
|
|
@ -5,7 +5,7 @@ use reqwest;
|
|||
use url::Url;
|
||||
|
||||
pub async fn fetch_favicon_as_base64(
|
||||
url: Url,
|
||||
url: Url
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let favicon_url = format!("https://favicone.com/{}", url.host_str().unwrap());
|
||||
|
|
120
src-tauri/src/utils/keys.rs
Normal file
120
src-tauri/src/utils/keys.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use chrono;
|
||||
use log::{LevelFilter, SetLoggerError};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use log::{ LevelFilter, SetLoggerError };
|
||||
use std::fs::{ File, OpenOptions };
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
|
||||
|
@ -50,19 +50,22 @@ pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError>
|
|||
|
||||
// Set up panic hook
|
||||
let panic_file = file.try_clone().expect("Failed to clone file handle");
|
||||
panic::set_hook(Box::new(move |panic_info| {
|
||||
panic::set_hook(
|
||||
Box::new(move |panic_info| {
|
||||
let mut file = panic_file.try_clone().expect("Failed to clone file handle");
|
||||
|
||||
let location = panic_info.location()
|
||||
let location = panic_info
|
||||
.location()
|
||||
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
|
||||
.unwrap_or_else(|| "unknown location".to_string());
|
||||
|
||||
let message = match panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s) => *s,
|
||||
None => match panic_info.payload().downcast_ref::<String>() {
|
||||
None =>
|
||||
match panic_info.payload().downcast_ref::<String>() {
|
||||
Some(s) => s.as_str(),
|
||||
None => "Unknown panic message",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let _ = writeln!(
|
||||
|
@ -72,10 +75,13 @@ pub fn init_logger(app_data_dir: &std::path::Path) -> Result<(), SetLoggerError>
|
|||
message,
|
||||
location
|
||||
);
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
let logger = Box::new(FileLogger { file });
|
||||
unsafe { log::set_logger_racy(Box::leak(logger))? };
|
||||
unsafe {
|
||||
log::set_logger_racy(Box::leak(logger))?;
|
||||
}
|
||||
log::set_max_level(LevelFilter::Debug);
|
||||
Ok(())
|
||||
}
|
|
@ -2,3 +2,4 @@ pub mod commands;
|
|||
pub mod favicon;
|
||||
pub mod types;
|
||||
pub mod logger;
|
||||
pub mod keys;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{ DateTime, Utc };
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use std::fmt;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -115,7 +115,7 @@ impl HistoryItem {
|
|||
content: String,
|
||||
favicon: Option<String>,
|
||||
source_icon: Option<String>,
|
||||
language: Option<String>,
|
||||
language: Option<String>
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
|
@ -130,7 +130,7 @@ impl HistoryItem {
|
|||
}
|
||||
|
||||
pub fn to_row(
|
||||
&self,
|
||||
&self
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"productName": "Qopy",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"identifier": "net.pandadev.qopy",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
|
217
types/keys.ts
Normal file
217
types/keys.ts
Normal 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',
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue