ENH - complete and secure EasyShare integration

This commit is contained in:
smx.pusha
2026-08-12 07:15:26 +02:00
parent 14f0b7a178
commit eeb3396f52
18 changed files with 959 additions and 39 deletions
+24
View File
@@ -13,6 +13,7 @@ import {
Plus,
Rows3,
Search,
Share2,
Trash2,
UserRound,
X,
@@ -22,6 +23,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { useAccountStore } from '@/stores/account'
import { useCalendarStore } from '@/stores/calendar'
import { useEasyShareStore } from '@/stores/easyshare'
import { usePhoneStore } from '@/stores/phone'
import type { CalendarEvent, CalendarEventDraft } from '@/types/calendar'
@@ -42,6 +44,7 @@ type YearOverview = {
const phone = usePhoneStore()
const account = useAccountStore()
const calendar = useCalendarStore()
const easyShare = useEasyShareStore()
const today = new Date()
const visibleYear = ref(today.getFullYear())
const selectedDate = ref(dateKey(today))
@@ -358,6 +361,19 @@ function openEdit(): void {
screen.value = 'form'
}
function shareEvent(): void {
if (!selectedEvent.value) return
easyShare.open({
appId: 'calendar',
copyText: `${selectedEvent.value.title}\n${selectedEvent.value.note}`,
id: selectedEvent.value.id,
kind: 'document',
link: `skyphone://calendar/event/${selectedEvent.value.id}`,
subtitle: formatLongDate(selectedEvent.value.startsAt),
title: selectedEvent.value.title,
})
}
function closeForm(): void {
timePickerField.value = null
screen.value = editingEvent.value ? 'detail' : 'main'
@@ -781,6 +797,10 @@ onMounted(async () => {
<p>{{ selectedEvent.note }}</p>
</div>
</section>
<button class="calendar__delete calendar__share" type="button" @click="shareEvent">
<Share2 :size="18" />
{{ phone.t('Apps.easyShare.share') }}
</button>
<button class="calendar__delete" type="button" @click="deleteEvent">
<Trash2 :size="18" />
{{ phone.t('Apps.calendar.deleteEvent') }}
@@ -1739,6 +1759,10 @@ onMounted(async () => {
font-size: 14px !important;
}
.calendar__share {
color: #0a84ff !important;
}
.calendar__group--fields input,
.calendar__group--fields textarea {
width: 100%;
+23
View File
@@ -50,6 +50,7 @@ import CityMarktSelect from '@/components/citymarkt/CityMarktSelect.vue'
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
import CityMarktOfferCard from '@/components/citymarkt/CityMarktOfferCard.vue'
import { useAccountStore } from '@/stores/account'
import { useEasyShareStore } from '@/stores/easyshare'
import { useMarketplaceStore } from '@/stores/marketplace'
import { useMessageMediaStore } from '@/stores/messageMedia'
import { usePagesStore } from '@/stores/pages'
@@ -115,6 +116,7 @@ const phone = usePhoneStore()
const route = useRoute()
const router = useRouter()
const account = useAccountStore()
const easyShare = useEasyShareStore()
const marketplace = useMarketplaceStore()
const messageMedia = useMessageMediaStore()
const pages = usePagesStore()
@@ -367,6 +369,19 @@ async function shareToLocalPages(): Promise<void> {
)
}
function shareListing(): void {
if (!selectedListing.value) return
easyShare.open({
appId: 'citymarkt',
copyText: `${selectedListing.value.title}\n${selectedListing.value.description}`,
id: selectedListing.value.id,
kind: 'link',
link: `skyphone://citymarkt/listing/${selectedListing.value.id}`,
subtitle: formatPrice(selectedListing.value),
title: selectedListing.value.title,
})
}
async function loadFeed(): Promise<void> {
await marketplace.load({
category: category.value,
@@ -1100,6 +1115,14 @@ onMounted(async () => {
{{ phone.t('Apps.citymarkt.phone') }}:
{{ selectedListing.phone_number }}
</p>
<button
v-if="selectedListing.status === 'active' || selectedListing.status === 'reserved'"
class="citymarkt__pages-share"
type="button"
@click="shareListing"
>
<Share2 :size="17" /><span><strong>{{ phone.t('Apps.easyShare.share') }}</strong></span>
</button>
<template v-if="selectedListing.is_owner">
<button
v-if="
+19
View File
@@ -56,6 +56,7 @@ import {
Plus,
RefreshCw,
Send,
Share2,
Settings2,
Trash2,
UserRound,
@@ -75,6 +76,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useCallsStore } from '@/stores/calls'
import { useCompaniesStore } from '@/stores/companies'
import { useEasyShareStore } from '@/stores/easyshare'
import { useMessageMediaStore } from '@/stores/messageMedia'
import { useMessagesStore } from '@/stores/messages'
import { usePhoneStore } from '@/stores/phone'
@@ -130,6 +132,7 @@ const phone = usePhoneStore()
const calls = useCallsStore()
const messages = useMessagesStore()
const companies = useCompaniesStore()
const easyShare = useEasyShareStore()
const mediaPicker = useMessageMediaStore()
const route = useRoute()
const router = useRouter()
@@ -494,6 +497,19 @@ async function setRoute(company: CompanySummary): Promise<void> {
showToast(phone.t('Apps.companies.routeSet'))
}
function shareCompany(company: Company): void {
easyShare.open({
appId: 'companies',
copyText: `${company.name}\n${company.description}`,
id: company.id,
imageUrl: company.logoUrl,
kind: 'profile',
link: `skyphone://companies/profile/${company.id}`,
subtitle: company.phoneNumber ?? company.categoryName,
title: company.name,
})
}
function openRequestComposer(company: Company): void {
if (!company.acceptsRequests) {
showToast(phone.t('Apps.companies.actionUnavailable.request'))
@@ -1618,6 +1634,9 @@ onBeforeUnmount(() => {
phone.t('Apps.companies.profile.request')
}}
</k-button>
<k-button rounded outline @click="shareCompany(activeCompany)">
<Share2 :size="17" />{{ phone.t('Apps.easyShare.share') }}
</k-button>
</k-glass>
</template>
</section>
+18
View File
@@ -27,6 +27,7 @@ import {
Navigation,
Plane,
Route,
Share2,
Sparkles,
Sailboat,
ShieldAlert,
@@ -37,6 +38,7 @@ import {
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useGarageStore } from '@/stores/garage'
import { useEasyShareStore } from '@/stores/easyshare'
import { usePhoneStore } from '@/stores/phone'
import type {
GarageVehicle,
@@ -50,6 +52,7 @@ type GarageFilter = 'all' | GarageVehicleStatus
const phone = usePhoneStore()
const garage = useGarageStore()
const easyShare = useEasyShareStore()
const activeFilter = ref<GarageFilter>('all')
const query = ref('')
const selectedVehicle = ref<GarageVehicle | null>(null)
@@ -110,6 +113,18 @@ function displayName(vehicle: GarageVehicle): string {
return phone.t('Apps.garage.unknownVehicle')
}
function shareVehicle(vehicle: GarageVehicle): void {
easyShare.open({
appId: 'garage',
copyText: `${displayName(vehicle)}\n${vehicle.plate}`,
id: vehicle.plate,
kind: 'document',
link: `skyphone://garage/vehicle/${vehicle.plate}`,
subtitle: vehicle.plate,
title: displayName(vehicle),
})
}
function modelName(vehicle: GarageVehicle): string {
if (vehicle.name && vehicle.nickname) return vehicle.name
if (typeof vehicle.model === 'string' && vehicle.model) return vehicle.model
@@ -480,6 +495,9 @@ onBeforeUnmount(() => {
<small>{{ phone.t('Apps.garage.vin') }}</small>
<strong>{{ selectedVehicle.vin }}</strong>
</div>
<k-button large rounded outline @click="shareVehicle(selectedVehicle)">
<Share2 :size="18" />{{ phone.t('Apps.easyShare.share') }}
</k-button>
</section>
</k-sheet>
<k-dialog
+24
View File
@@ -26,6 +26,7 @@ import {
LockOpen,
Plus,
Router,
Share2,
UserRound,
UsersRound,
WifiOff,
@@ -34,6 +35,7 @@ import {
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useHousingStore } from '@/stores/housing'
import { useEasyShareStore } from '@/stores/easyshare'
import { usePhoneStore } from '@/stores/phone'
import type {
HousingKey,
@@ -43,6 +45,7 @@ import type {
const phone = usePhoneStore()
const housing = useHousingStore()
const easyShare = useEasyShareStore()
const selectedPropertyId = ref<string | null>(null)
const candidatesOpened = ref(false)
const revokeCandidate = ref<HousingKey | null>(null)
@@ -195,6 +198,18 @@ function accessLabel(property: HousingProperty): string {
)
}
function shareProperty(property: HousingProperty): void {
easyShare.open({
appId: 'house',
copyText: property.name,
id: property.id,
kind: 'document',
link: `skyphone://house/property/${property.id}`,
subtitle: accessLabel(property),
title: property.name,
})
}
onMounted(() => {
void housing.load()
})
@@ -413,6 +428,15 @@ onBeforeUnmount(() => {
<Camera :size="23" />
<span>{{ phone.t('Apps.house.viewCamera') }}</span>
</k-glass>
<k-glass
:highlight="false"
component="button"
type="button"
@click="shareProperty(selectedProperty)"
>
<Share2 :size="23" />
<span>{{ phone.t('Apps.easyShare.share') }}</span>
</k-glass>
</div>
<k-list inset strong class="house-facts">
+17 -1
View File
@@ -11,6 +11,7 @@ import {
Images,
MapPin,
Plus,
Share2,
Store,
Trash2,
UserRound,
@@ -34,6 +35,7 @@ import CityMarktSelect from '@/components/citymarkt/CityMarktSelect.vue'
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
import { useAccountStore } from '@/stores/account'
import { useMessageMediaStore } from '@/stores/messageMedia'
import { useEasyShareStore } from '@/stores/easyshare'
import { usePagesStore } from '@/stores/pages'
import { usePhoneStore } from '@/stores/phone'
import type { PagesCategory, PagesPost } from '@/types/pages'
@@ -54,6 +56,7 @@ type Tab = 'feed' | 'create' | 'profile'
const phone = usePhoneStore()
const account = useAccountStore()
const messageMedia = useMessageMediaStore()
const easyShare = useEasyShareStore()
const pages = usePagesStore()
const route = useRoute()
const router = useRouter()
@@ -263,6 +266,19 @@ function openCityMarktListing(): void {
})
}
function sharePost(): void {
if (!selected.value) return
easyShare.open({
appId: 'local-pages',
copyText: `${selected.value.title}\n${selected.value.body}`,
id: selected.value.id,
kind: 'post',
link: `skyphone://local-pages/post/${selected.value.id}`,
subtitle: `@${selected.value.author_name}`,
title: selected.value.title,
})
}
onMounted(() => {
const selection = messageMedia.consumeMany<MediaContext>('local-pages:compose')
if (selection) {
@@ -390,7 +406,7 @@ onMounted(() => {
<div v-if="selected.images.length" class="pages__gallery" :style="{ background: selected.images[galleryIndex]?.gradient }"><button v-if="selected.images.length > 1" @click="moveGallery(-1)"><ChevronLeft /></button><button v-if="selected.images.length > 1" @click="moveGallery(1)"><ChevronRight /></button><span>{{ galleryIndex + 1 }} / {{ selected.images.length }}</span></div>
<article><div class="pages__author"><span>{{ selected.author_name.charAt(0).toUpperCase() }}</span><div><strong>@{{ selected.author_name }}</strong><small>{{ relativeDate(selected.created_at) }}</small></div><i>{{ label('categories', selected.category) }}</i></div><h1>{{ selected.title }}</h1><p>{{ selected.body }}</p><div class="pages__location"><MapPin :size="17" /><div><small>{{ phone.t('Apps.localPages.location') }}</small><strong>{{ selected.district ? phone.t(`Apps.citymarkt.districts.${selected.district}`) : phone.t('Apps.localPages.allLosSantos') }}</strong></div></div><button v-if="selected.source_type === 'citymarkt'" class="pages__market-link" @click="openCityMarktListing"><Store :size="18" /><span><small>{{ phone.t('Apps.localPages.sharedFrom') }}</small><strong>{{ phone.t('Apps.localPages.openCityMarkt') }}</strong></span><b v-if="selected.citymarkt_price">${{ Number(selected.citymarkt_price).toLocaleString() }}</b></button></article>
</div>
<div class="pages__detail-actions"><button :class="{ active: selected.is_liked }" @click="react('like')"><Heart :size="19" :fill="selected.is_liked ? 'currentColor' : 'none'" />{{ selected.like_count }} {{ phone.t('Apps.localPages.likes') }}</button><button @click="react('save')"><Bookmark :size="19" :fill="selected.is_saved ? 'currentColor' : 'none'" />{{ phone.t('Apps.localPages.save') }}</button></div>
<div class="pages__detail-actions"><button :class="{ active: selected.is_liked }" @click="react('like')"><Heart :size="19" :fill="selected.is_liked ? 'currentColor' : 'none'" />{{ selected.like_count }} {{ phone.t('Apps.localPages.likes') }}</button><button @click="sharePost"><Share2 :size="19" />{{ phone.t('Apps.easyShare.share') }}</button><button @click="react('save')"><Bookmark :size="19" :fill="selected.is_saved ? 'currentColor' : 'none'" />{{ phone.t('Apps.localPages.save') }}</button></div>
</section>
<section v-else class="pages__compose">
+23
View File
@@ -14,6 +14,7 @@ import {
RotateCcw,
Search,
Send,
Share2,
ShieldCheck,
SquarePen,
Trash2,
@@ -26,6 +27,7 @@ import MailMarkdownEditor, {
type MailEditorLabels,
} from '@/components/MailMarkdownEditor.vue'
import { useMailStore } from '@/stores/mail'
import { useEasyShareStore } from '@/stores/easyshare'
import { usePhoneStore } from '@/stores/phone'
import type {
MailComposeDraft,
@@ -75,6 +77,7 @@ const MAIL_SWIPE_COMMIT_ANIMATION_MS = 180
const phone = usePhoneStore()
const mail = useMailStore()
const easyShare = useEasyShareStore()
const authMode = ref<AuthMode>('login')
const authEmail = ref('')
const authPassword = ref('')
@@ -563,6 +566,19 @@ function composeForward(): void {
beginCompose(buildForwardDraft(selectedMessage.value))
}
function shareMessage(): void {
if (!selectedMessage.value) return
easyShare.open({
appId: 'mail',
copyText: `${selectedMessage.value.subject}\n${mailPlainText(selectedMessage.value.body)}`,
id: selectedMessage.value.id,
kind: 'document',
link: `skyphone://mail/message/${selectedMessage.value.id}`,
subtitle: selectedMessage.value.sender,
title: selectedMessage.value.subject || phone.t('Apps.mail.untitled'),
})
}
async function mutateSelected(
endpoint: string,
extra: Record<string, unknown> = {},
@@ -1004,6 +1020,13 @@ onBeforeUnmount(() => {
</article>
<footer class="mail-action-bar">
<button
type="button"
:aria-label="phone.t('Apps.easyShare.share')"
@click="shareMessage"
>
<Share2 :size="20" />
</button>
<button
type="button"
:aria-label="phone.t('Apps.mail.reply')"