system tray

This commit is contained in:
cast1e
2024-10-14 11:17:20 +08:00
parent 5a0769e91b
commit c91f67944d
15 changed files with 489 additions and 165 deletions
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1728872416760" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3377" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M874.666667 241.066667h-202.666667V170.666667c0-40.533333-34.133333-74.666667-74.666667-74.666667h-170.666666c-40.533333 0-74.666667 34.133333-74.666667 74.666667v70.4H149.333333c-17.066667 0-32 14.933333-32 32s14.933333 32 32 32h53.333334V853.333333c0 40.533333 34.133333 74.666667 74.666666 74.666667h469.333334c40.533333 0 74.666667-34.133333 74.666666-74.666667V305.066667H874.666667c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32zM416 170.666667c0-6.4 4.266667-10.666667 10.666667-10.666667h170.666666c6.4 0 10.666667 4.266667 10.666667 10.666667v70.4h-192V170.666667z m341.333333 682.666666c0 6.4-4.266667 10.666667-10.666666 10.666667H277.333333c-6.4 0-10.666667-4.266667-10.666666-10.666667V309.333333h490.666666V853.333333z" fill="#currentColor" p-id="3378"></path><path d="M426.666667 736c17.066667 0 32-14.933333 32-32V490.666667c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v213.333333c0 17.066667 14.933333 32 32 32zM597.333333 736c17.066667 0 32-14.933333 32-32V490.666667c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v213.333333c0 17.066667 14.933333 32 32 32z" fill="currentColor" p-id="3379"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+36 -10
View File
@@ -82,6 +82,13 @@ const addInfo = ref<AddInfo>({
})
const image_src = ref("");
const support_ext = [".mp4", ".mkv", ".flv", ".ts"];
const support_img_ext = [".jpg", ".png", ".gif", ".webp"];
const support_img_ext_map: { [key in (typeof support_img_ext)[number]]: string } = {
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp'
}
defineExpose({ open })
@@ -99,7 +106,10 @@ function selectMedia() {
let ext = file.substring(file.lastIndexOf("."));
if (support_ext.includes(ext)) {
addInfo.value.media = file;
}
} else ElMessage({
type: "error",
message: "文件格式不受支持"
})
}
})
}
@@ -107,14 +117,20 @@ function selectMedia() {
function selectPreview() {
handleFileOpen().then((file) => {
if (file) {
addInfo.value.preview = file;
invoke("get_file", {
path: file
}).then((res) => {
let binary_data_arr = new Uint8Array(res as number[]);
const blob = new Blob([binary_data_arr], { type: 'image/jpeg' });
const imageUrl = URL.createObjectURL(blob);
image_src.value = imageUrl;
let ext = file.substring(file.lastIndexOf("."));
if (support_img_ext.includes(ext)) {
addInfo.value.preview = file;
invoke("get_file", {
path: file
}).then((res) => {
let binary_data_arr = new Uint8Array(res as number[]);
const blob = new Blob([binary_data_arr], { type: support_img_ext_map[ext] });
const imageUrl = URL.createObjectURL(blob);
image_src.value = imageUrl;
})
} else ElMessage({
type: "error",
message: "文件格式不受支持"
})
}
})
@@ -124,8 +140,13 @@ function toggleVisible() {
visible.value = !visible.value
}
function checkInfo(info: AddInfo) {
if (!info.name || !info.media) return false
else return true;
}
function handleAdd() {
invoke("new_wallpaper", {
if (checkInfo(addInfo.value)) invoke("new_wallpaper", {
info: addInfo.value
}).then((res) => {
if (res as string == "Success") {
@@ -135,6 +156,7 @@ function handleAdd() {
media: "",
description: ""
}
image_src.value = "";
store.commit("getWpList");
toggleVisible();
ElMessage({
@@ -147,6 +169,10 @@ function handleAdd() {
message: `新建失败 ${res}`
})
})
else ElMessage({
type: "error",
message: "请填写名称并选择媒体文件"
})
}
</script>
+1 -1
View File
@@ -170,7 +170,7 @@ function apply() {
}
.apply-bar-mask {
z-index: 500;
z-index: 200;
position: absolute;
top: 0;
left: 0;
+1 -1
View File
@@ -65,7 +65,7 @@ export default defineComponent({
<style lang="scss" scoped>
#mask {
z-index: 500;
z-index: 200;
position: absolute;
top: 0;
left: 0;
+85
View File
@@ -0,0 +1,85 @@
<template>
<div class="cui-rmenu-mask" @click.self="handleClose" @contextmenu.prevent="handleClose" v-if="visible">
<div ref="bg" class="cui-rmenu-bg">
<div class="cui-rmenu-content">
<slot name="content"></slot>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, ref, defineEmits, defineExpose } from 'vue';
defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(["update:visible"])
defineExpose({ handleOpen });
const bg = ref<HTMLDivElement | null>(null);
function handleClose() {
if (bg.value) bg.value.style.animation = "cui-rmenu-disappear .15s ease-in";
setTimeout(() => emit("update:visible", false), 150);
}
function handleOpen(x: number, y: number) {
setTimeout(() => {
if (bg.value) {
bg.value.style.top = `${y}px`;
bg.value.style.left = `${x}px`;
}
}, 0)
}
</script>
<style>
@keyframes cui-rmenu-appear {
0% {
opacity: 0%;
transform: translate(-25%, -25%) scale(0.5);
}
100% {
opacity: 100%;
transform: scale(1);
}
}
@keyframes cui-rmenu-disappear {
0% {
opacity: 100%;
transform: scale(1);
}
100% {
opacity: 0%;
transform: translate(-25%, -25%) scale(0.5);
}
}
.cui-rmenu-mask {
z-index: 2007;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.cui-rmenu-bg {
position: absolute;
width: auto;
height: auto;
max-width: 80%;
border: solid var(--bd-color) 1px;
background-color: var(--bg-color-solid);
border-radius: 8px;
animation: cui-rmenu-appear .25s cubic-bezier(0, 0, 0.36, 1.29);
}
.cui-rmenu-content {
height: 100%;
}
</style>
@@ -0,0 +1,37 @@
<template>
<div class="cui-rmenu-cell-wrapper">
<div class="cui-rmenu-cell-icon">
<slot name="icon"></slot>
</div>
<slot></slot>
</div>
</template>
<style>
.cui-rmenu-cell-wrapper {
display: flex;
align-items: center;
flex-direction: row;
width: auto;
height: 30px;
margin: 5px;
border-radius: 3px;
transition: .5s;
background-color: var(--bg-color-solid);
font-size: 14px;
padding: 2px 7px 2px 2px;
cursor: pointer;
}
.cui-rmenu-cell-wrapper:hover {
background-color: var(--bg-color-alter);
}
.cui-rmenu-cell-icon {
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
+3 -3
View File
@@ -1,7 +1,7 @@
<template>
<div class="item-card" :style="{
backgroundImage: `url(${cell.img})`
}">
<div :style="{
backgroundImage: cell.img ? `url(${cell.img})` : 'linear-gradient(135deg, #00000020 0%, #FFFFFF20 100%)'
}" class="item-card">
<!-- <div class="item-card-main">
<img :src="config.img" />
</div> -->
+2 -1
View File
@@ -82,7 +82,7 @@ function toggleMaximize() {
}
function close() {
appWindow.close()
appWindow.hide();
}
</script>
@@ -124,6 +124,7 @@ function close() {
backdrop-filter: blur(10px) saturate(180%);
box-shadow: var(--shadow-edge-glow), var(--shadow);
background-color: var(--bg-color-alpha);
z-index: 300;
}
.titlebar-button {
+63 -35
View File
@@ -1,46 +1,74 @@
import { createStore } from 'vuex'
import { invoke } from '@tauri-apps/api/core';
import type {ResourceDir,wpConfig} from '@/ts/types'
import { invoke } from '@tauri-apps/api/core'
import type { ResourceDir, wpConfig, Cell } from '@/ts/types'
function arrayBufferToString(buffer: ArrayBuffer): string {
const decoder = new TextDecoder('utf-8');
return decoder.decode(buffer);
const decoder = new TextDecoder('utf-8')
return decoder.decode(buffer)
}
const support_ext = ['.jpg', '.png', '.gif', '.webp']
const support_ext_map: { [key in (typeof support_ext)[number]]: string } = {
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp'
}
export const store = createStore({
state() {
return {
wpList:[]
}
},
mutations: {
getWpList(state){
state.wpList = [];
invoke("read_resource_dir", {}).then((res) => {
const resource = JSON.parse(res as string) as ResourceDir;
for (let id of Object.keys(resource.files)) {
let dir = resource.files[id]
if ("preview.jpg" in dir) {
invoke("get_file", {
path: dir["preview.jpg"]
}).then((res) => {
let binary_data_arr = new Uint8Array(res as number[]);
const blob = new Blob([binary_data_arr], { type: 'image/jpeg' });
const imageUrl = URL.createObjectURL(blob);
invoke("get_file", {
path: `${id}\\config.json`
}).then((cfg) => {
let config: wpConfig = JSON.parse(arrayBufferToString(cfg as ArrayBuffer));
state.wpList.push({
path: id,
img: imageUrl,
config: config
})
state() {
return {
wpList: [] as Cell[]
}
},
mutations: {
getWpList(state) {
state.wpList = []
invoke('read_resource_dir', {}).then((res) => {
const resource = JSON.parse(res as string) as ResourceDir
for (const id of Object.keys(resource.files)) {
const dir = resource.files[id]
if ('config.json' in dir)
invoke('get_file', {
path: `${id}\\config.json`
}).then((cfg) => {
const config: wpConfig = JSON.parse(arrayBufferToString(cfg as ArrayBuffer))
let hasPreview = false
for (const ext of support_ext) {
const filename = 'preview' + ext
if (filename in dir) {
hasPreview = true
invoke('get_file', {
path: dir[filename]
}).then((res) => {
const binary_data_arr = new Uint8Array(res as number[])
const blob = new Blob([binary_data_arr], {
type: support_ext_map[ext]
})
const imageUrl = URL.createObjectURL(blob)
invoke('get_file', {
path: `${id}\\config.json`
}).then((cfg) => {
const config: wpConfig = JSON.parse(arrayBufferToString(cfg as ArrayBuffer))
state.wpList.push({
path: id,
img: imageUrl,
config: config
})
})
}
})
}
});
}
if (!hasPreview) {
state.wpList.push({
path: id,
img: '',
config: config
})
}
})
}
})
}
})
}
})
+10 -3
View File
@@ -1,14 +1,21 @@
import type { Store } from 'vuex'
import type { Cell } from '@/ts/types'
import type { Cell } from '@/ts/types'
declare module 'vuex' {
export * from 'vuex/types/index.d.ts'
export * from 'vuex/types/helpers.d.ts'
export * from 'vuex/types/logger.d.ts'
export * from 'vuex/types/vue.d.ts'
}
declare module 'vue' {
// 声明自己的 store state
interface State {
wpList:Cell[]
wpList: Cell[]
}
// 为 `this.$store` 提供类型声明
interface ComponentCustomProperties {
$store: Store<State>
}
}
}
+54 -9
View File
@@ -2,37 +2,82 @@
import ItemCard from '@/components/ItemCard.vue';
import ApplyBar from '@/components/ApplyBar.vue';
import AddItem from '@/components/AddItem.vue';
import { ref, onMounted, computed } from 'vue';
import { entry } from '@/ts/entry';
import { ref, onMounted, computed, nextTick } from 'vue';
import CRMenu from '@/components/CRMenu.vue';
import CRMenuCell from '@/components/CRMenuCell.vue';
import SvgIcon from '@/components/SvgIcon.vue';
import type { Cell } from '@/ts/types'
import { useStore } from 'vuex';
import { invoke } from '@tauri-apps/api/core';
import { ElMessage } from 'element-plus';
const store = useStore();
const items = computed<Cell[]>(()=>store.state.wpList);
const items = computed<Cell[]>(() => store.state.wpList);
const apply_bar_visible = ref(false);
const applyBar = ref<InstanceType<typeof ApplyBar> | null>(null);
const item_add_visible = ref(false);
const r_display = ref(false);
const r_data = ref<Cell>();
const menu = ref<InstanceType<typeof CRMenu> | null>(null);
const options = ref<{ name: string, icon: string, handler: (data: Cell) => void }[]>([{
name: "删除",
icon: "delete",
handler: del_wallpaper
}])
onMounted(() => {
const main = document.querySelector(".home-main") as HTMLElement;
setTimeout(() => {
entry("up", main, 20);
})
store.commit("getWpList");
store.commit("getWpList");
})
function openCard(config: Cell) {
console.log(applyBar.value)
if (applyBar.value) applyBar.value.open(config);
}
function handleRightClick(event: MouseEvent, data: Cell) {
r_data.value = data;
r_display.value = true;
nextTick(() => {
if (menu.value) menu.value.handleOpen(event.x, event.y);
})
}
function del_wallpaper(data: Cell) {
invoke("del_folder", {
path: data.path
}).then((res) => {
if (res) {
store.commit("getWpList");
ElMessage({
type: "success",
message: `已删除 ${data.path}`
})
}
else ElMessage({
type: "error",
message: `删除失败`
})
})
}
</script>
<template>
<main class="colbox home-main">
<ItemCard v-for="(item, index) in items" :key="index" :cell="item" @click="openCard(item)"></ItemCard>
<ItemCard v-for="(item, index) in items" :key="index" :cell="item" @click="openCard(item)"
@contextmenu.prevent="(e) => handleRightClick(e, item)"></ItemCard>
</main>
<ApplyBar v-model="apply_bar_visible" ref="applyBar"></ApplyBar>
<AddItem v-model="item_add_visible"></AddItem>
<CRMenu ref="menu" v-model:visible="r_display">
<template #content>
<CRMenuCell v-for="option in options" :key="option.name" @click="option.handler(r_data); r_display = false;">
<template #icon>
<SvgIcon size="20px" :name="option.icon" style="color: var(--text-color);"></SvgIcon>
</template>
{{ option.name }}
</CRMenuCell>
</template>
</CRMenu>
</template>
<style>