diff --git a/README.md b/README.md index f980839..01bfc8a 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,17 @@ An iFruit account is optional. Unlinked devices retain local settings, alarms, m - Two unique, non-stackable inventory items named `sky_phone_sim_registered` and `sky_phone_sim_anonymous`. Their metadata is initialized automatically on first use, so shops and crafting recipes add plain items without supplying a number. - `oxmysql` with MySQL/MariaDB. - `pma-voice` when `Config.Calls.VoiceProvider` is set to `"pma"`. +- A FiveManage V3 Media API token for Camera photo/video uploads and Gallery deletion. Set the + server-only `Config.Media.FiveManage.ApiKey` in `sky_phone/config/media.lua`; the token is never + sent to NUI because clients receive temporary presigned upload URLs instead. Database migrations run automatically. Existing `sky_phone_mail_accounts` installations are renamed to `sky_phone_accounts` while preserving account IDs and mail foreign keys. iFruit passwords are intentional in-character credentials and remain plaintext `VARCHAR(64)` values; registration screens warn players never to reuse a real password. +Camera and Gallery media is stored in `sky_phone_media`. Signed-out captures belong to the current +IMEI; linking an iFruit account moves those rows into the account gallery so every linked phone sees +them. Signing out hides cloud media without deleting it. Factory reset removes device-local media +and attempts to delete its remote FiveManage files, while account-owned media remains in the cloud. + For a fresh manual database installation, import `sky_phone/sql/install.sql`. It contains the complete current table, key, index, collation, and foreign-key schema. Runtime migrations remain authoritative for upgrading an existing installation and must stay enabled. Framework, inventory, callback, notification, and database integrations live under `sky_phone/source/bridge`. The resource has no dependency on any other Sky resource. diff --git a/frontend/package.json b/frontend/package.json index 944bab1..6901a51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "test": "vitest run" }, "dependencies": { + "fix-webm-duration": "^1.0.6", "konsta": "~5.2.0", "lucide-vue-next": "^0.525.0", "pinia": "^3.0.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3620d6d..7842796 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + fix-webm-duration: + specifier: ^1.0.6 + version: 1.0.6 konsta: specifier: ~5.2.0 version: 5.2.0 @@ -1152,6 +1155,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-webm-duration@1.0.6: + resolution: {integrity: sha512-zVAqi4gE+8ywxJuAyV/rlJVX6CMtvyapEbQx6jyoeX9TMjdqAlt/FdG5d7rXSSkDVzTvS0H7CtwzHcH/vh4FPA==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -3002,6 +3008,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-webm-duration@1.0.6: {} + flat-cache@4.0.1: dependencies: flatted: 3.4.4 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c7ea800..e9c8642 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -11,6 +11,7 @@ import { import { useRoute, useRouter } from 'vue-router' import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue' +import PhoneMediaCapture from '@/components/PhoneMediaCapture.vue' import PhoneLockScreen from '@/components/PhoneLockScreen.vue' import PhoneNotifications from '@/components/PhoneNotifications.vue' import NotificationPhonePreview from '@/components/NotificationPhonePreview.vue' @@ -24,7 +25,7 @@ import { useGamesStore } from '@/features/games/store' import { useCallsStore } from '@/stores/calls' import { useAccountStore } from '@/stores/account' import { useMailStore } from '@/stores/mail' -import { useMediaStore } from '@/stores/media' +import { useAppStoreStore } from '@/stores/app-store' import { useNotesStore } from '@/stores/notes' import { useWeatherStore } from '@/stores/weather' import { @@ -70,7 +71,7 @@ const clock = useClockStore() const games = useGamesStore() const calls = useCallsStore() const mail = useMailStore() -const media = useMediaStore() +const appStore = useAppStoreStore() const notes = useNotesStore() const weather = useWeatherStore() const notifications = useNotificationsStore() @@ -94,7 +95,6 @@ const phoneFrameImage = computed( ) let clockTicker: ReturnType | undefined let unlockTimer: number | undefined -let cameraTimer: number | undefined function getViewportScale(): number { const heightScale = window.innerHeight / REFERENCE_VIEWPORT_HEIGHT @@ -109,7 +109,7 @@ function hydratePhone(payload: PhoneOpenPayload): void { notes.hydrate(payload.notes ?? []) clock.hydrate(payload.device?.data.alarms?.payload) games.hydrate(payload.device?.data.games?.payload) - media.hydrate(payload.device?.data.media?.payload) + appStore.hydrate(payload.device?.data.apps?.payload) void mail.bootstrap(payload.account?.email ?? '') void calls.bootstrap() } @@ -188,22 +188,21 @@ function updateViewportScale(): void { viewportScale.value = getViewportScale() } -function unlockPhone(destination?: 'camera'): void { +function unlockPhone(): void { if (!isLocked.value) return isUnlocking.value = true isLocked.value = false - if (destination === 'camera') { - cameraTimer = window.setTimeout(() => { - void router.push('/apps/camera') - }, 260) - } - unlockTimer = window.setTimeout(() => { isUnlocking.value = false }, 720) } +function unlockCamera(): void { + unlockPhone() + window.setTimeout(() => void router.push('/apps/camera'), 0) +} + onMounted(() => { window.addEventListener('message', onMessage) window.addEventListener('keydown', onKeydown) @@ -289,7 +288,6 @@ watch( () => phone.isOpen, (isOpen) => { if (unlockTimer !== undefined) window.clearTimeout(unlockTimer) - if (cameraTimer !== undefined) window.clearTimeout(cameraTimer) if (!isOpen) { weather.stop() isLocked.value = false @@ -308,7 +306,6 @@ onBeforeUnmount(() => { weather.stop() if (clockTicker) clearInterval(clockTicker) if (unlockTimer !== undefined) window.clearTimeout(unlockTimer) - if (cameraTimer !== undefined) window.clearTimeout(cameraTimer) window.removeEventListener('message', onMessage) window.removeEventListener('keydown', onKeydown) window.removeEventListener('resize', updateViewportScale) @@ -317,6 +314,7 @@ onBeforeUnmount(() => {