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:
Type
2026-08-13 11:43:32 +02:00
parent b3db702a2e
commit c1f2f820c1
12 changed files with 2 additions and 485 deletions
-113
View File
@@ -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));
-22
View File
@@ -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',
-20
View File
@@ -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 -39
View File
@@ -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)
},
-5
View File
@@ -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 -52
View File
@@ -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"
-22
View File
@@ -4779,28 +4779,6 @@ app.post('/api/:endpoint', (request, response) => {
})
return
}
if (endpoint === 'weather:cameras') {
response.json({
success: true,
data: {
cameras: [
{ id: 'legion_square', region: 'los_santos' },
{ id: 'sandy_shores', region: 'blaine_county' },
{ id: 'cayo_airstrip', region: 'cayo_perico' },
],
},
})
return
}
if (endpoint === 'weather:camera-open') {
const ids = ['legion_square', 'sandy_shores', 'cayo_airstrip']
response.json(
ids.includes(request.body?.id)
? { success: true }
: { success: false, error: 'camera_not_found' },
)
return
}
if (endpoint === 'map:getPlayerCoords') {
response.json({
success: true,