mirror of
https://github.com/System-End/hackatime-desktop.git
synced 2026-04-19 16:28:19 +00:00
feat: add update card
This commit is contained in:
parent
7516012de4
commit
e6aacf5f4d
4 changed files with 577 additions and 24 deletions
|
|
@ -30,6 +30,11 @@ fn greet(name: &str) -> String {
|
|||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_app_version(app: tauri::AppHandle) -> String {
|
||||
app.package_info().version.to_string()
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct LogEntry {
|
||||
ts: i64,
|
||||
|
|
@ -103,6 +108,7 @@ pub fn run() {
|
|||
})))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
get_app_version,
|
||||
get_recent_logs,
|
||||
|
||||
database::get_platform_info,
|
||||
|
|
|
|||
114
src/App.vue
114
src/App.vue
|
|
@ -13,6 +13,8 @@ import Login from "./views/Login.vue";
|
|||
import UserProfileCard from "./components/UserProfileCard.vue";
|
||||
import CustomTitlebar from "./components/CustomTitlebar.vue";
|
||||
import WakatimeSetupModal from "./components/WakatimeSetupModal.vue";
|
||||
import UpdateNotification from "./components/UpdateNotification.vue";
|
||||
import UpdateModal from "./components/UpdateModal.vue";
|
||||
|
||||
if (!(window as any).__hackatimeConsoleWrapped) {
|
||||
(window as any).__hackatimeConsoleWrapped = true;
|
||||
|
|
@ -69,6 +71,13 @@ const showWakatimeSetupModal = ref(false);
|
|||
const wakatimeConfigCheck = ref<any>(null);
|
||||
const hasCheckedConfigThisSession = ref(false);
|
||||
|
||||
const updateAvailable = ref(false);
|
||||
const updateVersion = ref<string>('');
|
||||
const updateData = ref<any>(null);
|
||||
const showUpdateModal = ref(false);
|
||||
const isInstallingUpdate = ref(false);
|
||||
const currentVersion = ref<string>('1.5.1');
|
||||
|
||||
|
||||
const weeklyChartData = computed(() => {
|
||||
if (!userStats.value?.weekly_stats?.daily_hours) {
|
||||
|
|
@ -103,6 +112,13 @@ onMounted(async () => {
|
|||
await loadApiConfig();
|
||||
await loadHackatimeInfo();
|
||||
|
||||
try {
|
||||
const appVersion = await invoke("get_app_version") as string;
|
||||
currentVersion.value = appVersion;
|
||||
} catch (error) {
|
||||
console.warn("Failed to get app version:", error);
|
||||
}
|
||||
|
||||
isDevMode.value = apiConfig.value.base_url.includes('localhost') ||
|
||||
apiConfig.value.base_url.includes('127.0.0.1') ||
|
||||
window.location.hostname === 'localhost' ||
|
||||
|
|
@ -466,30 +482,9 @@ async function checkForUpdatesAndInstall() {
|
|||
|
||||
if (update) {
|
||||
console.info(`[AUTO-UPDATE] Update available: ${update.version}`);
|
||||
console.info('[AUTO-UPDATE] Downloading and installing update...');
|
||||
|
||||
let downloaded = 0;
|
||||
let contentLength = 0;
|
||||
|
||||
await update.downloadAndInstall((event) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
contentLength = event.data.contentLength ?? 0;
|
||||
console.info(`[AUTO-UPDATE] Started downloading ${event.data.contentLength} bytes`);
|
||||
break;
|
||||
case 'Progress':
|
||||
downloaded += event.data.chunkLength;
|
||||
const percentage = contentLength > 0 ? Math.round((downloaded / contentLength) * 100) : 0;
|
||||
console.info(`[AUTO-UPDATE] Download progress: ${percentage}% (${downloaded} / ${contentLength} bytes)`);
|
||||
break;
|
||||
case 'Finished':
|
||||
console.info('[AUTO-UPDATE] Download finished');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
console.info('[AUTO-UPDATE] Update installed successfully. Restarting app...');
|
||||
await relaunch();
|
||||
updateData.value = update;
|
||||
updateVersion.value = update.version;
|
||||
updateAvailable.value = true;
|
||||
} else {
|
||||
console.info('[AUTO-UPDATE] No updates available - app is up to date');
|
||||
}
|
||||
|
|
@ -497,6 +492,56 @@ async function checkForUpdatesAndInstall() {
|
|||
console.error('[AUTO-UPDATE] Auto-update check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
if (!updateData.value || isInstallingUpdate.value) return;
|
||||
|
||||
try {
|
||||
isInstallingUpdate.value = true;
|
||||
console.info('[AUTO-UPDATE] Downloading and installing update...');
|
||||
|
||||
let downloaded = 0;
|
||||
let contentLength = 0;
|
||||
|
||||
await updateData.value.downloadAndInstall((event: any) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
contentLength = event.data.contentLength ?? 0;
|
||||
console.info(`[AUTO-UPDATE] Started downloading ${event.data.contentLength} bytes`);
|
||||
break;
|
||||
case 'Progress':
|
||||
downloaded += event.data.chunkLength;
|
||||
const percentage = contentLength > 0 ? Math.round((downloaded / contentLength) * 100) : 0;
|
||||
console.info(`[AUTO-UPDATE] Download progress: ${percentage}% (${downloaded} / ${contentLength} bytes)`);
|
||||
break;
|
||||
case 'Finished':
|
||||
console.info('[AUTO-UPDATE] Download finished');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
console.info('[AUTO-UPDATE] Update installed successfully. Restarting app...');
|
||||
await relaunch();
|
||||
} catch (error) {
|
||||
console.error('[AUTO-UPDATE] Update installation failed:', error);
|
||||
alert('Failed to install update: ' + error);
|
||||
isInstallingUpdate.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function dismissUpdate() {
|
||||
updateAvailable.value = false;
|
||||
showUpdateModal.value = false;
|
||||
}
|
||||
|
||||
function showMoreInfo() {
|
||||
showUpdateModal.value = true;
|
||||
}
|
||||
|
||||
async function handleInstallNow() {
|
||||
showUpdateModal.value = false;
|
||||
await installUpdate();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -698,6 +743,27 @@ async function checkForUpdatesAndInstall() {
|
|||
@applied="handleWakatimeConfigApplied"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Update components teleported to body to avoid layout issues -->
|
||||
<Teleport to="body">
|
||||
<!-- Update Notification -->
|
||||
<UpdateNotification
|
||||
v-if="updateAvailable && !showUpdateModal"
|
||||
:version="updateVersion"
|
||||
@installNow="handleInstallNow"
|
||||
@moreInfo="showMoreInfo"
|
||||
@dismiss="dismissUpdate"
|
||||
/>
|
||||
|
||||
<!-- Update Modal -->
|
||||
<UpdateModal
|
||||
v-if="showUpdateModal"
|
||||
:version="updateVersion"
|
||||
:current-version="currentVersion"
|
||||
@installNow="handleInstallNow"
|
||||
@installLater="showUpdateModal = false"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
362
src/components/UpdateModal.vue
Normal file
362
src/components/UpdateModal.vue
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, nextTick } from 'vue';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
|
||||
const props = defineProps<{
|
||||
version: string;
|
||||
currentVersion: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
installNow: [];
|
||||
installLater: [];
|
||||
}>();
|
||||
|
||||
const releaseInfo = ref<any>(null);
|
||||
const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const releaseUrl = computed(() =>
|
||||
`https://github.com/hackclub/hackatime-desktop/releases/tag/app-v${props.version}`
|
||||
);
|
||||
|
||||
const fetchReleaseInfo = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/hackclub/hackatime-desktop/releases/tags/app-v${props.version}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch release information');
|
||||
}
|
||||
|
||||
releaseInfo.value = await response.json();
|
||||
} catch (err) {
|
||||
console.error('Error fetching release info:', err);
|
||||
error.value = 'Failed to load release information';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formattedDate = computed(() => {
|
||||
if (!releaseInfo.value?.published_at) return '';
|
||||
return new Date(releaseInfo.value.published_at).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
});
|
||||
|
||||
const formattedBody = computed(() => {
|
||||
if (!releaseInfo.value?.body) return 'No release notes available.';
|
||||
|
||||
let text = releaseInfo.value.body;
|
||||
|
||||
text = text.replace(/### (.*?)$/gm, '<h3>$1</h3>');
|
||||
text = text.replace(/## (.*?)$/gm, '<h2>$1</h2>');
|
||||
text = text.replace(/# (.*?)$/gm, '<h2>$1</h2>');
|
||||
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
|
||||
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
text = text.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" data-external-link>$1</a>');
|
||||
|
||||
text = text.replace(/^[\*\-] (.+)$/gm, '<li>$1</li>');
|
||||
|
||||
text = text.replace(/(<li>.*<\/li>\n?)+/g, (match: string) => `<ul>${match}</ul>`);
|
||||
|
||||
text = text.replace(/\n\n+/g, '</p><p>');
|
||||
|
||||
text = `<p>${text}</p>`;
|
||||
|
||||
text = text.replace(/<p>\s*<\/p>/g, '');
|
||||
text = text.replace(/<p>(<h[23]>)/g, '$1');
|
||||
text = text.replace(/(<\/h[23]>)<\/p>/g, '$1');
|
||||
text = text.replace(/<p>(<ul>)/g, '$1');
|
||||
text = text.replace(/(<\/ul>)<\/p>/g, '$1');
|
||||
|
||||
return text;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchReleaseInfo();
|
||||
|
||||
await nextTick();
|
||||
|
||||
const links = document.querySelectorAll('[data-external-link]');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const href = (e.target as HTMLAnchorElement).getAttribute('href');
|
||||
if (href) {
|
||||
try {
|
||||
await openUrl(href);
|
||||
} catch (error) {
|
||||
console.error('Failed to open link:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const handleInstallNow = () => {
|
||||
emit('installNow');
|
||||
};
|
||||
|
||||
const handleInstallLater = () => {
|
||||
emit('installLater');
|
||||
};
|
||||
|
||||
const openReleaseUrl = async () => {
|
||||
try {
|
||||
await openUrl(releaseUrl.value);
|
||||
} catch (error) {
|
||||
console.error('Failed to open release URL:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fixed inset-0 bg-black/80 flex justify-center items-center p-8"
|
||||
style="z-index: 10000;"
|
||||
@click="handleInstallLater"
|
||||
>
|
||||
<div class="card-3d max-w-3xl w-full max-h-[90vh]" @click.stop>
|
||||
<div class="rounded-[8px] border border-black card-3d-front flex flex-col max-h-[90vh]" style="background-color: #3D2C3E;">
|
||||
<div class="p-6 border-b border-[rgba(0,0,0,0.2)] flex-shrink-0">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2 class="text-3xl font-bold text-white m-0 mb-2" style="font-family: 'Outfit', sans-serif;">
|
||||
Update Available
|
||||
</h2>
|
||||
<div class="flex items-center gap-4 text-white/60 flex-wrap" style="font-family: 'Outfit', sans-serif;">
|
||||
<span class="text-base">Version {{ currentVersion }} → {{ version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="handleInstallLater"
|
||||
class="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[rgba(255,255,255,0.1)] transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-6 min-h-0" style="font-family: 'Outfit', sans-serif;">
|
||||
<div v-if="loading" class="flex items-center justify-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-2 border-[#EB9182] border-t-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="text-center py-12">
|
||||
<p class="text-red-400 mb-4">{{ error }}</p>
|
||||
<button
|
||||
@click="openReleaseUrl"
|
||||
class="pushable pushable-active inline-block"
|
||||
style="font-family: 'Outfit', sans-serif;"
|
||||
>
|
||||
<span class="front px-6 py-2 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold" style="background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;">
|
||||
View release on GitHub
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="releaseInfo">
|
||||
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-4 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-white/60 text-sm mb-1">Release</div>
|
||||
<div class="text-2xl font-bold text-white">
|
||||
{{ releaseInfo.name || `v${version}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-white/60 text-sm mb-1">Published</div>
|
||||
<div class="text-white font-medium">{{ formattedDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="releaseInfo.body">
|
||||
<h3 class="text-white text-lg font-bold mb-3">Release Notes</h3>
|
||||
<div class="release-notes text-white/80 text-sm leading-relaxed">
|
||||
<div v-html="formattedBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<button
|
||||
@click="openReleaseUrl"
|
||||
class="text-[#E99682] hover:text-[#E88592] text-sm flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
<span>View full release notes on GitHub</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 border-t border-[rgba(0,0,0,0.2)] flex gap-3 flex-shrink-0">
|
||||
<button
|
||||
@click="handleInstallLater"
|
||||
class="pushable pushable-inactive flex-1"
|
||||
style="font-family: 'Outfit', sans-serif;"
|
||||
>
|
||||
<span class="front w-full px-6 py-3 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold text-center" style="background-color: #543c55; color: white;">
|
||||
Install Later
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@click="handleInstallNow"
|
||||
class="pushable pushable-active flex-1"
|
||||
style="font-family: 'Outfit', sans-serif;"
|
||||
>
|
||||
<span class="front w-full px-6 py-3 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold text-center" style="background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;">
|
||||
Install Now
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-3d {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-3d::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
background: #2A1F2B;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card-3d-front {
|
||||
position: relative;
|
||||
transform: translateY(-6px);
|
||||
z-index: 1;
|
||||
box-shadow: 0 6px 0 #2A1F2B;
|
||||
}
|
||||
|
||||
.pushable {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline-offset: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pushable-active {
|
||||
background: linear-gradient(135deg, #B85E6D 0%, #B85E6D 33%, #B5546F 66%, #B55389 100%);
|
||||
}
|
||||
|
||||
.pushable-inactive {
|
||||
background-color: #2A1F2B;
|
||||
}
|
||||
|
||||
.front {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
transform: translateY(-4px);
|
||||
transition: transform 0.1s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pushable:active .front {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.release-notes :deep(h2) {
|
||||
color: white;
|
||||
font-size: 1.125rem;
|
||||
font-weight: bold;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.release-notes :deep(h3) {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.release-notes :deep(p) {
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.release-notes :deep(ul) {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.release-notes :deep(li) {
|
||||
margin: 0.25rem 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.release-notes :deep(code) {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: monospace;
|
||||
color: #EB9182;
|
||||
}
|
||||
|
||||
.release-notes :deep(strong) {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.release-notes :deep(a) {
|
||||
color: #E99682;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.release-notes :deep(a:hover) {
|
||||
color: #E88592;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: rgba(42, 31, 43, 0.5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: rgba(233, 150, 130, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(233, 150, 130, 0.5);
|
||||
}
|
||||
</style>
|
||||
|
||||
119
src/components/UpdateNotification.vue
Normal file
119
src/components/UpdateNotification.vue
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
version: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
installNow: [];
|
||||
moreInfo: [];
|
||||
dismiss: [];
|
||||
}>();
|
||||
|
||||
const isVisible = ref(true);
|
||||
|
||||
const handleInstallNow = () => {
|
||||
emit('installNow');
|
||||
};
|
||||
|
||||
const handleMoreInfo = () => {
|
||||
emit('moreInfo');
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
isVisible.value = false;
|
||||
emit('dismiss');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="translate-x-full opacity-0"
|
||||
enter-to-class="translate-x-0 opacity-100"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="translate-x-0 opacity-100"
|
||||
leave-to-class="translate-x-full opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="card-3d-update pointer-events-auto"
|
||||
style="position: fixed; z-index: 9999; bottom: 20px; right: 20px; width: 320px;"
|
||||
>
|
||||
<div class="rounded-lg border-2 border-black p-4 card-3d-update-front" style="background-color: #3D2C3E;">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-[#EB9182]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<h3 class="text-white text-sm font-bold m-0" style="font-family: 'Outfit', sans-serif;">
|
||||
Update Available
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="handleDismiss"
|
||||
class="text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-white/80 text-xs mb-4 leading-relaxed" style="font-family: 'Outfit', sans-serif;">
|
||||
Version <span class="font-semibold text-[#EB9182]">{{ version }}</span> is ready to install
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="handleMoreInfo"
|
||||
class="flex-1 px-3 py-2 rounded-lg text-xs font-medium transition-all duration-200"
|
||||
style="background-color: #543c55; color: white; font-family: 'Outfit', sans-serif; border: 1px solid rgba(0,0,0,0.3);"
|
||||
>
|
||||
More Info
|
||||
</button>
|
||||
<button
|
||||
@click="handleInstallNow"
|
||||
class="flex-1 px-3 py-2 rounded-lg text-xs font-bold transition-all duration-200"
|
||||
style="background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white; font-family: 'Outfit', sans-serif; border: 1px solid rgba(0,0,0,0.3);"
|
||||
>
|
||||
Install Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-3d-update {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-3d-update::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
background-color: #2A1F2B;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card-3d-update-front {
|
||||
position: relative;
|
||||
transform: translateY(-4px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Add table
Reference in a new issue