Release Vulpes OS 2.7-preview.24
|
|
@ -4,9 +4,58 @@
|
|||
|
||||
Updates by date and platform. Within each group: Add, Change, Fix, Note, Test. Entries describe the state at their date; a fix in development requires an updated installed version.
|
||||
|
||||
<a id="2.7-preview.24"></a>
|
||||
|
||||
## 04/11/2026: 2.7-preview.24
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Gaia shutdown animation now shows three blue rings without a background logo.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 24 APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Desktop and Gaia source published in the repository; launch with `./run.sh`.
|
||||
|
||||
### 
|
||||
|
||||
-   : Native package available. Flashing fully erases `userdata`; boot is not qualified.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24)
|
||||
|
||||
<a id="2.7-preview.23"></a>
|
||||
|
||||
## 27/10/2026: 2.7-preview.23 — built, not published
|
||||
|
||||
### All Platforms
|
||||
|
||||
- [Fix][Home] End-of-page spacing, icon editing and web pinning improved.
|
||||
- [Fix][Keyboard] Hiding on focus changes and first-tap search input corrected.
|
||||
- [Fix][Settings] Selectors, shortcuts, permissions and battery estimates improved.
|
||||
- [Fix][Marketplace] Catalogue return restored; unsupported privileged apps now show a clear message.
|
||||
- [Fix][Camera] Photo/video switching and viewfinder recovery improved; hardware qualification remains.
|
||||
- [Fix][System] Gaia screenshots added and shutdown animation no longer has an overlaid logo.
|
||||
|
||||
### Android / APK
|
||||
|
||||
- [Test][Installation] APK installed and launched on Android 16 (API 36); Gaia reached its lock screen. Unlocking and app flows remain to be qualified.
|
||||
|
||||
### Pixel 3a / Sargo
|
||||
|
||||
- [Change][Startup] Boot waits and checks reduced; battery standby qualification remains in progress.
|
||||
- [Note][Testing] System image and RAM boot built and verified; transfer to the Pixel awaits reconnection.
|
||||
|
||||
|
||||
**Limitations :** Nothing was published to Forgejo; reconnect the Pixel for RAM-boot qualification.
|
||||
|
||||
<a id="2.7-preview.22"></a>
|
||||
|
||||
## 22/10/2026 : 2.7-preview.22
|
||||
## 23/10/2026 : 2.7-preview.22
|
||||
|
||||

|
||||
|
||||
|
|
@ -33,7 +82,20 @@ Updates by date and platform. Within each group: Add, Change, Fix, Note, Test. E
|
|||
|
||||
### 
|
||||
|
||||
-   : This release does not include a new Pixel image. Preview 21 remains the latest published image; this Preview's changes are not installed on the phone.
|
||||
-   : Experimental Preview 22 package for Pixel 3a Sargo added to this release. Fastboot wrote the planned partitions and rebooted the phone; Gaia startup is not yet confirmed.
|
||||
-   : Flashing completely erases `userdata`, including the old OS, files, apps and settings. Back up before installation.
|
||||
|
||||
<a id="development-2026-10-26-sargo-startup"></a>
|
||||
|
||||
## 26/10/2026 : Sargo startup and battery life — in development
|
||||
|
||||
### 
|
||||
|
||||
-   : Boot no longer rereads the 3 GiB system image to calculate SHA-256. Provenance remains checked during preparation; boot checks file type and size before mounting read-only. The HAL wait is capped at 8 seconds.
|
||||
-   : Tundra waits for HAL initialization to finish, then enables Qualcomm low-power states. RAM boot v23 confirms `sleep_disabled=N`; CPU0 spent 3.6–4.0 seconds in C3 over 5.2-second samples.
|
||||
-   : Gaia startup no longer waits for the network login barrier.
|
||||
-   : With the backlight off in RAM, `Isolated Web Co` still used 21%, `crtc_commit` 13% and Firefox 10% of one core. Battery was 29.5 °C and CPU around 38 °C; the phone was plugged in, so discharge rate was not measured.
|
||||
-   : Private v23 image booted in RAM without partition writes. Gaia process profiling and a longer unplugged measurement remain.
|
||||
|
||||
<a id="development-2026-10-20-web-nokia"></a>
|
||||
|
||||
|
|
|
|||
66
CHANGELOG.md
|
|
@ -4,9 +4,58 @@
|
|||
|
||||
Les nouveautés, par date et par plateforme. Dans chaque groupe : Add, Change, Fix, Note, Test. Les entrées décrivent la situation à leur date ; une correction en développement nécessite une nouvelle version installée.
|
||||
|
||||
<a id="2.7-preview.24"></a>
|
||||
|
||||
## 04/11/2026 : 2.7-preview.24
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Animation d’arrêt Gaia avec trois anneaux bleus, sans logo en arrière-plan.
|
||||
|
||||
### 
|
||||
|
||||
-   : APK Preview 24.
|
||||
|
||||
### 
|
||||
|
||||
-   : Code Desktop et Gaia publiés dans le dépôt; lancement avec `./run.sh`.
|
||||
|
||||
### 
|
||||
|
||||
-   : Paquet natif disponible. Le flash efface entièrement `userdata`; démarrage non qualifié.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24)
|
||||
|
||||
<a id="2.7-preview.23"></a>
|
||||
|
||||
## 27/10/2026 : 2.7-preview.23 — construite, non publiée
|
||||
|
||||
### Toutes les plateformes
|
||||
|
||||
- [Fix][Accueil] Espacement de fin de page, édition des icônes et épinglage web repris.
|
||||
- [Fix][Clavier] Masquage au changement de focus et premier appui sur la recherche corrigés.
|
||||
- [Fix][Paramètres] Sélecteurs, raccourcis, permissions et estimation batterie améliorés.
|
||||
- [Fix][Marketplace] Retour catalogue restauré; les applications privilégiées non prises en charge sont signalées clairement.
|
||||
- [Fix][Caméra] Cycle photo/vidéo et reprise du viseur fiabilisés; qualification matérielle encore requise.
|
||||
- [Fix][Système] Capture d’écran Gaia et animation d’arrêt sans logo superposé.
|
||||
|
||||
### Android / APK
|
||||
|
||||
- [Test][Installation] APK installée et lancée sur Android 16 (API 36); Gaia a affiché son écran de verrouillage. Déverrouillage et parcours des apps restent à qualifier.
|
||||
|
||||
### Pixel 3a / Sargo
|
||||
|
||||
- [Change][Démarrage] Temps d’attente et vérifications au boot réduits; qualification batterie en veille toujours en cours.
|
||||
- [Note][Essais] Image système et boot RAM construits et vérifiés; transfert au Pixel en attente de sa reconnexion.
|
||||
|
||||
|
||||
**Limites :** Aucun changement Forgejo publié; le Pixel doit être reconnecté pour le démarrage de qualification.
|
||||
|
||||
<a id="2.7-preview.22"></a>
|
||||
|
||||
## 22/10/2026 : 2.7-preview.22
|
||||
## 23/10/2026 : 2.7-preview.22
|
||||
|
||||

