mirror of
https://github.com/0PandaDEV/Qopy.git
synced 2025-04-22 05:34:04 +02:00
implemented new image saving
This commit is contained in:
parent
720f10bdbe
commit
08f79ece60
9 changed files with 198 additions and 128 deletions
119
app.vue
119
app.vue
|
@ -32,18 +32,17 @@
|
||||||
:class="['result clothoid-corner', { 'selected': isSelected(groupIndex, index) }]"
|
:class="['result clothoid-corner', { 'selected': isSelected(groupIndex, index) }]"
|
||||||
@click="selectItem(groupIndex, index)"
|
@click="selectItem(groupIndex, index)"
|
||||||
:ref="el => { if (isSelected(groupIndex, index)) selectedElement = el }">
|
:ref="el => { if (isSelected(groupIndex, index)) selectedElement = el }">
|
||||||
<img v-if="item.content_type === 'image'" :src="`data:image/bmp;base64,${item.content}`" alt="Image" class="favicon-image">
|
<img v-if="item.content_type === 'image'" :src="getComputedImageUrl(item)" alt="Image" class="favicon-image">
|
||||||
<img v-else-if="isUrl(item.content)" :src="getFaviconFromDb(item.favicon)" alt="Favicon" class="favicon">
|
<img v-else-if="isUrl(item.content)" :src="getFaviconFromDb(item.favicon)" alt="Favicon" class="favicon">
|
||||||
<FileIcon class="file" v-else />
|
<FileIcon class="file" v-else />
|
||||||
<span v-if="item.content_type === 'image'">Image ({{ getImageDimensions(item.content) }})</span>
|
<span v-if="item.content_type === 'image'">Image ({{ item.dimensions || 'Loading...' }})</span>
|
||||||
<span v-else>{{ truncateContent(item.content) }}</span>
|
<span v-else>{{ truncateContent(item.content) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
<OverlayScrollbarsComponent class="content">
|
<OverlayScrollbarsComponent class="content">
|
||||||
<img v-if="selectedItem?.content_type === 'image'"
|
<img v-if="selectedItem?.content_type === 'image'" :src="getComputedImageUrl(selectedItem)" alt="Image"
|
||||||
:src="selectedItem.content.startsWith('data:image') ? selectedItem?.content : `data:image/bmp;base64,${selectedItem?.content}`"
|
class="image">
|
||||||
alt="Image" class="image">
|
|
||||||
<img v-else-if="isYoutubeWatchUrl(selectedItem?.content)" :src="getYoutubeThumbnail(selectedItem.content)"
|
<img v-else-if="isYoutubeWatchUrl(selectedItem?.content)" :src="getYoutubeThumbnail(selectedItem.content)"
|
||||||
alt="YouTube Thumbnail" class="full-image">
|
alt="YouTube Thumbnail" class="full-image">
|
||||||
<span v-else>{{ selectedItem?.content || '' }}</span>
|
<span v-else>{{ selectedItem?.content || '' }}</span>
|
||||||
|
@ -53,9 +52,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, watch, nextTick, shallowRef } from 'vue';
|
||||||
import Database from '@tauri-apps/plugin-sql';
|
import Database from '@tauri-apps/plugin-sql';
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText, writeImage } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
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';
|
||||||
|
@ -63,7 +62,6 @@ import { platform } from '@tauri-apps/plugin-os';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
import { enable, isEnabled } from "@tauri-apps/plugin-autostart";
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { register, unregister, isRegistered } from '@tauri-apps/plugin-global-shortcut';
|
|
||||||
|
|
||||||
const db = ref(null);
|
const db = ref(null);
|
||||||
const history = ref([]);
|
const history = ref([]);
|
||||||
|
@ -168,7 +166,11 @@ const selectItem = (groupIndex, itemIndex) => {
|
||||||
|
|
||||||
const pasteSelectedItem = async () => {
|
const pasteSelectedItem = async () => {
|
||||||
if (selectedItem.value) {
|
if (selectedItem.value) {
|
||||||
|
if (selectedItem.value.content_type === 'image') {
|
||||||
|
await writeImage(selectedItem.value.content);
|
||||||
|
} else {
|
||||||
await writeText(selectedItem.value.content);
|
await writeText(selectedItem.value.content);
|
||||||
|
}
|
||||||
await hideApp();
|
await hideApp();
|
||||||
await invoke("simulate_paste");
|
await invoke("simulate_paste");
|
||||||
}
|
}
|
||||||
|
@ -208,60 +210,87 @@ const getFaviconFromDb = (favicon) => {
|
||||||
return `data:image/png;base64,${favicon}`;
|
return `data:image/png;base64,${favicon}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageDimensions = (base64Image) => {
|
const getImageDimensions = (path) => {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = `data:image/bmp;base64,${base64Image}`;
|
img.onload = () => resolve(`${img.width}x${img.height}`);
|
||||||
return `${img.width}x${img.height}`;
|
img.onerror = () => resolve('0x0');
|
||||||
|
if (path.includes('AppData\\Roaming\\net.pandadev.qopy\\images\\')) {
|
||||||
|
const filename = path.split('\\').pop();
|
||||||
|
try {
|
||||||
|
const imageData = await invoke("read_image", { filename: filename });
|
||||||
|
const blob = new Blob([new Uint8Array(imageData)], { type: 'image/png' });
|
||||||
|
img.src = URL.createObjectURL(blob);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading image file:', error);
|
||||||
|
resolve('0x0');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
img.src = `data:image/png;base64,${path}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshHistory = async () => {
|
const imageUrls = shallowRef({});
|
||||||
history.value = [];
|
|
||||||
await loadMoreHistory();
|
const getComputedImageUrl = (item) => {
|
||||||
|
if (!imageUrls.value[item.id]) {
|
||||||
|
imageUrls.value[item.id] = '';
|
||||||
|
getImageUrl(item.content).then(url => {
|
||||||
|
imageUrls.value = { ...imageUrls.value, [item.id]: url };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return imageUrls.value[item.id] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScroll = () => {
|
const getImageUrl = async (path) => {
|
||||||
const resultsElement = resultsContainer.value.$el;
|
if (path.includes('AppData\\Roaming\\net.pandadev.qopy\\images\\')) {
|
||||||
console.log('Scroll position:', resultsElement.scrollTop, 'Client height:', resultsElement.clientHeight, 'Scroll height:', resultsElement.scrollHeight);
|
const filename = path.split('\\').pop();
|
||||||
if (resultsElement.scrollTop + resultsElement.clientHeight >= resultsElement.scrollHeight - 10) {
|
try {
|
||||||
console.log('Scrolled to the end, loading more history...');
|
const imageData = await invoke("read_image", { filename: filename });
|
||||||
loadMoreHistory();
|
const blob = new Blob([new Uint8Array(imageData)], { type: 'image/png' });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading image file:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `data:image/png;base64,${path}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreHistory = async () => {
|
const loadAllHistory = async () => {
|
||||||
const lastTimestamp = history.value.length > 0 ? history.value[history.value.length - 1].timestamp : '9999-12-31T23:59:59Z';
|
if (!db.value) return;
|
||||||
const batchSize = 100;
|
|
||||||
|
|
||||||
const rawHistory = await db.value.select(
|
const rawHistory = await db.value.select(
|
||||||
'SELECT * FROM history WHERE timestamp < ? ORDER BY timestamp DESC LIMIT ?',
|
'SELECT * FROM history ORDER BY timestamp DESC'
|
||||||
[lastTimestamp, batchSize]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const newItems = rawHistory.map(item => {
|
history.value = await Promise.all(rawHistory.map(async item => {
|
||||||
if (item.type === 'image' && !item.content.startsWith('data:image')) {
|
if (item.content_type === 'image') {
|
||||||
return { ...item, content: `data:image/bmp;base64,${item.content}` };
|
const dimensions = await getImageDimensions(item.content);
|
||||||
|
return { ...item, dimensions };
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
}));
|
||||||
|
|
||||||
history.value = [...history.value, ...newItems];
|
|
||||||
console.log('Loaded more history:', newItems.length, 'items');
|
|
||||||
if (newItems.length < batchSize) {
|
|
||||||
resultsContainer.value.$el.removeEventListener('scroll', onScroll);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
db.value = await Database.load('sqlite:data.db');
|
db.value = await Database.load('sqlite:data.db');
|
||||||
|
await loadAllHistory();
|
||||||
|
|
||||||
await listen('tauri://focus', focusSearchInput);
|
await listen('tauri://focus', async () => {
|
||||||
|
await loadAllHistory();
|
||||||
focusSearchInput();
|
focusSearchInput();
|
||||||
|
});
|
||||||
|
|
||||||
const resultsElement = resultsContainer.value.$el;
|
await listen('tauri://blur', () => {
|
||||||
resultsElement.addEventListener('scroll', onScroll);
|
if (searchInput.value) {
|
||||||
console.log('Scroll event listener added');
|
searchInput.value.blur();
|
||||||
onScroll();
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// autostart
|
||||||
if (!await isEnabled()) {
|
if (!await isEnabled()) {
|
||||||
await enable()
|
await enable()
|
||||||
}
|
}
|
||||||
|
@ -272,16 +301,6 @@ const hideApp = async () => {
|
||||||
await window.getCurrentWindow().hide();
|
await window.getCurrentWindow().hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
const showApp = async () => {
|
|
||||||
history.value = [];
|
|
||||||
await refreshHistory();
|
|
||||||
await app.show();
|
|
||||||
await window.getCurrent().show();
|
|
||||||
selectedGroupIndex.value = 0;
|
|
||||||
selectedItemIndex.value = 0;
|
|
||||||
focusSearchInput();
|
|
||||||
};
|
|
||||||
|
|
||||||
const focusSearchInput = () => {
|
const focusSearchInput = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
searchInput.value?.focus();
|
searchInput.value?.focus();
|
||||||
|
|
|
@ -132,12 +132,12 @@ body,
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favicon-image{
|
.favicon-image {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file{
|
.file {
|
||||||
margin-inline: 2px;
|
margin-inline: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,10 @@ body,
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: top left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
"@tauri-apps/cli": "^2.0.0-beta.22",
|
"@tauri-apps/cli": "^2.0.0-beta.22",
|
||||||
"@tauri-apps/plugin-autostart": "^2.0.0-beta.7",
|
"@tauri-apps/plugin-autostart": "^2.0.0-beta.7",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.1.0-beta.5",
|
"@tauri-apps/plugin-clipboard-manager": "^2.1.0-beta.5",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.0.0-beta.7",
|
|
||||||
"@tauri-apps/plugin-os": "^2.0.0-beta.7",
|
"@tauri-apps/plugin-os": "^2.0.0-beta.7",
|
||||||
"@tauri-apps/plugin-sql": "^2.0.0-beta.7",
|
"@tauri-apps/plugin-sql": "^2.0.0-beta.7",
|
||||||
"nuxt": "^3.12.3",
|
"nuxt": "^3.12.3",
|
||||||
|
|
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
|
@ -1528,24 +1528,6 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "global-hotkey"
|
|
||||||
version = "0.5.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "89cb13e8c52c87e28a46eae3e5e65b8f0cd465c4c9e67b13d56c70412e792bc3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.6.0",
|
|
||||||
"cocoa 0.25.0",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"keyboard-types",
|
|
||||||
"objc",
|
|
||||||
"once_cell",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
"x11-dl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
|
@ -3215,18 +3197,19 @@ dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"image 0.25.1",
|
"image 0.25.1",
|
||||||
|
"lazy_static",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rdev",
|
"rdev",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-global-shortcut",
|
|
||||||
"tauri-plugin-os",
|
"tauri-plugin-os",
|
||||||
"tauri-plugin-sql",
|
"tauri-plugin-sql",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -4511,9 +4494,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.0.0-beta.23"
|
version = "2.0.0-beta.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68725c4f17f62f0fb1fa2eecaf391200bf00a9414c84f30783ddca10570690c3"
|
checksum = "3eab508aad4ae86e23865e294b20a7bb89bd7afea523897b7478329b841d4295"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -4561,9 +4544,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "2.0.0-beta.18"
|
version = "2.0.0-beta.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1822847744f663babbfc8b7532a104734e9cf99e3408bba7109018bf9177917"
|
checksum = "498f587026501e4bbc5d6273b63f8956b03c37b3d3b2027f9c756fcd468e9c62"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
|
@ -4583,9 +4566,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.0.0-beta.18"
|
version = "2.0.0-beta.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e36fa3c2e3bd935827fef1eed459885414fb27c82f687d8b9a15112c8a5c8f0"
|
checksum = "43bbc731067e319ef60601bf5716d1e706ee9ae28e38c0587f7165c7d6824cdf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
|
@ -4610,9 +4593,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.0.0-beta.18"
|
version = "2.0.0-beta.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34aba4bed4648c3cb17d421af5783c7c29a033a94ab8597ef3791dadea69289d"
|
checksum = "36b4a44346577ccde75a24c62405a4c3b4f7a3a76614ee6cf1ed14a0b756795c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -4624,9 +4607,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin"
|
name = "tauri-plugin"
|
||||||
version = "2.0.0-beta.18"
|
version = "2.0.0-beta.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "431ac9636bf81e7a04042399918ffa6b9d2413926dabc9366a24f6b487f64653"
|
checksum = "1abe0b85472516d1033ba251ac81b9f18f02725aadcaad697c8b727e6505a6ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"glob",
|
"glob",
|
||||||
|
@ -4670,20 +4653,6 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tauri-plugin-global-shortcut"
|
|
||||||
version = "2.0.0-beta.7"
|
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v2#4003bdada2f8bc07e6a8092c106daf6cd9f712fd"
|
|
||||||
dependencies = [
|
|
||||||
"global-hotkey",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tauri",
|
|
||||||
"tauri-plugin",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-os"
|
name = "tauri-plugin-os"
|
||||||
version = "2.0.0-beta.7"
|
version = "2.0.0-beta.7"
|
||||||
|
@ -4723,9 +4692,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.0.0-beta.19"
|
version = "2.0.0-beta.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5fa872242a432195b814e87f91ce10f293ae5b01fbd1eb139455496260aa7c9"
|
checksum = "fe978df03966febbebc608931dc2cf26ef94df70855a18b05f07134cf474de09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dpi",
|
"dpi",
|
||||||
"gtk",
|
"gtk",
|
||||||
|
@ -4742,9 +4711,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "2.0.0-beta.19"
|
version = "2.0.0-beta.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ad6d5ef3c05d1c4b6cf97b9eac1ca1ad8ff2a7057ad0a92b3e4c476f009341e"
|
checksum = "11e4d568f61095f507b3fc4254dfbfff3b20de2a1d66167ffca3f6d90b14db8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa 0.25.0",
|
"cocoa 0.25.0",
|
||||||
"gtk",
|
"gtk",
|
||||||
|
@ -4766,9 +4735,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.0.0-beta.18"
|
version = "2.0.0-beta.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f81a672883c9a67eb24727c99cce583625c919a5fb696c661603b426c463c72"
|
checksum = "e20e51856f343c503892749b27d34042e6ca83a0369a12de3c5552d9874d04e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"cargo_metadata",
|
"cargo_metadata",
|
||||||
|
|
|
@ -13,7 +13,6 @@ tauri-build = { version = "2.0.0-beta.18", features = [] }
|
||||||
tauri = { version = "2.0.0-beta.23", features = ["tray-icon", "image-png"] }
|
tauri = { version = "2.0.0-beta.23", features = ["tray-icon", "image-png"] }
|
||||||
tauri-plugin-sql = {version = "2.0.0-beta.8", features = ["sqlite"] }
|
tauri-plugin-sql = {version = "2.0.0-beta.8", features = ["sqlite"] }
|
||||||
tauri-plugin-clipboard-manager = "2.1.0-beta.5"
|
tauri-plugin-clipboard-manager = "2.1.0-beta.5"
|
||||||
tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
|
||||||
tauri-plugin-autostart = "2.0.0-beta.8"
|
tauri-plugin-autostart = "2.0.0-beta.8"
|
||||||
tauri-plugin-os = "2.0.0-beta.7"
|
tauri-plugin-os = "2.0.0-beta.7"
|
||||||
sqlx = { version = "0.7.4", features = ["runtime-tokio-native-tls", "sqlite"] }
|
sqlx = { version = "0.7.4", features = ["runtime-tokio-native-tls", "sqlite"] }
|
||||||
|
@ -28,3 +27,5 @@ image = "0.25.1"
|
||||||
reqwest = { version = "0.12.5", features = ["blocking"] }
|
reqwest = { version = "0.12.5", features = ["blocking"] }
|
||||||
url = "2.5.2"
|
url = "2.5.2"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
sha2 = "0.10.6"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
|
@ -18,11 +18,6 @@
|
||||||
"sql:allow-load",
|
"sql:allow-load",
|
||||||
"sql:allow-select",
|
"sql:allow-select",
|
||||||
"sql:allow-execute",
|
"sql:allow-execute",
|
||||||
"global-shortcut:default",
|
|
||||||
"global-shortcut:allow-is-registered",
|
|
||||||
"global-shortcut:allow-register",
|
|
||||||
"global-shortcut:allow-unregister",
|
|
||||||
"global-shortcut:allow-unregister-all",
|
|
||||||
"autostart:allow-enable",
|
"autostart:allow-enable",
|
||||||
"autostart:allow-disable",
|
"autostart:allow-disable",
|
||||||
"autostart:allow-is-enabled",
|
"autostart:allow-is-enabled",
|
||||||
|
@ -34,6 +29,7 @@
|
||||||
"window:allow-set-focus",
|
"window:allow-set-focus",
|
||||||
"window:allow-is-focused",
|
"window:allow-is-focused",
|
||||||
"window:allow-is-visible",
|
"window:allow-is-visible",
|
||||||
"clipboard-manager:allow-write-text"
|
"clipboard-manager:allow-write-text",
|
||||||
|
"clipboard-manager:allow-write-image"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -3,19 +3,40 @@ use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use image::io::Reader as ImageReader;
|
use image::io::Reader as ImageReader;
|
||||||
use image::{DynamicImage, ImageBuffer, ImageFormat, Rgba};
|
use image::{DynamicImage, ImageBuffer, ImageFormat, Rgba};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use rand::distributions::Alphanumeric;
|
use rand::distributions::Alphanumeric;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use rdev::{simulate, EventType, Key};
|
use rdev::{simulate, EventType, Key};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
use std::fs;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::sync::Mutex;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::Manager;
|
use tauri::{AppHandle, Manager};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref APP_DATA_DIR: Mutex<Option<std::path::PathBuf>> = Mutex::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_app_data_dir(path: std::path::PathBuf) {
|
||||||
|
let mut dir = APP_DATA_DIR.lock().unwrap();
|
||||||
|
*dir = Some(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn read_image(filename: String) -> Result<Vec<u8>, String> {
|
||||||
|
let app_data_dir = APP_DATA_DIR.lock().unwrap();
|
||||||
|
let app_data_dir = app_data_dir.as_ref().expect("App data directory not set");
|
||||||
|
let image_path = app_data_dir.join("images").join(filename);
|
||||||
|
fs::read(image_path).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn simulate_paste() {
|
pub fn simulate_paste() {
|
||||||
let mut events = vec![
|
let mut events = vec![
|
||||||
|
@ -33,10 +54,18 @@ pub fn simulate_paste() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_image_path(app_handle: tauri::AppHandle, filename: String) -> String {
|
||||||
|
let app_data_dir = app_handle.path().app_data_dir().expect("Failed to get app data directory");
|
||||||
|
let image_path = app_data_dir.join("images").join(filename);
|
||||||
|
image_path.to_str().unwrap_or("").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn setup(app_handle: tauri::AppHandle) {
|
pub fn setup(app_handle: tauri::AppHandle) {
|
||||||
let is_processing = std::sync::Arc::new(std::sync::Mutex::new(false));
|
let is_processing = std::sync::Arc::new(std::sync::Mutex::new(false));
|
||||||
|
|
||||||
std::thread::spawn({
|
std::thread::spawn({
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
let is_processing = std::sync::Arc::clone(&is_processing);
|
let is_processing = std::sync::Arc::clone(&is_processing);
|
||||||
move || {
|
move || {
|
||||||
let mut clipboard = Clipboard::new().unwrap();
|
let mut clipboard = Clipboard::new().unwrap();
|
||||||
|
@ -53,7 +82,7 @@ pub fn setup(app_handle: tauri::AppHandle) {
|
||||||
if content != last_text {
|
if content != last_text {
|
||||||
last_text = content.clone();
|
last_text = content.clone();
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
insert_content_if_not_exists(&pool, "text", content).await;
|
insert_content_if_not_exists(&app_handle, &pool, "text", content).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +92,7 @@ pub fn setup(app_handle: tauri::AppHandle) {
|
||||||
Ok(png_image) => {
|
Ok(png_image) => {
|
||||||
let base64_image = STANDARD.encode(&png_image);
|
let base64_image = STANDARD.encode(&png_image);
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
insert_content_if_not_exists(&pool, "image", base64_image).await;
|
insert_content_if_not_exists(&app_handle, &pool, "image", base64_image).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -97,7 +126,7 @@ fn process_clipboard_image(
|
||||||
Ok(png_bytes)
|
Ok(png_bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn insert_content_if_not_exists(pool: &SqlitePool, content_type: &str, content: String) {
|
async fn insert_content_if_not_exists(app_handle: &AppHandle, pool: &SqlitePool, content_type: &str, content: String) {
|
||||||
let last_content: Option<String> = sqlx::query_scalar(
|
let last_content: Option<String> = sqlx::query_scalar(
|
||||||
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
"SELECT content FROM history WHERE content_type = ? ORDER BY timestamp DESC LIMIT 1",
|
||||||
)
|
)
|
||||||
|
@ -106,6 +135,18 @@ async fn insert_content_if_not_exists(pool: &SqlitePool, content_type: &str, con
|
||||||
.await
|
.await
|
||||||
.unwrap_or(None);
|
.unwrap_or(None);
|
||||||
|
|
||||||
|
let content = if content_type == "image" {
|
||||||
|
match save_image(app_handle, &content).await {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to save image: {}", e);
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
};
|
||||||
|
|
||||||
if last_content.as_deref() != Some(&content) {
|
if last_content.as_deref() != Some(&content) {
|
||||||
let id: String = thread_rng()
|
let id: String = thread_rng()
|
||||||
.sample_iter(&Alphanumeric)
|
.sample_iter(&Alphanumeric)
|
||||||
|
@ -113,7 +154,7 @@ async fn insert_content_if_not_exists(pool: &SqlitePool, content_type: &str, con
|
||||||
.map(char::from)
|
.map(char::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
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();
|
||||||
let favicon_base64 = if content_type == "text" && url_regex.is_match(&content) {
|
let favicon_base64 = if content_type == "text" && url_regex.is_match(&content) {
|
||||||
match Url::parse(&content) {
|
match Url::parse(&content) {
|
||||||
Ok(url) => match fetch_favicon_as_base64(url).await {
|
Ok(url) => match fetch_favicon_as_base64(url).await {
|
||||||
|
@ -138,13 +179,32 @@ async fn insert_content_if_not_exists(pool: &SqlitePool, content_type: &str, con
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(content_type)
|
.bind(content_type)
|
||||||
.bind(content)
|
.bind(&content)
|
||||||
.bind(favicon_base64)
|
.bind(favicon_base64)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn save_image(app_handle: &AppHandle, base64_image: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let image_data = STANDARD.decode(base64_image)?;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&image_data);
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
let filename = format!("{:x}.png", hash);
|
||||||
|
|
||||||
|
let app_data_dir = app_handle.path().app_data_dir().expect("Failed to get app data directory");
|
||||||
|
let images_dir = app_data_dir.join("images");
|
||||||
|
let path = images_dir.join(&filename);
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
fs::create_dir_all(&images_dir)?;
|
||||||
|
fs::write(&path, &image_data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(path.to_str().unwrap().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
async fn fetch_favicon_as_base64(url: Url) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
async fn fetch_favicon_as_base64(url: Url) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let favicon_url = format!("https://icon.horse/icon/{}", url.host_str().unwrap());
|
let favicon_url = format!("https://icon.horse/icon/{}", url.host_str().unwrap());
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use rdev::{listen, EventType, Key};
|
use rdev::{listen, EventType, Key};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
|
use crate::center_window_on_current_monitor;
|
||||||
|
|
||||||
pub fn setup(app_handle: tauri::AppHandle) {
|
pub fn setup(app_handle: tauri::AppHandle) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut meta_pressed = false;
|
let mut meta_pressed = false;
|
||||||
|
@ -18,6 +20,7 @@ pub fn setup(app_handle: tauri::AppHandle) {
|
||||||
let window = app_handle.get_webview_window("main").unwrap();
|
let window = app_handle.get_webview_window("main").unwrap();
|
||||||
window.show().unwrap();
|
window.show().unwrap();
|
||||||
window.set_focus().unwrap();
|
window.set_focus().unwrap();
|
||||||
|
center_window_on_current_monitor(&window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -12,18 +12,32 @@ use tauri::Manager;
|
||||||
use tauri::PhysicalPosition;
|
use tauri::PhysicalPosition;
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
|
||||||
fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
pub fn center_window_on_current_monitor(window: &tauri::WebviewWindow) {
|
||||||
if let Some(monitor) = window.current_monitor().unwrap() {
|
if let Some(monitor) = window.available_monitors().unwrap().iter().find(|m| {
|
||||||
|
let primary_monitor = window
|
||||||
|
.primary_monitor()
|
||||||
|
.unwrap()
|
||||||
|
.expect("Failed to get primary monitor");
|
||||||
|
let mouse_position = primary_monitor.position();
|
||||||
|
let monitor_position = m.position();
|
||||||
|
let monitor_size = m.size();
|
||||||
|
mouse_position.x >= monitor_position.x
|
||||||
|
&& mouse_position.x < monitor_position.x + monitor_size.width as i32
|
||||||
|
&& mouse_position.y >= monitor_position.y
|
||||||
|
&& mouse_position.y < monitor_position.y + monitor_size.height as i32
|
||||||
|
}) {
|
||||||
let monitor_size = monitor.size();
|
let 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.set_position(PhysicalPosition::new(
|
window
|
||||||
|
.set_position(PhysicalPosition::new(
|
||||||
monitor.position().x + x,
|
monitor.position().x + x,
|
||||||
monitor.position().y + y,
|
monitor.position().y + y,
|
||||||
)).unwrap();
|
))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +45,6 @@ fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_clipboard_manager::init())
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||||
.plugin(tauri_plugin_autostart::init(
|
.plugin(tauri_plugin_autostart::init(
|
||||||
MacosLauncher::LaunchAgent,
|
MacosLauncher::LaunchAgent,
|
||||||
|
@ -57,6 +70,9 @@ fn main() {
|
||||||
window.close_devtools();
|
window.close_devtools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let app_data_dir = app.path().app_data_dir().expect("Failed to get app data directory");
|
||||||
|
clipboard::set_app_data_dir(app_data_dir);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_window_event(|app, event| match event {
|
.on_window_event(|app, event| match event {
|
||||||
|
@ -68,7 +84,11 @@ fn main() {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![clipboard::simulate_paste])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
clipboard::simulate_paste,
|
||||||
|
clipboard::get_image_path,
|
||||||
|
clipboard::read_image
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue