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