|
||||
|
||||
|
|
@ -33,7 +82,20 @@ Les nouveautés, par date et par plateforme. Dans chaque groupe : Add, Change, F
|
|||
|
||||
### 
|
||||
|
||||
-   : Cette release ne contient pas de nouvelle image Pixel. La Preview 21 reste la dernière image publiée ; les changements de cette Preview ne sont pas installés sur le téléphone.
|
||||
-   : Paquet expérimental Preview 22 pour Pixel 3a Sargo ajouté à la release. Le flash a écrit les partitions prévues et redémarré le téléphone ; le démarrage de Gaia n’a pas encore été confirmé.
|
||||
-   : Le flash efface intégralement `userdata`, y compris l’ancien système, les fichiers, applications et réglages. Sauvegarde requise avant installation.
|
||||
|
||||
<a id="development-2026-10-26-sargo-startup"></a>
|
||||
|
||||
## 26/10/2026 : Démarrage Sargo et autonomie — en développement
|
||||
|
||||
### 
|
||||
|
||||
-   : Le boot ne relit plus les 3 Gio de l’image système en SHA-256. La provenance reste contrôlée à la préparation ; au démarrage, taille et type sont vérifiés avant le montage en lecture seule. L’attente HAL est plafonnée à 8 s.
|
||||
-   : Tundra attend la fin d’initialisation du HAL puis active les états basse consommation Qualcomm. Le boot RAM v23 confirme `sleep_disabled=N` ; CPU0 a résidé 3,6 à 4,0 s en C3 sur des fenêtres de 5,2 s.
|
||||
-   : Gaia ne dépend plus de l’attente de session réseau pour démarrer.
|
||||
-   : En RAM, l’écran éteint laisse encore `Isolated Web Co` à 21 %, `crtc_commit` à 13 % et Firefox à 10 % d’un cœur. Batterie à 29,5 °C et CPU autour de 38 °C ; téléphone branché, donc taux de décharge non mesuré.
|
||||
-   : Image v23 privée, démarrée en RAM sans écriture de partition. Le profilage des processus Gaia et la mesure prolongée sans câble restent à faire.
|
||||
|
||||
<a id="development-2026-10-20-web-nokia"></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<p align="center">Firefox OS fork · Open source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Vulpes-2.7%20Preview%2022-0755be?style=flat-square" alt="Vulpes : 2.7 Preview 22"></a>
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Vulpes-2.7%20Preview%2024-0755be?style=flat-square" alt="Vulpes : 2.7 Preview 24"></a>
|
||||
<img src="https://img.shields.io/badge/Gaia-2.7-0755be?style=flat-square" alt="Gaia: 2.7">
|
||||
<img src="https://img.shields.io/badge/Gecko-156.0.1-0099cc?style=flat-square" alt="Gecko: 156.0.1">
|
||||
<img src="https://img.shields.io/badge/Tundra-0.1.0-0099cc?style=flat-square" alt="Tundra: 0.1.0">
|
||||
|
|
@ -38,11 +38,9 @@ Je développe Vulpes principalement seul, sous le nom d’OverSu, avec l’aide
|
|||
|
||||
Sur Pixel 3a, **Tundra démarre directement Gecko et Gaia** et assure leur accès au matériel. Droidian a servi d’environnement de test au début du projet : il n’est pas inclus dans Vulpes OS et Tundra ne repose pas dessus.
|
||||
|
||||
### Preview 22 · 22 octobre 2026
|
||||
### Preview 24 · publiée
|
||||
|
||||
[Télécharger l’APK Preview 22](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22). Les corrections Gaia concernent le partage du navigateur et la saisie web ; les contrôles PC ont réussi. La release comprend aussi les sources Tundra/Nokia actuelles. L’installation et la mise à jour ont réussi sur émulateur Android 11 ; les téléphones Android réels restent à qualifier. Le clavier T9 du Nokia demande aussi un essai physique.
|
||||
|
||||
Une image expérimentale pour le Pixel 3a (Sargo) est jointe à la Preview 22. Elle a été flashée, mais son démarrage n’est pas encore qualifié. **Le flash efface toutes les données de `userdata`, y compris l’ancien système, les fichiers, applications et réglages. Sauvegardez avant l’installation.** Consultez la [release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22) et le [changelog](CHANGELOG.md).
|
||||
La Preview 24 réunit les sources Desktop, l’APK et une image expérimentale pour Pixel 3a. Le démarrage de cette image n’est pas qualifié; son installation efface entièrement `userdata`. Consultez le [changelog](CHANGELOG.md) et les avertissements de la [release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24) avant toute installation.
|
||||
|
||||
PC/Sargo utilisent Gecko **156.0.1** ; Android utilise GeckoView **156.0**. Android 8 / API 26 minimum est déclaré. L’APK est un build de développement signé, avec débogage via ADB autorisé.
|
||||
### Linux
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<p align="center">Firefox OS fork · Open source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Vulpes-2.7%20Preview%2022-0755be?style=flat-square" alt="Vulpes: 2.7 Preview 22"></a>
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Vulpes-2.7%20Preview%2024-0755be?style=flat-square" alt="Vulpes: 2.7 Preview 24"></a>
|
||||
<img src="https://img.shields.io/badge/Gaia-2.7-0755be?style=flat-square" alt="Gaia: 2.7">
|
||||
<img src="https://img.shields.io/badge/Gecko-156.0.1-0099cc?style=flat-square" alt="Gecko: 156.0.1">
|
||||
<img src="https://img.shields.io/badge/Tundra-0.1.0-0099cc?style=flat-square" alt="Tundra: 0.1.0">
|
||||
|
|
@ -38,11 +38,9 @@ I’m OverSu, and I develop Vulpes mostly on my own, with help from people who t
|
|||
|
||||
On the Pixel 3a, **Tundra boots Gecko and Gaia directly** and provides access to the hardware. Droidian was used as a testing environment early in development. It is not included in Vulpes OS, and Tundra is not based on it.
|
||||
|
||||
### Preview 22 · 22 October 2026
|
||||
### Preview 24 · released
|
||||
|
||||
[Download the Preview 22 APK](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22). Shared Gaia fixes cover browser sharing and web input; PC checks passed. The release also includes the current Tundra/Nokia sources. Android 11 emulator installation and upgrade passed; physical Android devices still need qualification. The Nokia T9 keyboard also needs physical qualification.
|
||||
|
||||
An experimental Pixel 3a (Sargo) image is attached to Preview 22. It was flashed, but boot is not yet qualified. **Flashing erases all data in `userdata`, including the existing OS, files, apps and settings. Back up first.** See the [release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22) and [changelog](CHANGELOG.en.md) for details.
|
||||
Preview 24 includes the Desktop source, APK and an experimental Pixel 3a image. Boot is not qualified; installation fully erases `userdata`. Read the [changelog](CHANGELOG.en.md) and the [release warnings](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24) before installing.
|
||||
|
||||
Desktop/Sargo use Gecko **156.0.1**; Android uses GeckoView **156.0**. Android 8 / API 26 minimum is declared. The APK is a signed development build with debugging available through authorized ADB.
|
||||
### Linux
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public final class MainActivity extends Activity {
|
|||
|
||||
void updateTrackingProtection(boolean enabled) {
|
||||
if (gaia != null) gaia.getSettings().setUseTrackingProtection(enabled);
|
||||
if (web != null) web.getSettings().setUseTrackingProtection(enabled);
|
||||
for (GeckoSession session : webTabs) session.getSettings().setUseTrackingProtection(enabled);
|
||||
}
|
||||
|
||||
private VulpesApplication app;
|
||||
|
|
@ -26,8 +26,12 @@ public final class MainActivity extends Activity {
|
|||
private LinearLayout root, webPanel;
|
||||
private TextView notice;
|
||||
private EditText address;
|
||||
private Button tabsButton;
|
||||
private Button home;
|
||||
private boolean webBack;
|
||||
private final java.util.List<GeckoSession> webTabs = new ArrayList<>();
|
||||
private final java.util.List<String> webUrls = new ArrayList<>();
|
||||
private final java.util.List<Boolean> webCanGoBack = new ArrayList<>();
|
||||
private boolean resumed, recoverGaia, recoverWeb;
|
||||
private long lastCrash;
|
||||
private int crashCount;
|
||||
|
|
@ -95,6 +99,13 @@ public final class MainActivity extends Activity {
|
|||
address.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
address.setTextSize(14);
|
||||
nav.addView(address, new LinearLayout.LayoutParams(0, -1, 1));
|
||||
tabsButton = new Button(this);
|
||||
tabsButton.setText("▢ 0");
|
||||
tabsButton.setContentDescription("Onglets");
|
||||
tabsButton.setMinWidth(0);
|
||||
tabsButton.setPadding(dp(8), 0, dp(8), 0);
|
||||
nav.addView(tabsButton, new LinearLayout.LayoutParams(dp(64), -1));
|
||||
tabsButton.setOnClickListener(v -> showBrowserTabs());
|
||||
address.setOnEditorActionListener((v, id, event) -> {
|
||||
String url = address.getText().toString().trim();
|
||||
openBrowser(url.contains("://") ? url : "https://" + url);
|
||||
|
|
@ -221,56 +232,99 @@ public final class MainActivity extends Activity {
|
|||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
if (web == null) {
|
||||
web = new GeckoSession(new GeckoSessionSettings.Builder()
|
||||
.useTrackingProtection(app.trackingProtection).build());
|
||||
web.setContentDelegate(content);
|
||||
web.setPromptDelegate(prompts);
|
||||
web.setPermissionDelegate(permissions);
|
||||
web.setNavigationDelegate(
|
||||
new GeckoSession.NavigationDelegate() {
|
||||
@Override
|
||||
public void onCanGoBack(GeckoSession s, boolean can) {
|
||||
webBack = can;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChange(
|
||||
GeckoSession s,
|
||||
String url,
|
||||
java.util.List<GeckoSession.PermissionDelegate.ContentPermission> permissions,
|
||||
Boolean gesture
|
||||
) {
|
||||
address.setText(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeckoResult<AllowOrDeny> onLoadRequest(GeckoSession s, LoadRequest r) {
|
||||
return GeckoResult.fromValue(
|
||||
(r.uri.startsWith("https://") || r.uri.startsWith("http://")) && !isGaia(r.uri)
|
||||
? AllowOrDeny.ALLOW
|
||||
: AllowOrDeny.DENY
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeckoResult<GeckoSession> onNewSession(GeckoSession s, String uri) {
|
||||
openBrowser(uri);
|
||||
return GeckoResult.fromValue(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
web.open(app.runtime);
|
||||
webView.setSession(web);
|
||||
if (web == null) newBrowserTab(url);
|
||||
else {
|
||||
int index = webTabs.indexOf(web);
|
||||
if (index >= 0) webUrls.set(index, url);
|
||||
web.loadUri(url);
|
||||
updateTabsButton();
|
||||
}
|
||||
gaiaView.setVisibility(View.GONE);
|
||||
webPanel.setVisibility(View.VISIBLE);
|
||||
setHomeSurface(false);
|
||||
address.setText(url);
|
||||
web.loadUri(url);
|
||||
syncActive();
|
||||
}
|
||||
|
||||
private GeckoSession createBrowserSession() {
|
||||
GeckoSession session = new GeckoSession(new GeckoSessionSettings.Builder()
|
||||
.useTrackingProtection(app.trackingProtection).build());
|
||||
session.setContentDelegate(content);
|
||||
session.setPromptDelegate(prompts);
|
||||
session.setPermissionDelegate(permissions);
|
||||
session.setNavigationDelegate(new GeckoSession.NavigationDelegate() {
|
||||
@Override public void onCanGoBack(GeckoSession s, boolean can) {
|
||||
int index = webTabs.indexOf(s);
|
||||
if (index < 0) return;
|
||||
webCanGoBack.set(index, can);
|
||||
if (s == web) webBack = can;
|
||||
}
|
||||
@Override public void onLocationChange(GeckoSession s, String url,
|
||||
java.util.List<GeckoSession.PermissionDelegate.ContentPermission> permissions,
|
||||
Boolean gesture) {
|
||||
int index = webTabs.indexOf(s);
|
||||
if (index >= 0) webUrls.set(index, url);
|
||||
if (s == web) address.setText(url);
|
||||
updateTabsButton();
|
||||
}
|
||||
@Override public GeckoResult<AllowOrDeny> onLoadRequest(GeckoSession s, LoadRequest r) {
|
||||
return GeckoResult.fromValue(
|
||||
(r.uri.startsWith("https://") || r.uri.startsWith("http://")) && !isGaia(r.uri)
|
||||
? AllowOrDeny.ALLOW : AllowOrDeny.DENY);
|
||||
}
|
||||
@Override public GeckoResult<GeckoSession> onNewSession(GeckoSession s, String uri) {
|
||||
newBrowserTab(uri);
|
||||
return GeckoResult.fromValue(null);
|
||||
}
|
||||
});
|
||||
session.open(app.runtime);
|
||||
return session;
|
||||
}
|
||||
|
||||
private void newBrowserTab(String url) {
|
||||
if (web != null) web.setActive(false);
|
||||
web = createBrowserSession();
|
||||
webTabs.add(web);
|
||||
webUrls.add(url);
|
||||
webCanGoBack.add(false);
|
||||
webView.setSession(web);
|
||||
webBack = false;
|
||||
address.setText(url);
|
||||
web.loadUri(url);
|
||||
updateTabsButton();
|
||||
syncActive();
|
||||
}
|
||||
|
||||
private void showBrowserTabs() {
|
||||
java.util.List<String> choices = new ArrayList<>();
|
||||
choices.add("+ Nouvel onglet");
|
||||
for (int i = 0; i < webUrls.size(); i++) {
|
||||
String url = webUrls.get(i);
|
||||
choices.add((webTabs.get(i) == web ? "✓ " : "") +
|
||||
(url == null || url.isEmpty() ? "Nouvel onglet" : url));
|
||||
}
|
||||
new AlertDialog.Builder(this).setTitle("Onglets")
|
||||
.setItems(choices.toArray(new String[0]), (dialog, which) -> {
|
||||
if (which == 0) newBrowserTab("https://duckduckgo.com/");
|
||||
else selectBrowserTab(which - 1);
|
||||
}).setNegativeButton("Fermer", null).show();
|
||||
}
|
||||
|
||||
private void selectBrowserTab(int index) {
|
||||
if (index < 0 || index >= webTabs.size()) return;
|
||||
if (web != null) web.setActive(false);
|
||||
web = webTabs.get(index);
|
||||
webView.setSession(web);
|
||||
webBack = webCanGoBack.get(index);
|
||||
address.setText(webUrls.get(index));
|
||||
updateTabsButton();
|
||||
syncActive();
|
||||
}
|
||||
|
||||
private void updateTabsButton() {
|
||||
if (tabsButton != null) tabsButton.setText("▢ " + webTabs.size());
|
||||
}
|
||||
|
||||
void setHomeSurface(boolean visible) {
|
||||
runOnUiThread(() -> {
|
||||
boolean overlay = visible && webPanel.getVisibility() != View.VISIBLE;
|
||||
|
|
@ -334,7 +388,8 @@ public final class MainActivity extends Activity {
|
|||
private void syncActive() {
|
||||
boolean browsing = webPanel != null && webPanel.getVisibility() == View.VISIBLE;
|
||||
if (gaia != null && gaia.isOpen()) gaia.setActive(resumed && !browsing);
|
||||
if (web != null && web.isOpen()) web.setActive(resumed && browsing);
|
||||
for (GeckoSession session : webTabs)
|
||||
if (session.isOpen()) session.setActive(resumed && browsing && session == web);
|
||||
}
|
||||
|
||||
private void recover(GeckoSession session, boolean crashed) {
|
||||
|
|
@ -442,7 +497,7 @@ public final class MainActivity extends Activity {
|
|||
app.gaiaSession = null;
|
||||
}
|
||||
}
|
||||
if (web != null) web.close();
|
||||
for (GeckoSession session : webTabs) session.close();
|
||||
if (fileResult != null) {
|
||||
fileResult.complete(filePrompt.dismiss());
|
||||
fileResult = null;
|
||||
|
|
|
|||
|
|
@ -794,21 +794,21 @@ function fetchIconBlob(iconUrl){return new Promise((resolve,reject)=>{var xhr=ne
|
|||
reject(new Error(`Got HTTP status ${xhr.status} trying to load ${iconUrl}.`));};xhr.onerror=xhr.ontimeout=()=>{reject(new Error(`Error while getting ${iconUrl}.`));};});}
|
||||
exports.IconsHelper={getIcon:getIcon,getIconBlob:getIconBlob,setElementIcon:setElementIcon,getBestIconFromWebManifest:getBestIconFromWebManifest,getBestIconFromMetaTags:getBestIconFromMetaTags,fetchIcon:fetchIcon,fetchIconBlob:fetchIconBlob,get defaultIconSize(){return getDefaultIconSize();},clear:clear,getNearestSize:getNearestSize,};})(window);;'use strict';(function(exports){const SETTINGS_STORE='homescreen_settings';const SETTINGS_VERSION=0;const COLUMNS_SETTING='grid.cols';const PAGING_SETTING='grid.paging';function Settings(){this.small=false;this.scrollSnapping=false;this.firstRun=false;if(navigator.getDataStores){navigator.getDataStores(SETTINGS_STORE).then(stores=>{if(stores.length<1){return Promise.reject('Settings datastore inaccessible');}
|
||||
var signalChange=()=>{this.save();window.dispatchEvent(new CustomEvent('settings-changed'));};var syncSmallSetting=(signal)=>{stores[0].get(COLUMNS_SETTING).then(cols=>{var oldSmall=this.small;this.small=cols>3;if(this.small!==oldSmall){signalChange();}});};var syncPagingSetting=()=>{stores[0].get(PAGING_SETTING).then(paging=>{var oldSnapping=this.scrollSnapping;this.scrollSnapping=paging||false;if(this.scrollSnapping!==oldSnapping){signalChange();}});};stores[0].addEventListener('change',e=>{switch(e.id){case COLUMNS_SETTING:syncSmallSetting();break;case PAGING_SETTING:syncPagingSetting();break;}});syncSmallSetting();syncPagingSetting();},e=>{console.error('Error retrieving home screen settings datastore:',e);});}else{console.error('Datastore API unavailable');}
|
||||
var settingsString=localStorage.getItem('settings');if(!settingsString){this.firstRun=true;return;}
|
||||
var settingsString;try{settingsString=localStorage.getItem('settings');}catch(e){console.error('Home screen local settings unavailable:',e);return;}if(!settingsString){this.firstRun=true;return;}
|
||||
var settings=JSON.parse(settingsString);if(settings.version!==SETTINGS_VERSION){return;}
|
||||
this.small=settings.small||false;this.scrollSnapping=settings.scrollSnapping||false;if(!navigator.getDataStores){console.error('Datastore API unavailable');return;}}
|
||||
Settings.prototype={save:function(){localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}};exports.Settings=Settings;}(window));;'use strict';(function(exports){const DB_NAME='home-metadata';const DB_ORDER_STORE='order';const DB_ICON_STORE='icon';const DB_VERSION=1;function AppsMetadata(){}
|
||||
Settings.prototype={save:function(){try{localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}catch(e){console.error('Home screen settings could not be saved:',e);}}};exports.Settings=Settings;}(window));;'use strict';(function(exports){const DB_NAME='home-metadata';const DB_ORDER_STORE='order';const DB_ICON_STORE='icon';const DB_VERSION=1;function AppsMetadata(){}
|
||||
AppsMetadata.prototype={db:null,init:function(){return new Promise((resolve,reject)=>{var req=window.indexedDB.open(DB_NAME,DB_VERSION);req.onupgradeneeded=this.upgradeSchema;req.onsuccess=(e)=>{this.db=e.target.result;resolve();};req.onerror=(e)=>{console.error('Error opening homescreen metadata db',e);reject(e);};});},upgradeSchema:function(e){var db=e.target.result;var fromVersion=e.oldVersion;if(fromVersion<1){var store=db.createObjectStore(DB_ORDER_STORE,{keyPath:'id'});store.createIndex('order','order',{unique:false});store=db.createObjectStore(DB_ICON_STORE,{keyPath:'id'});store.createIndex('icon','icon',{unique:false});}},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([DB_ORDER_STORE,DB_ICON_STORE],'readwrite');for(var entry of data){if(!entry.id){continue;}
|
||||
if(typeof entry.order!=='undefined'){txn.objectStore(DB_ORDER_STORE).put({id:entry.id,order:entry.order});}
|
||||
if(typeof entry.icon!=='undefined'){txn.objectStore(DB_ICON_STORE).put({id:entry.id,icon:entry.icon});}}
|
||||
txn.oncomplete=resolve;txn.onerror=reject;});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([DB_ORDER_STORE,DB_ICON_STORE],'readwrite');txn.objectStore(DB_ORDER_STORE).delete(id);txn.objectStore(DB_ICON_STORE).delete(id);txn.oncomplete=resolve;txn.onerror=reject;});},getAll:function(onResult){return new Promise((resolve,reject)=>{var txn=this.db.transaction([DB_ORDER_STORE,DB_ICON_STORE],'readonly');var orderStore=txn.objectStore(DB_ORDER_STORE);var iconStore=txn.objectStore(DB_ICON_STORE);var cursor=orderStore.index('order').openCursor();var results=[];cursor.onsuccess=e=>{var cursor=e.target.result;if(cursor){var result=cursor.value;var iconRequest=iconStore.get(result.id);iconRequest.onsuccess=function(result,e){if(e.target.result){result.icon=e.target.result.icon;}
|
||||
results.push(result);if(onResult){onResult(result);}}.bind(this,result);cursor.continue();}};txn.oncomplete=()=>{resolve(results);};});}};exports.AppsMetadata=AppsMetadata;}(window));;'use strict';(function(exports){function Datastore(name){this.name=name;}
|
||||
Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString=localStorage.getItem(this.revisionName);if(revisionString){this.lastRevision=JSON.parse(revisionString);}
|
||||
Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString;try{revisionString=localStorage.getItem(this.revisionName);}catch(e){console.error('Homescreen datastore revision unavailable:',e);}if(revisionString){this.lastRevision=JSON.parse(revisionString);}
|
||||
return new Promise((resolve,reject)=>{var req=window.indexedDB.open(this.idbName,this.DB_VERSION);req.onupgradeneeded=this.upgradeSchema.bind(this);req.onsuccess=(e)=>{this.db=e.target.result;resolve();if(!navigator.getDataStores){console.error('DataStore API is unavailable');return;}
|
||||
navigator.getDataStores(this.name).then((stores)=>{if(stores.length<1){console.error(this.name+' inaccessible');return;}
|
||||
this.datastore=stores[0];this.datastore.addEventListener('change',this.onChange.bind(this));this.synchronise();},(e)=>{console.error('Error getting datastore',e);});};req.onerror=(e)=>{console.error('Error opening datastore mirror database',e);reject(e);};});},upgradeSchema:function(e){var db=e.target.result;var fromVersion=e.oldVersion;if(fromVersion<1){var store=db.createObjectStore(this.storeName,{keyPath:'id'});store.createIndex('data','data',{unique:false});}},synchronise:function(){this._syncQueue=(this._syncQueue||Promise.resolve()).then(()=>this._synchronise());return this._syncQueue;},_synchronise:function(){return new Promise((resolve,reject)=>{var cursor=this.datastore.sync(this.lastRevision);var self=this;function cursorResolve(task){var promises=[];switch(task.operation){case'update':case'add':promises.push(self.set(task.data));break;case'remove':promises.push(self.remove(task.id));break;case'clear':promises.push(self.clear());break;case'done':self.updateRevision();resolve();return;}
|
||||
promises.push(cursor.next());Promise.all(promises).then((results)=>{cursorResolve(results.pop());},reject);}
|
||||
cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
|
||||
cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;try{localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));}catch(e){console.error('Homescreen datastore revision could not be saved:',e);}},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
|
||||
cursor.continue();}};});}};exports.Datastore=Datastore;}(window));;'use strict';(function(exports){function PagesStore(name){this.name=name;}
|
||||
PagesStore.prototype=Object.create(Datastore.prototype);PagesStore.prototype.DB_VERSION=1;PagesStore.prototype.set=function(data){data.id=data.url;return Datastore.prototype.set.call(this,data);};PagesStore.prototype.getAll=function(){var filter=(value)=>value.data&&value.data.pinned;return Datastore.prototype.getAll.call(this,filter);};exports.PagesStore=PagesStore;}(window));;'use strict';(function(exports){const PAGES_ICON_SIZE=30;function Pages(){this.panel=document.getElementById('pages-panel');this.panels=document.getElementById('panels');this.pages=document.getElementById('pages');this.scrollable=document.querySelector('#pages-panel > .scrollable');this.bottombar=document.getElementById('bottombar');this.remove=document.getElementById('remove');this.done=document.getElementById('done');this.empty=true;this.editMode=false;this.dialogs=[];this.pages.addEventListener('click',this);this.pages.addEventListener('contextmenu',this);this.pages.addEventListener('keydown',this);window.addEventListener('resize',this);this.done.addEventListener('click',e=>{e.preventDefault();this.exitEditMode();});this.remove.addEventListener('click',e=>{e.preventDefault();this.unpinSelectedCard();});this.pagesStore=new PagesStore('places');this.pagesStore.init().then(()=>{document.addEventListener('places-set',(e)=>{var id=e.detail.id;this.pagesStore.get(id).then((page)=>{for(var child of this.pages.children){if(child.dataset.id===id){if(!page.data.pinned){this.removeCard(child);}else{this.updatePinnedPage(child,page.data);}
|
||||
return;}}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
;'use strict';(function(exports){function Datastore(name){this.name=name;}
|
||||
Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString=localStorage.getItem(this.revisionName);if(revisionString){this.lastRevision=JSON.parse(revisionString);}
|
||||
Datastore.prototype={name:'',DB_VERSION:1,datastore:null,lastRevision:null,db:null,get revisionName(){return this.name+'_revision';},get idbName(){return this.name+'_mirror';},get storeName(){return this.name+'_store';},init:function(){var revisionString;try{revisionString=localStorage.getItem(this.revisionName);}catch(e){console.error('Homescreen datastore revision unavailable:',e);}if(revisionString){this.lastRevision=JSON.parse(revisionString);}
|
||||
return new Promise((resolve,reject)=>{var req=window.indexedDB.open(this.idbName,this.DB_VERSION);req.onupgradeneeded=this.upgradeSchema.bind(this);req.onsuccess=(e)=>{this.db=e.target.result;resolve();if(!navigator.getDataStores){console.error('DataStore API is unavailable');return;}
|
||||
navigator.getDataStores(this.name).then((stores)=>{if(stores.length<1){console.error(this.name+' inaccessible');return;}
|
||||
this.datastore=stores[0];this.datastore.addEventListener('change',this.onChange.bind(this));this.synchronise();},(e)=>{console.error('Error getting datastore',e);});};req.onerror=(e)=>{console.error('Error opening datastore mirror database',e);reject(e);};});},upgradeSchema:function(e){var db=e.target.result;var fromVersion=e.oldVersion;if(fromVersion<1){var store=db.createObjectStore(this.storeName,{keyPath:'id'});store.createIndex('data','data',{unique:false});}},synchronise:function(){this._syncQueue=(this._syncQueue||Promise.resolve()).then(()=>this._synchronise());return this._syncQueue;},_synchronise:function(){return new Promise((resolve,reject)=>{var cursor=this.datastore.sync(this.lastRevision);var self=this;function cursorResolve(task){var promises=[];switch(task.operation){case'update':case'add':promises.push(self.set(task.data));break;case'remove':promises.push(self.remove(task.id));break;case'clear':promises.push(self.clear());break;case'done':self.updateRevision();resolve();return;}
|
||||
promises.push(cursor.next());Promise.all(promises).then((results)=>{cursorResolve(results.pop());},reject);}
|
||||
cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
|
||||
cursor.next().then(cursorResolve,reject);});},set:function(data){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(data,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-set',{detail:{id:data.id}}));}.bind(this,data,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).put({id:data.id,data:data});}catch(e){console.error('Error putting data in '+this.idbName+':',e);resolve();}});},remove:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(id,resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-removed',{detail:{id:id}}));}.bind(this,id,resolve);txn.onerror=reject;try{txn.objectStore(this.storeName).delete(id);}catch(e){console.error('Error deleting data from '+this.idbName+':',e);resolve();}});},clear:function(){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readwrite');txn.oncomplete=function onComplete(resolve){resolve();document.dispatchEvent(new CustomEvent(this.name+'-cleared'));}.bind(this,resolve);txn.onerror=reject;txn.objectStore(this.storeName).clear();});},updateRevision:function(){this.lastRevision=this.datastore.revisionId;try{localStorage.setItem(this.revisionName,JSON.stringify(this.lastRevision));}catch(e){console.error('Homescreen datastore revision could not be saved:',e);}},onChange:function(e){this.synchronise().then(()=>{},(error)=>{console.error('Failed to handle '+this.name+' change',error);});},get:function(id){return new Promise((resolve,reject)=>{var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.objectStore(this.storeName).get(id).onsuccess=(event)=>{resolve(event.target.result);};});},getAll:function(filter=()=>true){return new Promise((resolve,reject)=>{var results=[];var txn=this.db.transaction([this.storeName],'readonly');txn.onerror=reject;txn.oncomplete=()=>{resolve(results);};txn.objectStore(this.storeName).openCursor().onsuccess=(event)=>{var cursor=event.target.result;if(cursor){if(filter(cursor.value)){results.push(cursor.value);}
|
||||
cursor.continue();}};});}};exports.Datastore=Datastore;}(window));
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
;'use strict';(function(exports){const SETTINGS_STORE='homescreen_settings';const SETTINGS_VERSION=0;const COLUMNS_SETTING='grid.cols';const PAGING_SETTING='grid.paging';function Settings(){this.small=false;this.scrollSnapping=false;this.firstRun=false;if(navigator.getDataStores){navigator.getDataStores(SETTINGS_STORE).then(stores=>{if(stores.length<1){return Promise.reject('Settings datastore inaccessible');}
|
||||
var signalChange=()=>{this.save();window.dispatchEvent(new CustomEvent('settings-changed'));};var syncSmallSetting=(signal)=>{stores[0].get(COLUMNS_SETTING).then(cols=>{var oldSmall=this.small;this.small=cols>3;if(this.small!==oldSmall){signalChange();}});};var syncPagingSetting=()=>{stores[0].get(PAGING_SETTING).then(paging=>{var oldSnapping=this.scrollSnapping;this.scrollSnapping=paging||false;if(this.scrollSnapping!==oldSnapping){signalChange();}});};stores[0].addEventListener('change',e=>{switch(e.id){case COLUMNS_SETTING:syncSmallSetting();break;case PAGING_SETTING:syncPagingSetting();break;}});syncSmallSetting();syncPagingSetting();},e=>{console.error('Error retrieving home screen settings datastore:',e);});}else{console.error('Datastore API unavailable');}
|
||||
var settingsString=localStorage.getItem('settings');if(!settingsString){this.firstRun=true;return;}
|
||||
var settingsString;try{settingsString=localStorage.getItem('settings');}catch(e){console.error('Home screen local settings unavailable:',e);return;}if(!settingsString){this.firstRun=true;return;}
|
||||
var settings=JSON.parse(settingsString);if(settings.version!==SETTINGS_VERSION){return;}
|
||||
this.small=settings.small||false;this.scrollSnapping=settings.scrollSnapping||false;if(!navigator.getDataStores){console.error('Datastore API unavailable');return;}}
|
||||
Settings.prototype={save:function(){localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}};exports.Settings=Settings;}(window));
|
||||
Settings.prototype={save:function(){try{localStorage.setItem('settings',JSON.stringify({version:SETTINGS_VERSION,small:this.small,scrollSnapping:this.scrollSnapping}));}catch(e){console.error('Home screen settings could not be saved:',e);}}};exports.Settings=Settings;}(window));
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
'The catalogue is closed. You can reopen it below.'));
|
||||
document.getElementById('open').hidden=true;
|
||||
});
|
||||
function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');}
|
||||
let catalogueUrl='';
|
||||
function browse(reload=false){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';const next='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');if(reload||!catalogueUrl){catalogueUrl=next;frame.src=next;}}
|
||||
function status(value){frame.style.visibility='hidden';message.hidden=false;document.getElementById('status').textContent=value;}
|
||||
async function prepare(url){
|
||||
if(busy)return;const current=++attempt;busy=true;prepared=null;installed=null;document.getElementById('open').hidden=true;
|
||||
|
|
@ -22,7 +23,10 @@
|
|||
document.getElementById('question').textContent=text('Installer cette application archivée ?','Install this archived application?')+' ('+(prepared.bytes/1048576).toFixed(1)+' Mio)';
|
||||
document.getElementById('permissions').textContent=text('Elle sera exécutée comme une application web, sans accès aux fonctions système. Certains anciens services peuvent ne plus fonctionner.','It will run as a web application without system access. Some old services may no longer work.');
|
||||
message.hidden=true;dialog.hidden=false;
|
||||
}catch(error){if(current!==attempt)return;status(text('Installation impossible : ','Cannot install: ')+error.message);busy=false;}
|
||||
}catch(error){if(current!==attempt)return;status(error.message==='PRIVILEGED_APP_UNSUPPORTED'?
|
||||
text('Cette application demande des API privilégiées qui ne sont pas disponibles dans Vulpes OS.',
|
||||
'This app requires privileged APIs that are not available in Vulpes OS.'):
|
||||
text('Installation impossible : ','Cannot install: ')+error.message);busy=false;}
|
||||
}
|
||||
dialog.addEventListener('submit',async event=>{event.preventDefault();if(!prepared)return;const token=prepared.token;prepared=null;dialog.hidden=true;status(text('Installation…','Installing…'));
|
||||
try {installed=await VulpesCompat.call('marketplace.install',{token});status(text('Application ajoutée à l’accueil.','Application added to Home.'));document.getElementById('open').hidden=false;}
|
||||
|
|
@ -32,7 +36,7 @@
|
|||
document.getElementById('cancel').onclick=cancel;
|
||||
document.getElementById('browse').onclick=cancel;
|
||||
document.getElementById('open').onclick=()=>installed && VulpesCompat.call('apps.launch',{manifestURL:installed.manifestURL});
|
||||
addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse();});
|
||||
addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse(true);});
|
||||
browse();
|
||||
setInterval(async()=>{if(busy)return;try{const url=await VulpesCompat.call('marketplace.poll');if(url)await prepare(url);}catch(error){console.error(error);}},750);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -10,152 +10,20 @@
|
|||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"name": "مُتصفّح",
|
||||
"description": "مُتصفّح نظام Gaia"
|
||||
},
|
||||
"bg": {
|
||||
"name": "Браузър",
|
||||
"description": "Системен уеб браузър на Gaia"
|
||||
},
|
||||
"bn-BD": {
|
||||
"name": "ব্রাউজার",
|
||||
"description": "গায়া সিস্টেম ওয়েব ব্রাউজার"
|
||||
},
|
||||
"ca": {
|
||||
"name": "Navegador",
|
||||
"description": "Navegador web del sistema del Gaia"
|
||||
},
|
||||
"cs": {
|
||||
"name": "Prohlížeč",
|
||||
"description": "Vyhledávání"
|
||||
},
|
||||
"da": {
|
||||
"name": "Browser",
|
||||
"description": "Gaia system-webbrowser"
|
||||
},
|
||||
"de": {
|
||||
"name": "Browser",
|
||||
"description": "System-Webbrowser für Gaia"
|
||||
},
|
||||
"el": {
|
||||
"name": "Περιηγητής",
|
||||
"description": "Περιηγητής Ιστού του Gaia"
|
||||
"description": "Gaia System Web Browser"
|
||||
},
|
||||
"en-US": {
|
||||
"name": "Browser",
|
||||
"description": "Gaia System Web Browser"
|
||||
},
|
||||
"es": {
|
||||
"name": "Navegador",
|
||||
"description": "Navegador del sistema Gaia"
|
||||
},
|
||||
"eu": {
|
||||
"name": "Nabigatzailea",
|
||||
"description": "Gaia, sistemaren web nagibatzailea"
|
||||
},
|
||||
"fr": {
|
||||
"name": "Navigateur",
|
||||
"description": "Navigateur web système de Gaia"
|
||||
},
|
||||
"gl": {
|
||||
"name": "Navegador",
|
||||
"description": "Navegador web do sistema Gaia"
|
||||
},
|
||||
"hr": {
|
||||
"zh-TW": {
|
||||
"name": "Browser",
|
||||
"description": "Gaia System Web Browser"
|
||||
},
|
||||
"hu": {
|
||||
"name": "Böngésző",
|
||||
"description": "Gaia webbögésző"
|
||||
},
|
||||
"it": {
|
||||
"name": "Browser",
|
||||
"description": "Browser di sistema di Gaia"
|
||||
},
|
||||
"ja": {
|
||||
"name": "ブラウザ",
|
||||
"description": "Gaia システムの Web ブラウザ"
|
||||
},
|
||||
"km": {
|
||||
"name": "Browser",
|
||||
"description": "កម្មវិធីអ៊ីនធឺណិតជាប្រព័ន្ធ Gaia"
|
||||
},
|
||||
"ko": {
|
||||
"name": "브라우저",
|
||||
"description": "가이아 시스템 웹 브라우저"
|
||||
},
|
||||
"lt": {
|
||||
"name": "Naršyklė",
|
||||
"description": "„Gaia“ saityno naršyklė"
|
||||
},
|
||||
"mk": {
|
||||
"name": "Пребарување",
|
||||
"description": "Резултати од пребарување"
|
||||
},
|
||||
"ms": {
|
||||
"name": "Pelayar",
|
||||
"description": "Pelayar Web Sistem Gaia"
|
||||
},
|
||||
"ne-NP": {
|
||||
"name": "ब्राउजर",
|
||||
"description": "गैया प्रणाली वेब ब्राउजर"
|
||||
},
|
||||
"nl": {
|
||||
"name": "Browser",
|
||||
"description": "Gaia-systeemwebbrowser"
|
||||
},
|
||||
"pa": {
|
||||
"name": "ਖੋਜ ਨਤੀਜੇ",
|
||||
"description": "ਖੋਜ ਨਤੀਜੇ"
|
||||
},
|
||||
"pl": {
|
||||
"name": "Firefox",
|
||||
"description": "Systemowa przeglądarka"
|
||||
},
|
||||
"pt-BR": {
|
||||
"name": "Navegador",
|
||||
"description": "Navegador do Gaia"
|
||||
},
|
||||
"ro": {
|
||||
"name": "Browser",
|
||||
"description": "Browser web în sistemul Gaia"
|
||||
},
|
||||
"ru": {
|
||||
"name": "Браузер",
|
||||
"description": "Системный веб-браузер Gaia"
|
||||
},
|
||||
"sk": {
|
||||
"name": "Prehliadač",
|
||||
"description": "Webový prehliadač Gaia"
|
||||
},
|
||||
"sq": {
|
||||
"name": "Shfletues",
|
||||
"description": "Shfletuesi Web i Sistemit Gaia"
|
||||
},
|
||||
"sr-Cyrl": {
|
||||
"name": "Прегледач",
|
||||
"description": "Gaia веб прегледач"
|
||||
},
|
||||
"sr-Latn": {
|
||||
"name": "Pregledač",
|
||||
"description": "Gaia veb pregledač"
|
||||
},
|
||||
"sv-SE": {
|
||||
"name": "Webbläsare",
|
||||
"description": "Gaia systemwebbläsare"
|
||||
},
|
||||
"tr": {
|
||||
"name": "Tarayıcı",
|
||||
"description": "Gaia Sistem Web Tarayıcısı"
|
||||
},
|
||||
"zh-CN": {
|
||||
"name": "浏览器",
|
||||
"description": "Gaga 系统的网页浏览器"
|
||||
},
|
||||
"zh-TW": {
|
||||
"name": "瀏覽器",
|
||||
"description": "Gaia 系統網頁瀏覽器"
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
|
|
@ -191,7 +59,12 @@
|
|||
}
|
||||
},
|
||||
"icons": {
|
||||
"284": "/shared/resources/branding/vulpes-browser.png"
|
||||
"84": "/shared/resources/branding/browser_84.png",
|
||||
"126": "/shared/resources/branding/browser_126.png",
|
||||
"142": "/shared/resources/branding/browser_142.png",
|
||||
"189": "/shared/resources/branding/browser_189.png",
|
||||
"256": "/shared/resources/branding/browser_256.png",
|
||||
"284": "/shared/resources/branding/browser_284.png"
|
||||
},
|
||||
"chrome": {
|
||||
"navigation": true
|
||||
|
|
@ -207,7 +80,5 @@
|
|||
"disposition": "window",
|
||||
"href": "/index.html"
|
||||
}
|
||||
},
|
||||
"origin": "app://search.gaiamobile.org",
|
||||
"version": "2.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -1 +1,119 @@
|
|||
{"name":"Settings","description":"Gaia Settings","type":"certified","launch_path":"/index.html#","developer":{"name":"The Gaia Team","url":"https://github.com/mozilla-b2g/gaia"},"permissions":{"themeable":{},"browser":{},"embed-apps":{},"mobileconnection":{},"voicemail":{},"bluetooth":{},"device-storage:pictures":{"access":"readonly"},"device-storage:music":{"access":"readonly"},"device-storage:videos":{"access":"readonly"},"device-storage:sdcard":{"access":"readwrite"},"device-storage:apps":{"access":"readonly"},"webapps-manage":{},"open-remote-window":{},"permissions":{},"systemXHR":{},"settings":{"access":"readwrite"},"wifi-manage":{},"time":{},"desktop-notification":{},"power":{},"idle":{},"telephony":{},"audio-channel-notification":{},"audio-channel-alarm":{},"downloads":{},"nfc":{},"sms":{},"firefox-accounts":{},"moz-extremely-unstable-and-will-change-webcomponents":{}},"activities":{"configure":{"filters":{"target":"device"},"href":"index.html#activity-configure","disposition":"inline","returnValue":true},"moz_configure_window":{"filters":{"target":"device"},"href":"index.html#moz-configure-window","disposition":"window"}},"connections":{"settingsstk":{"description":"Communicate between Settings app and System app for SIM Toolkit.","rules":{}}},"locales":{"ar":{"name":"الإعدادات","description":"الإعدادات"},"bg":{"name":"Настройки","description":"Gaia Настройки"},"bn-BD":{"name":"সেটিং","description":"গায়া সেটিং"},"ca":{"name":"Paràmetres","description":"Paràmetres del Gaia"},"cs":{"name":"Nastavení","description":"Nastavení systému Gaia"},"da":{"name":"Indstillinger","description":"Gaia indstillinger"},"de":{"name":"Einstellungen","description":"Einstellungen für Gaia"},"el":{"name":"Ρυθμίσεις","description":"Ρυθμίσεις του Gaia"},"en-US":{"name":"Settings","description":"Gaia Settings"},"es":{"name":"Ajustes","description":"Ajustes de Gaia"},"eu":{"name":"Ezarpenak","description":"Gaia ezarpenak"},"fr":{"name":"Paramètres","description":"Paramètres de Gaia"},"gl":{"name":"Axustes","description":"Axustes de Gaia"},"hr":{"name":"Postavke","description":"Gaia postavke"},"hu":{"name":"Beállítások","description":"Gaia beállítások"},"it":{"name":"Impostazioni","description":"Impostazioni Gaia"},"ja":{"name":"環境設定","description":"Gaia 設定"},"km":{"name":"ការកំណត់","description":"ការកំណត់ Gaia"},"ko":{"name":"설정","description":"Gaia 설정"},"lt":{"name":"Nuostatos","description":"„Gaia“ nuostatos"},"mk":{"name":"Поставки","description":"Поставки за Гаја"},"ms":{"name":"Tetapan","description":"Tetapan Gaia"},"ne-NP":{"name":"सेटिङहरू","description":"गाइया सेटिङहरू"},"nl":{"name":"Instellingen","description":"Gaia-instellingen"},"pa":{"name":"ਸੈਟਿੰਗ","description":"ਗੀਗਾ ਸੈਟਿੰਗ"},"pl":{"name":"Ustawienia","description":"Ustawienia Gaia"},"pt-BR":{"name":"Configurações","description":"Configurações do Gaia"},"ro":{"name":"Setări","description":"Setări Gaia"},"ru":{"name":"Параметры","description":"Параметры Gaia"},"sk":{"name":"Nastavenia","description":"Nastavenia Gaia"},"sq":{"name":"Rregullime","description":"Rregullimet Gaia"},"sr-Cyrl":{"name":"Поставке","description":"Gaia поставке"},"sr-Latn":{"name":"Postavke","description":"Gaia postavke"},"sv-SE":{"name":"Inställningar","description":"Gaia inställningar"},"tr":{"name":"Ayarlar","description":"Gaia Ayarları"},"zh-CN":{"name":"设置","description":"Gaia 设置"},"zh-TW":{"name":"設定","description":"Gaia 設定"}},"datastores-owned":{"homescreen_settings":{"access":"readwrite","description":"Stores home screen settings"}},"datastores-access":{"download_store":{"readonly":false,"description":"Stores download finished"},"vertical_preferences_store":{"readonly":false,"description":"Vertical homescreen preferences"}},"default_locale":"en-US","icons":{"84":"/style/icons/settings_84.png","126":"/style/icons/settings_126.png","142":"/style/icons/settings_142.png","189":"/style/icons/settings_189.png","284":"/style/icons/settings_284.png"},"orientation":"default","origin":"app://settings.gaiamobile.org","version":"2.5"}
|
||||
{
|
||||
"name": "Settings",
|
||||
"description": "Gaia Settings",
|
||||
"type": "certified",
|
||||
"launch_path": "/index.html#",
|
||||
"developer": {
|
||||
"name": "The Gaia Team",
|
||||
"url": "https://github.com/mozilla-b2g/gaia"
|
||||
},
|
||||
"permissions": {
|
||||
"themeable": {},
|
||||
"browser": {},
|
||||
"embed-apps": {},
|
||||
"mobileconnection": {},
|
||||
"voicemail": {},
|
||||
"bluetooth": {},
|
||||
"device-storage:pictures": {
|
||||
"access": "readonly"
|
||||
},
|
||||
"device-storage:music": {
|
||||
"access": "readonly"
|
||||
},
|
||||
"device-storage:videos": {
|
||||
"access": "readonly"
|
||||
},
|
||||
"device-storage:sdcard": {
|
||||
"access": "readwrite"
|
||||
},
|
||||
"device-storage:apps": {
|
||||
"access": "readonly"
|
||||
},
|
||||
"webapps-manage": {},
|
||||
"open-remote-window": {},
|
||||
"permissions": {},
|
||||
"systemXHR": {},
|
||||
"settings": {
|
||||
"access": "readwrite"
|
||||
},
|
||||
"wifi-manage": {},
|
||||
"time": {},
|
||||
"desktop-notification": {},
|
||||
"power": {},
|
||||
"idle": {},
|
||||
"telephony": {},
|
||||
"audio-channel-notification": {},
|
||||
"audio-channel-alarm": {},
|
||||
"downloads": {},
|
||||
"nfc": {},
|
||||
"sms": {},
|
||||
"firefox-accounts": {},
|
||||
"moz-extremely-unstable-and-will-change-webcomponents": {}
|
||||
},
|
||||
"activities": {
|
||||
"configure": {
|
||||
"filters": {
|
||||
"target": "device"
|
||||
},
|
||||
"href": "index.html#activity-configure",
|
||||
"disposition": "inline",
|
||||
"returnValue": true
|
||||
},
|
||||
"moz_configure_window": {
|
||||
"filters": {
|
||||
"target": "device"
|
||||
},
|
||||
"href": "index.html#moz-configure-window",
|
||||
"disposition": "window"
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"settingsstk": {
|
||||
"description": "Communicate between Settings app and System app for SIM Toolkit.",
|
||||
"rules": {}
|
||||
}
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"name": "الضبط",
|
||||
"description": "Gaia الضبط"
|
||||
},
|
||||
"en-US": {
|
||||
"name": "Settings",
|
||||
"description": "Gaia Settings"
|
||||
},
|
||||
"fr": {
|
||||
"name": "Paramètres",
|
||||
"description": "Paramètres de Gaia"
|
||||
},
|
||||
"zh-TW": {
|
||||
"name": "設定",
|
||||
"description": "Gaia 設定"
|
||||
}
|
||||
},
|
||||
"datastores-owned": {
|
||||
"homescreen_settings": {
|
||||
"access": "readwrite",
|
||||
"description": "Stores home screen settings"
|
||||
}
|
||||
},
|
||||
"datastores-access": {
|
||||
"download_store": {
|
||||
"readonly": false,
|
||||
"description": "Stores download finished"
|
||||
},
|
||||
"vertical_preferences_store": {
|
||||
"readonly": false,
|
||||
"description": "Vertical homescreen preferences"
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"icons": {
|
||||
"84": "/style/icons/settings_84.png",
|
||||
"126": "/style/icons/settings_126.png",
|
||||
"142": "/style/icons/settings_142.png",
|
||||
"189": "/style/icons/settings_189.png",
|
||||
"284": "/style/icons/settings_284.png"
|
||||
},
|
||||
"orientation": "default"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -475,20 +475,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Set the .icon property before saving for
|
||||
// backwards compatibility with verticalhome
|
||||
// Set the .icon property before saving for backwards compatibility with
|
||||
// verticalhome. A missing favicon must not prevent pinning the site.
|
||||
var saveSite = function(icon) {
|
||||
siteObject.icon = icon || '';
|
||||
BookmarksDatabase.put(siteObject, siteObject.id)
|
||||
.then(() => {
|
||||
this.app.debug('pinSite: ' + siteObject.id);
|
||||
this.systemBanner.show('site-pinned-to-home-screen');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('pinSite, Failed to pin site: ' + error);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
IconsHelper.getIcon(siteObject.url, null,
|
||||
{icons: this.app.favicons}, siteObject).then(icon => {
|
||||
siteObject.icon = icon;
|
||||
BookmarksDatabase.put(siteObject, siteObject.id)
|
||||
.then(() => {
|
||||
this.app.debug('pinSite: ' + siteObject.id);
|
||||
this.systemBanner.show('site-pinned-to-home-screen');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('pinSite, Failed to pin site: ' + error);
|
||||
});
|
||||
});
|
||||
{icons: this.app.favicons}, siteObject).then(saveSite, function(error) {
|
||||
this.app.debug('pinSite: using no favicon: ' + error);
|
||||
saveSite('');
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -986,12 +991,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Enable/disable the bookmark option
|
||||
BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
|
||||
// start_url is the bookmark key for installed web apps, while _currentURL
|
||||
// may be a later page in the app. Check the canonical key first.
|
||||
var siteUrl = this.getSiteUrl();
|
||||
BookmarksDatabase.get(siteUrl).then(function resolve(result) {
|
||||
this.addToHomeButton.hidden = !!result;
|
||||
}.bind(this),
|
||||
function reject() {
|
||||
this.addToHomeButton.hidden = true;
|
||||
this.addToHomeButton.hidden = false;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
|
@ -1282,7 +1289,13 @@
|
|||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.siteIcon.style.backgroundImage = '';
|
||||
// Installed apps have a manifest icon even when the Places cache has
|
||||
// no favicon. Keep the original chrome slot and select a crisp source.
|
||||
const icons = this.app.manifest && this.app.manifest.icons;
|
||||
const sizes = Object.keys(icons || {}).map(Number).filter(Number.isFinite).sort((a,b)=>a-b);
|
||||
const size = sizes.find(n => n >= ICON_SIZE * devicePixelRatio) || sizes[sizes.length-1];
|
||||
const icon = size && icons[size];
|
||||
this.siteIcon.style.backgroundImage = icon ? 'url(' + JSON.stringify(new URL(icon, this.app.manifestURL).href) + ')' : '';
|
||||
this.app.debug('setSiteIcon, error from getSiteIcon: %s', err);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -346,13 +346,26 @@
|
|||
|
||||
case this.fullApp:
|
||||
// XXX: This should be replaced probably by Web Activities
|
||||
var host = document.location.host;
|
||||
var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
|
||||
var protocol = document.location.protocol + '//';
|
||||
applications.getByManifestURL(protocol + 'settings.' +
|
||||
domain + '/manifest.webapp').launch();
|
||||
|
||||
UtilityTray.hide();
|
||||
var settingsURL = new URL(document.location.href);
|
||||
if (settingsURL.hostname.indexOf('system.') !== 0) {
|
||||
console.error('Quick settings: cannot resolve Settings app host');
|
||||
return;
|
||||
}
|
||||
settingsURL.hostname = 'settings.' +
|
||||
settingsURL.hostname.slice('system.'.length);
|
||||
settingsURL.pathname = '/manifest.webapp';
|
||||
settingsURL.search = '';
|
||||
settingsURL.hash = '';
|
||||
applications.waitForReady().then(function() {
|
||||
var settingsApp = applications.getByManifestURL(
|
||||
settingsURL.href);
|
||||
if (!settingsApp) {
|
||||
console.error('Quick settings: Settings app is not installed');
|
||||
return;
|
||||
}
|
||||
settingsApp.launch();
|
||||
UtilityTray.hide();
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -57,5 +57,5 @@
|
|||
#os-logo { background: #071d35 url('/shared/resources/branding/vulpes-wallpaper.png') center / cover; display:flex; flex-direction:column; align-items:center; justify-content:center; }
|
||||
#os-logo::before { content:""; width:190px; height:190px; flex:none; background:url('/shared/resources/branding/vulpes-symbol.png') center / contain no-repeat; }
|
||||
#os-logo::after { content:"Vulpes 2.7\A A community continuation of Firefox OS"; white-space:pre-line; padding:12px 22px; color:#f5f8fa; text-shadow:0 2px 8px #071d35; font:500 20px/1.8 sans-serif; text-align:center; }
|
||||
#poweroff-splash { background:#071d35 url('/shared/resources/branding/vulpes-symbol.png') center / 190px 190px no-repeat; }
|
||||
#poweroff-splash { background:#071d35; }
|
||||
/* VULPES BRAND END */
|
||||
|
|
|
|||
|
|
@ -88,17 +88,17 @@ textarea,
|
|||
|
||||
#poweroff-ring-1 {
|
||||
top: 25%;
|
||||
background-color: #e66600;
|
||||
background-color: #36a9e8;
|
||||
}
|
||||
|
||||
#poweroff-ring-2 {
|
||||
top: 50%;
|
||||
background-color: #dc4e00;
|
||||
background-color: #218bd1;
|
||||
}
|
||||
|
||||
#poweroff-ring-3 {
|
||||
top: 75%;
|
||||
background-color: #d24500;
|
||||
background-color: #1473b8;
|
||||
}
|
||||
|
||||
.poweroff-ring > span {
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ for(i in appIconsByManifestUrl){this.removeIconFromGrid(i);}
|
|||
toAdd.forEach(function _toAdd(newApp){this.addIconToGrid(newApp.app);},this);app.itemStore.deferredSave(app.grid.getItems());},addIconToGrid:function(application){this.makeIcons(application);var appObject=this.mapToApp({manifestURL:application.manifestURL});app.grid.appendItemToExpandedGroup(appObject);app.grid.render();},removeIconFromGrid:function(manifestURL){var icons=app.grid.getIcons();var appObject=icons[manifestURL];if(appObject){appObject.removeFromGrid();}},populate:function(success){success(this.entries);},makeIcons:function(eachApp){var manifest=eachApp.manifest||eachApp.updateManifest;if(app.HIDDEN_ROLES.indexOf(manifest.role)!==-1){return;}
|
||||
function eachIcon(icon){if(!icon.icon){return;}
|
||||
icon.setPosition(this.store.getNextPosition());this.entries.push(icon);}
|
||||
this.entriesByManifestUrl[eachApp.manifestURL]=eachApp;if(manifest.entry_points){for(var i in manifest.entry_points){eachIcon.call(this,new GaiaGrid.Mozapp(eachApp,i));}}else{eachIcon.call(this,new GaiaGrid.Mozapp(eachApp));}},mapToApp:function(entry){var app=this.entriesByManifestUrl[entry.manifestURL];app=app||{manifestURL:entry.manifestURL,manifest:{name:'',icons:[]}};return new GaiaGrid.Mozapp(app,entry.entryPoint,{decoratedIconBlob:entry.decoratedIconBlob});},addPreviouslyInstalledSvApp:function(manifest){this.svPreviouslyInstalledApps.push({manifestURL:manifest});},isPreviouslyInstalled:function(manifest){for(var i=0,elemNum=this.svPreviouslyInstalledApps.length;i<elemNum;i++){if(this.svPreviouslyInstalledApps[i].manifestURL===manifest){return true;}}
|
||||
this.entriesByManifestUrl[eachApp.manifestURL]=eachApp;if(manifest.entry_points){for(var i in manifest.entry_points){eachIcon.call(this,new GaiaGrid.Mozapp(eachApp,i));}}else{eachIcon.call(this,new GaiaGrid.Mozapp(eachApp));}},mapToApp:function(entry){var app=this.entriesByManifestUrl[entry.manifestURL];app=app||{manifestURL:entry.manifestURL,manifest:{name:'',icons:[]}};var isVulpesBrowser=/^(?:app:\/\/search\.gaiamobile\.org|http:\/\/search\.localhost(?::\d+)?)\/manifest\.webapp$/.test(entry.manifestURL);return new GaiaGrid.Mozapp(app,entry.entryPoint,{decoratedIconBlob:isVulpesBrowser?null:entry.decoratedIconBlob});},addPreviouslyInstalledSvApp:function(manifest){this.svPreviouslyInstalledApps.push({manifestURL:manifest});},isPreviouslyInstalled:function(manifest){for(var i=0,elemNum=this.svPreviouslyInstalledApps.length;i<elemNum;i++){if(this.svPreviouslyInstalledApps[i].manifestURL===manifest){return true;}}
|
||||
return false;}};exports.ApplicationSource=ApplicationSource;}(window));;'use strict';(function(exports){function AppManager(){var grid=document.querySelector('gaia-grid');var self=this;navigator.mozApps.getSelf().onsuccess=function(evt){self.app=evt.target.result;window.dispatchEvent(new CustomEvent('appmanager-ready'));};grid.addEventListener('removeitem',this);}
|
||||
AppManager.prototype={dialogVisible:false,get self(){return this.app;},handleItemRemoval:function(item){function errorLogger(err){console.error('Error while trying to remove',item.name,err);}
|
||||
switch(item.detail.type){case'bookmark':BookmarksDatabase.remove(item.identifier).catch(errorLogger);break;default:console.error('Cannot handle remove for item type ',item.detail.type);}},handleEvent:function(e){var nameObj={name:e.detail&&e.detail.name};switch(e.type){case'removeitem':if(this.dialogVisible){return;}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
[
|
||||
"Vulpes_Blue.png",
|
||||
"Illus_Flow2_Blue.jpg",
|
||||
"Illus_Flow2_Orange@2.25x.jpg",
|
||||
"FX_Bolt.jpg",
|
||||
|
|
@ -43,5 +42,6 @@
|
|||
"PH_Tree.jpg",
|
||||
"PH_TreeForest.jpg",
|
||||
"PH_Wheat.jpg",
|
||||
"PH_WhiteFlower.jpg"
|
||||
"PH_WhiteFlower.jpg",
|
||||
"Vulpes_Blue.png"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,44 @@
|
|||
"en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nUpdates by date and platform. Within each group: Add, Change, Fix, Note, Test. Entries describe the state at their date; a fix in development requires an updated installed version.\n"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"id": "2.7-preview.24",
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"fr": "## 04/11/2026 : 2.7-preview.24\n\n\n\n### \n\n-   : Animation d’arrêt Gaia avec trois anneaux bleus, sans logo en arrière-plan.\n\n### \n\n-   : APK Preview 24.\n\n### \n\n-   : Code Desktop et Gaia publiés dans le dépôt; lancement avec `./run.sh`.\n\n### \n\n-   : Paquet natif disponible. Le flash efface entièrement `userdata`; démarrage non qualifié.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24)\n",
|
||||
"en": "## 04/11/2026: 2.7-preview.24\n\n\n\n### \n\n-   : Gaia shutdown animation now shows three blue rings without a background logo.\n\n### \n\n-   : Preview 24 APK.\n\n### \n\n-   : Desktop and Gaia source published in the repository; launch with `./run.sh`.\n\n### \n\n-   : Native package available. Flashing fully erases `userdata`; boot is not qualified.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24)\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.23",
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"fr": "## 27/10/2026 : 2.7-preview.23 — construite, non publiée\n\n### Toutes les plateformes\n\n- [Fix][Accueil] Espacement de fin de page, édition des icônes et épinglage web repris.\n- [Fix][Clavier] Masquage au changement de focus et premier appui sur la recherche corrigés.\n- [Fix][Paramètres] Sélecteurs, raccourcis, permissions et estimation batterie améliorés.\n- [Fix][Marketplace] Retour catalogue restauré; les applications privilégiées non prises en charge sont signalées clairement.\n- [Fix][Caméra] Cycle photo/vidéo et reprise du viseur fiabilisés; qualification matérielle encore requise.\n- [Fix][Système] Capture d’écran Gaia et animation d’arrêt sans logo superposé.\n\n### Android / APK\n\n- [Test][Installation] APK installée et lancée sur Android 16 (API 36); Gaia a affiché son écran de verrouillage. Déverrouillage et parcours des apps restent à qualifier.\n\n### Pixel 3a / Sargo\n\n- [Change][Démarrage] Temps d’attente et vérifications au boot réduits; qualification batterie en veille toujours en cours.\n- [Note][Essais] Image système et boot RAM construits et vérifiés; transfert au Pixel en attente de sa reconnexion.\n\n\n**Limites :** Aucun changement Forgejo publié; le Pixel doit être reconnecté pour le démarrage de qualification.\n",
|
||||
"en": "## 27/10/2026: 2.7-preview.23 — built, not published\n\n### All Platforms\n\n- [Fix][Home] End-of-page spacing, icon editing and web pinning improved.\n- [Fix][Keyboard] Hiding on focus changes and first-tap search input corrected.\n- [Fix][Settings] Selectors, shortcuts, permissions and battery estimates improved.\n- [Fix][Marketplace] Catalogue return restored; unsupported privileged apps now show a clear message.\n- [Fix][Camera] Photo/video switching and viewfinder recovery improved; hardware qualification remains.\n- [Fix][System] Gaia screenshots added and shutdown animation no longer has an overlaid logo.\n\n### Android / APK\n\n- [Test][Installation] APK installed and launched on Android 16 (API 36); Gaia reached its lock screen. Unlocking and app flows remain to be qualified.\n\n### Pixel 3a / Sargo\n\n- [Change][Startup] Boot waits and checks reduced; battery standby qualification remains in progress.\n- [Note][Testing] System image and RAM boot built and verified; transfer to the Pixel awaits reconnection.\n\n\n**Limitations :** Nothing was published to Forgejo; reconnect the Pixel for RAM-boot qualification.\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.22",
|
||||
"metadata": {
|
||||
|
|
@ -21,8 +59,25 @@
|
|||
]
|
||||
},
|
||||
"text": {
|
||||
"fr": "## 22/10/2026 : 2.7-preview.22\n\n\n\n### \n\n-   : Le bouton Partager du navigateur propose Messages et Courrier ; le clavier se masque pour laisser le choix accessible.\n\n### \n\n-   : Le focus des champs et listes web est rétabli dans le navigateur PC.\n-   : Premier appui, saisie, sélecteurs et partage vers un brouillon Messages contrôlés sur PC.\n\n### \n\n-   : APK Preview 22 construit avec les corrections Gaia communes et une version Android incrémentée.\n-   : Un message GeckoView de restauration de session apparaît au premier lancement de l’émulateur ; Gaia atteint néanmoins l’accueil sans plantage.\n-   : Installation de la Preview 21 puis mise à jour vers la 22 réussies sur Android 11 en émulation ; Gaia démarre sur l’accueil. Les téléphones signalés restent à qualifier, dont le Pixel 4.\n\n### \n\n-   : Première saisie T9 hors ligne avec les lexiques Gaia. Qualification physique du clavier encore nécessaire.\n-   : Wi-Fi, reconnexion et cycle du mode avion vérifiés en session RAM ; les contrôles Gaia suivent l’état matériel.\n\n\n### \n\n-   : Cette release ne contient pas de nouvelle image Pixel. La Preview 21 reste la dernière image publiée ; les changements de cette Preview ne sont pas installés sur le téléphone.\n",
|
||||
"en": "## 22/10/2026 : 2.7-preview.22\n\n\n\n### \n\n-   : Browser Share offers Messages and Email; the keyboard hides so the chooser remains accessible.\n\n### \n\n-   : Focus is restored for fields and dropdowns in the desktop browser.\n-   : First-click input, typing, selectors and sharing to a Messages draft checked on desktop.\n\n### \n\n-   : Preview 22 APK built with shared Gaia fixes and an incremented Android version.\n-   : GeckoView logs a session-restore message on the emulator first launch; Gaia still reaches Home without crashing.\n-   : Preview 21 installation and upgrade to Preview 22 succeeded in Android 11 emulation; Gaia reached Home. Reported phones still need qualification, including Pixel 4.\n\n### \n\n-   : Initial offline T9 input using Gaia dictionaries. Physical keyboard qualification is still needed.\n-   : Wi-Fi, reconnection and airplane-mode cycle checked in a RAM session; Gaia controls follow hardware state.\n\n\n### \n\n-   : This release does not include a new Pixel image. Preview 21 remains the latest published image; this Preview's changes are not installed on the phone.\n"
|
||||
"fr": "## 23/10/2026 : 2.7-preview.22\n\n\n\n### \n\n-   : Le bouton Partager du navigateur propose Messages et Courrier ; le clavier se masque pour laisser le choix accessible.\n\n### \n\n-   : Le focus des champs et listes web est rétabli dans le navigateur PC.\n-   : Premier appui, saisie, sélecteurs et partage vers un brouillon Messages contrôlés sur PC.\n\n### \n\n-   : APK Preview 22 construit avec les corrections Gaia communes et une version Android incrémentée.\n-   : Un message GeckoView de restauration de session apparaît au premier lancement de l’émulateur ; Gaia atteint néanmoins l’accueil sans plantage.\n-   : Installation de la Preview 21 puis mise à jour vers la 22 réussies sur Android 11 en émulation ; Gaia démarre sur l’accueil. Les téléphones signalés restent à qualifier, dont le Pixel 4.\n\n### \n\n-   : Première saisie T9 hors ligne avec les lexiques Gaia. Qualification physique du clavier encore nécessaire.\n-   : Wi-Fi, reconnexion et cycle du mode avion vérifiés en session RAM ; les contrôles Gaia suivent l’état matériel.\n\n\n### \n\n-   : Paquet expérimental Preview 22 pour Pixel 3a Sargo ajouté à la release. Le flash a écrit les partitions prévues et redémarré le téléphone ; le démarrage de Gaia n’a pas encore été confirmé.\n-   : Le flash efface intégralement `userdata`, y compris l’ancien système, les fichiers, applications et réglages. Sauvegarde requise avant installation.\n",
|
||||
"en": "## 23/10/2026 : 2.7-preview.22\n\n\n\n### \n\n-   : Browser Share offers Messages and Email; the keyboard hides so the chooser remains accessible.\n\n### \n\n-   : Focus is restored for fields and dropdowns in the desktop browser.\n-   : First-click input, typing, selectors and sharing to a Messages draft checked on desktop.\n\n### \n\n-   : Preview 22 APK built with shared Gaia fixes and an incremented Android version.\n-   : GeckoView logs a session-restore message on the emulator first launch; Gaia still reaches Home without crashing.\n-   : Preview 21 installation and upgrade to Preview 22 succeeded in Android 11 emulation; Gaia reached Home. Reported phones still need qualification, including Pixel 4.\n\n### \n\n-   : Initial offline T9 input using Gaia dictionaries. Physical keyboard qualification is still needed.\n-   : Wi-Fi, reconnection and airplane-mode cycle checked in a RAM session; Gaia controls follow hardware state.\n\n\n### \n\n-   : Experimental Preview 22 package for Pixel 3a Sargo added to this release. Fastboot wrote the planned partitions and rebooted the phone; Gaia startup is not yet confirmed.\n-   : Flashing completely erases `userdata`, including the old OS, files, apps and settings. Back up before installation.\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-26-sargo-startup",
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
},
|
||||
"text": {
|
||||
"fr": "## 26/10/2026 : Démarrage Sargo et autonomie — en développement\n\n### \n\n-   : Le boot ne relit plus les 3 Gio de l’image système en SHA-256. La provenance reste contrôlée à la préparation ; au démarrage, taille et type sont vérifiés avant le montage en lecture seule. L’attente HAL est plafonnée à 8 s.\n-   : Tundra attend la fin d’initialisation du HAL puis active les états basse consommation Qualcomm. Le boot RAM v23 confirme `sleep_disabled=N` ; CPU0 a résidé 3,6 à 4,0 s en C3 sur des fenêtres de 5,2 s.\n-   : Gaia ne dépend plus de l’attente de session réseau pour démarrer.\n-   : En RAM, l’écran éteint laisse encore `Isolated Web Co` à 21 %, `crtc_commit` à 13 % et Firefox à 10 % d’un cœur. Batterie à 29,5 °C et CPU autour de 38 °C ; téléphone branché, donc taux de décharge non mesuré.\n-   : Image v23 privée, démarrée en RAM sans écriture de partition. Le profilage des processus Gaia et la mesure prolongée sans câble restent à faire.\n",
|
||||
"en": "## 26/10/2026 : Sargo startup and battery life — in development\n\n### \n\n-   : Boot no longer rereads the 3 GiB system image to calculate SHA-256. Provenance remains checked during preparation; boot checks file type and size before mounting read-only. The HAL wait is capped at 8 seconds.\n-   : Tundra waits for HAL initialization to finish, then enables Qualcomm low-power states. RAM boot v23 confirms `sleep_disabled=N`; CPU0 spent 3.6–4.0 seconds in C3 over 5.2-second samples.\n-   : Gaia startup no longer waits for the network login barrier.\n-   : With the backlight off in RAM, `Isolated Web Co` still used 21%, `crtc_commit` 13% and Firefox 10% of one core. Battery was 29.5 °C and CPU around 38 °C; the phone was plugged in, so discharge rate was not measured.\n-   : Private v23 image booted in RAM without partition writes. Gaia process profiling and a longer unplugged measurement remain.\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -1237,7 +1292,14 @@
|
|||
"after": "2.7-preview.21",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.22",
|
||||
"include_entry": true,
|
||||
"summary": "# Vulpes OS — 2.7-preview.22\n\n🇫🇷 Preview expérimentale PC / Android. L’APK Preview 22 et les sources PC/Tundra sont publiés ensemble. L’APK peut être installé par-dessus une Preview précédente signée avec la clé du projet. Android 8 minimum déclaré ; Gaia 2.7.0 · GeckoView Android 156.0.\n\nCette version apporte les corrections de partage et de saisie décrites ci-dessous. L’installation de la Preview 21, la mise à jour vers la 22 et l’ouverture de Gaia ont réussi sur un émulateur Android 11. Le parcours de partage et les sélecteurs ont été contrôlés sur PC. Gaia a affiché l’accueil ; GeckoView a toutefois journalisé une erreur de restauration de session non bloquante. Les téléphones signalés restent à qualifier ; le Pixel 4 concerné n’était pas disponible pour diagnostiquer son échec d’installation.\n\nLes sources Tundra incluent les changements pour le Nokia 2720 Flip. Wi-Fi, reconnexion, commandes matérielles et cycle du mode avion ont été contrôlés en session RAM ; le clavier T9 reste à qualifier physiquement.\n\n**Pixel 3a : aucune nouvelle image native n’est fournie dans cette release.** La Preview 21 demeure la dernière image publiée pour Sargo ; les changements de cette version ne sont pas flashés sur le téléphone. La release ne prétend pas résoudre tous les tickets ouverts.\n\n🇬🇧 Experimental desktop / Android Preview. The Preview 22 APK and desktop/Tundra sources are published together. Install over a previous Preview signed with the project key. Android 8 minimum declared; Gaia 2.7.0 · Android GeckoView 156.0.\n\nThis version includes the sharing and input fixes listed below. Preview 21 installation, upgrade to 22 and Gaia startup succeeded in an Android 11 emulator. The sharing flow and selectors were checked on desktop. Gaia reached Home, though GeckoView logged a non-blocking session-restore error. Reported phones still need qualification; the affected Pixel 4 was unavailable for diagnosis.\n\nTundra sources include Nokia 2720 Flip changes. Wi-Fi, reconnection, hardware controls and the airplane-mode cycle were checked in a RAM session; the T9 keyboard still needs physical qualification.\n\n**Pixel 3a: this release does not include a new native image.** Preview 21 remains the latest published Sargo image; this version's changes are not flashed onto the phone. This release does not claim to resolve every open ticket.\n\nAndroid 8 minimum · Signed development APK · Desktop Gecko 156.0.1 · Android GeckoView 156.0."
|
||||
"summary": "# Vulpes OS — 2.7-preview.22\n\n🇫🇷 **Preview expérimentale PC / Android / Sargo.** APK Preview 22 et image native expérimentale pour Pixel 3a (Sargo). Le flash remplace entièrement `userdata` : système installé, fichiers, applications et réglages sont effacés. Sauvegarde indispensable. Le flash a réussi sur un Sargo ; le démarrage de cette image n’a pas encore été confirmé. Gaia 2.7.0 · GeckoView Android 156.0 · Gecko Sargo 156.0.1.\n\n🇬🇧 **Experimental desktop / Android / Sargo Preview.** Preview 22 APK and experimental native image for Pixel 3a (Sargo). Flashing completely replaces `userdata`: the installed OS, files, apps and settings are erased. Back up first. Flashing succeeded on a Sargo; booting this image has not yet been confirmed. Gaia 2.7.0 · Android GeckoView 156.0 · Sargo Gecko 156.0.1."
|
||||
},
|
||||
"v2.7-preview.24": {
|
||||
"version": "2.7-preview.24",
|
||||
"after": "v2.7-preview.22",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.24",
|
||||
"include_entry": true,
|
||||
"summary": "# Vulpes OS — 2.7-preview.24\n\n🇫🇷 **Preview expérimentale Desktop / Android / Pixel 3a.** Sources Desktop et APK Preview 24, plus une image expérimentale pour Pixel 3a (Sargo). Le flash remplace entièrement `userdata` et efface le système, les fichiers, applications et réglages présents. Démarrage de cette image non qualifié.\n\n🇬🇧 **Experimental Desktop / Android / Pixel 3a Preview.** Desktop source and Preview 24 APK, plus an experimental image for Pixel 3a (Sargo). Flashing fully replaces `userdata` and erases the existing OS, files, apps and settings. This image has not been boot-qualified."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,23 @@ var debug = require('debug')('view:setting-options');
|
|||
var attach = require('attach');
|
||||
var View = require('view');
|
||||
|
||||
var optionFallbacks = {
|
||||
'self-timer-off': ['Off', 'Désactivé'],
|
||||
'self-timer-2-seconds': ['2 seconds', '2 secondes'],
|
||||
'self-timer-5-seconds': ['5 seconds', '5 secondes'],
|
||||
'self-timer-10-seconds': ['10 seconds', '10 secondes'],
|
||||
'grid-off': ['Off', 'Désactivée'],
|
||||
'grid-on': ['On', 'Activées']
|
||||
};
|
||||
|
||||
function fallbackTitle(key) {
|
||||
var values = optionFallbacks[key];
|
||||
if (!values) { return key; }
|
||||
var locale = (document.documentElement.lang || navigator.language || 'en')
|
||||
.toLowerCase();
|
||||
return values[locale.indexOf('fr') === 0 ? 1 : 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
|
@ -67,7 +84,9 @@ module.exports = View.extend({
|
|||
if (localizable) {
|
||||
li.setAttribute('data-l10n-id', option.title);
|
||||
this.l10n.formatValue(option.title).then((title) => {
|
||||
li.setAttribute('aria-label', title);
|
||||
var visibleTitle = title || fallbackTitle(option.title);
|
||||
li.textContent = visibleTitle;
|
||||
li.setAttribute('aria-label', visibleTitle);
|
||||
});
|
||||
} else {
|
||||
li.textContent = option.title;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,25 @@ var debug = require('debug')('view:setting');
|
|||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
var settingFallbacks = {
|
||||
'self-timer': ['Self-Timer', 'Minuteur'],
|
||||
'self-timer-off': ['Off', 'Désactivé'],
|
||||
'self-timer-2-seconds': ['2 seconds', '2 secondes'],
|
||||
'self-timer-5-seconds': ['5 seconds', '5 secondes'],
|
||||
'self-timer-10-seconds': ['10 seconds', '10 secondes'],
|
||||
grid: ['Grid Lines', 'Quadrillage'],
|
||||
'grid-off': ['Off', 'Désactivée'],
|
||||
'grid-on': ['On', 'Activé']
|
||||
};
|
||||
|
||||
function fallbackText(key) {
|
||||
var values = settingFallbacks[key];
|
||||
if (!values) { return key; }
|
||||
var locale = (document.documentElement.lang || navigator.language || 'en')
|
||||
.toLowerCase();
|
||||
return values[locale.indexOf('fr') === 0 ? 1 : 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
|
@ -50,6 +69,7 @@ module.exports = View.extend({
|
|||
}
|
||||
|
||||
localizedValue.then(valueString => {
|
||||
valueString = valueString || fallbackText(data.value);
|
||||
this.l10n.setAttributes(
|
||||
this.el,
|
||||
'setting-option-' + data.title,
|
||||
|
|
@ -62,6 +82,15 @@ module.exports = View.extend({
|
|||
valueText: data.optionsLocalizable === false ? data.value : '',
|
||||
});
|
||||
|
||||
var title = this.el.querySelector('.setting_title');
|
||||
if (title && !title.textContent) {
|
||||
title.textContent = fallbackText(data.title);
|
||||
}
|
||||
var value = this.el.querySelector('.setting_value');
|
||||
if (value && !value.textContent) {
|
||||
value.textContent = valueString;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,11 @@ var Common = {
|
|||
},
|
||||
|
||||
closeApplication: function() {
|
||||
if (window.VulpesCompat) {
|
||||
return window.VulpesCompat.call('apps.close', {}).catch(function(error) {
|
||||
console.error('Unable to close Usage:', error);
|
||||
});
|
||||
}
|
||||
return setTimeout(function _close() {
|
||||
debug('Closing.');
|
||||
window.close();
|
||||
|
|
|
|||
|
|
@ -44,7 +44,13 @@
|
|||
},
|
||||
|
||||
init: function() {
|
||||
var revisionString = localStorage.getItem(this.revisionName);
|
||||
var revisionString;
|
||||
try {
|
||||
revisionString = localStorage.getItem(this.revisionName);
|
||||
} catch (e) {
|
||||
// The shell may deny partitioned localStorage; IndexedDB remains usable.
|
||||
console.error('Homescreen datastore revision unavailable:', e);
|
||||
}
|
||||
if (revisionString) {
|
||||
this.lastRevision = JSON.parse(revisionString);
|
||||
}
|
||||
|
|
@ -187,8 +193,12 @@
|
|||
|
||||
updateRevision: function() {
|
||||
this.lastRevision = this.datastore.revisionId;
|
||||
localStorage.setItem(this.revisionName,
|
||||
JSON.stringify(this.lastRevision));
|
||||
try {
|
||||
localStorage.setItem(this.revisionName,
|
||||
JSON.stringify(this.lastRevision));
|
||||
} catch (e) {
|
||||
console.error('Homescreen datastore revision could not be saved:', e);
|
||||
}
|
||||
},
|
||||
|
||||
onChange: function(e) {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,15 @@
|
|||
}
|
||||
|
||||
// Restore existing settings
|
||||
var settingsString = localStorage.getItem('settings');
|
||||
var settingsString;
|
||||
try {
|
||||
settingsString = localStorage.getItem('settings');
|
||||
} catch (e) {
|
||||
// Firefox partitions storage when Gaia is hosted in the desktop shell.
|
||||
// Keep launching the home screen with defaults if that storage is denied.
|
||||
console.error('Home screen local settings unavailable:', e);
|
||||
return;
|
||||
}
|
||||
if (!settingsString) {
|
||||
this.firstRun = true;
|
||||
return;
|
||||
|
|
@ -105,11 +113,15 @@
|
|||
|
||||
Settings.prototype = {
|
||||
save: function() {
|
||||
localStorage.setItem('settings', JSON.stringify({
|
||||
version: SETTINGS_VERSION,
|
||||
small: this.small,
|
||||
scrollSnapping: this.scrollSnapping
|
||||
}));
|
||||
try {
|
||||
localStorage.setItem('settings', JSON.stringify({
|
||||
version: SETTINGS_VERSION,
|
||||
small: this.small,
|
||||
scrollSnapping: this.scrollSnapping
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Home screen settings could not be saved:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
'The catalogue is closed. You can reopen it below.'));
|
||||
document.getElementById('open').hidden=true;
|
||||
});
|
||||
function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');}
|
||||
let catalogueUrl='';
|
||||
function browse(reload=false){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';const next='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');if(reload||!catalogueUrl){catalogueUrl=next;frame.src=next;}}
|
||||
function status(value){frame.style.visibility='hidden';message.hidden=false;document.getElementById('status').textContent=value;}
|
||||
async function prepare(url){
|
||||
if(busy)return;const current=++attempt;busy=true;prepared=null;installed=null;document.getElementById('open').hidden=true;
|
||||
|
|
@ -22,7 +23,10 @@
|
|||
document.getElementById('question').textContent=text('Installer cette application archivée ?','Install this archived application?')+' ('+(prepared.bytes/1048576).toFixed(1)+' Mio)';
|
||||
document.getElementById('permissions').textContent=text('Elle sera exécutée comme une application web, sans accès aux fonctions système. Certains anciens services peuvent ne plus fonctionner.','It will run as a web application without system access. Some old services may no longer work.');
|
||||
message.hidden=true;dialog.hidden=false;
|
||||
}catch(error){if(current!==attempt)return;status(text('Installation impossible : ','Cannot install: ')+error.message);busy=false;}
|
||||
}catch(error){if(current!==attempt)return;status(error.message==='PRIVILEGED_APP_UNSUPPORTED'?
|
||||
text('Cette application demande des API privilégiées qui ne sont pas disponibles dans Vulpes OS.',
|
||||
'This app requires privileged APIs that are not available in Vulpes OS.'):
|
||||
text('Installation impossible : ','Cannot install: ')+error.message);busy=false;}
|
||||
}
|
||||
dialog.addEventListener('submit',async event=>{event.preventDefault();if(!prepared)return;const token=prepared.token;prepared=null;dialog.hidden=true;status(text('Installation…','Installing…'));
|
||||
try {installed=await VulpesCompat.call('marketplace.install',{token});status(text('Application ajoutée à l’accueil.','Application added to Home.'));document.getElementById('open').hidden=false;}
|
||||
|
|
@ -32,7 +36,7 @@
|
|||
document.getElementById('cancel').onclick=cancel;
|
||||
document.getElementById('browse').onclick=cancel;
|
||||
document.getElementById('open').onclick=()=>installed && VulpesCompat.call('apps.launch',{manifestURL:installed.manifestURL});
|
||||
addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse();});
|
||||
addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse(true);});
|
||||
browse();
|
||||
setInterval(async()=>{if(busy)return;try{const url=await VulpesCompat.call('marketplace.poll');if(url)await prepare(url);}catch(error){console.error(error);}},750);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -59,12 +59,12 @@
|
|||
}
|
||||
},
|
||||
"icons": {
|
||||
"84": "/shared/resources/branding/firefox-browser.png",
|
||||
"126": "/shared/resources/branding/firefox-browser.png",
|
||||
"142": "/shared/resources/branding/firefox-browser.png",
|
||||
"189": "/shared/resources/branding/firefox-browser.png",
|
||||
"256": "/shared/resources/branding/firefox-browser.png",
|
||||
"284": "/shared/resources/branding/firefox-browser.png"
|
||||
"84": "/shared/resources/branding/browser_84.png",
|
||||
"126": "/shared/resources/branding/browser_126.png",
|
||||
"142": "/shared/resources/branding/browser_142.png",
|
||||
"189": "/shared/resources/branding/browser_189.png",
|
||||
"256": "/shared/resources/branding/browser_256.png",
|
||||
"284": "/shared/resources/branding/browser_284.png"
|
||||
},
|
||||
"chrome": {
|
||||
"navigation": true
|
||||
|
|
|
|||
BIN
gaia/apps/search/shared/resources/branding/browser_126.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
gaia/apps/search/shared/resources/branding/browser_142.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
gaia/apps/search/shared/resources/branding/browser_189.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
gaia/apps/search/shared/resources/branding/browser_256.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
gaia/apps/search/shared/resources/branding/browser_284.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
gaia/apps/search/shared/resources/branding/browser_336.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
gaia/apps/search/shared/resources/branding/browser_84.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -290,8 +290,12 @@ define(function(require) {
|
|||
*/
|
||||
_loadApps: function pl__loadApps(apps) {
|
||||
var table = this._permissionTable;
|
||||
var internalRoles = ['system', 'input', 'homescreen', 'search', 'addon', 'langpack'];
|
||||
apps.forEach(function(app) {
|
||||
var manifest = app.manifest ? app.manifest : app.updateManifest;
|
||||
if (manifest.role && internalRoles.indexOf(manifest.role) !== -1) {
|
||||
return;
|
||||
}
|
||||
if (manifest.type != 'certified') {
|
||||
this._apps.push(app);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ define(function(require) {
|
|||
}
|
||||
} else if (time !== Infinity && time === 0) {
|
||||
l10nAttrs.id = 'battery-' + type + '-complete';
|
||||
} else {
|
||||
l10nAttrs.id = 'battery-' + type + '-unavailable';
|
||||
}
|
||||
|
||||
return l10nAttrs;
|
||||
|
|
|
|||
|
|
@ -1356,6 +1356,8 @@ battery-level-percent={{level}}%
|
|||
battery-current-level=Current Level ({{level}}%)
|
||||
battery-charging-calculating=Calculating time required…
|
||||
battery-discharging-calculating=Calculating time remaining…
|
||||
battery-charging-unavailable=Charge time unavailable
|
||||
battery-discharging-unavailable=Time remaining unavailable
|
||||
battery-charging-complete=Fully charged
|
||||
battery-discharging-complete=Fully depleted
|
||||
battery-time-hour={[ plural(hours) ]}
|
||||
|
|
|
|||
|
|
@ -475,20 +475,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Set the .icon property before saving for
|
||||
// backwards compatibility with verticalhome
|
||||
// Set the .icon property before saving for backwards compatibility with
|
||||
// verticalhome. A missing favicon must not prevent pinning the site.
|
||||
var saveSite = function(icon) {
|
||||
siteObject.icon = icon || '';
|
||||
BookmarksDatabase.put(siteObject, siteObject.id)
|
||||
.then(() => {
|
||||
this.app.debug('pinSite: ' + siteObject.id);
|
||||
this.systemBanner.show('site-pinned-to-home-screen');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('pinSite, Failed to pin site: ' + error);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
IconsHelper.getIcon(siteObject.url, null,
|
||||
{icons: this.app.favicons}, siteObject).then(icon => {
|
||||
siteObject.icon = icon;
|
||||
BookmarksDatabase.put(siteObject, siteObject.id)
|
||||
.then(() => {
|
||||
this.app.debug('pinSite: ' + siteObject.id);
|
||||
this.systemBanner.show('site-pinned-to-home-screen');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('pinSite, Failed to pin site: ' + error);
|
||||
});
|
||||
});
|
||||
{icons: this.app.favicons}, siteObject).then(saveSite, function(error) {
|
||||
this.app.debug('pinSite: using no favicon: ' + error);
|
||||
saveSite('');
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -986,12 +991,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Enable/disable the bookmark option
|
||||
BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
|
||||
// start_url is the bookmark key for installed web apps, while _currentURL
|
||||
// may be a later page in the app. Check the canonical key first.
|
||||
var siteUrl = this.getSiteUrl();
|
||||
BookmarksDatabase.get(siteUrl).then(function resolve(result) {
|
||||
this.addToHomeButton.hidden = !!result;
|
||||
}.bind(this),
|
||||
function reject() {
|
||||
this.addToHomeButton.hidden = true;
|
||||
this.addToHomeButton.hidden = false;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -242,76 +242,29 @@
|
|||
div.dataset.zIndexLevel = 'poweroff-splash';
|
||||
div.id = 'poweroff-splash';
|
||||
|
||||
this.poweroffLogoLoader = new LogoLoader(this._poweroff);
|
||||
// Vulpes uses Gaia's three-ring shutdown animation without a carrier
|
||||
// image or video layered over it.
|
||||
var inner;
|
||||
for (var i = 1; i <= 3; i++) {
|
||||
var outer = document.createElement('span');
|
||||
outer.className = 'poweroff-ring';
|
||||
outer.id = 'poweroff-ring-' + i;
|
||||
div.appendChild(outer);
|
||||
inner = document.createElement('span');
|
||||
outer.appendChild(inner);
|
||||
}
|
||||
|
||||
this.poweroffLogoLoader.onload = function(elem) {
|
||||
// Perform customized animation.
|
||||
div.appendChild(elem);
|
||||
div.className = 'step1';
|
||||
|
||||
if (elem.tagName.toLowerCase() == 'video' && !elem.ended) {
|
||||
elem.onended = function() {
|
||||
elem.classList.add('hide');
|
||||
// XXX workaround of bug 831747
|
||||
// Unload the video. This releases the video decoding hardware
|
||||
// so other apps can use it.
|
||||
elem.removeAttribute('src');
|
||||
elem.load();
|
||||
};
|
||||
elem.play();
|
||||
} else {
|
||||
div.addEventListener('animationend', function() {
|
||||
elem.classList.add('hide');
|
||||
if (elem.tagName.toLowerCase() == 'video') {
|
||||
// XXX workaround of bug 831747
|
||||
// Unload the video. This releases the video decoding hardware
|
||||
// so other apps can use it.
|
||||
elem.removeAttribute('src');
|
||||
elem.load();
|
||||
}
|
||||
});
|
||||
div.className = 'step1';
|
||||
var nextAnimation = function nextAnimation(e) {
|
||||
if (e.target == div) {
|
||||
div.className = 'step2';
|
||||
}
|
||||
|
||||
elem.addEventListener('transitionend', function() {
|
||||
if (e.target == inner) {
|
||||
self._actualPowerOff(reboot);
|
||||
});
|
||||
document.getElementById('screen').appendChild(div);
|
||||
};
|
||||
|
||||
this.poweroffLogoLoader.onnotfound = function() {
|
||||
// - Perform OS default animation.
|
||||
|
||||
// The overall animation ends when the inner span of the bottom ring
|
||||
// is animated, so we store it for detecting.
|
||||
var inner;
|
||||
|
||||
for (var i = 1; i <= 3; i++) {
|
||||
var outer = document.createElement('span');
|
||||
outer.className = 'poweroff-ring';
|
||||
outer.id = 'poweroff-ring-' + i;
|
||||
div.appendChild(outer);
|
||||
|
||||
inner = document.createElement('span');
|
||||
outer.appendChild(inner);
|
||||
}
|
||||
|
||||
div.className = 'step1';
|
||||
var nextAnimation = function nextAnimation(e) {
|
||||
// Switch to next class
|
||||
if (e.target == div) {
|
||||
div.className = 'step2';
|
||||
}
|
||||
|
||||
if (e.target != inner) {
|
||||
return;
|
||||
}
|
||||
|
||||
self._actualPowerOff(reboot);
|
||||
};
|
||||
div.addEventListener('animationend', nextAnimation);
|
||||
|
||||
document.getElementById('screen').appendChild(div);
|
||||
};
|
||||
div.addEventListener('animationend', nextAnimation);
|
||||
document.getElementById('screen').appendChild(div);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -265,7 +265,8 @@
|
|||
// until it works. It may fail at the moment the screenchange
|
||||
// event has been fired, so we may need to try it several times.
|
||||
var tryLockOrientation = () => {
|
||||
if (screen.mozLockOrientation('portrait-primary')) {
|
||||
// Allow the lock screen to follow the physical display orientation.
|
||||
if (screen.mozLockOrientation('any')) {
|
||||
if (!this.orientationLockID) {
|
||||
throw new Error('No orientation ID. This function should only' +
|
||||
'be invoked as a interval callback');
|
||||
|
|
|
|||
|
|
@ -346,13 +346,26 @@
|
|||
|
||||
case this.fullApp:
|
||||
// XXX: This should be replaced probably by Web Activities
|
||||
var host = document.location.host;
|
||||
var domain = host.replace(/(^[\w\d]+\.)?([\w\d]+\.[a-z]+)/, '$2');
|
||||
var protocol = document.location.protocol + '//';
|
||||
applications.getByManifestURL(protocol + 'settings.' +
|
||||
domain + '/manifest.webapp').launch();
|
||||
|
||||
UtilityTray.hide();
|
||||
var settingsURL = new URL(document.location.href);
|
||||
if (settingsURL.hostname.indexOf('system.') !== 0) {
|
||||
console.error('Quick settings: cannot resolve Settings app host');
|
||||
return;
|
||||
}
|
||||
settingsURL.hostname = 'settings.' +
|
||||
settingsURL.hostname.slice('system.'.length);
|
||||
settingsURL.pathname = '/manifest.webapp';
|
||||
settingsURL.search = '';
|
||||
settingsURL.hash = '';
|
||||
applications.waitForReady().then(function() {
|
||||
var settingsApp = applications.getByManifestURL(
|
||||
settingsURL.href);
|
||||
if (!settingsApp) {
|
||||
console.error('Quick settings: Settings app is not installed');
|
||||
return;
|
||||
}
|
||||
settingsApp.launch();
|
||||
UtilityTray.hide();
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -187,6 +187,15 @@
|
|||
},
|
||||
|
||||
hasPermission: function wf_hasPermission(app, permission) {
|
||||
// The embedded Gaia browser is the trusted system web client. Newer
|
||||
// Gecko builds no longer expose mozPermissionSettings to this code, so
|
||||
// honour its declared window permission and keep popup navigation in
|
||||
// Gaia instead of letting Gecko open a desktop browser window.
|
||||
if (app && app.manifest && app.manifest.role === 'search' &&
|
||||
permission === 'open-remote-window' && app.manifest.permissions &&
|
||||
app.manifest.permissions['open-remote-window']) {
|
||||
return true;
|
||||
}
|
||||
var mozPerms = navigator.mozPermissionSettings;
|
||||
if (!mozPerms) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
#app-install-dialog p {
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
border-top: none;
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
#app-install-dialog span {
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
color: inherit;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,6 @@
|
|||
#os-logo { background:#071d35; display:flex; flex-direction:column; align-items:center; justify-content:center; }
|
||||
#os-logo::before { content:""; width:min(42vw,190px); aspect-ratio:1; flex:none; background:url('/shared/resources/branding/vulpes-symbol.png') center / contain no-repeat; animation:vulpes-boot 2s ease-in-out infinite alternate; }
|
||||
#os-logo::after { content:"VULPES\A Firefox OS Community Project"; white-space:pre-line; padding:12px 22px; color:#f5f8fa; font:500 20px/1.8 FiraSans,sans-serif; text-align:center; }
|
||||
#poweroff-splash { background:#071d35 url('/shared/resources/branding/vulpes-symbol.png') center / 190px 190px no-repeat; }
|
||||
#poweroff-splash { background:#071d35; }
|
||||
@keyframes vulpes-boot { from {opacity:.75} to {opacity:1} }
|
||||
@media (prefers-reduced-motion: reduce) { #os-logo::before {animation:none} }
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ requireApp('system/shared/test/unit/mocks/mock_navigator_moz_settings.js');
|
|||
requireApp('system/shared/test/unit/mocks/mock_navigator_moz_power.js');
|
||||
|
||||
suite('CoverScreen >', function() {
|
||||
var settingsCore, realSettings, subject, realMozPower;
|
||||
var settingsCore, realSettings, subject, realMozPower, fakeScreen;
|
||||
const DEFAULT_VIDEO_ON = '/resources/power/carrier_power_on.mp4';
|
||||
const DEFAULT_VIDEO_OFF = '/resources/power/carrier_power_off.mp4';
|
||||
const DEFAULT_IMAGE_ON = '/resources/power/carrier_power_on.png';
|
||||
|
|
@ -113,6 +113,9 @@ suite('CoverScreen >', function() {
|
|||
var fakeElement = document.createElement('div');
|
||||
if (id === 'poweroff-splash') {
|
||||
return null;
|
||||
} else if (id === 'screen') {
|
||||
fakeScreen = fakeElement;
|
||||
return fakeScreen;
|
||||
} else {
|
||||
return fakeElement.cloneNode(true);
|
||||
}
|
||||
|
|
@ -135,10 +138,9 @@ suite('CoverScreen >', function() {
|
|||
test('restart requested', function() {
|
||||
var stub = this.sinon.stub(navigator.mozPower, 'reboot');
|
||||
subject.poweroff(true);
|
||||
var element = document.createElement('div');
|
||||
var transitionStub = this.sinon.stub(element, 'addEventListener');
|
||||
subject.poweroffLogoLoader.onload(element);
|
||||
transitionStub.getCall(0).args[1]();
|
||||
fakeScreen.lastChild.dispatchEvent(new Event('animationend'));
|
||||
fakeScreen.lastChild.querySelector('#poweroff-ring-3 > span')
|
||||
.dispatchEvent(new Event('animationend', { bubbles: true }));
|
||||
assert.ok(stub.calledOnce);
|
||||
});
|
||||
|
||||
|
|
@ -151,11 +153,9 @@ suite('CoverScreen >', function() {
|
|||
test('poweroff requested', function() {
|
||||
var stub = this.sinon.stub(navigator.mozPower, 'powerOff');
|
||||
subject.poweroff(false);
|
||||
|
||||
var element = document.createElement('div');
|
||||
var transitionStub = this.sinon.stub(element, 'addEventListener');
|
||||
subject.poweroffLogoLoader.onload(element);
|
||||
transitionStub.getCall(0).args[1]();
|
||||
fakeScreen.lastChild.dispatchEvent(new Event('animationend'));
|
||||
fakeScreen.lastChild.querySelector('#poweroff-ring-3 > span')
|
||||
.dispatchEvent(new Event('animationend', { bubbles: true }));
|
||||
assert.ok(stub.calledOnce);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -253,9 +253,15 @@
|
|||
}
|
||||
};
|
||||
|
||||
// The Browser artwork changed with Vulpes branding. Don't keep its
|
||||
// decorated icon from an older homescreen database; let mozApps return
|
||||
// the manifest icon at the current grid's best available resolution.
|
||||
var isVulpesBrowser = /^(?:app:\/\/search\.gaiamobile\.org|http:\/\/search\.localhost(?::\d+)?)\/manifest\.webapp$/
|
||||
.test(entry.manifestURL);
|
||||
|
||||
return new GaiaGrid.Mozapp(app, entry.entryPoint, {
|
||||
// cached decorated icon blob to load faster
|
||||
decoratedIconBlob: entry.decoratedIconBlob
|
||||
decoratedIconBlob: isVulpesBrowser ? null : entry.decoratedIconBlob
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -54,7 +54,7 @@
|
|||
position: relative;
|
||||
font-weight: 300;
|
||||
font-size: 2.2rem;
|
||||
line-height: 3.9rem;
|
||||
line-height: normal;
|
||||
color: #fff;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
|
@ -81,8 +81,10 @@ html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li
|
|||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li label span {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
line-height: 4rem;
|
||||
box-sizing: border-box;
|
||||
min-height: 5.2rem;
|
||||
padding: 1.2rem 1.5rem;
|
||||
line-height: 2.8rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ import {Subprocess} from 'resource://gre/modules/Subprocess.sys.mjs';
|
|||
import {setTimeout, clearTimeout} from 'resource://gre/modules/Timer.sys.mjs';
|
||||
|
||||
export class NativeCamera {
|
||||
constructor() { this.session = null; this.queue = Promise.resolve(); this.closing = Promise.resolve(); this.nextSession = 0; }
|
||||
constructor(device = '') {
|
||||
this.device = device;
|
||||
this.previewSize = device === 'beatles' ? {width:320,height:240} : {width:640,height:480};
|
||||
this.session = null;
|
||||
this.queue = Promise.resolve();
|
||||
this.closing = Promise.resolve();
|
||||
this.nextSession = 0;
|
||||
}
|
||||
|
||||
request(actor, params) {
|
||||
const work = this.queue.then(() => this.perform(actor, params));
|
||||
|
|
@ -24,7 +31,8 @@ export class NativeCamera {
|
|||
if(length>65536) throw Error('CAMERA_INVALID_FORMAT');
|
||||
return pipe.readJSON(length);
|
||||
}
|
||||
if(length!==0 && length!==640*480*3/2) throw Error('CAMERA_INVALID_FRAME');
|
||||
const expected = this.session.width * this.session.height * 3 / 2;
|
||||
if(length!==0 && length!==expected) throw Error('CAMERA_INVALID_FRAME');
|
||||
return {sequence,data:length ? await pipe.read(length) : null};
|
||||
})(),
|
||||
new Promise((_, reject) => { timer=setTimeout(()=>reject(Error('CAMERA_TIMEOUT')),15000); }),
|
||||
|
|
@ -40,15 +48,28 @@ export class NativeCamera {
|
|||
if (params.action === 'start') {
|
||||
if (this.session && this.session.actor !== actor) throw Error('CAMERA_BUSY');
|
||||
await this.close();
|
||||
const process = await Subprocess.call({command:'/usr/bin/python3',
|
||||
arguments:['-u', '/etc/tundra/camera-preview.py', '--camera', params.camera === 'front' ? '1' : '0'],
|
||||
if (this.device === 'beatles' && params.camera === 'front') throw Error('CAMERA_UNAVAILABLE');
|
||||
const beatles = this.device === 'beatles';
|
||||
const process = await Subprocess.call({command:beatles?'/usr/bin/python3.11':'/usr/bin/python3',
|
||||
arguments:beatles
|
||||
? ['-u','/opt/vulpes/tundra/runtime/beatles-camera-client.py']
|
||||
: ['-u', '/etc/tundra/camera-preview.py', '--camera', params.camera === 'front' ? '1' : '0',
|
||||
'--width',String(this.previewSize.width),'--height',String(this.previewSize.height)],
|
||||
stderr:'ignore'});
|
||||
this.session = {actor, process, id: ++this.nextSession};
|
||||
try {
|
||||
const metadata = await this.packet(this.session, true);
|
||||
if (!metadata.ready || metadata.format !== 'nv21' || metadata.width !== 640 || metadata.height !== 480)
|
||||
if (!metadata.ready || metadata.format !== 'nv21' ||
|
||||
metadata.width !== this.previewSize.width || metadata.height !== this.previewSize.height)
|
||||
throw Error('CAMERA_INVALID_FORMAT');
|
||||
return {...metadata, session: this.session.id};
|
||||
this.session.width = metadata.width;
|
||||
this.session.height = metadata.height;
|
||||
return {...metadata, session: this.session.id,
|
||||
// The 2720 Flip has 400 MB of RAM and copies each frame through
|
||||
// CameraService, Gecko IPC and a software canvas. Polling at the
|
||||
// sensor's nominal rate exhausts its zram; keep its preview bounded.
|
||||
frameRate: beatles ? 5 : 30,
|
||||
cameras: beatles ? ['back'] : ['back','front']};
|
||||
} catch (error) { this.close(); throw error; }
|
||||
}
|
||||
if (!this.session || this.session.actor !== actor || this.session.id !== params.session)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export class Platform {
|
|||
this.notifications = new Map();
|
||||
this.nextId = 1;
|
||||
this.cameraWindow = null;
|
||||
this.nativeCamera = new NativeCamera();
|
||||
this.nativeCamera = new NativeCamera(Services.env.get('VULPES_DEVICE') || '');
|
||||
this.picking = false;
|
||||
}
|
||||
async init() {
|
||||
|
|
@ -242,6 +242,7 @@ export class Platform {
|
|||
throw Error('NOT_SUPPORTED');
|
||||
}
|
||||
if (Services.env.get('VULPES_TUNDRA_STATUS') === '1') {
|
||||
if (operation === 'cameraPreview') return this.nativeCamera.request(actor, params);
|
||||
if (operation === 'wifi') return tundraWifi(params);
|
||||
const radio = Services.env.get('VULPES_TUNDRA_RADIO') === '1';
|
||||
if (radio && operation === 'radioPower') return tundraRadio('radioPower',params);
|
||||
|
|
@ -255,7 +256,7 @@ export class Platform {
|
|||
adapter:{version:'0.1.0-dev',device:'beatles'}, tundra:'0.1.0-dev',
|
||||
version:this.runtime.release.version, gaia:this.runtime.release.gaia,
|
||||
gecko:Services.appinfo.platformVersion, hardwareStatus:true,
|
||||
radioStatus:true, wifiControl:Services.env.get('VULPES_TUNDRA_WIFI') === '1', camera:false, media:true, contacts:true,
|
||||
radioStatus:true, wifiControl:Services.env.get('VULPES_TUNDRA_WIFI') === '1', camera:Services.env.get('VULPES_TUNDRA_CAMERA') === '1', media:true, contacts:true,
|
||||
notifications:true, dial:radio, callControl:radio, sms:radio, mms:false, callAudio:radio, alarms:false,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,6 +281,16 @@ export const Runtime = {
|
|||
this.actors.add(actor);
|
||||
const { method, params } = request;
|
||||
if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } };
|
||||
if (method === 'system.screenshot') {
|
||||
if (app.id !== 'system') return { error: { code: 'PERMISSION_DENIED' } };
|
||||
try { return { result: await Views.captureSystem(actor) }; }
|
||||
catch (error) { return { error: { code: error.message || 'SCREENSHOT_FAILED' } }; }
|
||||
}
|
||||
if (method === 'apps.close') {
|
||||
if (app.id !== 'costcontrol') return { error: { code: 'PERMISSION_DENIED' } };
|
||||
this.broadcast('home', {}, 'system');
|
||||
return { result: true };
|
||||
}
|
||||
if(method.startsWith('marketplace.')) {
|
||||
if(app.id!=='marketplace')return {error:{code:'PERMISSION_DENIED'}};
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export async function auditTickets({win,evaluate,wait,tap,sleep,root}) {
|
|||
await sleep(600);
|
||||
};
|
||||
win.focus();win.document.getElementById('system').focus();
|
||||
await step('unlock',()=>ev('system','ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});return true;'));
|
||||
await step('unlock',()=>ev('system','ScreenManager.turnScreenOn(true);if(Service.query("locked"))lockScreen.unlock(true);return !Service.query("locked");'));
|
||||
await step('cameraOpen',async()=>{
|
||||
await launch('camera');
|
||||
await wait('camera','return !!app.camera.mozCamera && !app.camera.isBusy;',15000);
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ export function exchange(path, params, limit=65536, timeout=20000) {
|
|||
const bytes=new TextEncoder().encode(JSON.stringify(params)+'\n');
|
||||
if (bytes.length>1228800) return Promise.reject(Error('REQUEST_TOO_LARGE'));
|
||||
return new Promise((resolve,reject)=>{
|
||||
let transport,input,output,reader,timer,finished=false,received='',offset=0;
|
||||
let transport,input,output,reader,pump,timer,finished=false,received='',offset=0;
|
||||
const finish=(error,value)=>{
|
||||
if(finished)return;finished=true;clearTimeout(timer);
|
||||
try{pump?.cancel(Cr.NS_BINDING_ABORTED);}catch(_){}
|
||||
try{input?.close();}catch(_){}try{output?.close();}catch(_){}
|
||||
try{transport?.close(Cr.NS_BINDING_ABORTED);}catch(_){}
|
||||
if(error)reject(error);else resolve(value);
|
||||
|
|
@ -35,20 +36,31 @@ export function exchange(path, params, limit=65536, timeout=20000) {
|
|||
output=transport.openOutputStream(0,0,0).QueryInterface(Ci.nsIAsyncOutputStream);
|
||||
reader=Cc['@mozilla.org/scriptableinputstream;1'].createInstance(Ci.nsIScriptableInputStream);
|
||||
reader.init(input);
|
||||
const read={onInputStreamReady(){
|
||||
if(finished)return;
|
||||
try{
|
||||
const count=input.available();
|
||||
if(!count)throw Error('BROKER_CLOSED');
|
||||
if(received.length+count>limit)throw Error('BROKER_REPLY_TOO_LARGE');
|
||||
received+=reader.readBytes(count);
|
||||
const read={
|
||||
onStartRequest() {},
|
||||
onDataAvailable(_request,stream,_offset,count){
|
||||
if(finished)return;
|
||||
try{
|
||||
if(received.length+count>limit)throw Error('BROKER_REPLY_TOO_LARGE');
|
||||
received+=reader.readBytes(count);
|
||||
const end=received.indexOf('\n');
|
||||
if(end>=0){
|
||||
const data=Uint8Array.from(received.slice(0,end),c=>c.charCodeAt(0));
|
||||
finish(null,JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(data)));
|
||||
}
|
||||
}catch(error){finish(error);}
|
||||
},
|
||||
onStopRequest(_request,status){
|
||||
if(finished)return;
|
||||
const end=received.indexOf('\n');
|
||||
if(end>=0){
|
||||
const data=Uint8Array.from(received.slice(0,end),c=>c.charCodeAt(0));
|
||||
finish(null,JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(data)));
|
||||
}else input.asyncWait(read,0,0,Services.tm.currentThread);
|
||||
}catch(error){finish(error);}
|
||||
}};
|
||||
try{
|
||||
const data=Uint8Array.from(received.slice(0,end),c=>c.charCodeAt(0));
|
||||
finish(null,JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(data)));
|
||||
}catch(error){finish(error);}
|
||||
}else finish(Error(status===Cr.NS_OK?'BROKER_CLOSED':'BROKER_READ_FAILED'));
|
||||
}
|
||||
};
|
||||
const write={onOutputStreamReady(){
|
||||
if(finished)return;
|
||||
try{
|
||||
|
|
@ -57,7 +69,13 @@ export function exchange(path, params, limit=65536, timeout=20000) {
|
|||
if(offset<bytes.length)output.asyncWait(write,0,0,Services.tm.currentThread);
|
||||
}catch(error){finish(error);}
|
||||
}};
|
||||
input.asyncWait(read,0,0,Services.tm.currentThread);
|
||||
pump=Cc['@mozilla.org/network/input-stream-pump;1'].createInstance(Ci.nsIInputStreamPump);
|
||||
// Gecko removed the stream position/length parameters. Its current
|
||||
// signature is (stream, segmentSize, segmentCount, closeWhenDone,
|
||||
// mainThreadTarget), so passing the legacy six arguments puts an
|
||||
// integer where the optional event target is expected.
|
||||
pump.init(input,0,0,false);
|
||||
pump.asyncRead(read);
|
||||
output.asyncWait(write,0,0,Services.tm.currentThread);
|
||||
}catch(error){finish(error);}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,28 @@
|
|||
// real top-level browser, never by removing CSP or frame-ancestors protection.
|
||||
export const Views = {
|
||||
entries: new Map(),
|
||||
async captureSystem(actor) {
|
||||
const context = actor.browsingContext;
|
||||
const uri = context?.currentWindowGlobal?.documentURI;
|
||||
if (!context || context.parent || uri?.scheme !== 'http' || uri?.host !== 'system.localhost' ||
|
||||
uri?.filePath !== '/index.html') {
|
||||
throw new Error('SCREENSHOT_NOT_AVAILABLE');
|
||||
}
|
||||
const image = await context.currentWindowGlobal.drawSnapshot(null, 1, 'white');
|
||||
const doc = context.top.embedderElement?.ownerDocument;
|
||||
if (!doc) {
|
||||
image.close();
|
||||
throw new Error('SCREENSHOT_NOT_AVAILABLE');
|
||||
}
|
||||
const canvas = doc.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
canvas.getContext('2d').drawImage(image, 0, 0);
|
||||
image.close();
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('SCREENSHOT_FAILED')), 'image/png');
|
||||
});
|
||||
},
|
||||
syncContext(entry) {
|
||||
const context=entry.browser.browsingContext;
|
||||
if (!context || context.id===entry.topContextId) return;
|
||||
|
|
|
|||
|
|
@ -5,14 +5,7 @@ import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
|
|||
export class KeypadChild extends JSWindowActorChild {
|
||||
receiveMessage(message) {
|
||||
if (message.name === 'Vulpes:FocusBrowserToolbar') {
|
||||
if (this.document.location.hostname !== 'system.localhost') return;
|
||||
const bar = [...this.document.querySelectorAll('.chrome .urlbar')].find(element => {
|
||||
if (!element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && element.contains(this.document.elementFromPoint(
|
||||
rect.x + rect.width / 2, rect.y + rect.height / 2));
|
||||
});
|
||||
if (bar) this.select(bar);
|
||||
this.focusBrowserToolbar();
|
||||
return;
|
||||
}
|
||||
if (message.name !== 'Vulpes:Navigate') return;
|
||||
|
|
@ -37,6 +30,13 @@ export class KeypadChild extends JSWindowActorChild {
|
|||
this.hashStartReturnMode=this.composer.returnMode;
|
||||
}
|
||||
const target = document.activeElement;
|
||||
// The collapsed Gaia rocketbar can leave its search input focused after
|
||||
// dismissing address entry. On keypad devices, Up should still reopen the
|
||||
// browser toolbar instead of moving the caret in that hidden input.
|
||||
if (key === 'ArrowUp' && document.location.hostname === 'system.localhost' &&
|
||||
target?.id === 'rocketbar-input' && !document.querySelector('#rocketbar')?.classList.contains('active')) {
|
||||
if (this.focusBrowserToolbar()) return;
|
||||
}
|
||||
const editing = !target?.readOnly && (target?.isContentEditable || target?.localName === 'textarea' ||
|
||||
(target?.localName === 'input' && ['text','search','email','url','tel','password','number'].includes(target.type)));
|
||||
const modal = [...document.querySelectorAll('[aria-modal="true"],[data-type="confirm"],[data-type="action"]')]
|
||||
|
|
@ -172,6 +172,18 @@ export class KeypadChild extends JSWindowActorChild {
|
|||
}
|
||||
this.select(elements[index]);
|
||||
}
|
||||
focusBrowserToolbar() {
|
||||
if (this.document.location.hostname !== 'system.localhost') return false;
|
||||
const bar = [...this.document.querySelectorAll('.chrome .urlbar')].find(element => {
|
||||
if (!element.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.width > 0 && element.contains(this.document.elementFromPoint(
|
||||
rect.x + rect.width / 2, rect.y + rect.height / 2));
|
||||
});
|
||||
if (!bar) return false;
|
||||
this.select(bar);
|
||||
return true;
|
||||
}
|
||||
select(element) {
|
||||
const document = this.document;
|
||||
if (!this.style) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
// Local Vulpes bootstrap, loaded only for the isolated host launcher.
|
||||
try {
|
||||
if (Services.env.get('VULPES_DEVICE') === 'sargo') {
|
||||
// Keep a pinch gesture from scaling the phone UI as a whole.
|
||||
pref('apz.allow_zooming', false);
|
||||
}
|
||||
if (Services.env.get('VULPES_HOST_ROOT')) {
|
||||
var root = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsIFile);
|
||||
root.initWithPath(Services.env.get('VULPES_HOST_ROOT'));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
// The Gaia camera UI consumes mozCameras. Only live MediaStream capabilities are exposed here.
|
||||
(() => {
|
||||
if (location.hostname !== 'camera.localhost') return;
|
||||
const videoMime = typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus') ? 'video/webm;codecs=vp8,opus' : null;
|
||||
const supportedMime = candidates => typeof MediaRecorder !== 'undefined' &&
|
||||
typeof MediaRecorder.isTypeSupported === 'function' ?
|
||||
candidates.find(type => MediaRecorder.isTypeSupported(type)) || null : null;
|
||||
const videoOnlyMime = supportedMime([
|
||||
'video/webm;codecs=vp8', 'video/webm;codecs=vp9', 'video/webm'
|
||||
]);
|
||||
const videoAudioMime = supportedMime([
|
||||
'video/webm;codecs=vp8,opus', 'video/webm;codecs=vp9,opus'
|
||||
]);
|
||||
const nativeCaptureAvailable = window.VulpesPlatform !== 'tundra' ||
|
||||
typeof HTMLCanvasElement.prototype.captureStream === 'function';
|
||||
const videoMime = nativeCaptureAvailable ? videoOnlyMime : null;
|
||||
window.VulpesVideoExtension = videoMime ? 'webm' : null;
|
||||
let cameras = ['back'],
|
||||
devices = new Map(),
|
||||
|
|
@ -143,7 +154,7 @@
|
|||
}
|
||||
async function discover(stream, name) {
|
||||
if (stream.vulpesNative) {
|
||||
cameras = ['back', 'front'];
|
||||
cameras = stream.vulpesCameraMetadata.cameras || ['back', 'front'];
|
||||
return;
|
||||
}
|
||||
// Device enumeration may wait indefinitely for focus in embedded desktop contexts.
|
||||
|
|
@ -178,19 +189,26 @@
|
|||
const call = (params) => VulpesCompat.call('platform.cameraPreview', {...params, session});
|
||||
const meta = await call({action:'start', camera:name === 'front' ? 'front' : 'back'});
|
||||
session = meta.session;
|
||||
const frameRate = meta.frameRate || 30;
|
||||
const canvas = nativeCanvas || (nativeCanvas = document.createElement('canvas'));
|
||||
const sideways = meta.orientation % 180 !== 0;
|
||||
const width = sideways ? meta.height : meta.width;
|
||||
const height = sideways ? meta.width : meta.height;
|
||||
if (canvas.width !== width) canvas.width = width;
|
||||
if (canvas.height !== height) canvas.height = height;
|
||||
// Convert NV21 in a shader instead of blocking Gaia's UI with one JS loop
|
||||
// per pixel. The two textures contain Y and interleaved V/U respectively.
|
||||
// Prefer the GPU converter, but older feature-phone runtimes may expose no
|
||||
// WebGL context at all. Their 320x240 preview is small enough for 2D canvas.
|
||||
const gl = canvas.getContext('webgl', {alpha:false, antialias:false, preserveDrawingBuffer:true});
|
||||
if (!gl) {
|
||||
const context2d = gl ? null : canvas.getContext('2d', {alpha:false});
|
||||
if (!gl && !context2d) {
|
||||
await call({action:'stop'});
|
||||
throw new DOMException('Camera preview requires WebGL', 'NotSupportedError');
|
||||
throw new DOMException('Camera preview has no supported canvas renderer', 'NotSupportedError');
|
||||
}
|
||||
const rawCanvas = context2d ? document.createElement('canvas') : null;
|
||||
if (rawCanvas) { rawCanvas.width=meta.width; rawCanvas.height=meta.height; }
|
||||
const rawContext = rawCanvas?.getContext('2d', {alpha:false});
|
||||
const imageData = rawContext?.createImageData(meta.width,meta.height);
|
||||
const pixels = meta.width * meta.height;
|
||||
const timing = {frames:0, startedAt:performance.now(), conversionMs:0, transferMs:0, sensorFrames:0};
|
||||
let stopped = false, timer, stream, wakeLock, sequence = -1, closing, program, buffer;
|
||||
const textures = [], shaders = [];
|
||||
|
|
@ -200,12 +218,14 @@
|
|||
wakeLock?.unlock(); wakeLock = null;
|
||||
closing = call({action:'stop'}).finally(() => {
|
||||
if (stream) stream.vulpesClosed = true;
|
||||
textures.forEach(texture => gl.deleteTexture(texture));
|
||||
shaders.forEach(value => gl.deleteShader(value));
|
||||
if (buffer) gl.deleteBuffer(buffer);
|
||||
if (program) gl.deleteProgram(program);
|
||||
// The next lens reuses this context after its old stream has stopped.
|
||||
gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
if (gl) {
|
||||
textures.forEach(texture => gl.deleteTexture(texture));
|
||||
shaders.forEach(value => gl.deleteShader(value));
|
||||
if (buffer) gl.deleteBuffer(buffer);
|
||||
if (program) gl.deleteProgram(program);
|
||||
// The next lens reuses this context after its old stream has stopped.
|
||||
gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
} else context2d.clearRect(0,0,canvas.width,canvas.height);
|
||||
});
|
||||
return closing;
|
||||
};
|
||||
|
|
@ -217,6 +237,7 @@
|
|||
if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw Error('Camera shader compilation failed');
|
||||
return value;
|
||||
}
|
||||
if (gl) {
|
||||
program = gl.createProgram();
|
||||
gl.attachShader(program, shader(gl.VERTEX_SHADER, `
|
||||
attribute vec2 position; varying vec2 coord;
|
||||
|
|
@ -253,11 +274,11 @@
|
|||
gl.uniform1i(gl.getUniformLocation(program,name),index);
|
||||
});
|
||||
gl.viewport(0,0,canvas.width,canvas.height);
|
||||
const pixels = meta.width * meta.height;
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,meta.width,meta.height,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,null);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,meta.width/2,meta.height/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,null);
|
||||
}
|
||||
const draw = async () => {
|
||||
const requested = performance.now();
|
||||
const frame = await call({action:'frame'});
|
||||
|
|
@ -267,11 +288,32 @@
|
|||
if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame');
|
||||
sequence = frame.sequence; timing.sensorFrames = sequence;
|
||||
const start=performance.now();
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,pixels));
|
||||
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w/2,h/2,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(pixels));
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
if (gl) {
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,pixels));
|
||||
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w/2,h/2,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(pixels));
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
} else {
|
||||
const rgba=imageData.data;
|
||||
for(let y=0;y<h;y++) {
|
||||
const row=y*w,uv=pixels+(y>>1)*w;
|
||||
for(let x=0;x<w;x++) {
|
||||
const yy=Math.max(0,data[row+x]-16),pair=uv+(x&~1);
|
||||
const v=data[pair]-128,u=data[pair+1]-128,c=298*yy,offset=(row+x)*4;
|
||||
rgba[offset]=Math.max(0,Math.min(255,(c+409*v+128)>>8));
|
||||
rgba[offset+1]=Math.max(0,Math.min(255,(c-100*u-208*v+128)>>8));
|
||||
rgba[offset+2]=Math.max(0,Math.min(255,(c+516*u+128)>>8));
|
||||
rgba[offset+3]=255;
|
||||
}
|
||||
}
|
||||
rawContext.putImageData(imageData,0,0);
|
||||
context2d.save();
|
||||
if(meta.orientation===90){context2d.translate(canvas.width,0);context2d.rotate(Math.PI/2);}
|
||||
else if(meta.orientation===180){context2d.translate(canvas.width,canvas.height);context2d.rotate(Math.PI);}
|
||||
else if(meta.orientation===270){context2d.translate(0,canvas.height);context2d.rotate(-Math.PI/2);}
|
||||
context2d.drawImage(rawCanvas,0,0);context2d.restore();
|
||||
}
|
||||
timing.frames++;timing.conversionMs+=performance.now()-start;
|
||||
return true;
|
||||
};
|
||||
|
|
@ -302,9 +344,9 @@
|
|||
if (!stopped) { recordError(error); await stop().catch(recordError); }
|
||||
return;
|
||||
}
|
||||
if (!stopped) timer = setTimeout(next, Math.max(0, 1000/30-(performance.now()-started)));
|
||||
if (!stopped) timer = setTimeout(next, Math.max(0, 1000/frameRate-(performance.now()-started)));
|
||||
};
|
||||
timer = setTimeout(next, 1000/30);
|
||||
timer = setTimeout(next, 1000/frameRate);
|
||||
return stream;
|
||||
} catch (error) { await stop().catch(() => {}); throw error; }
|
||||
}
|
||||
|
|
@ -408,7 +450,8 @@
|
|||
let pictureSize = pictureSizes.reduce((a,b)=>a.width*a.height>b.width*b.height?a:b);
|
||||
let flashMode = 'off', flashUpdate = Promise.resolve();
|
||||
let focusArea = null, zoom = 1, mode = config.mode || 'picture', recording, startingRecording = false;
|
||||
const profile = {containerFormat:'webm',video:{width:size.width,height:size.height,codec:'vp8',bitrate:1500000,framerate:30},audio:{codec:'opus',samplerate:48000,channels:1,bitrate:64000}};
|
||||
const frameRate = native?.frameRate || 30;
|
||||
const profile = {containerFormat:'webm',video:{width:size.width,height:size.height,codec:videoMime?.includes('vp9')?'vp9':'vp8',bitrate:frameRate < 10 ? 500000 : 1500000,framerate:frameRate},audio:{codec:'opus',samplerate:48000,channels:1,bitrate:64000}};
|
||||
const configuration = () => ({
|
||||
mode,
|
||||
previewSize: size,
|
||||
|
|
@ -500,16 +543,55 @@
|
|||
if(!videoMime)throw new DOMException('Video unavailable','NotSupportedError');
|
||||
if(recording || startingRecording)throw new DOMException('Recording in progress','InvalidStateError');
|
||||
startingRecording=true;
|
||||
let audio, captured;
|
||||
let audio = null, captured, copyTimer, recordingCanvas, recordingContext;
|
||||
try {
|
||||
audio=await navigator.mediaDevices.getUserMedia({audio:true,video:false});
|
||||
// Video capture can still be useful when audio capture is unavailable.
|
||||
// Camera track failures remain fatal; only the optional audio track falls back.
|
||||
try {
|
||||
audio=await navigator.mediaDevices.getUserMedia({audio:true,video:false});
|
||||
} catch(error) {
|
||||
// Tundra may expose the camera through its HAL while Gecko has no
|
||||
// microphone capture backend. Audio is optional: that must not
|
||||
// prevent the video encoder from starting.
|
||||
console.info('Vulpes Camera: microphone unavailable; recording video without audio', error.name || error);
|
||||
}
|
||||
if(current!==stream || document.hidden)throw new DOMException('Camera closed','AbortError');
|
||||
// Only recording captures the native canvas. The preview keeps its
|
||||
// direct WebGL surface and the capture track is stopped before a switch.
|
||||
captured=native ? stream.vulpesPreviewCanvas.captureStream(30) : new MediaStream(stream.getVideoTracks().map(t=>t.clone()));
|
||||
for(const track of audio.getAudioTracks())captured.addTrack(track);
|
||||
const recorder=new MediaRecorder(captured,{mimeType:videoMime,videoBitsPerSecond:1500000,audioBitsPerSecond:64000});
|
||||
const chunks=[];let bytes=0,failed=false,finish;
|
||||
if(native) {
|
||||
// Gecko 156 on the Flip does not emit MediaRecorder data from a
|
||||
// WebGL captureStream. Copy the already-rendered sensor preview
|
||||
// into a small 2D canvas at the device's bounded frame rate.
|
||||
const source=stream.vulpesPreviewCanvas;
|
||||
recordingCanvas=document.createElement('canvas');
|
||||
recordingCanvas.width=source.width;recordingCanvas.height=source.height;
|
||||
recordingContext=recordingCanvas.getContext('2d',{alpha:false});
|
||||
if(!recordingContext)throw new DOMException('Video canvas unavailable','NotSupportedError');
|
||||
captured=recordingCanvas.captureStream(frameRate);
|
||||
} else captured=new MediaStream(stream.getVideoTracks().map(t=>t.clone()));
|
||||
for(const track of audio?.getAudioTracks() || [])captured.addTrack(track);
|
||||
const recorderMime=audio && videoAudioMime ? videoAudioMime : videoOnlyMime;
|
||||
if (audio && !videoAudioMime) {
|
||||
audio.getTracks().forEach(track => track.stop());
|
||||
audio = null;
|
||||
}
|
||||
const recorderOptions={mimeType:recorderMime,
|
||||
videoBitsPerSecond:profile.video.bitrate,
|
||||
...(audio ? {audioBitsPerSecond:profile.audio.bitrate} : {})};
|
||||
let recorder;
|
||||
try {
|
||||
recorder=new MediaRecorder(captured,recorderOptions);
|
||||
} catch(error) {
|
||||
if(!audio || !videoAudioMime)throw error;
|
||||
// Some encoders report Opus support but fail to initialize it
|
||||
// for a live device stream. Retry video-only instead of losing
|
||||
// the entire recording.
|
||||
audio.getTracks().forEach(track=>track.stop());audio=null;
|
||||
captured.getAudioTracks().forEach(track=>captured.removeTrack(track));
|
||||
recorder=new MediaRecorder(captured,{mimeType:videoOnlyMime,
|
||||
videoBitsPerSecond:profile.video.bitrate});
|
||||
}
|
||||
const chunks=[];let bytes=0,failed=false,finish,sizeLimitTimer;
|
||||
const done=new Promise(resolve=>{finish=resolve;});
|
||||
const cap=Math.min(options.maxFileSizeBytes || Infinity,32*1024*1024);
|
||||
recorder.ondataavailable=event=>{
|
||||
|
|
@ -521,9 +603,11 @@
|
|||
};
|
||||
recorder.onerror=()=>{failed=true;emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});};
|
||||
recorder.onstop=async()=>{
|
||||
clearTimeout(sizeLimitTimer);
|
||||
try {
|
||||
if(failed || !bytes)throw Error('Empty or failed recording');
|
||||
const blob=new Blob(chunks,{type:'video/webm'});
|
||||
const blob=new Blob(chunks,{type:recorder.mimeType || recorderMime || 'video/webm'});
|
||||
if(blob.size>cap)throw new Error('Video exceeds the available storage limit');
|
||||
await storage.addNamed(blob,filepath);
|
||||
const canvas=document.createElement('canvas');
|
||||
const source=native ? stream.vulpesPreviewCanvas : preview;
|
||||
|
|
@ -536,16 +620,44 @@
|
|||
} catch(error) {
|
||||
recordError(error);emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});
|
||||
} finally {
|
||||
clearTimeout(copyTimer);
|
||||
captured.getTracks().forEach(t=>t.stop());
|
||||
recordingCanvas?.remove();
|
||||
chunks.length=0;recording=null;finish();
|
||||
emit(stream,'recorderstatechange',{newState:'Stopped'});
|
||||
}
|
||||
};
|
||||
recording={recorder,done};
|
||||
recorder.start(500);
|
||||
// Gecko 156's ARM/feature-phone media recorder can enter the
|
||||
// inactive state without firing dataavailable/onstop when a
|
||||
// timeslice is supplied. Collect the final chunk on stop instead.
|
||||
recorder.start();
|
||||
if(recordingCanvas) {
|
||||
const track=captured.getVideoTracks()[0];
|
||||
const copyFrame=()=>{
|
||||
if(recorder.state!=='recording')return;
|
||||
try {recordingContext.drawImage(stream.vulpesPreviewCanvas,0,0,recordingCanvas.width,recordingCanvas.height);track.requestFrame?.();}
|
||||
catch(error) {recordError(error);}
|
||||
if(recorder.state==='recording')copyTimer=setTimeout(copyFrame,Math.max(40,1000/frameRate));
|
||||
};
|
||||
recordingContext.drawImage(stream.vulpesPreviewCanvas,0,0,recordingCanvas.width,recordingCanvas.height);
|
||||
track.requestFrame?.();
|
||||
copyTimer=setTimeout(copyFrame,Math.max(40,1000/frameRate));
|
||||
}
|
||||
if(Number.isFinite(cap)) {
|
||||
const bitrate=Math.max(1,profile.video.bitrate||500000);
|
||||
sizeLimitTimer=setTimeout(()=>{
|
||||
if(recorder.state==='recording') {
|
||||
emit(stream,'recorderstatechange',{newState:'FileSizeLimitReached'});
|
||||
recorder.stop();
|
||||
}
|
||||
},Math.max(1000,Math.floor(cap*8/bitrate*1000*.95)));
|
||||
}
|
||||
emit(stream,'recorderstatechange',{newState:'Started'});
|
||||
} catch(error) {
|
||||
clearTimeout(copyTimer);
|
||||
captured?.getTracks().forEach(t=>t.stop());audio?.getTracks().forEach(t=>t.stop());
|
||||
recordingCanvas?.remove();
|
||||
recording=null;throw error;
|
||||
} finally {startingRecording=false;}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,9 +30,14 @@
|
|||
const next = state.battery;
|
||||
if (next && Number.isFinite(next.level) && next.level >= 0 && next.level <= 1) {
|
||||
const oldLevel = battery.level, oldCharging = battery.charging;
|
||||
const oldChargingTime = battery.chargingTime, oldDischargingTime = battery.dischargingTime;
|
||||
battery.level = next.level; battery.charging = !!next.charging;
|
||||
battery.chargingTime = Number.isFinite(next.chargingTime) && next.chargingTime > 0 ? next.chargingTime : Infinity;
|
||||
battery.dischargingTime = Number.isFinite(next.dischargingTime) && next.dischargingTime > 0 ? next.dischargingTime : Infinity;
|
||||
if (oldLevel !== battery.level) battery.dispatchEvent(new Event('levelchange'));
|
||||
if (oldCharging !== battery.charging) battery.dispatchEvent(new Event('chargingchange'));
|
||||
if (oldChargingTime !== battery.chargingTime) battery.dispatchEvent(new Event('chargingtimechange'));
|
||||
if (oldDischargingTime !== battery.dischargingTime) battery.dispatchEvent(new Event('dischargingtimechange'));
|
||||
}
|
||||
} catch (_) { /* An unknown battery is not an empty battery. */ }
|
||||
}
|
||||
|
|
@ -278,6 +283,12 @@
|
|||
if (location.hostname==='system.localhost') addEventListener('mozContentEvent', event=>{
|
||||
if (event.detail?.type==='activity-choice')
|
||||
call('activities.select',{id:event.detail.id,value:event.detail.value}).catch(console.error);
|
||||
if (event.detail?.type==='take-screenshot')
|
||||
call('system.screenshot',{}).then(file=>dispatchEvent(new CustomEvent('mozChromeEvent',{
|
||||
detail:{type:'take-screenshot-success',file}
|
||||
}))).catch(error=>dispatchEvent(new CustomEvent('mozChromeEvent',{
|
||||
detail:{type:'take-screenshot-error',error:error.message || 'SCREENSHOT_FAILED'}
|
||||
})));
|
||||
});
|
||||
function application(record) {
|
||||
// DOMApplication never exposed the registry's internal id. Gaia uses `id`
|
||||
|
|
@ -412,6 +423,12 @@
|
|||
navigator.mozHasPendingMessage = name => name === 'activity' && (new URLSearchParams(location.search).has('activity') || /\/(pick|share)\.html$/.test(location.pathname));
|
||||
navigator.mozSetMessageHandlerPromise = (promise) => promise;
|
||||
window.VulpesCompat = { call, request, handlers };
|
||||
// Cost Control calls window.close() after its SIM-ready error dialog.
|
||||
// Desktop and Android use Gaia's in-app task switcher, so close the app
|
||||
// through the privileged host bridge instead of leaving the dialog stuck.
|
||||
if (location.hostname === 'costcontrol.localhost') {
|
||||
window.close = () => call('apps.close', {}).catch(console.error);
|
||||
}
|
||||
window.MozActivity = function (source) {
|
||||
return request(call('activities.start', source));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -43,6 +43,18 @@
|
|||
padding: 1rem 1.5rem;
|
||||
transform: none;
|
||||
}
|
||||
#display #vulpes-layout-row {
|
||||
display: block !important;
|
||||
min-height: 10rem;
|
||||
height: auto !important;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
#vulpes-layout-row > p {
|
||||
height: auto;
|
||||
line-height: 2.4rem;
|
||||
padding: 1rem 1.5rem;
|
||||
transform: none;
|
||||
}
|
||||
:root[data-vulpes-theme="dark"][data-vulpes-app="settings"] h2 {
|
||||
background-color: var(--vulpes-surface) !important;
|
||||
}
|
||||
|
|
@ -155,6 +167,12 @@ html[data-vulpes-layout="compact"][data-vulpes-app="homescreen"] #apps-panel > .
|
|||
padding-top: 2px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
html[data-vulpes-app="homescreen"] gaia-grid#icons {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html[data-vulpes-app="homescreen"] body.edit-mode gaia-grid#icons {
|
||||
padding-bottom: calc(5rem + var(--software-home-button-height, 5rem));
|
||||
}
|
||||
|
||||
/* A single applications panel; saved pinned pages remain available in classic mode. */
|
||||
html[data-vulpes-layout="compact"][data-vulpes-app="homescreen"] :is(#page-indicator-header, #pages-panel, #shadow) {
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@
|
|||
}
|
||||
// Settings follows delayed power changes. Adapters with live association
|
||||
// state also refresh System, so reconnects update the status-bar icon.
|
||||
if(location.hostname.startsWith('settings.') || state.state) {
|
||||
if(location.hostname.startsWith('settings.') || location.hostname.startsWith('system.')) {
|
||||
let pending=false;
|
||||
const refresh=setInterval(async()=>{
|
||||
if((location.hostname.startsWith('settings.') && document.hidden) || pending) return;
|
||||
|
|
|
|||
42
overrides/marketplace.gaiamobile.org/marketplace.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
(async()=>{
|
||||
const frame=document.getElementById('catalogue'),message=document.getElementById('message'),dialog=document.getElementById('confirm');
|
||||
let attempt=0,busy=false,prepared=null,installed=null,language=await VulpesCompat.call('locale.current');
|
||||
const fr=()=>/^fr(?:-|$)/i.test(language);
|
||||
const text=(f,e)=>fr()?f:e;
|
||||
function labels(){document.getElementById('browse').textContent=text('Ouvrir le catalogue','Open catalogue');document.getElementById('open').textContent=text('Ouvrir','Open');document.getElementById('install').textContent=text('Installer','Install');document.getElementById('cancel').textContent=text('Annuler','Cancel');}
|
||||
function loadFailed(){status(text('Catalogue inaccessible. Vérifiez la connexion Internet, puis réessayez.','Catalogue unavailable. Check your Internet connection, then try again.'));document.getElementById('open').hidden=true;}
|
||||
frame.addEventListener('mozbrowsererror',loadFailed);
|
||||
frame.addEventListener('mozbrowserclose',()=>{
|
||||
if(busy)return;
|
||||
status(text('Le catalogue est fermé. Vous pouvez le rouvrir ci-dessous.',
|
||||
'The catalogue is closed. You can reopen it below.'));
|
||||
document.getElementById('open').hidden=true;
|
||||
});
|
||||
let catalogueUrl='';
|
||||
function browse(reload=false){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';const next='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');if(reload||!catalogueUrl){catalogueUrl=next;frame.src=next;}}
|
||||
function status(value){frame.style.visibility='hidden';message.hidden=false;document.getElementById('status').textContent=value;}
|
||||
async function prepare(url){
|
||||
if(busy)return;const current=++attempt;busy=true;prepared=null;installed=null;document.getElementById('open').hidden=true;
|
||||
status(text('Vérification de l’application…','Checking application…'));
|
||||
try {const value=await VulpesCompat.call('marketplace.prepare',{url});if(current!==attempt)return;prepared=value;
|
||||
document.getElementById('title').textContent=prepared.name;
|
||||
document.getElementById('question').textContent=text('Installer cette application archivée ?','Install this archived application?')+' ('+(prepared.bytes/1048576).toFixed(1)+' Mio)';
|
||||
document.getElementById('permissions').textContent=text('Elle sera exécutée comme une application web, sans accès aux fonctions système. Certains anciens services peuvent ne plus fonctionner.','It will run as a web application without system access. Some old services may no longer work.');
|
||||
message.hidden=true;dialog.hidden=false;
|
||||
}catch(error){if(current!==attempt)return;status(error.message==='PRIVILEGED_APP_UNSUPPORTED'?
|
||||
text('Cette application demande des API privilégiées qui ne sont pas disponibles dans Vulpes OS.',
|
||||
'This app requires privileged APIs that are not available in Vulpes OS.'):
|
||||
text('Installation impossible : ','Cannot install: ')+error.message);busy=false;}
|
||||
}
|
||||
dialog.addEventListener('submit',async event=>{event.preventDefault();if(!prepared)return;const token=prepared.token;prepared=null;dialog.hidden=true;status(text('Installation…','Installing…'));
|
||||
try {installed=await VulpesCompat.call('marketplace.install',{token});status(text('Application ajoutée à l’accueil.','Application added to Home.'));document.getElementById('open').hidden=false;}
|
||||
catch(error){status(text('Installation impossible : ','Cannot install: ')+error.message);}finally{busy=false;}
|
||||
});
|
||||
const cancel=()=>{attempt++;prepared=null;busy=false;VulpesCompat.call('marketplace.cancel').catch(console.error);browse();};
|
||||
document.getElementById('cancel').onclick=cancel;
|
||||
document.getElementById('browse').onclick=cancel;
|
||||
document.getElementById('open').onclick=()=>installed && VulpesCompat.call('apps.launch',{manifestURL:installed.manifestURL});
|
||||
addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse(true);});
|
||||
browse();
|
||||
setInterval(async()=>{if(busy)return;try{const url=await VulpesCompat.call('marketplace.poll');if(url)await prepare(url);}catch(error){console.error(error);}},750);
|
||||
})();
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,366 @@
|
|||
|
||||
define('modules/navigator/mozPermissionSettings',[],function(){'use strict';return window.navigator.mozPermissionSettings;});define('panels/app_permissions_list/app_permissions_list',["require", "modules/settings_service", "shared/manifest_helper", "modules/apps_cache", "modules/navigator/mozApps", "modules/navigator/mozPermissionSettings"],function(require) {
|
||||
'use strict';
|
||||
|
||||
var SettingsService = require('modules/settings_service');
|
||||
var ManifestHelper = require('shared/manifest_helper');
|
||||
var AppsCache = require('modules/apps_cache');
|
||||
var mozApps = require('modules/navigator/mozApps');
|
||||
var mozPerms = require('modules/navigator/mozPermissionSettings');
|
||||
|
||||
var PermissionsList = function pl() {
|
||||
this._listRoot = null;
|
||||
this._permissionsTable = null;
|
||||
this._permissionTableHaveProcessed = false;
|
||||
this._apps = null;
|
||||
this._enabled = false;
|
||||
};
|
||||
|
||||
PermissionsList.prototype = {
|
||||
/**
|
||||
* initialization
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {HTMLElement} listRoot
|
||||
* @access public
|
||||
*/
|
||||
init: function pl_init(listRoot) {
|
||||
this._listRoot = listRoot;
|
||||
|
||||
this._boundOnAppChoose = this._onAppChoose.bind(this);
|
||||
this._boundOnApplicationInstall = this._onApplicationInstall.bind(this);
|
||||
this._boundOnApplicationUninstall =
|
||||
this._onApplicationUninstall.bind(this);
|
||||
},
|
||||
|
||||
set enabled(value) {
|
||||
if (value !== this._enabled) {
|
||||
this._enabled = value;
|
||||
if (this._enabled) {
|
||||
this._bindEvents();
|
||||
} else {
|
||||
this._unbindEvents();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_bindEvents: function pl__bindEvents() {
|
||||
this._listRoot.addEventListener('click', this._boundOnAppChoose);
|
||||
AppsCache.addEventListener('oninstall', this._boundOnApplicationInstall);
|
||||
AppsCache.addEventListener('onuninstall',
|
||||
this._boundOnApplicationUninstall);
|
||||
},
|
||||
|
||||
_unbindEvents: function pl__unbindEvents() {
|
||||
this._listRoot.removeEventListener('click', this._boundOnAppChoose);
|
||||
AppsCache.removeEventListener('oninstall',
|
||||
this._boundOnApplicationInstall);
|
||||
AppsCache.removeEventListener('onuninstall',
|
||||
this._boundOnApplicationUninstall);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set /resources/permissions_table.json.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Object} permissionTable
|
||||
* @access public
|
||||
*/
|
||||
setPermissionsTable: function pl_setPermissionsTable(permissionTable) {
|
||||
this._permissionTable = permissionTable;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the app list when we enter into panel.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @access public
|
||||
* @return {Promise}
|
||||
*/
|
||||
refresh: function pl_refresh() {
|
||||
var self = this;
|
||||
this._apps = [];
|
||||
if (this._permissionTableHaveProcessed) {
|
||||
return this.loadApps();
|
||||
} else {
|
||||
return mozApps.getSelf().then(function(app) {
|
||||
return self._initExplicitPermissionsTable(app);
|
||||
}).then(function() {
|
||||
return self.loadApps();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Go to app_permissions_detail panel when user select an app.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Event} evt
|
||||
* @access public
|
||||
*/
|
||||
_onAppChoose: function pl__onAppChoose(evt) {
|
||||
var target = evt.target.closest('[data-app-index]');
|
||||
if (target && this._listRoot.contains(target)) {
|
||||
SettingsService.navigate('appPermissions-details', {
|
||||
app: this._apps[target.dataset.appIndex],
|
||||
permissionsTable: this._permissionTable
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When new application is installed, we push the app to list, sort them and
|
||||
* rerender the app list.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Event} evt
|
||||
* @access public
|
||||
*/
|
||||
_onApplicationInstall: function pl__onApplicationInstall(evt) {
|
||||
var app = evt.application;
|
||||
this._apps.push(app);
|
||||
this._sortApps();
|
||||
this.renderList();
|
||||
},
|
||||
|
||||
/**
|
||||
* When application is uninstalled, we remove it from list and rerender the
|
||||
* app list.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Event} evt
|
||||
* @access public
|
||||
*/
|
||||
_onApplicationUninstall: function pl__onApplicationUninstall(evt) {
|
||||
var app;
|
||||
var appIndex;
|
||||
this._apps.some(function findApp(anApp, index) {
|
||||
if (anApp.origin === evt.application.origin) {
|
||||
app = anApp;
|
||||
appIndex = index;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
SettingsService.navigate('appPermissions');
|
||||
this._apps.splice(appIndex, 1);
|
||||
this.renderList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sort the applist by the name of its manifest.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @access private
|
||||
*/
|
||||
_sortApps: function pl__sortApps() {
|
||||
this._apps.sort(function alphabeticalSort(app, otherApp) {
|
||||
var manifest = new ManifestHelper(app.manifest ?
|
||||
app.manifest : app.updateManifest);
|
||||
var otherManifest = new ManifestHelper(otherApp.manifest ?
|
||||
otherApp.manifest : otherApp.updateManifest);
|
||||
return manifest.name > otherManifest.name;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Genrate UI template of app item.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @access private
|
||||
* @param {Object} itemData
|
||||
* @return {HTMLDivElement}
|
||||
*/
|
||||
_genAppItemTemplate: function pl__genAppItemTemplate(itemData) {
|
||||
var icon = document.createElement('img');
|
||||
var item = document.createElement('li');
|
||||
var link = document.createElement('a');
|
||||
var span = document.createElement('span');
|
||||
span.textContent = itemData.name;
|
||||
span.dir = 'auto';
|
||||
icon.src = itemData.iconSrc;
|
||||
link.dataset.appIndex = itemData.index;
|
||||
link.href = '#';
|
||||
link.classList.add('menu-item');
|
||||
link.appendChild(icon);
|
||||
link.appendChild(span);
|
||||
item.appendChild(link);
|
||||
return item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Genrate UI template of app item.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @access public
|
||||
*/
|
||||
renderList: function pl_renderList() {
|
||||
this._listRoot.innerHTML = '';
|
||||
var listFragment = document.createDocumentFragment();
|
||||
this._apps.forEach(function appIterator(app, index) {
|
||||
var manifest = new ManifestHelper(app.manifest ?
|
||||
app.manifest : app.updateManifest);
|
||||
var li = this._genAppItemTemplate({
|
||||
name: manifest.displayName,
|
||||
index: index,
|
||||
iconSrc: this._getBestIconURL(app, manifest.icons)
|
||||
});
|
||||
listFragment.appendChild(li);
|
||||
}.bind(this));
|
||||
this._listRoot.appendChild(listFragment);
|
||||
},
|
||||
|
||||
/**
|
||||
* Genrate explicitCertifiedPermissions table from plainPermissions table
|
||||
* table and "composedPermissions + accessModes" table.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Event} evt
|
||||
* @access private
|
||||
*/
|
||||
_initExplicitPermissionsTable:
|
||||
function pl__initExplicitPermissionsTable(app) {
|
||||
this._currentApp = app;
|
||||
|
||||
var table = this._permissionTable;
|
||||
table.explicitCertifiedPermissions = [];
|
||||
table.plainPermissions.forEach(function plainPermIterator(perm) {
|
||||
if (this._isExplicitPerm(perm)) {
|
||||
table.explicitCertifiedPermissions.push({
|
||||
explicitPermission: perm,
|
||||
permission: perm
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
table.composedPermissions.forEach(function permIterator(perm) {
|
||||
table.accessModes.forEach(function modeIterator(mode) {
|
||||
var composedPerm = perm + '-' + mode;
|
||||
if (this._isExplicitPerm(composedPerm)) {
|
||||
table.explicitCertifiedPermissions.push({
|
||||
explicitPermission: composedPerm,
|
||||
permission: perm
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
this._permissionTableHaveProcessed = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Identify the permission whether is explict or not.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @access private
|
||||
* @return {Bool}
|
||||
*/
|
||||
_isExplicitPerm: function pl_isExplicitPerm(perm) {
|
||||
return mozPerms && mozPerms.isExplicit(perm, this._currentApp.manifestURL,
|
||||
this._currentApp.origin, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter explicit apps from moz apps, sort them, and render to screen.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @access public
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadApps: function pl_loadApps() {
|
||||
var self = this;
|
||||
return AppsCache.apps().then(function(apps) {
|
||||
self._loadApps(apps);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterate internal apps and render them on UI.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Object[]} apps
|
||||
* @access private
|
||||
*/
|
||||
_loadApps: function pl__loadApps(apps) {
|
||||
var table = this._permissionTable;
|
||||
var internalRoles = ['system', 'input', 'homescreen', 'search', 'addon', 'langpack'];
|
||||
apps.forEach(function(app) {
|
||||
var manifest = app.manifest ? app.manifest : app.updateManifest;
|
||||
if (manifest.role && internalRoles.indexOf(manifest.role) !== -1) {
|
||||
return;
|
||||
}
|
||||
if (manifest.type != 'certified') {
|
||||
this._apps.push(app);
|
||||
return;
|
||||
}
|
||||
|
||||
var display = table.explicitCertifiedPermissions
|
||||
.some(function iterator(perm) {
|
||||
var permInfo = mozPerms.get(perm.explicitPermission,
|
||||
app.manifestURL, app.origin, false);
|
||||
return permInfo != 'unknown';
|
||||
}.bind(this));
|
||||
|
||||
if (display) {
|
||||
this._apps.push(app);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this._sortApps();
|
||||
this.renderList();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get icon URL.
|
||||
*
|
||||
* @memberOf PermissionsList
|
||||
* @param {Object} app
|
||||
* @param {Object} icons
|
||||
* @access private
|
||||
*/
|
||||
_getBestIconURL: function pl__getBestIconURL(app, icons) {
|
||||
if (!icons || !Object.keys(icons).length) {
|
||||
return '../style/images/default.png';
|
||||
}
|
||||
|
||||
// The preferred size is 30 by the default. If we use HDPI device, we may
|
||||
// use the image larger than 30 * 1.5 = 45 pixels.
|
||||
var preferredIconSize = 30 * (window.devicePixelRatio || 1);
|
||||
var preferredSize = Number.MAX_VALUE;
|
||||
var max = 0;
|
||||
|
||||
for (var size in icons) {
|
||||
size = parseInt(size, 10);
|
||||
if (size > max) {
|
||||
max = size;
|
||||
}
|
||||
|
||||
if (size >= preferredIconSize && size < preferredSize) {
|
||||
preferredSize = size;
|
||||
}
|
||||
}
|
||||
// If there is an icon matching the preferred size, we return the result,
|
||||
// if there isn't, we will return the maximum available size.
|
||||
if (preferredSize === Number.MAX_VALUE) {
|
||||
preferredSize = max;
|
||||
}
|
||||
|
||||
var url = icons[preferredSize];
|
||||
|
||||
if (url) {
|
||||
return !(/^(http|https|data):/.test(url)) ? app.origin + url : url;
|
||||
} else {
|
||||
return '../style/images/default.png';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return function ctor_permissions_list() {
|
||||
return new PermissionsList();
|
||||
};
|
||||
});
|
||||
define('panels/app_permissions_list/panel',['require','modules/settings_panel','panels/app_permissions_list/app_permissions_list','shared/lazy_loader'],function(require){'use strict';var SettingsPanel=require('modules/settings_panel');var PermissionList=require('panels/app_permissions_list/app_permissions_list');var LazyLoader=require('shared/lazy_loader');return function ctor_app_permissions_list_panel(){var permissionsTableHasBeenLoaded=false;var elements={};var permissionListModule=PermissionList();return SettingsPanel({onInit:function(panel){elements={list:panel.querySelector('.app-list')};permissionListModule.init(elements.list);},onBeforeShow:function(){if(permissionsTableHasBeenLoaded){permissionListModule.refresh();}else{LazyLoader.getJSON('/resources/permissions_table.json').then(function(data){permissionsTableHasBeenLoaded=true;permissionListModule.setPermissionsTable(data);permissionListModule.refresh();});}
|
||||
permissionListModule.enabled=true;},onBeforeHide:function(){permissionListModule.enabled=false;}});};});
|
||||
75
overrides/settings.gaiamobile.org/js/panels/battery/panel.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* The battery panel displays battery information provided by Battery.
|
||||
*/
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var SettingsPanel = require('modules/settings_panel');
|
||||
var Battery = require('modules/battery');
|
||||
|
||||
return function ctor_battery_panel() {
|
||||
var _batteryLevelText = null;
|
||||
var _batteryTimeText = null;
|
||||
var _getBatteryTimeL10n = function(type) {
|
||||
type = type === 'charging' ? 'charging': 'discharging';
|
||||
|
||||
var time = Battery[type + 'Time'];
|
||||
var l10nAttrs = {
|
||||
id: 'battery-' + type + '-calculating',
|
||||
args: {}
|
||||
};
|
||||
|
||||
if (time !== Infinity && time > 0) {
|
||||
var timeLeft = new Date(0, 0, 0, 0, 0, time);
|
||||
l10nAttrs.args.hours = timeLeft.getHours();
|
||||
l10nAttrs.args.minutes = timeLeft.getMinutes();
|
||||
|
||||
if (l10nAttrs.args.hours > 0) {
|
||||
l10nAttrs.id = 'battery-' + type + '-hours';
|
||||
} else {
|
||||
l10nAttrs.id = 'battery-' + type + '-minutes';
|
||||
}
|
||||
} else if (time !== Infinity && time === 0) {
|
||||
l10nAttrs.id = 'battery-' + type + '-complete';
|
||||
} else {
|
||||
l10nAttrs.id = 'battery-' + type + '-unavailable';
|
||||
}
|
||||
|
||||
return l10nAttrs;
|
||||
};
|
||||
var _refreshText = function() {
|
||||
document.l10n.setAttributes(_batteryLevelText,
|
||||
'battery-current-level', { level: Battery.level });
|
||||
|
||||
if (Battery.state === 'unplugged') {
|
||||
var l10nDischarging = _getBatteryTimeL10n('discharging');
|
||||
document.l10n.setAttributes(_batteryTimeText,
|
||||
l10nDischarging.id, l10nDischarging.args);
|
||||
}
|
||||
else {
|
||||
var l10nCharging = _getBatteryTimeL10n('charging');
|
||||
document.l10n.setAttributes(_batteryTimeText,
|
||||
l10nCharging.id, l10nCharging.args);
|
||||
}
|
||||
};
|
||||
|
||||
return SettingsPanel({
|
||||
onInit: function bp_onInit(rootElement) {
|
||||
_batteryLevelText = rootElement.querySelector(
|
||||
'#battery-level');
|
||||
_batteryTimeText = rootElement.querySelector(
|
||||
'#battery-time');
|
||||
},
|
||||
onBeforeShow: function bp_onBeforeShow(rootElement) {
|
||||
Battery.observe('level', _refreshText);
|
||||
Battery.observe('state', _refreshText);
|
||||
Battery.observe('chargingTime', _refreshText);
|
||||
Battery.observe('dischargingTime', _refreshText);
|
||||
_refreshText();
|
||||
},
|
||||
onBeforeHide: function bp_onBeforeHide() {
|
||||
Battery.unobserve(_refreshText);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
240
overrides/shared/style/value_selector.css
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
/* ----------------------------------
|
||||
* Value selector (Single & Multiple)
|
||||
* ---------------------------------- */
|
||||
|
||||
/* Main dialog setup */
|
||||
[role="dialog"][data-type="value-selector"] {
|
||||
background: #2d2d2d;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 0 0 7rem;
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] > section {
|
||||
padding: 0 1.5rem;
|
||||
box-sizing: padding-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] h1 {
|
||||
font-weight: 400;
|
||||
font-size: 1.9rem;
|
||||
line-height: 4.8rem;
|
||||
color: #fff;
|
||||
border-bottom: 0.1rem solid #616262;
|
||||
background-color: rgba(0, 0, 0, .2);
|
||||
margin: 0 -1.5rem;
|
||||
padding: 0 3rem 1rem;
|
||||
height: 4.8rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Specific component code */
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0 -1.5rem;
|
||||
max-height: calc(100% - 5rem);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li {
|
||||
margin: 0;
|
||||
padding: 0 1.5rem;
|
||||
height: auto;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
font-weight: 300;
|
||||
font-size: 2.2rem;
|
||||
line-height: normal;
|
||||
color: #fff;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"],
|
||||
html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"] li {
|
||||
text-align: left;
|
||||
}
|
||||
html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"],
|
||||
html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li:first-child label {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li label {
|
||||
outline: none;
|
||||
display: block;
|
||||
color: #fff;
|
||||
border-top: 0.1rem solid #666;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li label span {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
min-height: 5.2rem;
|
||||
padding: 1.2rem 1.5rem;
|
||||
line-height: 2.8rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Pressed status */
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li:active {
|
||||
background-color: #00ABCC;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li:active label,
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li:active + li label {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li:active label span {
|
||||
color: #fff !important;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
/* Subheaders */
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] {
|
||||
border: none;
|
||||
background-color: #242424;
|
||||
pointer-events: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] label {
|
||||
color: #f4f4f4;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] label,
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] + li label {
|
||||
border: none;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[role="subheader"] label span {
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
/* Checked status */
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] label {
|
||||
background-image: url(value_selector/images/icons/checked.png);
|
||||
background-color: transparent;
|
||||
background-size: 2.4rem;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] label {
|
||||
background-position: right 1.2rem top 50%;
|
||||
}
|
||||
html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] label {
|
||||
background-position: left 1.2rem top 50%;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] span {
|
||||
color: #00abcd;
|
||||
}
|
||||
html[dir="ltr"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] span {
|
||||
padding-right: 2.6rem;
|
||||
margin-right: 1.2rem;
|
||||
}
|
||||
html[dir="rtl"] [role="dialog"][data-type="value-selector"] [role="listbox"] li[aria-selected="true"] span {
|
||||
padding-left: 2.6rem;
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
|
||||
/* Menu & buttons setup */
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons {
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 1.5rem;
|
||||
background: #4d4d4d;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button::-moz-focus-inner {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button {
|
||||
width: calc((100% - 1rem) / 2);
|
||||
font-family: sans-serif;
|
||||
font-style: italic;
|
||||
height: 4rem;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0 1.2rem;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
background: #d8d8d8;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
font-weight: normal;
|
||||
font-size: 1.6rem;
|
||||
line-height: 4rem;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Affirmative */
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button.affirmative,
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button.recommend {
|
||||
background-color: #00caf2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Pressed */
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button:active {
|
||||
background: #00aacc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
[role="dialog"][data-type="value-selector"] > menu.value-selector-buttons > button[disabled] {
|
||||
background-color: #565656;
|
||||
color: rgba(255,255,255,0.4);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[role="dialog"][data-type="value-selector"] > menu.value-selector-buttons > button[disabled].recommend {
|
||||
background-color: #006579;
|
||||
}
|
||||
|
||||
button[disabled]::-moz-selection {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button:last-child {
|
||||
margin-inline-start: 1rem;
|
||||
}
|
||||
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button,
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.value-selector > [role="dialog"][data-type="value-selector"] menu.value-selector-buttons button.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -475,20 +475,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Set the .icon property before saving for
|
||||
// backwards compatibility with verticalhome
|
||||
// Set the .icon property before saving for backwards compatibility with
|
||||
// verticalhome. A missing favicon must not prevent pinning the site.
|
||||
var saveSite = function(icon) {
|
||||
siteObject.icon = icon || '';
|
||||
BookmarksDatabase.put(siteObject, siteObject.id)
|
||||
.then(() => {
|
||||
this.app.debug('pinSite: ' + siteObject.id);
|
||||
this.systemBanner.show('site-pinned-to-home-screen');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('pinSite, Failed to pin site: ' + error);
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
IconsHelper.getIcon(siteObject.url, null,
|
||||
{icons: this.app.favicons}, siteObject).then(icon => {
|
||||
siteObject.icon = icon;
|
||||
BookmarksDatabase.put(siteObject, siteObject.id)
|
||||
.then(() => {
|
||||
this.app.debug('pinSite: ' + siteObject.id);
|
||||
this.systemBanner.show('site-pinned-to-home-screen');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('pinSite, Failed to pin site: ' + error);
|
||||
});
|
||||
});
|
||||
{icons: this.app.favicons}, siteObject).then(saveSite, function(error) {
|
||||
this.app.debug('pinSite: using no favicon: ' + error);
|
||||
saveSite('');
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -986,12 +991,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Enable/disable the bookmark option
|
||||
BookmarksDatabase.get(this._currentURL).then(function resolve(result) {
|
||||
// start_url is the bookmark key for installed web apps, while _currentURL
|
||||
// may be a later page in the app. Check the canonical key first.
|
||||
var siteUrl = this.getSiteUrl();
|
||||
BookmarksDatabase.get(siteUrl).then(function resolve(result) {
|
||||
this.addToHomeButton.hidden = !!result;
|
||||
}.bind(this),
|
||||
function reject() {
|
||||
this.addToHomeButton.hidden = true;
|
||||
this.addToHomeButton.hidden = false;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
/* dialog */
|
||||
/* FIXME we have to see why we need such specific style here and why
|
||||
* the general style in confirm.css isn't sufficient */
|
||||
#setup-installed-app-dialog,
|
||||
#ime-layout-dialog,
|
||||
#app-install-dialog,
|
||||
#app-install-cancel-dialog,
|
||||
#app-download-cancel-dialog {
|
||||
display: none;
|
||||
top: 3rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Shift the bottom of the prompt to account for the height of the
|
||||
software home button */
|
||||
#screen.software-button-enabled #setup-installed-app-dialog,
|
||||
#screen.software-button-enabled #ime-layout-dialog,
|
||||
#screen.software-button-enabled #app-install-dialog,
|
||||
#screen.software-button-enabled #app-install-cancel-dialog,
|
||||
#screen.software-button-enabled #app-download-cancel-dialog {
|
||||
bottom: var(--software-home-button-height);
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
#screen.software-button-enabled #setup-installed-app-dialog,
|
||||
#screen.software-button-enabled #ime-layout-dialog,
|
||||
#screen.software-button-enabled #app-install-dialog,
|
||||
#screen.software-button-enabled #app-install-cancel-dialog,
|
||||
#screen.software-button-enabled #app-download-cancel-dialog {
|
||||
bottom: 0;
|
||||
/* BiDi note: the software home button should not be mirrored in RTL. */
|
||||
right: var(--software-home-button-height);
|
||||
}
|
||||
}
|
||||
|
||||
#setup-installed-app-dialog.visible,
|
||||
#ime-layout-dialog.visible,
|
||||
#app-install-dialog.visible,
|
||||
#app-install-cancel-dialog.visible,
|
||||
#app-download-cancel-dialog.visible {
|
||||
display: inline-block;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#app-uninstall-dialog {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#setup-installed-app-dialog section,
|
||||
#ime-layout-dialog section,
|
||||
#app-install-dialog section,
|
||||
#app-install-cancel-dialog section,
|
||||
#app-download-cancel-dialog section {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#setup-installed-app-dialog h1,
|
||||
#ime-layout-dialog h1,
|
||||
#app-install-dialog h1,
|
||||
#app-install-cancel-dialog h1,
|
||||
#app-download-cancel-dialog h1 {
|
||||
border-bottom: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
#app-install-dialog dl > dt {
|
||||
width: 8rem;
|
||||
margin-inline-end: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#app-install-dialog p {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
border-top: none;
|
||||
}
|
||||
#app-install-dialog #system-addon-warning p + p {
|
||||
margin-top: 0;
|
||||
}
|
||||
#app-install-dialog span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
color: inherit;
|
||||
}
|
||||
#app-install-dialog #system-addon-icon {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
html[dir="ltr"] #app-install-dialog #system-addon-icon {
|
||||
float: left;
|
||||
margin: 1.5rem 0 0 1.5rem;
|
||||
}
|
||||
html[dir="rtl"] #app-install-dialog #system-addon-icon {
|
||||
float: right;
|
||||
margin: 1.5rem 1.5rem 0 0;
|
||||
}
|
||||
|
||||
#app-install-author-url {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#app-install-cancel-dialog small:first-child {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ... end of dialog */
|
||||
|
||||
/* notification */
|
||||
#install-manager-notification-container > .fake-notification {
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#install-manager-notification-container > .fake-notification > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#install-manager-notification-container [data-icon] {
|
||||
color: #9ab0c0;
|
||||
background-color: #edf1f4;
|
||||
}
|
||||
|
||||
#install-manager-notification-container .title-container {
|
||||
margin: 1.4rem 0 0;
|
||||
line-height: 3rem;
|
||||
}
|
||||
html[dir="ltr"] #install-manager-notification-container .title-container {
|
||||
margin-left: 3.7rem;
|
||||
}
|
||||
html[dir="rtl"] #install-manager-notification-container .title-container {
|
||||
margin-right: 3.7rem;
|
||||
}
|
||||
|
||||
#install-manager-notification-container progress {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0.4rem;
|
||||
left: 0;
|
||||
top: 5.6rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#install-manager-notification-container progress::-moz-progress-bar {
|
||||
background: url(../shared/progress.gif) repeat-x;
|
||||
}
|
||||
|
||||
/* ... end of notification */
|
||||
|
||||
/* ... start of keyboard layout */
|
||||
ul#ime-list {
|
||||
padding-inline-start: 1.5rem;
|
||||
}
|
||||
|
||||
ul#ime-list > li {
|
||||
/* Positioning */
|
||||
position: relative;
|
||||
|
||||
/* Display & Box Model */
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
padding-top: 2rem;
|
||||
|
||||
/* Other */
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul#ime-list > li label {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* ... end of keyboard layout */
|
||||
|
|
@ -57,6 +57,6 @@
|
|||
#os-logo { background:#071d35; display:flex; flex-direction:column; align-items:center; justify-content:center; }
|
||||
#os-logo::before { content:""; width:min(42vw,190px); aspect-ratio:1; flex:none; background:url('/shared/resources/branding/vulpes-symbol.png') center / contain no-repeat; animation:vulpes-boot 2s ease-in-out infinite alternate; }
|
||||
#os-logo::after { content:"VULPES\A Firefox OS Community Project"; white-space:pre-line; padding:12px 22px; color:#f5f8fa; font:500 20px/1.8 FiraSans,sans-serif; text-align:center; }
|
||||
#poweroff-splash { background:#071d35 url('/shared/resources/branding/vulpes-symbol.png') center / 190px 190px no-repeat; }
|
||||
#poweroff-splash { background:#071d35; }
|
||||
@keyframes vulpes-boot { from {opacity:.75} to {opacity:1} }
|
||||
@media (prefers-reduced-motion: reduce) { #os-logo::before {animation:none} }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"gaia": "2.7.0",
|
||||
"channel": "preview",
|
||||
"revision": 22,
|
||||
"androidVersionCode": 22,
|
||||
"revision": 24,
|
||||
"androidVersionCode": 24,
|
||||
"geckoview": "156.0.20260921121718",
|
||||
"engineAlignment": {
|
||||
"family": "156",
|
||||
"patchParity": false,
|
||||
"reason": "Firefox desktop 156.0.1; Mozilla's GeckoView release repository currently offers 156.0 only. Keep the desktop patch and report actual engine versions."
|
||||
},
|
||||
"version": "2.7-preview.22",
|
||||
"version": "2.7-preview.24",
|
||||
"adapters": {
|
||||
"desktop": "0.1.0-dev",
|
||||
"android": "0.1.0-dev"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export function isGaiaActivity(source) {
|
||||
return source?.name === 'pick' || source?.name === 'share' ||
|
||||
(source?.name === 'moz_configure_window' && source.data?.target === 'device') ||
|
||||
(source?.name === 'configure' && source.data?.target === 'device') ||
|
||||
(source?.name === 'configure' && source.data?.target === 'ringtone') ||
|
||||
(source?.name === 'dial' && source.data?.type === 'webtelephony/number') ||
|
||||
(source?.name === 'new' && source.data?.type === 'websms/sms') ||
|
||||
|
|
@ -33,6 +34,7 @@ export class Activities {
|
|||
else if (types.some(t=>typeof t==='string' && t.startsWith('audio/'))) id='music';
|
||||
else if (types.some(t=>typeof t==='string' && t.startsWith('video/'))) id='video';
|
||||
} else if (name === 'configure' && source.data?.target === 'ringtone') id='ringtones';
|
||||
else if (name === 'configure' && source.data?.target === 'device') id='settings';
|
||||
else if (name === 'dial' && types.includes('webtelephony/number')) id='communications';
|
||||
else if (name === 'new' && types.includes('websms/sms')) id='sms';
|
||||
else if (['new','update','open'].includes(name) && types.includes('webcontacts/contact')) id='communications';
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export async function unpackPackage(bytes, source) {
|
|||
if(cursor!==end || !files['manifest.webapp'])throw Error('MISSING_MANIFEST');
|
||||
const original=JSON.parse(decoder.decode(Uint8Array.from(atob(files['manifest.webapp']),c=>c.charCodeAt(0))));
|
||||
if(typeof original.name!=='string' || !original.name.trim() || original.name.length>120)throw Error('INVALID_MANIFEST');
|
||||
if (original.type === 'privileged' || original.type === 'certified')
|
||||
throw Error('PRIVILEGED_APP_UNSUPPORTED');
|
||||
function path(value) {if(typeof value!=='string')throw Error('INVALID_MANIFEST');const u=new URL(value,'https://app.invalid/');
|
||||
if(u.origin!=='https://app.invalid' || !Object.hasOwn(files,decodeURIComponent(u.pathname.slice(1))))throw Error('MISSING_APP_FILE');return u.pathname+u.search+u.hash;}
|
||||
const launch=path(original.launch_path || '/index.html'),icons={};
|
||||
|
|
|
|||
|
|
@ -59,6 +59,20 @@ test('toolbar focus does not select a bar hidden behind the lockscreen or a dial
|
|||
assert.equal(selected,1);
|
||||
});
|
||||
|
||||
test('Up reopens the browser toolbar when the collapsed rocketbar search input kept focus',()=>{
|
||||
let selected=0;
|
||||
const bar={checkVisibility:()=>true,getBoundingClientRect:()=>({x:0,y:20,width:200,height:40}),
|
||||
contains:()=>true};
|
||||
const child=actor('KeypadChild');
|
||||
child.document={location:{hostname:'system.localhost'},activeElement:{id:'rocketbar-input',localName:'input',type:'search'},
|
||||
querySelector:()=>({classList:{contains:()=>false}}),querySelectorAll:()=>[bar],elementFromPoint:()=>bar,
|
||||
documentElement:{dataset:{}}};
|
||||
child.contentWindow={innerWidth:240,innerHeight:320};
|
||||
child.select=()=>selected++;
|
||||
child.receiveMessage({name:'Vulpes:Navigate',data:{key:'ArrowUp'}});
|
||||
assert.equal(selected,1);
|
||||
});
|
||||
|
||||
test('Down leaves the toolbar and routes to its page instead of retaining toolbar focus',()=>{
|
||||
let focused=false;
|
||||
const messages=[];
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import vm from 'node:vm';
|
|||
|
||||
function camera() {
|
||||
const workers = [];
|
||||
const context = {setTimeout, clearTimeout, Subprocess: {async call() {
|
||||
const context = {setTimeout, clearTimeout, Subprocess: {async call(options) {
|
||||
let finish;
|
||||
const done = new Promise(resolve => { finish = resolve; });
|
||||
const worker = {closed:false, writes:[],
|
||||
const worker = {closed:false, writes:[], options,
|
||||
stdin:{async close() {worker.closed=true;finish();}, async write(text) {worker.writes.push(text);}},
|
||||
wait:()=>done, kill:()=>finish()};
|
||||
workers.push(worker); return worker;
|
||||
|
|
@ -16,15 +16,15 @@ function camera() {
|
|||
const source = readFileSync(new URL('../host/NativeCamera.sys.mjs', import.meta.url),'utf8')
|
||||
.replace(/^import .*;\n/gm,'').replace('export class NativeCamera','globalThis.NativeCamera = class NativeCamera');
|
||||
vm.runInNewContext(source,context);
|
||||
const service = new context.NativeCamera();
|
||||
service.packet = async () => ({ready:true,format:'nv21',width:640,height:480});
|
||||
const service = new context.NativeCamera('beatles');
|
||||
service.packet = async () => ({ready:true,format:'nv21',...service.previewSize});
|
||||
return {service,workers};
|
||||
}
|
||||
|
||||
test('a delayed stop or frame from the old camera cannot close its replacement',async()=>{
|
||||
const {service,workers}=camera(), actor={};
|
||||
const old=await service.request(actor,{action:'start',camera:'back'});
|
||||
const next=await service.request(actor,{action:'start',camera:'front'});
|
||||
const next=await service.request(actor,{action:'start',camera:'back'});
|
||||
assert.notEqual(old.session,next.session);assert.equal(workers[0].closed,true);
|
||||
await service.request(actor,{action:'stop',session:old.session});
|
||||
assert.equal(workers[1].closed,false);
|
||||
|
|
@ -36,6 +36,34 @@ test('a delayed stop or frame from the old camera cannot close its replacement',
|
|||
assert.equal(workers[1].closed,true);
|
||||
});
|
||||
|
||||
test('Beatles uses the stock KaiOS 320x240 preview while other devices keep 640x480',async()=>{
|
||||
const {service,workers}=camera(), actor={};
|
||||
const metadata=await service.request(actor,{action:'start',camera:'back'});
|
||||
assert.equal(service.previewSize.width,320);
|
||||
assert.equal(service.previewSize.height,240);
|
||||
assert.equal(metadata.width,320);
|
||||
assert.equal(metadata.height,240);
|
||||
assert.equal(metadata.frameRate,5);
|
||||
assert.deepEqual([...metadata.cameras],['back']);
|
||||
assert.equal(workers[0].options.command,'/usr/bin/python3.11');
|
||||
assert.equal(workers[0].options.arguments[1],'/opt/vulpes/tundra/runtime/beatles-camera-client.py');
|
||||
await service.request(actor,{action:'stop',session:metadata.session});
|
||||
|
||||
const sargo=new service.constructor('sargo');
|
||||
assert.equal(sargo.previewSize.width,640);
|
||||
assert.equal(sargo.previewSize.height,480);
|
||||
sargo.packet=async()=>({ready:true,format:'nv21',...sargo.previewSize});
|
||||
const sargoMetadata=await sargo.request(actor,{action:'start',camera:'back'});
|
||||
assert.equal(sargoMetadata.frameRate,30);
|
||||
await sargo.request(actor,{action:'stop',session:sargoMetadata.session});
|
||||
});
|
||||
|
||||
test('Beatles reports the missing front sensor instead of opening its rear sensor twice',async()=>{
|
||||
const {service,workers}=camera(), actor={};
|
||||
await assert.rejects(service.request(actor,{action:'start',camera:'front'}),/CAMERA_UNAVAILABLE/);
|
||||
assert.equal(workers.length,0);
|
||||
});
|
||||
|
||||
test('a different actor cannot stop or use the current camera',async()=>{
|
||||
const {service,workers}=camera(), owner={}, other={};
|
||||
const meta=await service.request(owner,{action:'start'});
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ sys.stdout.buffer.write(b.getvalue())`,JSON.stringify({extra,manifest})]));
|
|||
}
|
||||
assert.equal(packageURL(source+'&lang=fr'),source);
|
||||
for(const url of ['http://vulpes-os.org/marketplace/index.php?download=42','https://evil.test/marketplace/index.php?download=42','https://vulpes-os.org/x?download=42','https://vulpes-os.org/marketplace/index.php?download=../42'])assert.throws(()=>packageURL(url));
|
||||
const bytes=zip({}, {type:'certified',permissions:{settings:{access:'readwrite'},'webapps-manage':{}}});
|
||||
const bytes=zip({}, {permissions:{settings:{access:'readwrite'},'webapps-manage':{}}});
|
||||
const item=await unpackPackage(bytes,source);
|
||||
assert.equal(item.manifest.type,'web');assert.deepEqual(item.manifest.permissions,{});assert.match(item.id,/^user-[a-f0-9]{24}$/);
|
||||
for (const type of ['privileged','certified'])
|
||||
await assert.rejects(unpackPackage(zip({}, {type}), source), /PRIVILEGED_APP_UNSUPPORTED/);
|
||||
for(const name of ['../escape','/absolute','dir/../../escape','dir\\escape','a:b'])await assert.rejects(unpackPackage(zip({[name]:'bad'}),source));
|
||||
await assert.rejects(unpackPackage(bytes.slice(0,-2),source));
|
||||
const corrupt=bytes.slice();corrupt[100]^=255;await assert.rejects(unpackPackage(corrupt,source));
|
||||
|
|
@ -24,7 +26,7 @@ const service=new Packages({save:async value=>saved=value,changed:async()=>chang
|
|||
const prepared=await service.prepare(source);assert.equal(saved,null);assert.equal(prepared.name,'Test');
|
||||
assert.equal(await service.install(prepared.token),item.id);assert.equal(changes,1);assert.equal(saved.id,item.id);
|
||||
await assert.rejects(service.install(prepared.token),/INSTALL_EXPIRED/);
|
||||
console.log('Package URL policy, ZIP boundaries/CRC, permission stripping and one-use confirmation: OK');
|
||||
console.log('Package URL policy, ZIP boundaries/CRC, permission stripping, privileged-app rejection and one-use confirmation: OK');
|
||||
|
||||
// Cancellation during download must not produce a confirmation token.
|
||||
let finishDownload;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
# Editable Gaia files shared by desktop serving and Android packaging.
|
||||
SOURCE_OVERRIDES = {
|
||||
"system": ["js/launcher.js", "js/gaia_scheduler.js", "js/keyboard_manager.js",
|
||||
"js/core.js", "js/rocketbar.js"],
|
||||
"js/core.js", "js/rocketbar.js", "js/wrapper_factory.js",
|
||||
"js/cover_screen.js", "js/lockscreen_window.js"],
|
||||
"camera": ["js/views/setting-options.js", "js/views/setting.js"],
|
||||
"communications": ["dialer/js/telephony_helper.js"],
|
||||
"settings": ["js/modules/storage_helper.js", "js/startup.js"],
|
||||
"keyboard": ["js/settings/layout_dictionary_downloader.js"] + [
|
||||
|
|
@ -42,7 +45,7 @@ def validate_assets():
|
|||
|
||||
def build_info():
|
||||
"""Read this checkout's revision; archives without .git report it as unknown."""
|
||||
unknown = {"revision": "unknown", "timestamp": 0, "dirty": None}
|
||||
unknown = {"revision": "unknown", "timestamp": int(time.time()), "dirty": None}
|
||||
if not (ROOT / ".git").exists():
|
||||
return unknown
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,18 @@ TARGET=ROOT/'overrides/camera.gaiamobile.org/js/main.js'
|
|||
dcf=ROOT/'overrides/camera.gaiamobile.org/js/lib/dcf.js'
|
||||
dcf.parent.mkdir(parents=True,exist_ok=True)
|
||||
dcf.write_text((ROOT/'gaia/apps/camera/js/lib/dcf.js').read_text())
|
||||
for name in ['lib/storage','controllers/storage','controllers/activity']:
|
||||
for name in [
|
||||
'app',
|
||||
'lib/camera/camera',
|
||||
'lib/storage',
|
||||
'controllers/activity',
|
||||
'controllers/battery',
|
||||
'controllers/overlay',
|
||||
'controllers/settings',
|
||||
'controllers/storage',
|
||||
'controllers/viewfinder',
|
||||
'views/viewfinder',
|
||||
]:
|
||||
source=(ROOT/'gaia/apps/camera/js'/f'{name}.js').read_text()
|
||||
deps=list(dict.fromkeys(re.findall(r"require\(['\"]([^'\"]+)['\"]\)",source)))
|
||||
source=source.replace('define(function(require, exports, module)', 'define('+json.dumps(name)+','+json.dumps(['require','exports','module',*deps])+',function(require, exports, module)',1)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ modules = [
|
|||
('modules/date_time', 'js/modules/date_time.js'),
|
||||
('panels/downloads/download_api_manager', 'js/panels/downloads/panel.js'),
|
||||
('panels/help/support', 'js/panels/help/panel.js'),
|
||||
('panels/app_permissions_list/app_permissions_list',
|
||||
'js/panels/app_permissions_list/panel.js'),
|
||||
]
|
||||
names=list(dict.fromkeys(name for name,relative in modules))
|
||||
base=ROOT/'assets/webapps/settings.gaiamobile.org'
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ done(canvas.toDataURL());}).catch(e=>done({error:String(e)}));""",
|
|||
"browser-localized-name",
|
||||
ev(
|
||||
"system",
|
||||
"const a=(await navigator.mozApps.mgmt.getAll()).find(a=>a.origin.includes('search.localhost'));return a.manifest.locales.fr.name==='Navigateur' && Object.values(a.manifest.icons).every(i=>i.includes('vulpes-browser.png'));",
|
||||
"const a=(await navigator.mozApps.mgmt.getAll()).find(a=>a.origin.includes('search.localhost'));return a.manifest.locales.fr.name==='Navigateur' && Object.values(a.manifest.icons).every(i=>i.includes('/shared/resources/branding/browser_'));",
|
||||
),
|
||||
)
|
||||
shot("home-transparent")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ if [ "$(date +%s)" -lt "${BUILD_EPOCH:-0}" ]; then date -s "@$BUILD_EPOCH"; fi
|
|||
trap 'echo TUNDRA_NATIVE_SETUP_FAILED; while :; do /bin/busybox sleep 5; done' EXIT
|
||||
LOWER=/installed/home/droidian/vulpes-tundra-tests/runtime-reference-20260925/root
|
||||
if [ -n "${SYSTEM_IMAGE:-}" ]; then
|
||||
test "$(sha256sum "$SYSTEM_IMAGE" | cut -d' ' -f1)" = "$SYSTEM_IMAGE_SHA"
|
||||
test -f "$SYSTEM_IMAGE" && test ! -L "$SYSTEM_IMAGE"
|
||||
test "$(stat -c %s "$SYSTEM_IMAGE")" = "$SYSTEM_IMAGE_BYTES"
|
||||
mkdir -p /run/tundra-system
|
||||
mount -t ext4 -o loop,ro,noload "$SYSTEM_IMAGE" /run/tundra-system
|
||||
LOWER=/run/tundra-system
|
||||
|
|
|
|||
|
|
@ -45,7 +45,19 @@ done
|
|||
/sbin/dropbear -E -s -j -k -p 10.15.19.82:2222 -r /etc/tundra-hostkey -P /run/dropbear.pid
|
||||
echo TUNDRA_USB_SSH_READY
|
||||
# No LVM activation, fsck, journal replay, resizing, or writes to a block device.
|
||||
if [ -b /dev/mmcblk0p72 ] && [ "$(dd if=/dev/mmcblk0p72 bs=1048576 count=1 2>/dev/null | sha256sum | cut -d' ' -f1)" = "$EXPECTED_STORAGE_HASH" ]; then
|
||||
if [ "${RAW_USERDATA_EXT4:-0}" = 1 ]; then
|
||||
# Ext4 updates mutable mount counters and timestamps in the first MiB; use
|
||||
# the filesystem UUID at the fixed superblock offset as the stable identity.
|
||||
actual_uuid=$(dd if=/dev/mmcblk0p72 bs=1 skip=1128 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||
if [ -b /dev/mmcblk0p72 ] && [ "$actual_uuid" = "$EXPECTED_STORAGE_UUID_HEX" ]; then
|
||||
mkdir -p /installed
|
||||
if [ "${TUNDRA_USERDATA_RW:-0}" = 1 ]; then
|
||||
mount -t ext4 -o rw /dev/mmcblk0p72 /installed
|
||||
else
|
||||
mount -t ext4 -o ro,noload /dev/mmcblk0p72 /installed
|
||||
fi
|
||||
fi
|
||||
elif [ -b /dev/mmcblk0p72 ] && [ "$(dd if=/dev/mmcblk0p72 bs=1048576 count=1 2>/dev/null | sha256sum | cut -d' ' -f1)" = "$EXPECTED_STORAGE_HASH" ]; then
|
||||
mkdir -p /dev/mapper /installed
|
||||
/sbin/dmsetup create tundra-installed-ro --readonly --table '0 104448000 linear /dev/mmcblk0p72 329728'
|
||||
/sbin/dmsetup mknodes
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
#define _start unused_stage0_start
|
||||
#include "init.c"
|
||||
#undef _start
|
||||
#ifndef TUNDRA_WATCHDOG_SECONDS
|
||||
#define TUNDRA_WATCHDOG_SECONDS 600
|
||||
#endif
|
||||
__attribute__((noreturn)) void _start(void) {
|
||||
long remaining[2] = {600, 0};
|
||||
long remaining[2] = {TUNDRA_WATCHDOG_SECONDS, 0};
|
||||
while (call(NR_nanosleep, (long)remaining, (long)remaining, 0, 0, 0) == -4) {}
|
||||
say("TUNDRA_TRIAL_DEADLINE_REBOOT\n");
|
||||
call(NR_reboot, 0xfee1dead, 672274793, 0x01234567, 0, 0);
|
||||
|
|
|
|||