mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
ENH - complete and secure EasyShare integration
This commit is contained in:
@@ -239,14 +239,18 @@ EasyShare browser data is available from every app that exposes a share action.
|
|||||||
Gallery directly, share an item, and then use the EasyShare and History actions in the sheet:
|
Gallery directly, share an item, and then use the EasyShare and History actions in the sheet:
|
||||||
|
|
||||||
- Full data, including incoming, transferring, accepted, completed, declined, cancelled, expired,
|
- Full data, including incoming, transferring, accepted, completed, declined, cancelled, expired,
|
||||||
and failed transfers: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-full#/apps/gallery`
|
and failed transfers: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-full#/apps/photos`
|
||||||
- Incoming request only: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-incoming#/apps/gallery`
|
- Incoming request only: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-incoming#/apps/photos`
|
||||||
- Transfer history without active requests: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-history#/apps/gallery`
|
- Transfer history without active requests: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-history#/apps/photos`
|
||||||
- Empty nearby and history states: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-empty#/apps/gallery`
|
- Complete content catalog with contact, document, link, location, note, photo, playlist, post,
|
||||||
|
profile, text, track, and video: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-catalog#/apps/photos`
|
||||||
|
- Empty nearby and history states: `http://localhost:5174/?apiPort=3002&testScenario=easyshare-empty#/apps/photos`
|
||||||
|
|
||||||
In the full scenario, sending to Mia or Jamie creates a pending transfer. Sending to Noah creates a
|
In the full scenario, sending to Mia or Jamie creates a pending transfer. Sending to Noah creates a
|
||||||
transfer at 58 percent so the progress and cancel states can be tested. Visibility changes and
|
transfer at 58 percent so the progress and cancel states can be tested. Visibility changes and
|
||||||
accepting or declining the seeded incoming request are kept in memory until the mock server restarts.
|
accepting or declining the seeded incoming request are kept in memory until the mock server restarts.
|
||||||
|
The catalog also contains source examples from Companies, Mail, Garage, and House; completed history
|
||||||
|
rows and rich chat cards can be clicked to verify app/deep-link navigation.
|
||||||
|
|
||||||
The full-data scenario includes posts, replies, quotes, media grids, profiles, ranked hashtags, network search results, and every notification type. Run `pnpm test`, `pnpm typecheck`, `pnpm lint`, and `pnpm build` before packaging.
|
The full-data scenario includes posts, replies, quotes, media grids, profiles, ranked hashtags, network search results, and every notification type. Run `pnpm test`, `pnpm typecheck`, `pnpm lint`, and `pnpm build` before packaging.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { kButton, kGlass, kLink, kPreloader, kSheet } from 'konsta/vue'
|
import { kButton, kGlass, kLink, kList, kListInput, kListItem, kPreloader, kSheet } from 'konsta/vue'
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
Clock3,
|
Clock3,
|
||||||
@@ -27,6 +27,7 @@ import type {
|
|||||||
EasyShareTransfer,
|
EasyShareTransfer,
|
||||||
EasyShareVisibility,
|
EasyShareVisibility,
|
||||||
} from '@/types/easyshare'
|
} from '@/types/easyshare'
|
||||||
|
import { openEasySharePayload } from '@/utils/easyshare'
|
||||||
|
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const appStore = useAppStoreStore()
|
const appStore = useAppStoreStore()
|
||||||
@@ -44,7 +45,7 @@ let dragPointerId: number | null = null
|
|||||||
let dragStartTime = 0
|
let dragStartTime = 0
|
||||||
let dragStartY = 0
|
let dragStartY = 0
|
||||||
const visibilityOptions: EasyShareVisibility[] = ['everyone', 'contacts', 'hidden']
|
const visibilityOptions: EasyShareVisibility[] = ['everyone', 'contacts', 'hidden']
|
||||||
type EasyShareDestinationAppId = 'darkchat' | 'messages' | 'notes'
|
type EasyShareDestinationAppId = 'darkchat' | 'flare' | 'messages' | 'notes'
|
||||||
const app = computed(() =>
|
const app = computed(() =>
|
||||||
easyShare.payload ? getPhoneApp(easyShare.payload.appId) : undefined,
|
easyShare.payload ? getPhoneApp(easyShare.payload.appId) : undefined,
|
||||||
)
|
)
|
||||||
@@ -57,7 +58,7 @@ const sharePeople = computed(() => {
|
|||||||
}> = []
|
}> = []
|
||||||
const phoneNumbers = new Set<string>()
|
const phoneNumbers = new Set<string>()
|
||||||
|
|
||||||
if (easyShare.payload?.appId === 'flare') {
|
if (!appStore.homeLayout.hidden.includes('flare')) {
|
||||||
for (const match of flare.matches) {
|
for (const match of flare.matches) {
|
||||||
people.push({
|
people.push({
|
||||||
avatar: match.profile.photoUrls[0],
|
avatar: match.profile.photoUrls[0],
|
||||||
@@ -107,6 +108,7 @@ const sharePeople = computed(() => {
|
|||||||
const shareAppIds: EasyShareDestinationAppId[] = [
|
const shareAppIds: EasyShareDestinationAppId[] = [
|
||||||
'messages',
|
'messages',
|
||||||
'darkchat',
|
'darkchat',
|
||||||
|
'flare',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
const shareApps = computed(() =>
|
const shareApps = computed(() =>
|
||||||
@@ -193,14 +195,14 @@ function shareToChat(kind: EasyShareChatApp, targetId: string): void {
|
|||||||
void router.push(`/apps/${kind}`)
|
void router.push(`/apps/${kind}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openChatApp(kind: 'darkchat' | 'messages'): void {
|
function openChatApp(kind: EasyShareChatApp): void {
|
||||||
if (!easyShare.prepareChatDraft(kind)) return
|
if (!easyShare.prepareChatDraft(kind)) return
|
||||||
close()
|
close()
|
||||||
void router.push(`/apps/${kind}`)
|
void router.push(`/apps/${kind}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openShareApp(appId: EasyShareDestinationAppId): void {
|
function openShareApp(appId: EasyShareDestinationAppId): void {
|
||||||
if (appId === 'messages' || appId === 'darkchat') {
|
if (appId === 'messages' || appId === 'darkchat' || appId === 'flare') {
|
||||||
openChatApp(appId)
|
openChatApp(appId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -235,6 +237,11 @@ async function cancelTransfer(transfer: EasyShareTransfer): Promise<void> {
|
|||||||
feedback.value = label('errors.request_failed')
|
feedback.value = label('errors.request_failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openTransfer(transfer: EasyShareTransfer): Promise<void> {
|
||||||
|
close()
|
||||||
|
await openEasySharePayload(router, transfer.payload)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -329,18 +336,19 @@ async function cancelTransfer(transfer: EasyShareTransfer): Promise<void> {
|
|||||||
<strong>{{ label('nearby') }}</strong>
|
<strong>{{ label('nearby') }}</strong>
|
||||||
<k-link component="button" @click="easyShare.showHistory"><History :size="18" /></k-link>
|
<k-link component="button" @click="easyShare.showHistory"><History :size="18" /></k-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="easyshare-visibility">
|
<k-list inset strong class="easyshare-visibility">
|
||||||
<ShieldCheck :size="18" />
|
<k-list-input
|
||||||
<select
|
type="select"
|
||||||
|
:label="label('visibility')"
|
||||||
:value="easyShare.visibility"
|
:value="easyShare.visibility"
|
||||||
:aria-label="label('visibility')"
|
|
||||||
@change="easyShare.setVisibility(($event.target as HTMLSelectElement).value as EasyShareVisibility)"
|
@change="easyShare.setVisibility(($event.target as HTMLSelectElement).value as EasyShareVisibility)"
|
||||||
>
|
>
|
||||||
|
<template #media><ShieldCheck :size="18" /></template>
|
||||||
<option v-for="option in visibilityOptions" :key="option" :value="option">
|
<option v-for="option in visibilityOptions" :key="option" :value="option">
|
||||||
{{ label(`visibilityOptions.${option}`) }}
|
{{ label(`visibilityOptions.${option}`) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</k-list-input>
|
||||||
</div>
|
</k-list>
|
||||||
|
|
||||||
<k-glass v-if="easyShare.incomingTransfer" class="easyshare-incoming">
|
<k-glass v-if="easyShare.incomingTransfer" class="easyshare-incoming">
|
||||||
<UserRound :size="28" />
|
<UserRound :size="28" />
|
||||||
@@ -369,13 +377,19 @@ async function cancelTransfer(transfer: EasyShareTransfer): Promise<void> {
|
|||||||
</k-glass>
|
</k-glass>
|
||||||
|
|
||||||
<div v-if="easyShare.loading" class="easyshare-loading"><k-preloader /></div>
|
<div v-if="easyShare.loading" class="easyshare-loading"><k-preloader /></div>
|
||||||
<div v-else-if="easyShare.targets.length" class="easyshare-targets">
|
<k-list v-else-if="easyShare.targets.length" inset strong class="easyshare-targets">
|
||||||
<button v-for="target in easyShare.targets" :key="target.id" type="button" @click="requestTransfer(target.id)">
|
<k-list-item
|
||||||
<span><UserRound /></span>
|
v-for="target in easyShare.targets"
|
||||||
<div><strong>{{ target.name }}</strong><small>{{ label('distance', { distance: String(target.distance) }) }}</small></div>
|
:key="target.id"
|
||||||
<Share2 :size="18" />
|
link
|
||||||
</button>
|
:title="target.name"
|
||||||
</div>
|
:subtitle="label('distance', { distance: String(target.distance) })"
|
||||||
|
@click="requestTransfer(target.id)"
|
||||||
|
>
|
||||||
|
<template #media><span><UserRound /></span></template>
|
||||||
|
<template #after><Share2 :size="18" /></template>
|
||||||
|
</k-list-item>
|
||||||
|
</k-list>
|
||||||
<p v-else class="easyshare-empty">{{ label('noNearby') }}</p>
|
<p v-else class="easyshare-empty">{{ label('noNearby') }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -385,17 +399,20 @@ async function cancelTransfer(transfer: EasyShareTransfer): Promise<void> {
|
|||||||
<strong>{{ label('history') }}</strong>
|
<strong>{{ label('history') }}</strong>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="easyshare-history">
|
<k-list inset strong class="easyshare-history">
|
||||||
<article v-for="transfer in easyShare.history" :key="transfer.id">
|
<k-list-item
|
||||||
<span><Clock3 /></span>
|
v-for="transfer in easyShare.history"
|
||||||
<div>
|
:key="transfer.id"
|
||||||
<strong>{{ transfer.payload.title }}</strong>
|
link
|
||||||
<small>{{ transfer.otherName }} · {{ statusLabel(transfer) }}</small>
|
:title="transfer.payload.title"
|
||||||
</div>
|
:subtitle="`${transfer.otherName} · ${statusLabel(transfer)}`"
|
||||||
<small>{{ transfer.direction === 'incoming' ? '↓' : '↑' }}</small>
|
@click="openTransfer(transfer)"
|
||||||
</article>
|
>
|
||||||
|
<template #media><span><Clock3 /></span></template>
|
||||||
|
<template #after>{{ transfer.direction === 'incoming' ? '↓' : '↑' }}</template>
|
||||||
|
</k-list-item>
|
||||||
<p v-if="!easyShare.history.length" class="easyshare-empty">{{ label('noHistory') }}</p>
|
<p v-if="!easyShare.history.length" class="easyshare-empty">{{ label('noHistory') }}</p>
|
||||||
</div>
|
</k-list>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<p v-if="feedback" class="easyshare-feedback">{{ feedback }}</p>
|
<p v-if="feedback" class="easyshare-feedback">{{ feedback }}</p>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Image, MapPin, Music2, Play, UserRound } from 'lucide-vue-next'
|
import { Image, MapPin, Music2, Play, UserRound } from 'lucide-vue-next'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { getPhoneApp, getPhoneAppLabel } from '@/config/apps'
|
import { getPhoneApp, getPhoneAppLabel } from '@/config/apps'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
import type { EasySharePayload } from '@/types/easyshare'
|
import type { EasySharePayload } from '@/types/easyshare'
|
||||||
|
import { openEasySharePayload } from '@/utils/easyshare'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -15,6 +17,7 @@ const props = withDefaults(
|
|||||||
{ compact: false, variant: 'messages' },
|
{ compact: false, variant: 'messages' },
|
||||||
)
|
)
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
|
const router = useRouter()
|
||||||
const imageFailed = ref(false)
|
const imageFailed = ref(false)
|
||||||
const sourceApp = computed(() => getPhoneApp(props.payload.appId))
|
const sourceApp = computed(() => getPhoneApp(props.payload.appId))
|
||||||
const isVideo = computed(
|
const isVideo = computed(
|
||||||
@@ -47,6 +50,11 @@ watch(
|
|||||||
`shared-content-card--${variant}`,
|
`shared-content-card--${variant}`,
|
||||||
{ 'shared-content-card--compact': compact },
|
{ 'shared-content-card--compact': compact },
|
||||||
]"
|
]"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="openEasySharePayload(router, payload)"
|
||||||
|
@keydown.enter.prevent="openEasySharePayload(router, payload)"
|
||||||
|
@keydown.space.prevent="openEasySharePayload(router, payload)"
|
||||||
>
|
>
|
||||||
<div class="shared-content-card__source">
|
<div class="shared-content-card__source">
|
||||||
<img v-if="sourceApp?.iconImage" :src="sourceApp.iconImage" alt="" />
|
<img v-if="sourceApp?.iconImage" :src="sourceApp.iconImage" alt="" />
|
||||||
@@ -92,6 +100,12 @@ watch(
|
|||||||
color: #171719;
|
color: #171719;
|
||||||
background: rgb(255 255 255 / 96%);
|
background: rgb(255 255 255 / 96%);
|
||||||
box-shadow: 0 8px 24px rgb(19 45 78 / 13%);
|
box-shadow: 0 8px 24px rgb(19 45 78 / 13%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-content-card:focus-visible {
|
||||||
|
outline: 3px solid #0a84ff;
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-content-card__source {
|
.shared-content-card__source {
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
destinations: 'Share destinations',
|
destinations: 'Share destinations',
|
||||||
newMessage: 'New Message',
|
newMessage: 'New Message',
|
||||||
shareProfile: 'Share Profile',
|
shareProfile: 'Share Profile',
|
||||||
|
share: 'Share',
|
||||||
kinds: {
|
kinds: {
|
||||||
contact: 'Contact',
|
contact: 'Contact',
|
||||||
document: 'Document',
|
document: 'Document',
|
||||||
@@ -404,6 +405,7 @@ const defaultLocales: LocaleTree = {
|
|||||||
transfer_not_found: 'That transfer is no longer available.',
|
transfer_not_found: 'That transfer is no longer available.',
|
||||||
too_far: 'The recipient is too far away.',
|
too_far: 'The recipient is too far away.',
|
||||||
not_owned: 'This item no longer belongs to this phone.',
|
not_owned: 'This item no longer belongs to this phone.',
|
||||||
|
unsupported_payload: 'This item is not available for EasyShare.',
|
||||||
payload_too_large: 'This item is too large to share.',
|
payload_too_large: 'This item is too large to share.',
|
||||||
location_unavailable: 'Your location is unavailable.',
|
location_unavailable: 'Your location is unavailable.',
|
||||||
rate_limited: 'Too many share requests. Try again shortly.',
|
rate_limited: 'Too many share requests. Try again shortly.',
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import type { EasySharePayload } from '@/types/easyshare'
|
||||||
|
import { easyShareRoute } from '@/utils/easyshare'
|
||||||
|
|
||||||
|
function payload(overrides: Partial<EasySharePayload>): EasySharePayload {
|
||||||
|
return {
|
||||||
|
appId: 'notes',
|
||||||
|
copyText: 'Shared content',
|
||||||
|
kind: 'note',
|
||||||
|
title: 'Shared content',
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EasyShare deep links', () => {
|
||||||
|
it.each([
|
||||||
|
['skyphone://media/42', '/apps/photos'],
|
||||||
|
['skyphone://location/current', '/apps/map'],
|
||||||
|
['skyphone://phone/5550142', '/apps/phone'],
|
||||||
|
['skyphone://music/server/night-drive', '/apps/music'],
|
||||||
|
['skyphone://citymarkt/listing/listing-1', '/apps/citymarkt'],
|
||||||
|
])('routes %s into its source app', (link, path) => {
|
||||||
|
expect(easyShareRoute(payload({ link })).path).toBe(path)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the payload app and preserves share context', () => {
|
||||||
|
expect(
|
||||||
|
easyShareRoute(payload({ appId: 'calendar', id: 'event-1' })),
|
||||||
|
).toEqual({
|
||||||
|
path: '/apps/calendar',
|
||||||
|
query: {
|
||||||
|
easyShareId: 'event-1',
|
||||||
|
easyShareKind: 'note',
|
||||||
|
easyShareLink: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens CityMarkt links on the referenced listing', () => {
|
||||||
|
expect(
|
||||||
|
easyShareRoute(
|
||||||
|
payload({
|
||||||
|
appId: 'citymarkt',
|
||||||
|
id: 'listing-1',
|
||||||
|
kind: 'link',
|
||||||
|
link: 'skyphone://citymarkt/listing/listing-1',
|
||||||
|
}),
|
||||||
|
).query.listingId,
|
||||||
|
).toBe('listing-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Router } from 'vue-router'
|
||||||
|
|
||||||
|
import { getPhoneApp } from '@/config/apps'
|
||||||
|
import type { EasySharePayload } from '@/types/easyshare'
|
||||||
|
|
||||||
|
const schemeRoutes: Record<string, string> = {
|
||||||
|
location: '/apps/map',
|
||||||
|
media: '/apps/photos',
|
||||||
|
phone: '/apps/phone',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function easyShareRoute(payload: EasySharePayload): {
|
||||||
|
path: string
|
||||||
|
query: Record<string, string>
|
||||||
|
} {
|
||||||
|
const match = payload.link?.match(/^skyphone:\/\/([^/]+)(?:\/([^/]+))?(?:\/(.+))?$/)
|
||||||
|
const schemeApp = match?.[1]
|
||||||
|
const app = schemeApp ? getPhoneApp(schemeApp) : getPhoneApp(payload.appId)
|
||||||
|
const path =
|
||||||
|
(schemeApp && schemeRoutes[schemeApp]) || app?.route || '/'
|
||||||
|
const query: Record<string, string> = {
|
||||||
|
easyShareId: String(payload.id ?? match?.[3] ?? match?.[2] ?? ''),
|
||||||
|
easyShareKind: payload.kind,
|
||||||
|
easyShareLink: payload.link ?? '',
|
||||||
|
}
|
||||||
|
if (schemeApp === 'citymarkt' && match?.[2] === 'listing' && match[3]) {
|
||||||
|
query.listingId = decodeURIComponent(match[3])
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openEasySharePayload(
|
||||||
|
router: Router,
|
||||||
|
payload: EasySharePayload,
|
||||||
|
): Promise<void> {
|
||||||
|
await router.push(easyShareRoute(payload))
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Rows3,
|
Rows3,
|
||||||
Search,
|
Search,
|
||||||
|
Share2,
|
||||||
Trash2,
|
Trash2,
|
||||||
UserRound,
|
UserRound,
|
||||||
X,
|
X,
|
||||||
@@ -22,6 +23,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|||||||
|
|
||||||
import { useAccountStore } from '@/stores/account'
|
import { useAccountStore } from '@/stores/account'
|
||||||
import { useCalendarStore } from '@/stores/calendar'
|
import { useCalendarStore } from '@/stores/calendar'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
import type { CalendarEvent, CalendarEventDraft } from '@/types/calendar'
|
import type { CalendarEvent, CalendarEventDraft } from '@/types/calendar'
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ type YearOverview = {
|
|||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const account = useAccountStore()
|
const account = useAccountStore()
|
||||||
const calendar = useCalendarStore()
|
const calendar = useCalendarStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const visibleYear = ref(today.getFullYear())
|
const visibleYear = ref(today.getFullYear())
|
||||||
const selectedDate = ref(dateKey(today))
|
const selectedDate = ref(dateKey(today))
|
||||||
@@ -358,6 +361,19 @@ function openEdit(): void {
|
|||||||
screen.value = 'form'
|
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 {
|
function closeForm(): void {
|
||||||
timePickerField.value = null
|
timePickerField.value = null
|
||||||
screen.value = editingEvent.value ? 'detail' : 'main'
|
screen.value = editingEvent.value ? 'detail' : 'main'
|
||||||
@@ -781,6 +797,10 @@ onMounted(async () => {
|
|||||||
<p>{{ selectedEvent.note }}</p>
|
<p>{{ selectedEvent.note }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<button class="calendar__delete" type="button" @click="deleteEvent">
|
||||||
<Trash2 :size="18" />
|
<Trash2 :size="18" />
|
||||||
{{ phone.t('Apps.calendar.deleteEvent') }}
|
{{ phone.t('Apps.calendar.deleteEvent') }}
|
||||||
@@ -1739,6 +1759,10 @@ onMounted(async () => {
|
|||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar__share {
|
||||||
|
color: #0a84ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar__group--fields input,
|
.calendar__group--fields input,
|
||||||
.calendar__group--fields textarea {
|
.calendar__group--fields textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import CityMarktSelect from '@/components/citymarkt/CityMarktSelect.vue'
|
|||||||
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
|
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
|
||||||
import CityMarktOfferCard from '@/components/citymarkt/CityMarktOfferCard.vue'
|
import CityMarktOfferCard from '@/components/citymarkt/CityMarktOfferCard.vue'
|
||||||
import { useAccountStore } from '@/stores/account'
|
import { useAccountStore } from '@/stores/account'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { useMarketplaceStore } from '@/stores/marketplace'
|
import { useMarketplaceStore } from '@/stores/marketplace'
|
||||||
import { useMessageMediaStore } from '@/stores/messageMedia'
|
import { useMessageMediaStore } from '@/stores/messageMedia'
|
||||||
import { usePagesStore } from '@/stores/pages'
|
import { usePagesStore } from '@/stores/pages'
|
||||||
@@ -115,6 +116,7 @@ const phone = usePhoneStore()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const account = useAccountStore()
|
const account = useAccountStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const marketplace = useMarketplaceStore()
|
const marketplace = useMarketplaceStore()
|
||||||
const messageMedia = useMessageMediaStore()
|
const messageMedia = useMessageMediaStore()
|
||||||
const pages = usePagesStore()
|
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> {
|
async function loadFeed(): Promise<void> {
|
||||||
await marketplace.load({
|
await marketplace.load({
|
||||||
category: category.value,
|
category: category.value,
|
||||||
@@ -1100,6 +1115,14 @@ onMounted(async () => {
|
|||||||
{{ phone.t('Apps.citymarkt.phone') }}:
|
{{ phone.t('Apps.citymarkt.phone') }}:
|
||||||
{{ selectedListing.phone_number }}
|
{{ selectedListing.phone_number }}
|
||||||
</p>
|
</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">
|
<template v-if="selectedListing.is_owner">
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Send,
|
Send,
|
||||||
|
Share2,
|
||||||
Settings2,
|
Settings2,
|
||||||
Trash2,
|
Trash2,
|
||||||
UserRound,
|
UserRound,
|
||||||
@@ -75,6 +76,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { useCallsStore } from '@/stores/calls'
|
import { useCallsStore } from '@/stores/calls'
|
||||||
import { useCompaniesStore } from '@/stores/companies'
|
import { useCompaniesStore } from '@/stores/companies'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { useMessageMediaStore } from '@/stores/messageMedia'
|
import { useMessageMediaStore } from '@/stores/messageMedia'
|
||||||
import { useMessagesStore } from '@/stores/messages'
|
import { useMessagesStore } from '@/stores/messages'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
@@ -130,6 +132,7 @@ const phone = usePhoneStore()
|
|||||||
const calls = useCallsStore()
|
const calls = useCallsStore()
|
||||||
const messages = useMessagesStore()
|
const messages = useMessagesStore()
|
||||||
const companies = useCompaniesStore()
|
const companies = useCompaniesStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const mediaPicker = useMessageMediaStore()
|
const mediaPicker = useMessageMediaStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -494,6 +497,19 @@ async function setRoute(company: CompanySummary): Promise<void> {
|
|||||||
showToast(phone.t('Apps.companies.routeSet'))
|
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 {
|
function openRequestComposer(company: Company): void {
|
||||||
if (!company.acceptsRequests) {
|
if (!company.acceptsRequests) {
|
||||||
showToast(phone.t('Apps.companies.actionUnavailable.request'))
|
showToast(phone.t('Apps.companies.actionUnavailable.request'))
|
||||||
@@ -1618,6 +1634,9 @@ onBeforeUnmount(() => {
|
|||||||
phone.t('Apps.companies.profile.request')
|
phone.t('Apps.companies.profile.request')
|
||||||
}}
|
}}
|
||||||
</k-button>
|
</k-button>
|
||||||
|
<k-button rounded outline @click="shareCompany(activeCompany)">
|
||||||
|
<Share2 :size="17" />{{ phone.t('Apps.easyShare.share') }}
|
||||||
|
</k-button>
|
||||||
</k-glass>
|
</k-glass>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
Navigation,
|
Navigation,
|
||||||
Plane,
|
Plane,
|
||||||
Route,
|
Route,
|
||||||
|
Share2,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Sailboat,
|
Sailboat,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useGarageStore } from '@/stores/garage'
|
import { useGarageStore } from '@/stores/garage'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
import type {
|
import type {
|
||||||
GarageVehicle,
|
GarageVehicle,
|
||||||
@@ -50,6 +52,7 @@ type GarageFilter = 'all' | GarageVehicleStatus
|
|||||||
|
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const garage = useGarageStore()
|
const garage = useGarageStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const activeFilter = ref<GarageFilter>('all')
|
const activeFilter = ref<GarageFilter>('all')
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const selectedVehicle = ref<GarageVehicle | null>(null)
|
const selectedVehicle = ref<GarageVehicle | null>(null)
|
||||||
@@ -110,6 +113,18 @@ function displayName(vehicle: GarageVehicle): string {
|
|||||||
return phone.t('Apps.garage.unknownVehicle')
|
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 {
|
function modelName(vehicle: GarageVehicle): string {
|
||||||
if (vehicle.name && vehicle.nickname) return vehicle.name
|
if (vehicle.name && vehicle.nickname) return vehicle.name
|
||||||
if (typeof vehicle.model === 'string' && vehicle.model) return vehicle.model
|
if (typeof vehicle.model === 'string' && vehicle.model) return vehicle.model
|
||||||
@@ -480,6 +495,9 @@ onBeforeUnmount(() => {
|
|||||||
<small>{{ phone.t('Apps.garage.vin') }}</small>
|
<small>{{ phone.t('Apps.garage.vin') }}</small>
|
||||||
<strong>{{ selectedVehicle.vin }}</strong>
|
<strong>{{ selectedVehicle.vin }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<k-button large rounded outline @click="shareVehicle(selectedVehicle)">
|
||||||
|
<Share2 :size="18" />{{ phone.t('Apps.easyShare.share') }}
|
||||||
|
</k-button>
|
||||||
</section>
|
</section>
|
||||||
</k-sheet>
|
</k-sheet>
|
||||||
<k-dialog
|
<k-dialog
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
LockOpen,
|
LockOpen,
|
||||||
Plus,
|
Plus,
|
||||||
Router,
|
Router,
|
||||||
|
Share2,
|
||||||
UserRound,
|
UserRound,
|
||||||
UsersRound,
|
UsersRound,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useHousingStore } from '@/stores/housing'
|
import { useHousingStore } from '@/stores/housing'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
import type {
|
import type {
|
||||||
HousingKey,
|
HousingKey,
|
||||||
@@ -43,6 +45,7 @@ import type {
|
|||||||
|
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const housing = useHousingStore()
|
const housing = useHousingStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const selectedPropertyId = ref<string | null>(null)
|
const selectedPropertyId = ref<string | null>(null)
|
||||||
const candidatesOpened = ref(false)
|
const candidatesOpened = ref(false)
|
||||||
const revokeCandidate = ref<HousingKey | null>(null)
|
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(() => {
|
onMounted(() => {
|
||||||
void housing.load()
|
void housing.load()
|
||||||
})
|
})
|
||||||
@@ -413,6 +428,15 @@ onBeforeUnmount(() => {
|
|||||||
<Camera :size="23" />
|
<Camera :size="23" />
|
||||||
<span>{{ phone.t('Apps.house.viewCamera') }}</span>
|
<span>{{ phone.t('Apps.house.viewCamera') }}</span>
|
||||||
</k-glass>
|
</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>
|
</div>
|
||||||
|
|
||||||
<k-list inset strong class="house-facts">
|
<k-list inset strong class="house-facts">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Images,
|
Images,
|
||||||
MapPin,
|
MapPin,
|
||||||
Plus,
|
Plus,
|
||||||
|
Share2,
|
||||||
Store,
|
Store,
|
||||||
Trash2,
|
Trash2,
|
||||||
UserRound,
|
UserRound,
|
||||||
@@ -34,6 +35,7 @@ import CityMarktSelect from '@/components/citymarkt/CityMarktSelect.vue'
|
|||||||
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
|
import CityMarktGallery from '@/components/citymarkt/CityMarktGallery.vue'
|
||||||
import { useAccountStore } from '@/stores/account'
|
import { useAccountStore } from '@/stores/account'
|
||||||
import { useMessageMediaStore } from '@/stores/messageMedia'
|
import { useMessageMediaStore } from '@/stores/messageMedia'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { usePagesStore } from '@/stores/pages'
|
import { usePagesStore } from '@/stores/pages'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
import type { PagesCategory, PagesPost } from '@/types/pages'
|
import type { PagesCategory, PagesPost } from '@/types/pages'
|
||||||
@@ -54,6 +56,7 @@ type Tab = 'feed' | 'create' | 'profile'
|
|||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const account = useAccountStore()
|
const account = useAccountStore()
|
||||||
const messageMedia = useMessageMediaStore()
|
const messageMedia = useMessageMediaStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const pages = usePagesStore()
|
const pages = usePagesStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
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(() => {
|
onMounted(() => {
|
||||||
const selection = messageMedia.consumeMany<MediaContext>('local-pages:compose')
|
const selection = messageMedia.consumeMany<MediaContext>('local-pages:compose')
|
||||||
if (selection) {
|
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>
|
<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>
|
<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>
|
||||||
<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>
|
||||||
|
|
||||||
<section v-else class="pages__compose">
|
<section v-else class="pages__compose">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
RotateCcw,
|
RotateCcw,
|
||||||
Search,
|
Search,
|
||||||
Send,
|
Send,
|
||||||
|
Share2,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -26,6 +27,7 @@ import MailMarkdownEditor, {
|
|||||||
type MailEditorLabels,
|
type MailEditorLabels,
|
||||||
} from '@/components/MailMarkdownEditor.vue'
|
} from '@/components/MailMarkdownEditor.vue'
|
||||||
import { useMailStore } from '@/stores/mail'
|
import { useMailStore } from '@/stores/mail'
|
||||||
|
import { useEasyShareStore } from '@/stores/easyshare'
|
||||||
import { usePhoneStore } from '@/stores/phone'
|
import { usePhoneStore } from '@/stores/phone'
|
||||||
import type {
|
import type {
|
||||||
MailComposeDraft,
|
MailComposeDraft,
|
||||||
@@ -75,6 +77,7 @@ const MAIL_SWIPE_COMMIT_ANIMATION_MS = 180
|
|||||||
|
|
||||||
const phone = usePhoneStore()
|
const phone = usePhoneStore()
|
||||||
const mail = useMailStore()
|
const mail = useMailStore()
|
||||||
|
const easyShare = useEasyShareStore()
|
||||||
const authMode = ref<AuthMode>('login')
|
const authMode = ref<AuthMode>('login')
|
||||||
const authEmail = ref('')
|
const authEmail = ref('')
|
||||||
const authPassword = ref('')
|
const authPassword = ref('')
|
||||||
@@ -563,6 +566,19 @@ function composeForward(): void {
|
|||||||
beginCompose(buildForwardDraft(selectedMessage.value))
|
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(
|
async function mutateSelected(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
extra: Record<string, unknown> = {},
|
extra: Record<string, unknown> = {},
|
||||||
@@ -1004,6 +1020,13 @@ onBeforeUnmount(() => {
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<footer class="mail-action-bar">
|
<footer class="mail-action-bar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-label="phone.t('Apps.easyShare.share')"
|
||||||
|
@click="shareMessage"
|
||||||
|
>
|
||||||
|
<Share2 :size="20" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="phone.t('Apps.mail.reply')"
|
:aria-label="phone.t('Apps.mail.reply')"
|
||||||
|
|||||||
@@ -3711,7 +3711,7 @@ const easyShareHistory = [
|
|||||||
id: 'easyshare-outgoing-transferring',
|
id: 'easyshare-outgoing-transferring',
|
||||||
otherName: 'Noah Walker',
|
otherName: 'Noah Walker',
|
||||||
payload: {
|
payload: {
|
||||||
appId: 'gallery',
|
appId: 'photos',
|
||||||
copyText: 'Sunset over Los Santos.',
|
copyText: 'Sunset over Los Santos.',
|
||||||
id: 3,
|
id: 3,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
@@ -3801,7 +3801,7 @@ const easyShareHistory = [
|
|||||||
id: 'easyshare-failed',
|
id: 'easyshare-failed',
|
||||||
otherName: 'Noah Walker',
|
otherName: 'Noah Walker',
|
||||||
payload: {
|
payload: {
|
||||||
appId: 'gallery',
|
appId: 'photos',
|
||||||
copyText: 'Vehicle walkaround video.',
|
copyText: 'Vehicle walkaround video.',
|
||||||
id: 7,
|
id: 7,
|
||||||
kind: 'video',
|
kind: 'video',
|
||||||
@@ -3811,6 +3811,158 @@ const easyShareHistory = [
|
|||||||
status: 'failed',
|
status: 'failed',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
const easyShareCatalog = [
|
||||||
|
{
|
||||||
|
appId: 'phone',
|
||||||
|
copyText: 'Mia Santos\n5550142',
|
||||||
|
id: 'contact-mia-santos',
|
||||||
|
kind: 'contact',
|
||||||
|
link: 'skyphone://phone/5550142',
|
||||||
|
subtitle: '5550142',
|
||||||
|
title: 'Mia Santos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'calendar',
|
||||||
|
copyText: 'Downtown meetup\nBring the project notes.',
|
||||||
|
id: 'calendar-event-easyshare',
|
||||||
|
kind: 'document',
|
||||||
|
link: 'skyphone://calendar/event/calendar-event-easyshare',
|
||||||
|
subtitle: 'Tonight, 20:30',
|
||||||
|
title: 'Downtown meetup',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'citymarkt',
|
||||||
|
copyText: 'Comet Retro Custom in excellent condition.',
|
||||||
|
id: 'listing-easyshare-comet',
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1503736334956-4c8f8e92946d?w=900',
|
||||||
|
kind: 'link',
|
||||||
|
link: 'skyphone://citymarkt/listing/listing-easyshare-comet',
|
||||||
|
subtitle: '$84,000',
|
||||||
|
title: 'Comet Retro Custom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'map',
|
||||||
|
copyText: 'Legion Square meeting point',
|
||||||
|
kind: 'location',
|
||||||
|
link: 'skyphone://location/current',
|
||||||
|
title: 'Legion Square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'notes',
|
||||||
|
copyText: 'Check fuel, tires and radio before departure.',
|
||||||
|
id: 'note-easyshare-checklist',
|
||||||
|
kind: 'note',
|
||||||
|
title: 'Departure checklist',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'photos',
|
||||||
|
copyText: 'Sunset over Los Santos.',
|
||||||
|
id: 3,
|
||||||
|
imageUrl: 'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=900',
|
||||||
|
kind: 'photo',
|
||||||
|
link: 'skyphone://media/3',
|
||||||
|
title: 'Los Santos sunset',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'music',
|
||||||
|
copyText: 'Night Drive collection · 8 tracks',
|
||||||
|
id: 'playlist-easyshare-night-drive',
|
||||||
|
kind: 'playlist',
|
||||||
|
link: 'skyphone://music/playlist/playlist-easyshare-night-drive',
|
||||||
|
subtitle: '8 tracks',
|
||||||
|
title: 'Night Drive collection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'local-pages',
|
||||||
|
copyText: 'Road closure\nAlta Street is closed until midnight.',
|
||||||
|
id: 'pages-post-easyshare-road-closure',
|
||||||
|
kind: 'post',
|
||||||
|
link: 'skyphone://local-pages/post/pages-post-easyshare-road-closure',
|
||||||
|
subtitle: '@nightshiftls',
|
||||||
|
title: 'Road closure',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'picstagram',
|
||||||
|
copyText: '@mia.santos',
|
||||||
|
id: 'picstagram-profile-easyshare-mia',
|
||||||
|
imageUrl: 'https://i.pravatar.cc/320?img=47',
|
||||||
|
kind: 'profile',
|
||||||
|
link: 'skyphone://picstagram/profile/picstagram-profile-easyshare-mia',
|
||||||
|
subtitle: '@mia.santos',
|
||||||
|
title: 'Mia Santos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'darkchat',
|
||||||
|
copyText: 'Use the north entrance. The south gate is locked.',
|
||||||
|
id: 'darkchat-message-easyshare-entrance',
|
||||||
|
kind: 'text',
|
||||||
|
subtitle: 'NightOwl',
|
||||||
|
title: 'Use the north entrance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'music',
|
||||||
|
copyText: 'Night Drive — Neon Coast',
|
||||||
|
id: 'night-drive',
|
||||||
|
imageUrl: 'https://picsum.photos/seed/easyshare-night-drive/720/720',
|
||||||
|
kind: 'track',
|
||||||
|
link: 'skyphone://music/server/night-drive',
|
||||||
|
subtitle: 'Neon Coast',
|
||||||
|
title: 'Night Drive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'photos',
|
||||||
|
copyText: 'Vehicle walkaround video.',
|
||||||
|
id: 7,
|
||||||
|
imageUrl: 'https://videos.pexels.com/video-files/3130284/3130284-hd_1920_1080_30fps.mp4',
|
||||||
|
kind: 'video',
|
||||||
|
link: 'skyphone://media/7',
|
||||||
|
title: 'Vehicle walkaround',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'companies',
|
||||||
|
copyText: 'Los Santos Customs\nRepairs, tuning and roadside support.',
|
||||||
|
id: 'mechanic',
|
||||||
|
kind: 'profile',
|
||||||
|
link: 'skyphone://companies/profile/mechanic',
|
||||||
|
subtitle: '555-MECH',
|
||||||
|
title: 'Los Santos Customs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'mail',
|
||||||
|
copyText: 'Project handoff\nThe final checklist is attached below.',
|
||||||
|
id: 17,
|
||||||
|
kind: 'document',
|
||||||
|
link: 'skyphone://mail/message/17',
|
||||||
|
subtitle: 'mia@ifruit.com',
|
||||||
|
title: 'Project handoff',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'garage',
|
||||||
|
copyText: 'Comet Retro Custom\nSKY 2048',
|
||||||
|
id: 'SKY 2048',
|
||||||
|
kind: 'document',
|
||||||
|
link: 'skyphone://garage/vehicle/SKY%202048',
|
||||||
|
subtitle: 'SKY 2048',
|
||||||
|
title: 'Comet Retro Custom',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 'house',
|
||||||
|
copyText: 'Vespucci Canals Apartment',
|
||||||
|
id: 'vespucci-apartment-4',
|
||||||
|
kind: 'document',
|
||||||
|
link: 'skyphone://house/property/vespucci-apartment-4',
|
||||||
|
subtitle: 'Owner',
|
||||||
|
title: 'Vespucci Canals Apartment',
|
||||||
|
},
|
||||||
|
].map((payload, index) => ({
|
||||||
|
createdAt: Date.now() - (index + 1) * 5 * 60 * 1000,
|
||||||
|
direction: index % 2 === 0 ? 'incoming' : 'outgoing',
|
||||||
|
id: `easyshare-catalog-${payload.kind}`,
|
||||||
|
otherName: index % 2 === 0 ? 'Mia Santos' : 'Noah Walker',
|
||||||
|
payload,
|
||||||
|
progress: 100,
|
||||||
|
status: 'completed',
|
||||||
|
}))
|
||||||
let easyShareVisibility = 'everyone'
|
let easyShareVisibility = 'everyone'
|
||||||
|
|
||||||
function easyShareHistoryForScenario(testScenario) {
|
function easyShareHistoryForScenario(testScenario) {
|
||||||
@@ -3825,6 +3977,10 @@ function easyShareHistoryForScenario(testScenario) {
|
|||||||
(transfer) => !['pending', 'transferring'].includes(transfer.status),
|
(transfer) => !['pending', 'transferring'].includes(transfer.status),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (testScenario === 'easyshare-catalog') return easyShareCatalog
|
||||||
|
if (testScenario === 'easyshare-full') {
|
||||||
|
return [...easyShareHistory, ...easyShareCatalog]
|
||||||
|
}
|
||||||
return easyShareHistory
|
return easyShareHistory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,9 @@ Config.EasyShare = {
|
|||||||
PendingSeconds = 30,
|
PendingSeconds = 30,
|
||||||
TransferDurationMs = 3000,
|
TransferDurationMs = 3000,
|
||||||
RequestsPerMinute = 12,
|
RequestsPerMinute = 12,
|
||||||
|
BootstrapRequestsPerMinute = 30,
|
||||||
|
VisibilityUpdatesPerMinute = 10,
|
||||||
|
ActionsPerMinute = 30,
|
||||||
PayloadMaxBytes = 24000,
|
PayloadMaxBytes = 24000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,19 @@ local valid_kinds = {
|
|||||||
video = true,
|
video = true,
|
||||||
}
|
}
|
||||||
local valid_apps = {
|
local valid_apps = {
|
||||||
|
calendar = true,
|
||||||
camera = true,
|
camera = true,
|
||||||
|
citymarkt = true,
|
||||||
|
companies = true,
|
||||||
crewlink = true,
|
crewlink = true,
|
||||||
darkchat = true,
|
darkchat = true,
|
||||||
feather = true,
|
feather = true,
|
||||||
flare = true,
|
flare = true,
|
||||||
fliptok = true,
|
fliptok = true,
|
||||||
|
garage = true,
|
||||||
|
house = true,
|
||||||
|
["local-pages"] = true,
|
||||||
|
mail = true,
|
||||||
map = true,
|
map = true,
|
||||||
messages = true,
|
messages = true,
|
||||||
music = true,
|
music = true,
|
||||||
@@ -33,6 +40,12 @@ local valid_apps = {
|
|||||||
}
|
}
|
||||||
local valid_visibilities = { contacts = true, everyone = true, hidden = true }
|
local valid_visibilities = { contacts = true, everyone = true, hidden = true }
|
||||||
|
|
||||||
|
Bridge.Database.Query([[
|
||||||
|
UPDATE `sky_phone_easyshare_transfers`
|
||||||
|
SET `status` = 'expired', `updated_at` = CURRENT_TIMESTAMP, `completed_at` = CURRENT_TIMESTAMP
|
||||||
|
WHERE `status` IN ('pending', 'transferring')
|
||||||
|
]], {})
|
||||||
|
|
||||||
local function uuid()
|
local function uuid()
|
||||||
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
|
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
|
||||||
local id = rows[1] and rows[1].id
|
local id = rows[1] and rows[1].id
|
||||||
@@ -79,6 +92,370 @@ local function append_params(target, values)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function first_row(query, params)
|
||||||
|
local rows = Bridge.Database.Query(query, params)
|
||||||
|
return rows[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canonical_profile(device, app_id, id)
|
||||||
|
if app_id == "picstagram" then
|
||||||
|
local profile = first_row([[
|
||||||
|
SELECT p.`display_name`, p.`handle`, p.`bio`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_picstagram_profiles` p
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = p.`avatar_media_id`
|
||||||
|
WHERE p.`id` = ? AND p.`status` = 'active'
|
||||||
|
AND (p.`private` = 0 OR EXISTS(
|
||||||
|
SELECT 1 FROM `sky_phone_picstagram_sessions` session
|
||||||
|
WHERE session.`device_imei` = ? AND session.`profile_id` = p.`id`
|
||||||
|
) OR EXISTS(
|
||||||
|
SELECT 1 FROM `sky_phone_picstagram_follows` follow
|
||||||
|
WHERE follow.`following_id` = p.`id` AND follow.`status` = 'accepted'
|
||||||
|
AND follow.`follower_id` = (
|
||||||
|
SELECT session.`profile_id` FROM `sky_phone_picstagram_sessions` session
|
||||||
|
WHERE session.`device_imei` = ? LIMIT 1
|
||||||
|
)
|
||||||
|
)) LIMIT 1
|
||||||
|
]], { id, device.imei, device.imei })
|
||||||
|
if profile then
|
||||||
|
return {
|
||||||
|
title = profile.display_name,
|
||||||
|
subtitle = "@" .. profile.handle,
|
||||||
|
copyText = "@" .. profile.handle,
|
||||||
|
imageUrl = profile.image_url,
|
||||||
|
link = "skyphone://picstagram/profile/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "feather" then
|
||||||
|
local profile = first_row([[
|
||||||
|
SELECT p.`display_name`, p.`handle`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_feather_profiles` p
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = p.`avatar_media_id`
|
||||||
|
WHERE p.`id` = ? LIMIT 1
|
||||||
|
]], { id })
|
||||||
|
if profile then
|
||||||
|
return {
|
||||||
|
title = profile.display_name,
|
||||||
|
subtitle = "@" .. profile.handle,
|
||||||
|
copyText = "@" .. profile.handle,
|
||||||
|
imageUrl = profile.image_url,
|
||||||
|
link = "skyphone://feather/profile/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "fliptok" then
|
||||||
|
local profile = first_row("SELECT `display_name`, `handle` FROM `sky_phone_fliptok_profiles` WHERE `id` = ? LIMIT 1", { id })
|
||||||
|
if profile then
|
||||||
|
return {
|
||||||
|
title = profile.display_name,
|
||||||
|
subtitle = "@" .. profile.handle,
|
||||||
|
copyText = "@" .. profile.handle,
|
||||||
|
link = "skyphone://fliptok/profile/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "flare" and device.account_id then
|
||||||
|
local profile = first_row([[
|
||||||
|
SELECT p.`name`, p.`age`, p.`bio`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_flare_profiles` p
|
||||||
|
LEFT JOIN `sky_phone_flare_profile_photos` photo ON photo.`profile_id` = p.`id` AND photo.`sort_order` = 1
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = photo.`media_id`
|
||||||
|
WHERE p.`id` = ? AND p.`account_id` = ? LIMIT 1
|
||||||
|
]], { id, tonumber(device.account_id) })
|
||||||
|
if profile then
|
||||||
|
local title = ("%s, %s"):format(profile.name, profile.age)
|
||||||
|
return {
|
||||||
|
title = title,
|
||||||
|
copyText = title .. "\n" .. profile.bio,
|
||||||
|
imageUrl = profile.image_url,
|
||||||
|
link = "skyphone://flare/profile/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "crewlink" and device.account_id then
|
||||||
|
local profile = first_row([[
|
||||||
|
SELECT p.`username`, g.`name` AS `group_name`
|
||||||
|
FROM `sky_phone_crewlink_profiles` p
|
||||||
|
LEFT JOIN `sky_phone_crewlink_groups` g ON g.`id` = p.`active_group_id`
|
||||||
|
WHERE p.`id` = ? AND p.`account_id` = ? LIMIT 1
|
||||||
|
]], { id, tonumber(device.account_id) })
|
||||||
|
if profile then
|
||||||
|
return {
|
||||||
|
title = "@" .. profile.username,
|
||||||
|
subtitle = profile.group_name,
|
||||||
|
copyText = "@" .. profile.username,
|
||||||
|
link = "skyphone://crewlink/profile/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "darkchat" and device.account_id then
|
||||||
|
local profile = first_row([[
|
||||||
|
SELECT `alias`, `dark_id`, `invite_code` FROM `sky_phone_darkchat_profiles`
|
||||||
|
WHERE `id` = ? AND `account_id` = ? LIMIT 1
|
||||||
|
]], { id, tonumber(device.account_id) })
|
||||||
|
if profile then
|
||||||
|
return {
|
||||||
|
title = profile.alias,
|
||||||
|
subtitle = profile.dark_id,
|
||||||
|
copyText = profile.alias .. "\n" .. profile.dark_id .. "\n" .. profile.invite_code,
|
||||||
|
link = "skyphone://darkchat/invite/" .. profile.invite_code,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "companies" then
|
||||||
|
local definition = Config.Companies.Enabled and Config.Companies.Definitions[id] or nil
|
||||||
|
if definition and definition.Public then
|
||||||
|
local profile = first_row([[
|
||||||
|
SELECT profile.`description`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_company_profiles` profile
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = profile.`logo_media_id`
|
||||||
|
WHERE profile.`company_id` = ? LIMIT 1
|
||||||
|
]], { id })
|
||||||
|
return {
|
||||||
|
title = definition.Name,
|
||||||
|
subtitle = definition.ServiceLine.Number,
|
||||||
|
copyText = definition.Name .. "\n" .. (profile and profile.description or definition.Description),
|
||||||
|
imageUrl = profile and profile.image_url or nil,
|
||||||
|
link = "skyphone://companies/profile/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canonical_post(device, app_id, id)
|
||||||
|
if app_id == "picstagram" then
|
||||||
|
local post = first_row([[
|
||||||
|
SELECT post.`caption`, profile.`display_name`, profile.`handle`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_picstagram_posts` post
|
||||||
|
JOIN `sky_phone_picstagram_profiles` profile ON profile.`id` = post.`profile_id`
|
||||||
|
LEFT JOIN `sky_phone_picstagram_post_media` post_media
|
||||||
|
ON post_media.`post_id` = post.`id` AND post_media.`position` = 1
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = post_media.`media_id`
|
||||||
|
WHERE post.`id` = ? AND post.`status` = 'published' AND profile.`status` = 'active'
|
||||||
|
AND (profile.`private` = 0 OR EXISTS(
|
||||||
|
SELECT 1 FROM `sky_phone_picstagram_sessions` session
|
||||||
|
WHERE session.`device_imei` = ? AND session.`profile_id` = profile.`id`
|
||||||
|
) OR EXISTS(
|
||||||
|
SELECT 1 FROM `sky_phone_picstagram_follows` follow
|
||||||
|
WHERE follow.`following_id` = profile.`id` AND follow.`status` = 'accepted'
|
||||||
|
AND follow.`follower_id` = (
|
||||||
|
SELECT session.`profile_id` FROM `sky_phone_picstagram_sessions` session
|
||||||
|
WHERE session.`device_imei` = ? LIMIT 1
|
||||||
|
)
|
||||||
|
)) LIMIT 1
|
||||||
|
]], { id, device.imei, device.imei })
|
||||||
|
if post then
|
||||||
|
local title = post.caption ~= "" and post.caption or post.display_name
|
||||||
|
return {
|
||||||
|
title = title,
|
||||||
|
subtitle = "@" .. post.handle,
|
||||||
|
copyText = "@" .. post.handle .. ": " .. post.caption,
|
||||||
|
imageUrl = post.image_url,
|
||||||
|
link = "skyphone://picstagram/post/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "feather" then
|
||||||
|
local post = first_row([[
|
||||||
|
SELECT post.`body`, profile.`handle`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_feather_posts` post
|
||||||
|
JOIN `sky_phone_feather_profiles` profile ON profile.`id` = post.`profile_id`
|
||||||
|
LEFT JOIN `sky_phone_feather_post_media` post_media
|
||||||
|
ON post_media.`post_id` = post.`id` AND post_media.`sort_order` = 0
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = post_media.`media_id`
|
||||||
|
WHERE post.`id` = ? AND post.`status` = 'published' LIMIT 1
|
||||||
|
]], { id })
|
||||||
|
if post then
|
||||||
|
return {
|
||||||
|
title = post.body,
|
||||||
|
subtitle = "@" .. post.handle,
|
||||||
|
copyText = "@" .. post.handle .. ": " .. post.body,
|
||||||
|
imageUrl = post.image_url,
|
||||||
|
link = "skyphone://feather/post/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "fliptok" then
|
||||||
|
local post = first_row([[
|
||||||
|
SELECT video.`caption`, profile.`display_name`, profile.`handle`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_fliptok_videos` video
|
||||||
|
JOIN `sky_phone_fliptok_profiles` profile ON profile.`id` = video.`profile_id`
|
||||||
|
JOIN `sky_phone_media` media ON media.`id` = video.`media_id`
|
||||||
|
WHERE video.`id` = ? AND video.`status` = 'published' AND (
|
||||||
|
video.`visibility` = 'public' OR EXISTS(
|
||||||
|
SELECT 1 FROM `sky_phone_fliptok_sessions` session
|
||||||
|
WHERE session.`device_imei` = ? AND session.`profile_id` = profile.`id`
|
||||||
|
) OR (video.`visibility` = 'followers' AND EXISTS(
|
||||||
|
SELECT 1 FROM `sky_phone_fliptok_follows` follow
|
||||||
|
WHERE follow.`following_id` = profile.`id` AND follow.`follower_id` = (
|
||||||
|
SELECT session.`profile_id` FROM `sky_phone_fliptok_sessions` session
|
||||||
|
WHERE session.`device_imei` = ? LIMIT 1
|
||||||
|
)
|
||||||
|
))
|
||||||
|
) LIMIT 1
|
||||||
|
]], { id, device.imei, device.imei })
|
||||||
|
if post then
|
||||||
|
local title = post.caption ~= "" and post.caption or post.display_name
|
||||||
|
return {
|
||||||
|
title = title,
|
||||||
|
subtitle = "@" .. post.handle,
|
||||||
|
copyText = "@" .. post.handle .. ": " .. post.caption,
|
||||||
|
imageUrl = post.image_url,
|
||||||
|
link = "skyphone://fliptok/video/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "local-pages" then
|
||||||
|
local post = first_row([[
|
||||||
|
SELECT post.`title`, post.`body`, SUBSTRING_INDEX(account.`email`, '@', 1) AS `author_name`,
|
||||||
|
media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_pages_posts` post
|
||||||
|
JOIN `sky_phone_accounts` account ON account.`id` = post.`account_id`
|
||||||
|
LEFT JOIN `sky_phone_pages_images` image ON image.`post_id` = post.`id` AND image.`sort_order` = 1
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = image.`media_id`
|
||||||
|
WHERE post.`id` = ? LIMIT 1
|
||||||
|
]], { id })
|
||||||
|
if post then
|
||||||
|
return {
|
||||||
|
title = post.title,
|
||||||
|
subtitle = post.author_name,
|
||||||
|
copyText = post.title .. "\n" .. post.body,
|
||||||
|
imageUrl = post.image_url,
|
||||||
|
link = "skyphone://local-pages/post/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "citymarkt" then
|
||||||
|
local post = first_row([[
|
||||||
|
SELECT listing.`title`, listing.`description`, listing.`price`, listing.`price_type`, media.`url` AS `image_url`
|
||||||
|
FROM `sky_phone_marketplace_listings` listing
|
||||||
|
LEFT JOIN `sky_phone_marketplace_images` image
|
||||||
|
ON image.`listing_id` = listing.`id` AND image.`sort_order` = 1
|
||||||
|
LEFT JOIN `sky_phone_media` media ON media.`id` = image.`media_id`
|
||||||
|
WHERE listing.`id` = ? AND listing.`status` IN ('active', 'reserved') LIMIT 1
|
||||||
|
]], { id })
|
||||||
|
if post then
|
||||||
|
local price = post.price_type == "fixed" and tostring(post.price) or post.price_type
|
||||||
|
return {
|
||||||
|
title = post.title,
|
||||||
|
subtitle = price,
|
||||||
|
copyText = post.title .. "\n" .. post.description,
|
||||||
|
imageUrl = post.image_url,
|
||||||
|
link = "skyphone://citymarkt/listing/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canonical_music(device, data)
|
||||||
|
if data.kind == "track" and type(data.meta) == "table" and data.meta.source == "server" then
|
||||||
|
for _, track in ipairs(Config.Music.Tracks) do
|
||||||
|
if track.Id == data.id then
|
||||||
|
return {
|
||||||
|
title = track.Title,
|
||||||
|
subtitle = track.Artist,
|
||||||
|
copyText = track.Title .. " — " .. track.Artist,
|
||||||
|
link = "skyphone://music/server/" .. track.Id,
|
||||||
|
meta = { source = "server" },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif data.kind == "track" and type(data.meta) == "table" and data.meta.source == "youtube" then
|
||||||
|
local condition, params = owner_condition(device, "song")
|
||||||
|
local query_params = { data.id }
|
||||||
|
append_params(query_params, params)
|
||||||
|
local track = first_row(([=[
|
||||||
|
SELECT song.`title`, song.`artist`, song.`video_id`
|
||||||
|
FROM `sky_phone_music_youtube_songs` song
|
||||||
|
WHERE song.`id` = ? AND %s LIMIT 1
|
||||||
|
]=]):format(condition), query_params)
|
||||||
|
if track then
|
||||||
|
return {
|
||||||
|
title = track.title,
|
||||||
|
subtitle = track.artist,
|
||||||
|
copyText = track.title .. " — " .. track.artist,
|
||||||
|
imageUrl = "https://i.ytimg.com/vi/" .. track.video_id .. "/hqdefault.jpg",
|
||||||
|
link = "skyphone://music/youtube/" .. data.id,
|
||||||
|
meta = { source = "youtube" },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif data.kind == "playlist" then
|
||||||
|
local condition, params = owner_condition(device, "playlist")
|
||||||
|
local query_params = { data.id }
|
||||||
|
append_params(query_params, params)
|
||||||
|
local playlist = first_row(([=[
|
||||||
|
SELECT playlist.`name`, COUNT(item.`id`) AS `song_count`
|
||||||
|
FROM `sky_phone_music_playlists` playlist
|
||||||
|
LEFT JOIN `sky_phone_music_playlist_items` item ON item.`playlist_id` = playlist.`id`
|
||||||
|
WHERE playlist.`id` = ? AND %s
|
||||||
|
GROUP BY playlist.`id`, playlist.`name` LIMIT 1
|
||||||
|
]=]):format(condition), query_params)
|
||||||
|
if playlist then
|
||||||
|
return {
|
||||||
|
title = playlist.name,
|
||||||
|
subtitle = tostring(playlist.song_count) .. " tracks",
|
||||||
|
copyText = playlist.name .. " · " .. tostring(playlist.song_count),
|
||||||
|
link = "skyphone://music/playlist/" .. data.id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canonical_document(source, device, app_id, id)
|
||||||
|
if app_id == "calendar" and device.account_id then
|
||||||
|
local event = first_row([[
|
||||||
|
SELECT `title`, `note`, `starts_at`, `ends_at` FROM `sky_phone_calendar_events`
|
||||||
|
WHERE `id` = ? AND `account_id` = ? LIMIT 1
|
||||||
|
]], { id, tonumber(device.account_id) })
|
||||||
|
if event then
|
||||||
|
return {
|
||||||
|
title = event.title,
|
||||||
|
subtitle = tostring(event.starts_at),
|
||||||
|
copyText = event.title .. "\n" .. event.note,
|
||||||
|
link = "skyphone://calendar/event/" .. id,
|
||||||
|
meta = { startAt = tostring(event.starts_at), endAt = tostring(event.ends_at) },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "mail" and device.account_id then
|
||||||
|
local message = first_row([[
|
||||||
|
SELECT message.`subject`, message.`body`, sender.`email` AS `sender_email`
|
||||||
|
FROM `sky_phone_mail_entries` entry
|
||||||
|
JOIN `sky_phone_mail_messages` message ON message.`id` = entry.`message_id`
|
||||||
|
LEFT JOIN `sky_phone_accounts` sender ON sender.`id` = message.`sender_account_id`
|
||||||
|
WHERE entry.`id` = ? AND entry.`account_id` = ? AND entry.`trashed_at` IS NULL LIMIT 1
|
||||||
|
]], { id, tonumber(device.account_id) })
|
||||||
|
if message then
|
||||||
|
return {
|
||||||
|
title = message.subject,
|
||||||
|
subtitle = message.sender_email,
|
||||||
|
copyText = message.subject .. "\n" .. message.body,
|
||||||
|
link = "skyphone://mail/message/" .. id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
elseif app_id == "garage" then
|
||||||
|
return SkyPhoneGarage.ResolveShare(source, id)
|
||||||
|
elseif app_id == "house" then
|
||||||
|
return SkyPhoneHousing.ResolveShare(source, id)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canonical_text(device, app_id, id)
|
||||||
|
if app_id ~= "darkchat" or not device.account_id then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local message = first_row([[
|
||||||
|
SELECT message.`body`, message.`message_type`, sender.`alias` AS `sender_alias`
|
||||||
|
FROM `sky_phone_darkchat_messages` message
|
||||||
|
JOIN `sky_phone_darkchat_members` member ON member.`conversation_id` = message.`conversation_id`
|
||||||
|
JOIN `sky_phone_darkchat_profiles` viewer ON viewer.`id` = member.`profile_id`
|
||||||
|
LEFT JOIN `sky_phone_darkchat_profiles` sender ON sender.`id` = message.`sender_profile_id`
|
||||||
|
WHERE message.`id` = ? AND viewer.`account_id` = ? AND message.`deleted_for_everyone` = 0
|
||||||
|
AND message.`message_type` IN ('text', 'emoji') LIMIT 1
|
||||||
|
]], { id, tonumber(device.account_id) })
|
||||||
|
if not message then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
title = message.body,
|
||||||
|
subtitle = message.sender_alias,
|
||||||
|
copyText = message.body,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local function display_name(source)
|
local function display_name(source)
|
||||||
local first = trim(Bridge.Framework.GetFirstname(source), 80)
|
local first = trim(Bridge.Framework.GetFirstname(source), 80)
|
||||||
local last = trim(Bridge.Framework.GetLastname(source), 80)
|
local last = trim(Bridge.Framework.GetLastname(source), 80)
|
||||||
@@ -250,8 +627,30 @@ local function sanitize_payload(source, device, data)
|
|||||||
end
|
end
|
||||||
local coords = GetEntityCoords(ped)
|
local coords = GetEntityCoords(ped)
|
||||||
payload.meta = { x = coords.x, y = coords.y, z = coords.z }
|
payload.meta = { x = coords.x, y = coords.y, z = coords.z }
|
||||||
elseif type(data.imageUrl) == "string" and #data.imageUrl <= 2048 and data.imageUrl:match("^https://") then
|
else
|
||||||
payload.imageUrl = data.imageUrl
|
local canonical
|
||||||
|
if data.kind == "profile" and payload.id then
|
||||||
|
canonical = canonical_profile(device, app_id, payload.id)
|
||||||
|
elseif data.kind == "post" and payload.id then
|
||||||
|
canonical = canonical_post(device, app_id, payload.id)
|
||||||
|
elseif (data.kind == "track" or data.kind == "playlist") and app_id == "music" and payload.id then
|
||||||
|
canonical = canonical_music(device, data)
|
||||||
|
elseif data.kind == "document" and payload.id then
|
||||||
|
canonical = canonical_document(source, device, app_id, payload.id)
|
||||||
|
elseif data.kind == "text" and payload.id then
|
||||||
|
canonical = canonical_text(device, app_id, payload.id)
|
||||||
|
elseif data.kind == "link" and payload.id and (app_id == "citymarkt" or app_id == "local-pages") then
|
||||||
|
canonical = canonical_post(device, app_id, payload.id)
|
||||||
|
end
|
||||||
|
if not canonical then
|
||||||
|
return nil, "unsupported_payload"
|
||||||
|
end
|
||||||
|
payload.title = canonical.title
|
||||||
|
payload.copyText = canonical.copyText
|
||||||
|
payload.subtitle = canonical.subtitle
|
||||||
|
payload.imageUrl = canonical.imageUrl
|
||||||
|
payload.link = canonical.link
|
||||||
|
payload.meta = canonical.meta
|
||||||
end
|
end
|
||||||
|
|
||||||
local encoded = json.encode(payload)
|
local encoded = json.encode(payload)
|
||||||
@@ -262,6 +661,9 @@ local function sanitize_payload(source, device, data)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SkyPhoneEasyShare.SanitizeChatPayload(source, data)
|
function SkyPhoneEasyShare.SanitizeChatPayload(source, data)
|
||||||
|
if not Config.EasyShare.Enabled then
|
||||||
|
return nil, "disabled"
|
||||||
|
end
|
||||||
local device, error_response = current_device(source)
|
local device, error_response = current_device(source)
|
||||||
if not device then
|
if not device then
|
||||||
return nil, error_response.error
|
return nil, error_response.error
|
||||||
@@ -328,12 +730,15 @@ local function apply_received_payload(transfer)
|
|||||||
meta.name, meta.notes or "", meta.organization or "", meta.phoneNumber,
|
meta.name, meta.notes or "", meta.organization or "", meta.phoneNumber,
|
||||||
})
|
})
|
||||||
TriggerClientEvent("sky_phone:contacts:changed", transfer.recipient_source, {})
|
TriggerClientEvent("sky_phone:contacts:changed", transfer.recipient_source, {})
|
||||||
elseif transfer.payload.kind == "note" then
|
elseif transfer.payload.kind == "note" or transfer.payload.kind == "text" or transfer.payload.kind == "document" then
|
||||||
Bridge.Database.Query([[
|
Bridge.Database.Query([[
|
||||||
INSERT INTO `sky_phone_notes`
|
INSERT INTO `sky_phone_notes`
|
||||||
(`id`, `account_id`, `device_imei`, `title`, `body`, `pinned`)
|
(`id`, `account_id`, `device_imei`, `title`, `body`, `pinned`)
|
||||||
VALUES (?, ?, ?, ?, ?, 0)
|
VALUES (?, ?, ?, ?, ?, 0)
|
||||||
]], { uuid(), account_id, account_id and nil or device.imei, meta.title or "", meta.body or "" })
|
]], {
|
||||||
|
uuid(), account_id, account_id and nil or device.imei,
|
||||||
|
meta.title or transfer.payload.title, meta.body or transfer.payload.copyText,
|
||||||
|
})
|
||||||
if account_id then
|
if account_id then
|
||||||
SkyPhone.RefreshAccount(account_id)
|
SkyPhone.RefreshAccount(account_id)
|
||||||
else
|
else
|
||||||
@@ -353,6 +758,15 @@ local function apply_received_payload(transfer)
|
|||||||
account_id, account_id and nil or device.imei, meta.url, meta.remoteId, transfer.payload.kind,
|
account_id, account_id and nil or device.imei, meta.url, meta.remoteId, transfer.payload.kind,
|
||||||
})
|
})
|
||||||
TriggerClientEvent("sky_phone:gallery:changed", transfer.recipient_source, {})
|
TriggerClientEvent("sky_phone:gallery:changed", transfer.recipient_source, {})
|
||||||
|
elseif transfer.payload.kind == "post"
|
||||||
|
or transfer.payload.kind == "profile"
|
||||||
|
or transfer.payload.kind == "track"
|
||||||
|
or transfer.payload.kind == "playlist"
|
||||||
|
or transfer.payload.kind == "link"
|
||||||
|
then
|
||||||
|
return type(transfer.payload.link) == "string"
|
||||||
|
else
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -415,6 +829,9 @@ Bridge.Callbacks.Register("sky_phone:easyshare:bootstrap", function(source)
|
|||||||
if not Config.EasyShare.Enabled then
|
if not Config.EasyShare.Enabled then
|
||||||
return { success = false, error = "disabled" }
|
return { success = false, error = "disabled" }
|
||||||
end
|
end
|
||||||
|
if not SkyPhone.AllowOperation(source, "easyshare_bootstrap", Config.EasyShare.BootstrapRequestsPerMinute, 60) then
|
||||||
|
return { success = false, error = "rate_limited" }
|
||||||
|
end
|
||||||
local device, error_response = current_device(source)
|
local device, error_response = current_device(source)
|
||||||
if not device then
|
if not device then
|
||||||
return error_response
|
return error_response
|
||||||
@@ -437,6 +854,12 @@ Bridge.Callbacks.Register("sky_phone:easyshare:bootstrap", function(source)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
Bridge.Callbacks.Register("sky_phone:easyshare:set-visibility", function(source, data)
|
Bridge.Callbacks.Register("sky_phone:easyshare:set-visibility", function(source, data)
|
||||||
|
if not Config.EasyShare.Enabled then
|
||||||
|
return { success = false, error = "disabled" }
|
||||||
|
end
|
||||||
|
if not SkyPhone.AllowOperation(source, "easyshare_visibility", Config.EasyShare.VisibilityUpdatesPerMinute, 60) then
|
||||||
|
return { success = false, error = "rate_limited" }
|
||||||
|
end
|
||||||
local device, error_response = current_device(source)
|
local device, error_response = current_device(source)
|
||||||
if not device then
|
if not device then
|
||||||
return error_response
|
return error_response
|
||||||
@@ -453,6 +876,9 @@ Bridge.Callbacks.Register("sky_phone:easyshare:set-visibility", function(source,
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
Bridge.Callbacks.Register("sky_phone:easyshare:request", function(source, data)
|
Bridge.Callbacks.Register("sky_phone:easyshare:request", function(source, data)
|
||||||
|
if not Config.EasyShare.Enabled then
|
||||||
|
return { success = false, error = "disabled" }
|
||||||
|
end
|
||||||
if not SkyPhone.AllowOperation(source, "easyshare_request", Config.EasyShare.RequestsPerMinute, 60) then
|
if not SkyPhone.AllowOperation(source, "easyshare_request", Config.EasyShare.RequestsPerMinute, 60) then
|
||||||
return { success = false, error = "rate_limited" }
|
return { success = false, error = "rate_limited" }
|
||||||
end
|
end
|
||||||
@@ -503,6 +929,12 @@ Bridge.Callbacks.Register("sky_phone:easyshare:request", function(source, data)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
Bridge.Callbacks.Register("sky_phone:easyshare:respond", function(source, data)
|
Bridge.Callbacks.Register("sky_phone:easyshare:respond", function(source, data)
|
||||||
|
if not Config.EasyShare.Enabled then
|
||||||
|
return { success = false, error = "disabled" }
|
||||||
|
end
|
||||||
|
if not SkyPhone.AllowOperation(source, "easyshare_action", Config.EasyShare.ActionsPerMinute, 60) then
|
||||||
|
return { success = false, error = "rate_limited" }
|
||||||
|
end
|
||||||
local id = type(data) == "table" and data.id or nil
|
local id = type(data) == "table" and data.id or nil
|
||||||
local transfer = type(id) == "string" and active_transfers[id] or nil
|
local transfer = type(id) == "string" and active_transfers[id] or nil
|
||||||
if not transfer or transfer.recipient_source ~= source or transfer.status ~= "pending" or type(data.accepted) ~= "boolean" then
|
if not transfer or transfer.recipient_source ~= source or transfer.status ~= "pending" or type(data.accepted) ~= "boolean" then
|
||||||
@@ -527,6 +959,12 @@ Bridge.Callbacks.Register("sky_phone:easyshare:respond", function(source, data)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
Bridge.Callbacks.Register("sky_phone:easyshare:cancel", function(source, data)
|
Bridge.Callbacks.Register("sky_phone:easyshare:cancel", function(source, data)
|
||||||
|
if not Config.EasyShare.Enabled then
|
||||||
|
return { success = false, error = "disabled" }
|
||||||
|
end
|
||||||
|
if not SkyPhone.AllowOperation(source, "easyshare_action", Config.EasyShare.ActionsPerMinute, 60) then
|
||||||
|
return { success = false, error = "rate_limited" }
|
||||||
|
end
|
||||||
local id = type(data) == "table" and data.id or nil
|
local id = type(data) == "table" and data.id or nil
|
||||||
local transfer = type(id) == "string" and active_transfers[id] or nil
|
local transfer = type(id) == "string" and active_transfers[id] or nil
|
||||||
if not transfer or (transfer.sender_source ~= source and transfer.recipient_source ~= source) then
|
if not transfer or (transfer.sender_source ~= source and transfer.recipient_source ~= source) then
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
SkyPhoneGarage = {}
|
||||||
|
|
||||||
Bridge.Database.AfterMigration("sky_phone", function()
|
Bridge.Database.AfterMigration("sky_phone", function()
|
||||||
|
|
||||||
local supported_systems = {
|
local supported_systems = {
|
||||||
@@ -200,6 +202,30 @@ local function owned_vehicle_row(identifier, plate)
|
|||||||
return rows[1], table_name, owner_column, garage_system
|
return rows[1], table_name, owner_column, garage_system
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function SkyPhoneGarage.ResolveShare(source, plate_value)
|
||||||
|
local plate = normalized_plate(plate_value)
|
||||||
|
if not plate then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||||
|
if type(identifier) ~= "string" or identifier == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local row, _, _, garage_system = owned_vehicle_row(identifier, plate)
|
||||||
|
if not row then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local vehicle = vehicle_dto(row, garage_system)
|
||||||
|
local title = vehicle.nickname ~= "" and vehicle.nickname or vehicle.plate
|
||||||
|
return {
|
||||||
|
title = title,
|
||||||
|
subtitle = vehicle.plate,
|
||||||
|
copyText = title .. "\n" .. vehicle.plate .. " · " .. vehicle.status,
|
||||||
|
link = "skyphone://garage/vehicle/" .. vehicle.plate,
|
||||||
|
meta = { kind = vehicle.kind, location = vehicle.location, status = vehicle.status },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local function status_snapshot(row)
|
local function status_snapshot(row)
|
||||||
local snapshot = {}
|
local snapshot = {}
|
||||||
for _, column in ipairs({ "stored", "state", "in_garage", "parked" }) do
|
for _, column in ipairs({ "stored", "state", "in_garage", "parked" }) do
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
|
SkyPhoneHousing = {}
|
||||||
|
|
||||||
|
function SkyPhoneHousing.ResolveShare(source, property_id)
|
||||||
|
local overview = Bridge.Housing.GetOverview(source)
|
||||||
|
if not overview or type(overview.properties) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
for _, property in ipairs(overview.properties) do
|
||||||
|
if property.id == property_id then
|
||||||
|
return {
|
||||||
|
title = property.name,
|
||||||
|
subtitle = property.access,
|
||||||
|
copyText = property.name,
|
||||||
|
link = "skyphone://house/property/" .. property.id,
|
||||||
|
meta = { access = property.access, entrance = property.entrance },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
Bridge.Callbacks.Register("sky_phone:housing:overview", function(source)
|
Bridge.Callbacks.Register("sky_phone:housing:overview", function(source)
|
||||||
if not SkyPhone.AllowOperation(
|
if not SkyPhone.AllowOperation(
|
||||||
source,
|
source,
|
||||||
|
|||||||
Reference in New Issue
Block a user