offroad UI: replace stock home with clean grid launcher
- Strip out date, version, update/alert widgets from OffroadHome - Replace with grid layout: Dashcam and Settings buttons - Skip sidebar when tapping splash screen - Settings button still opens original comma settings - Dashcam button placeholder (viewer not yet built) - Add DASHCAM_PROJECT.md with plans for footage viewer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
49
DASHCAM_PROJECT.md
Normal file
49
DASHCAM_PROJECT.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Dashcam Project
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
### Completed (2026-04-11)
|
||||||
|
|
||||||
|
- Disabled comma training data video encoding (`encoderd`) — CAN/sensor logs still recorded
|
||||||
|
- Re-enabled FrogPilot OMX screen recorder (H.264 MP4, 1440x720, 2Mbps, hardware encoded)
|
||||||
|
- Auto-start/stop recording tied to car ignition (`scene.started`)
|
||||||
|
- `ScreenRecorderDebug` param for bench testing without car connected
|
||||||
|
- Hidden all recorder UI elements — invisible to driver
|
||||||
|
- Videos saved to `/data/media/0/videos/YYYYMMDD-HHMMSS.mp4`, 3-minute segments
|
||||||
|
- Deleter updated: 9 GB free space threshold, rotates oldest videos first
|
||||||
|
- Cleaned up offroad UI: replaced stock home screen with grid launcher (Settings + Dashcam buttons)
|
||||||
|
|
||||||
|
### Next: Dashcam Footage Viewer
|
||||||
|
|
||||||
|
Build a native Qt widget accessible from the offroad home screen "Dashcam" button.
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- List MP4 files from `/data/media/0/videos/` sorted newest first
|
||||||
|
- Tap a file to play it back using Qt Multimedia (`QMediaPlayer` + `QVideoWidget`)
|
||||||
|
- Only accessible when offroad (car in park or off)
|
||||||
|
- Back button to return to offroad home
|
||||||
|
- No rotation hacks needed — native Qt widget in existing UI tree handles rotation correctly
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
- New widget class (e.g. `DashcamViewer`) added to `selfdrive/ui/qt/`
|
||||||
|
- Wire into `HomeWindow`'s `QStackedLayout` alongside `home`, `onroad`, `ready`, `driver_view`
|
||||||
|
- Dashcam button in `OffroadHome` switches `slayout` to the viewer
|
||||||
|
- Viewer has a file list view and a playback view (sub-stacked layout)
|
||||||
|
- Back button returns to `OffroadHome`
|
||||||
|
- Build: add to `qt_src` list in `selfdrive/ui/SConscript`, link `Qt5Multimedia`
|
||||||
|
|
||||||
|
**Previous webview attempts (abandoned):**
|
||||||
|
- Brian tried QWebEngineView for browser-based playback but the WebEngine subprocess renders independently of Qt's widget tree
|
||||||
|
- Screen rotation (`view.rotate(90)`, Wayland `wl_surface_set_buffer_transform`) did not work for WebEngine content
|
||||||
|
- Native Qt widget approach avoids this problem entirely
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `selfdrive/frogpilot/screenrecorder/screenrecorder.cc` | Recording logic, auto-start/stop |
|
||||||
|
| `selfdrive/frogpilot/screenrecorder/omx_encoder.cc` | OMX H.264 hardware encoder |
|
||||||
|
| `selfdrive/ui/qt/onroad.cc` | Timer driving frame capture |
|
||||||
|
| `selfdrive/ui/qt/home.cc` | Offroad home with Dashcam button |
|
||||||
|
| `system/loggerd/deleter.py` | Storage rotation |
|
||||||
|
| `/data/media/0/videos/` | Video output directory |
|
||||||
@@ -5,12 +5,7 @@
|
|||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
|
|
||||||
#include "selfdrive/ui/qt/util.h"
|
#include "selfdrive/ui/qt/util.h"
|
||||||
#include "selfdrive/ui/qt/widgets/drive_stats.h"
|
|
||||||
#include "system/hardware/hw.h"
|
|
||||||
|
|
||||||
#include <QWebEngineView>
|
|
||||||
|
|
||||||
// HomeWindow: the container for the offroad and onroad UIs
|
// HomeWindow: the container for the offroad and onroad UIs
|
||||||
|
|
||||||
@@ -102,9 +97,9 @@ void HomeWindow::mousePressEvent(QMouseEvent* e) {
|
|||||||
// CLEARPILOT todo - tap on main goes straight to settings
|
// CLEARPILOT todo - tap on main goes straight to settings
|
||||||
// Unless we click a debug widget.
|
// Unless we click a debug widget.
|
||||||
|
|
||||||
// CLEARPILOT - click ready shows home
|
// CLEARPILOT - click ready shows home (no sidebar)
|
||||||
if (!onroad->isVisible() && ready->isVisible()) {
|
if (!onroad->isVisible() && ready->isVisible()) {
|
||||||
sidebar->setVisible(true);
|
sidebar->setVisible(false);
|
||||||
slayout->setCurrentWidget(home);
|
slayout->setCurrentWidget(home);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,132 +114,59 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
|||||||
// const SubMaster &sm = *(uiState()->sm);
|
// const SubMaster &sm = *(uiState()->sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// OffroadHome: the offroad home page
|
// CLEARPILOT: OffroadHome — clean grid launcher
|
||||||
|
|
||||||
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
|
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
|
||||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||||
main_layout->setContentsMargins(40, 40, 40, 40);
|
main_layout->setContentsMargins(80, 80, 80, 80);
|
||||||
|
main_layout->setSpacing(0);
|
||||||
|
|
||||||
// top header
|
// grid of launcher buttons
|
||||||
QHBoxLayout* header_layout = new QHBoxLayout();
|
QGridLayout *grid = new QGridLayout();
|
||||||
header_layout->setContentsMargins(0, 0, 0, 0);
|
grid->setSpacing(40);
|
||||||
header_layout->setSpacing(16);
|
|
||||||
|
|
||||||
update_notif = new QPushButton(tr("UPDATE"));
|
// Dashcam viewer button
|
||||||
update_notif->setVisible(false);
|
QPushButton *dashcam_btn = new QPushButton("Dashcam");
|
||||||
update_notif->setStyleSheet("background-color: #364DEF;");
|
dashcam_btn->setFixedSize(400, 300);
|
||||||
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); });
|
dashcam_btn->setStyleSheet(R"(
|
||||||
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
QPushButton {
|
||||||
|
background-color: #333333;
|
||||||
|
color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #555555;
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
grid->addWidget(dashcam_btn, 0, 0);
|
||||||
|
|
||||||
alert_notif = new QPushButton();
|
// Settings button
|
||||||
alert_notif->setVisible(false);
|
QPushButton *settings_btn = new QPushButton("Settings");
|
||||||
alert_notif->setStyleSheet("background-color: #E22C2C;");
|
settings_btn->setFixedSize(400, 300);
|
||||||
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); });
|
settings_btn->setStyleSheet(R"(
|
||||||
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
QPushButton {
|
||||||
|
background-color: #333333;
|
||||||
|
color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
QPushButton:pressed {
|
||||||
|
background-color: #555555;
|
||||||
|
}
|
||||||
|
)");
|
||||||
|
QObject::connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(); });
|
||||||
|
grid->addWidget(settings_btn, 0, 1);
|
||||||
|
|
||||||
date = new ElidedLabel();
|
main_layout->addStretch();
|
||||||
header_layout->addWidget(date, 0, Qt::AlignHCenter | Qt::AlignLeft);
|
main_layout->addLayout(grid);
|
||||||
|
main_layout->addStretch();
|
||||||
version = new ElidedLabel();
|
|
||||||
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight);
|
|
||||||
|
|
||||||
main_layout->addLayout(header_layout);
|
|
||||||
|
|
||||||
// main content
|
|
||||||
main_layout->addSpacing(25);
|
|
||||||
center_layout = new QStackedLayout();
|
|
||||||
|
|
||||||
QWidget *home_widget = new QWidget(this);
|
|
||||||
{
|
|
||||||
QHBoxLayout *home_layout = new QHBoxLayout(home_widget);
|
|
||||||
home_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
home_layout->setSpacing(30);
|
|
||||||
|
|
||||||
// // // Create a QWebEngineView
|
|
||||||
// QWebEngineView *web_view = new QWebEngineView();
|
|
||||||
// web_view->load(QUrl("http://fark.com"));
|
|
||||||
|
|
||||||
// // Add the QWebEngineView to the layout
|
|
||||||
// home_layout->addWidget(web_view);
|
|
||||||
}
|
|
||||||
center_layout->addWidget(home_widget);
|
|
||||||
|
|
||||||
// add update & alerts widgets
|
|
||||||
update_widget = new UpdateAlert();
|
|
||||||
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
|
|
||||||
center_layout->addWidget(update_widget);
|
|
||||||
alerts_widget = new OffroadAlert();
|
|
||||||
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
|
|
||||||
center_layout->addWidget(alerts_widget);
|
|
||||||
|
|
||||||
main_layout->addLayout(center_layout, 1);
|
|
||||||
|
|
||||||
// set up refresh timer
|
|
||||||
timer = new QTimer(this);
|
|
||||||
timer->callOnTimeout(this, &OffroadHome::refresh);
|
|
||||||
|
|
||||||
setStyleSheet(R"(
|
setStyleSheet(R"(
|
||||||
* {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
OffroadHome {
|
OffroadHome {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
OffroadHome > QPushButton {
|
|
||||||
padding: 15px 30px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
OffroadHome > QLabel {
|
|
||||||
font-size: 55px;
|
|
||||||
}
|
|
||||||
)");
|
)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Refresh data on screen every 5 seconds. */
|
|
||||||
void OffroadHome::showEvent(QShowEvent *event) {
|
|
||||||
refresh();
|
|
||||||
timer->start(5 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void OffroadHome::hideEvent(QHideEvent *event) {
|
|
||||||
timer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void OffroadHome::refresh() {
|
|
||||||
QString model = QString::fromStdString(params.get("ModelName"));
|
|
||||||
|
|
||||||
date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d"));
|
|
||||||
version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + model);
|
|
||||||
|
|
||||||
// bool updateAvailable = update_widget->refresh();
|
|
||||||
|
|
||||||
int alerts = alerts_widget->refresh();
|
|
||||||
|
|
||||||
if (alerts > 0 && !alerts_widget->isVisible()) {
|
|
||||||
alerts_widget->setVisible(true);
|
|
||||||
} else if (alerts == 0 && alerts_widget->isVisible()) {
|
|
||||||
alerts_widget->setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pop-up new notification
|
|
||||||
// CLEARPILOT temp disabled update notifications
|
|
||||||
// int idx = center_layout->currentIndex();
|
|
||||||
// if (!updateAvailable && !alerts && false) {
|
|
||||||
// idx = 0;
|
|
||||||
// } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) {
|
|
||||||
// idx = 1;
|
|
||||||
// } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) {
|
|
||||||
// idx = 2;
|
|
||||||
// }
|
|
||||||
// center_layout->setCurrentIndex(idx);
|
|
||||||
|
|
||||||
// CLEARPILOT temp disabled update notifications
|
|
||||||
// update_notif->setVisible(updateAvailable);
|
|
||||||
// alert_notif->setVisible(alerts);
|
|
||||||
alert_notif->setVisible(false);
|
|
||||||
if (alerts) {
|
|
||||||
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,24 +24,6 @@ public:
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void openSettings(int index = 0, const QString ¶m = "");
|
void openSettings(int index = 0, const QString ¶m = "");
|
||||||
|
|
||||||
private:
|
|
||||||
void showEvent(QShowEvent *event) override;
|
|
||||||
void hideEvent(QHideEvent *event) override;
|
|
||||||
void refresh();
|
|
||||||
|
|
||||||
Params params;
|
|
||||||
|
|
||||||
QTimer* timer;
|
|
||||||
ElidedLabel* version;
|
|
||||||
QStackedLayout* center_layout;
|
|
||||||
UpdateAlert *update_widget;
|
|
||||||
OffroadAlert* alerts_widget;
|
|
||||||
QPushButton* alert_notif;
|
|
||||||
QPushButton* update_notif;
|
|
||||||
|
|
||||||
// FrogPilot variables
|
|
||||||
ElidedLabel* date;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class HomeWindow : public QWidget {
|
class HomeWindow : public QWidget {
|
||||||
|
|||||||
@@ -818,15 +818,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>تحديث</translation>
|
<translation type="vanished">تحديث</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> التنبهات</translation>
|
<translation type="vanished"> التنبهات</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> تنبيه</translation>
|
<translation type="vanished"> تنبيه</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -771,15 +771,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>Aktualisieren</translation>
|
<translation type="vanished">Aktualisieren</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> HINWEISE</translation>
|
<translation type="vanished"> HINWEISE</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> HINWEIS</translation>
|
<translation type="vanished"> HINWEIS</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -814,15 +814,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>MISE À JOUR</translation>
|
<translation type="vanished">MISE À JOUR</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> ALERTES</translation>
|
<translation type="vanished"> ALERTES</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> ALERTE</translation>
|
<translation type="vanished"> ALERTE</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -770,15 +770,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>更新</translation>
|
<translation type="vanished">更新</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> 警告</translation>
|
<translation type="vanished"> 警告</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> 警告</translation>
|
<translation type="vanished"> 警告</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -813,15 +813,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>업데이트</translation>
|
<translation type="vanished">업데이트</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> 알림</translation>
|
<translation type="vanished"> 알림</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> 알림</translation>
|
<translation type="vanished"> 알림</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -814,15 +814,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>ATUALIZAÇÃO</translation>
|
<translation type="vanished">ATUALIZAÇÃO</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> ALERTAS</translation>
|
<translation type="vanished"> ALERTAS</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> ALERTA</translation>
|
<translation type="vanished"> ALERTA</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -813,15 +813,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>อัปเดต</translation>
|
<translation type="vanished">อัปเดต</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> การแจ้งเตือน</translation>
|
<translation type="vanished"> การแจ้งเตือน</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> การแจ้งเตือน</translation>
|
<translation type="vanished"> การแจ้งเตือน</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -770,15 +770,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>GÜNCELLE</translation>
|
<translation type="vanished">GÜNCELLE</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> UYARILAR</translation>
|
<translation type="vanished"> UYARILAR</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> UYARI</translation>
|
<translation type="vanished"> UYARI</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -813,15 +813,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>更新</translation>
|
<translation type="vanished">更新</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> 警报</translation>
|
<translation type="vanished"> 警报</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> 警报</translation>
|
<translation type="vanished"> 警报</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
@@ -813,15 +813,15 @@
|
|||||||
<name>OffroadHome</name>
|
<name>OffroadHome</name>
|
||||||
<message>
|
<message>
|
||||||
<source>UPDATE</source>
|
<source>UPDATE</source>
|
||||||
<translation>更新</translation>
|
<translation type="vanished">更新</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERTS</source>
|
<source> ALERTS</source>
|
||||||
<translation> 提醒</translation>
|
<translation type="vanished"> 提醒</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<source> ALERT</source>
|
<source> ALERT</source>
|
||||||
<translation> 提醒</translation>
|
<translation type="vanished"> 提醒</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
<context>
|
<context>
|
||||||
|
|||||||
Reference in New Issue
Block a user