mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 09:13:24 +00:00
CLN - remove weather camera views
Remove the remote weather-camera experience from the phone UI, NUI test server, and weather state.\n\nDelete the associated client and server camera handlers, configuration, locale copy, and manifest entries while retaining the core weather forecast app.
This commit is contained in:
@@ -3515,119 +3515,6 @@ button {
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.weather-camera-card {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.weather-camera-preview {
|
||||
position: relative;
|
||||
min-height: 146px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 75% 20%, #bfe8ff9c, transparent 28%),
|
||||
linear-gradient(160deg, #82b4d5 0%, #496783 52%, #27364c 100%);
|
||||
}
|
||||
.weather-camera-preview--rain,
|
||||
.weather-camera-preview--thunder {
|
||||
background:
|
||||
radial-gradient(circle at 70% 18%, #9cb4c380, transparent 26%),
|
||||
linear-gradient(160deg, #667d8d 0%, #34495f 50%, #1a2839 100%);
|
||||
}
|
||||
.weather-camera-preview--clear,
|
||||
.weather-camera-preview--sunny {
|
||||
background:
|
||||
radial-gradient(circle at 76% 19%, #fff4c9 0 5%, #ffd78370 6%, transparent 29%),
|
||||
linear-gradient(160deg, #72b7df 0%, #4d86b2 55%, #35506e 100%);
|
||||
}
|
||||
.weather-camera-preview::after {
|
||||
position: absolute;
|
||||
inset: -50% 0 auto;
|
||||
height: 3px;
|
||||
background: #ffffff38;
|
||||
box-shadow: 0 0 12px #fff8;
|
||||
content: '';
|
||||
animation: weather-camera-scan 4s linear infinite;
|
||||
}
|
||||
.weather-camera-skyline {
|
||||
position: absolute;
|
||||
right: -5%;
|
||||
bottom: 0;
|
||||
left: -5%;
|
||||
height: 51%;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 0 5%, #142333 5% 17%, transparent 17% 21%, #192b3c 21% 34%, transparent 34% 42%, #11202f 42% 60%, transparent 60% 65%, #182a3b 65% 76%, transparent 76% 81%, #102131 81% 95%, transparent 95%),
|
||||
linear-gradient(0deg, #111c2a 0 18%, transparent 18%);
|
||||
clip-path: polygon(0 43%, 9% 43%, 9% 22%, 18% 22%, 18% 57%, 27% 57%, 27% 10%, 37% 10%, 37% 48%, 48% 48%, 48% 30%, 61% 30%, 61% 62%, 71% 62%, 71% 16%, 83% 16%, 83% 49%, 100% 49%, 100% 100%, 0 100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.weather-camera-meta,
|
||||
.weather-camera-copy {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-shadow: 0 2px 8px #000c;
|
||||
}
|
||||
.weather-camera-meta {
|
||||
top: 11px;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.weather-camera-live {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.weather-camera-live i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #ff4e5b;
|
||||
box-shadow: 0 0 0 4px #ff4e5b2e;
|
||||
animation: weather-camera-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
.weather-camera-copy {
|
||||
bottom: 10px;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
}
|
||||
.weather-camera-copy strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
.weather-camera-copy span {
|
||||
color: #ffffffc4;
|
||||
font-size: 10px;
|
||||
}
|
||||
.weather-camera-action {
|
||||
padding: 11px 12px 0;
|
||||
}
|
||||
.weather-camera-action button span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
}
|
||||
.weather-camera-action p {
|
||||
margin: 8px 3px 0;
|
||||
color: #ffd2d2;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.weather-camera-unavailable {
|
||||
padding: 16px;
|
||||
color: #ffffffb3;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
@keyframes weather-camera-scan {
|
||||
to { transform: translateY(290px); }
|
||||
}
|
||||
@keyframes weather-camera-pulse {
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
.weather-hourly {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
|
||||
@@ -2338,28 +2338,6 @@ const defaultLocales: LocaleTree = {
|
||||
unavailable: 'Weather is unavailable',
|
||||
stale: 'Showing the last available weather update.',
|
||||
tryAgain: 'Try Again',
|
||||
webcam: {
|
||||
title: 'Weather Cameras',
|
||||
live: 'LIVE',
|
||||
view: 'View Live Camera',
|
||||
subtitle: 'A live look at current conditions',
|
||||
unavailable: 'No weather cameras are available.',
|
||||
locations: {
|
||||
legion_square: 'Legion Square',
|
||||
sandy_shores: 'Sandy Shores',
|
||||
cayo_airstrip: 'Cayo Perico Airstrip',
|
||||
},
|
||||
errors: {
|
||||
camera_disabled: 'Weather cameras are currently disabled.',
|
||||
camera_not_found: 'This weather camera is no longer available.',
|
||||
camera_requires_safe_position:
|
||||
'Stand still and exit your vehicle before opening a camera.',
|
||||
camera_active: 'A weather camera is already open.',
|
||||
rate_limited: 'Too many camera requests. Try again shortly.',
|
||||
request_failed: 'The weather camera could not be opened.',
|
||||
not_authenticated: 'Unlock the phone before opening a camera.',
|
||||
},
|
||||
},
|
||||
regions: {
|
||||
los_santos: 'Los Santos',
|
||||
blaine_county: 'Blaine County',
|
||||
|
||||
@@ -61,24 +61,4 @@ describe('weather store', () => {
|
||||
expect(weather.error).toBe('offline')
|
||||
})
|
||||
|
||||
it('loads the server-approved camera list and opens a camera by id', async () => {
|
||||
vi.mocked(nuiCall)
|
||||
.mockResolvedValueOnce({
|
||||
data: { cameras: [{ id: 'legion_square', region: 'los_santos' }] },
|
||||
success: true,
|
||||
})
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
const weather = useWeatherStore()
|
||||
|
||||
await weather.loadCameras()
|
||||
const opened = await weather.openCamera('legion_square')
|
||||
|
||||
expect(weather.cameras).toEqual([
|
||||
{ id: 'legion_square', region: 'los_santos' },
|
||||
])
|
||||
expect(nuiCall).toHaveBeenLastCalledWith('weather:camera-open', {
|
||||
id: 'legion_square',
|
||||
})
|
||||
expect(opened).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import type {
|
||||
RawWeatherSnapshot,
|
||||
WeatherCamera,
|
||||
WeatherForecast,
|
||||
} from '@/types/weather'
|
||||
import type { RawWeatherSnapshot, WeatherForecast } from '@/types/weather'
|
||||
import { nuiCall } from '@/utils/nui'
|
||||
import { buildWeatherForecast } from '@/utils/weather'
|
||||
|
||||
@@ -13,45 +9,12 @@ const REFRESH_INTERVAL = 30_000
|
||||
export const useWeatherStore = defineStore('weather', {
|
||||
state: () => ({
|
||||
error: null as string | null,
|
||||
cameraError: null as string | null,
|
||||
cameras: [] as WeatherCamera[],
|
||||
camerasLoaded: false,
|
||||
forecast: null as WeatherForecast | null,
|
||||
intervalId: undefined as number | undefined,
|
||||
isLoading: false,
|
||||
isOpeningCamera: false,
|
||||
lastFetchedAt: 0,
|
||||
}),
|
||||
actions: {
|
||||
async loadCameras(force = false): Promise<void> {
|
||||
if (this.camerasLoaded && !force) return
|
||||
const response = await nuiCall<{ cameras: WeatherCamera[] }>(
|
||||
'weather:cameras',
|
||||
)
|
||||
this.camerasLoaded = true
|
||||
if (!response.success || !Array.isArray(response.data?.cameras)) {
|
||||
this.cameraError = response.error ?? 'request_failed'
|
||||
return
|
||||
}
|
||||
this.cameras = response.data.cameras.filter(
|
||||
(camera) =>
|
||||
typeof camera?.id === 'string' &&
|
||||
['los_santos', 'blaine_county', 'cayo_perico'].includes(
|
||||
camera.region,
|
||||
),
|
||||
)
|
||||
this.cameraError = null
|
||||
},
|
||||
async openCamera(id: string): Promise<boolean> {
|
||||
if (this.isOpeningCamera) return false
|
||||
this.isOpeningCamera = true
|
||||
const response = await nuiCall('weather:camera-open', { id })
|
||||
this.isOpeningCamera = false
|
||||
this.cameraError = response.success
|
||||
? null
|
||||
: (response.error ?? 'request_failed')
|
||||
return response.success
|
||||
},
|
||||
async refresh(force = false): Promise<void> {
|
||||
if (this.isLoading) return
|
||||
if (!force && Date.now() - this.lastFetchedAt < REFRESH_INTERVAL) return
|
||||
@@ -68,7 +31,6 @@ export const useWeatherStore = defineStore('weather', {
|
||||
},
|
||||
start(): void {
|
||||
void this.refresh(true)
|
||||
void this.loadCameras()
|
||||
if (this.intervalId !== undefined) return
|
||||
this.intervalId = window.setInterval(() => void this.refresh(true), REFRESH_INTERVAL)
|
||||
},
|
||||
|
||||
@@ -10,11 +10,6 @@ export type WeatherConditionId =
|
||||
|
||||
export type WeatherRegionId = 'los_santos' | 'blaine_county' | 'cayo_perico'
|
||||
|
||||
export type WeatherCamera = {
|
||||
id: string
|
||||
region: WeatherRegionId
|
||||
}
|
||||
|
||||
export type WeatherClock = {
|
||||
day: number
|
||||
hour: number
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { kButton, kCard, kLink, kNavbar, kPage, kPreloader } from 'konsta/vue'
|
||||
import { kCard, kLink, kNavbar, kPage, kPreloader } from 'konsta/vue'
|
||||
import {
|
||||
CloudSun,
|
||||
Droplets,
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
RefreshCw,
|
||||
ThermometerSun,
|
||||
Umbrella,
|
||||
Video,
|
||||
Wind,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
@@ -21,11 +20,6 @@ import type { WeatherConditionId } from '@/types/weather'
|
||||
const phone = usePhoneStore()
|
||||
const weather = useWeatherStore()
|
||||
const forecast = computed(() => weather.forecast)
|
||||
const selectedCamera = computed(
|
||||
() =>
|
||||
weather.cameras.find((camera) => camera.region === forecast.value?.region) ??
|
||||
weather.cameras[0],
|
||||
)
|
||||
const rainy = computed(
|
||||
() =>
|
||||
forecast.value?.condition === 'rain' ||
|
||||
@@ -71,11 +65,6 @@ function formatHour(timestamp: number, index: number): string {
|
||||
}).format(timestamp)
|
||||
}
|
||||
|
||||
function cameraErrorLabel(): string {
|
||||
return phone.t(
|
||||
`Apps.weather.webcam.errors.${weather.cameraError ?? 'request_failed'}`,
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -168,46 +157,6 @@ function cameraErrorLabel(): string {
|
||||
</k-card>
|
||||
</section>
|
||||
|
||||
<k-card
|
||||
v-if="selectedCamera"
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-panel weather-camera-card"
|
||||
>
|
||||
<div class="weather-camera-preview" :class="`weather-camera-preview--${forecast.condition}`">
|
||||
<div class="weather-camera-skyline" aria-hidden="true"></div>
|
||||
<div class="weather-camera-meta">
|
||||
<span class="weather-camera-live"><i></i>{{ phone.t('Apps.weather.webcam.live') }}</span>
|
||||
<span>{{ phone.t(`Apps.weather.webcam.locations.${selectedCamera.id}`) }}</span>
|
||||
</div>
|
||||
<div class="weather-camera-copy">
|
||||
<strong>{{ phone.t('Apps.weather.webcam.title') }}</strong>
|
||||
<span>{{ phone.t('Apps.weather.webcam.subtitle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="weather-camera-action">
|
||||
<k-button
|
||||
large
|
||||
rounded
|
||||
:disabled="weather.isOpeningCamera"
|
||||
@click="weather.openCamera(selectedCamera.id)"
|
||||
>
|
||||
<Video :size="17" />
|
||||
{{ phone.t('Apps.weather.webcam.view') }}
|
||||
</k-button>
|
||||
<p v-if="weather.cameraError">{{ cameraErrorLabel() }}</p>
|
||||
</div>
|
||||
</k-card>
|
||||
|
||||
<k-card
|
||||
v-else-if="weather.camerasLoaded"
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
class="weather-panel weather-camera-unavailable"
|
||||
>
|
||||
{{ phone.t('Apps.weather.webcam.unavailable') }}
|
||||
</k-card>
|
||||
|
||||
<k-card
|
||||
:colors="cardColors"
|
||||
:content-wrap="false"
|
||||
|
||||
Reference in New Issue
Block a user