Repository: darkreader/darkreader
Pull request #1513 (2019-08-21 20:28:11)

Add option to automatically toggle dark mode on sunrise/sunset ( #72 )
Hi,
I added an option to automatically toggle dark mode on sunrise/sunset.  ( #72 )

You need to enter your approximate location (latitude and longitude) to use it. (the algorithm to calculate sunrise/sunset time is mostly taken from [here](https://gist.github.com/Tafkas/4742250) )

![image](https://user-images.githubusercontent.com/29901058/63465554-cbb4b400-c461-11e9-9630-dbd0fcdbf511.png)

![image](https://user-images.githubusercontent.com/29901058/63465560-d1aa9500-c461-11e9-9cbd-8216ba80e56c.png)

Also, I had to add auto-translated strings for the tests to pass.
This is my first pull request here, so suggestions and feedback are welcome! 

alexanderby: Great work! Previously I was afraid to ask users location permission. But this solution with manual location is good.

Give me some time to take a look into it.
khalilgharbaoui: Exactly what i was looking for!
alexanderby: Sorry for the delay, currently I'm in a business trip, I will review it on Monday.

Patch:
diff --git a/src/_locales/be.config b/src/_locales/be.config
index ae794ffa..1df422b0 100644
--- a/src/_locales/be.config
+++ b/src/_locales/be.config
@@ -33,6 +33,15 @@
 @set_active_hours
 Усталюйце перыяд актыўнасці
 
+@set_location
+Увядзіце прыблізнае месца для аўтаматычнай актывацыі на заходзе
+
+@longitude
+Даўгата
+
+@latitude
+Шырата
+
 @page_protected
 Гэтая старонка ахоўваецца
 браўзэрам
diff --git a/src/_locales/cs.config b/src/_locales/cs.config
index a9e06bfd..3d92a813 100644
--- a/src/_locales/cs.config
+++ b/src/_locales/cs.config
@@ -33,6 +33,15 @@ Nastavení času
 @set_active_hours
 Změňte doby aktivního používání
 
+@set_location
+Zadejte vaši přibližnou polohu pro automatickou aktivaci při západu slunce
+
+@longitude
+Zeměpisná délka
+
+@latitude
+Zeměpisná šířka
+
 @page_protected
 Tato stránka je chráněna
 prohlížečem
diff --git a/src/_locales/de.config b/src/_locales/de.config
index ed165ee3..6a41239c 100644
--- a/src/_locales/de.config
+++ b/src/_locales/de.config
@@ -33,6 +33,15 @@ Zeit Einstellungen
 @set_active_hours
 Ändern die Nutzungszeit
 
+@set_location
+Geben Sie Ihren ungefähren Standort für die automatische Aktivierung bei Sonnenuntergang
+
+@longitude
+Längengrad
+
+@latitude
+Breite
+
 @page_protected
 Diese Seite ist durch
 den Browser geschützt
diff --git a/src/_locales/el.config b/src/_locales/el.config
index a7d47a3c..d9c02630 100644
--- a/src/_locales/el.config
+++ b/src/_locales/el.config
@@ -33,6 +33,15 @@ Off
 @set_active_hours
 Ορισμός ενεργών ωρών
 
+@set_location
+Καταχωρίστε την κατά προσέγγιση τοποθεσία σας για αυτόματη ενεργοποίηση κατά το ηλιοβασίλεμα
+
+@longitude
+Γεωγραφικό μήκος
+
+@latitude
+Γεωγραφικό πλάτος
+
 @page_protected
 Η σελίδα αυτή
 προστατεύεται
diff --git a/src/_locales/en.config b/src/_locales/en.config
index e8157d23..35b963ba 100644
--- a/src/_locales/en.config
+++ b/src/_locales/en.config
@@ -33,6 +33,15 @@ Time settings
 @set_active_hours
 Set active hours
 
+@set_location
+Enter your approximate location for automatic activation at sunset
+
+@longitude
+Longitude
+
+@latitude
+Latitude
+
 @page_protected
 This page is protected
 by browser
diff --git a/src/_locales/en_GB.config b/src/_locales/en_GB.config
index 7351c098..4a3a75da 100644
--- a/src/_locales/en_GB.config
+++ b/src/_locales/en_GB.config
@@ -33,6 +33,15 @@ Time settings
 @set_active_hours
 Set active hours
 
+@set_location
+Enter your approximate location for automatic activation at sunset
+
+@longitude
+Longitude
+
+@latitude
+Latitude
+
 @page_protected
 This page is protected
 by browser
diff --git a/src/_locales/en_US.config b/src/_locales/en_US.config
index e8157d23..35b963ba 100644
--- a/src/_locales/en_US.config
+++ b/src/_locales/en_US.config
@@ -33,6 +33,15 @@ Time settings
 @set_active_hours
 Set active hours
 
+@set_location
+Enter your approximate location for automatic activation at sunset
+
+@longitude
+Longitude
+
+@latitude
+Latitude
+
 @page_protected
 This page is protected
 by browser
diff --git a/src/_locales/es.config b/src/_locales/es.config
index a28f1765..e9e0d875 100644
--- a/src/_locales/es.config
+++ b/src/_locales/es.config
@@ -33,6 +33,15 @@ Ajustes de hora
 @set_active_hours
 Establece horas activas
 
+@set_location
+Ingrese su ubicación aproximada para la activación automática al atardecer
+
+@longitude
+Longitud
+
+@latitude
+Latitud
+
 @page_protected
 Esta página está protegida
 por el navegador
diff --git a/src/_locales/fr.config b/src/_locales/fr.config
index 29107e22..35906b2f 100644
--- a/src/_locales/fr.config
+++ b/src/_locales/fr.config
@@ -33,6 +33,15 @@ Paramètres de temps
 @set_active_hours
 Définissez les heures d’activité
 
+@set_location
+Entrez votre emplacement approximatif pour l'activation automatique au coucher du soleil
+
+@longitude
+Longitude
+
+@latitude
+Latitude
+
 @page_protected
 Cette page est protégée
 par le navigateur
diff --git a/src/_locales/hi.config b/src/_locales/hi.config
index 441a3c6b..79534c99 100644
--- a/src/_locales/hi.config
+++ b/src/_locales/hi.config
@@ -33,6 +33,15 @@
 @set_active_hours
 सक्रिय घंटे सेट करें
 
+@set_location
+सूर्यास्त पर स्वचालित सक्रियण के लिए अपना अनुमानित स्थान दर्ज करें
+
+@longitude
+देशान्तर
+
+@latitude
+अक्षांश
+
 @page_protected
 यह पृष्ठ ब्राउज़र
 द्वारा संरक्षित है
diff --git a/src/_locales/id.config b/src/_locales/id.config
index beaebc08..48fdbca0 100644
--- a/src/_locales/id.config
+++ b/src/_locales/id.config
@@ -33,6 +33,15 @@ Pengaturan waktu
 @set_active_hours
 Ubah waktu aktivitas
 
+@set_location
+Masukkan perkiraan lokasi Anda untuk aktivasi otomatis saat matahari terbenam
+
+@longitude
+Garis bujur
+
+@latitude
+Lintang
+
 @page_protected
 Halaman ini dilindungi
 oleh browser
diff --git a/src/_locales/it.config b/src/_locales/it.config
index eb1b8c21..e0c9090a 100644
--- a/src/_locales/it.config
+++ b/src/_locales/it.config
@@ -33,6 +33,15 @@ Impostazioni del tempo
 @set_active_hours
 Imposta l'orario di attività
 
+@set_location
+Inserisci la posizione approssimativa per l'attivazione automatica al tramonto
+
+@longitude
+Longitudine
+
+@latitude
+Latitudine
+
 @page_protected
 Questa pagina è protetta
 dal browser
diff --git a/src/_locales/ja.config b/src/_locales/ja.config
index 29023788..86a3321f 100644
--- a/src/_locales/ja.config
+++ b/src/_locales/ja.config
@@ -33,6 +33,15 @@
 @set_active_hours
 アクティブ時間を設定する
 
+@set_location
+日没時に自動起動するおおよその場所を入力してください
+
+@longitude
+経度
+
+@latitude
+緯度
+
 @page_protected
 このページはブラウザに
 よって保護されています
diff --git a/src/_locales/ko.config b/src/_locales/ko.config
index c7047e37..e514d63d 100644
--- a/src/_locales/ko.config
+++ b/src/_locales/ko.config
@@ -33,6 +33,15 @@
 @set_active_hours
 사용 시간 설정
 
+@set_location
+일몰시 자동 활성화를위한 대략적인 위치를 입력하십시오
+
+@longitude
+경도
+
+@latitude
+위도
+
 @page_protected
 이 페이지는
 브라우저에 의해 보호되어 있습니다.
diff --git a/src/_locales/nl.config b/src/_locales/nl.config
index 4a89e8af..3a61d959 100644
--- a/src/_locales/nl.config
+++ b/src/_locales/nl.config
@@ -33,6 +33,15 @@ Tijd instellingen
 @set_active_hours
 Actieve uren instellen
 
+@set_location
+Voer uw geschatte locatie in voor automatische activering bij zonsondergang
+
+@longitude
+Lengtegraad
+
+@latitude
+Breedtegraad
+
 @page_protected
 Deze pagina is beschermd
 door de browser
diff --git a/src/_locales/no.config b/src/_locales/no.config
index 83a06d7e..ed9aa9fa 100644
--- a/src/_locales/no.config
+++ b/src/_locales/no.config
@@ -33,6 +33,15 @@ Tidsinnstillinger
 @set_active_hours
 Angi aktivitetsperiode
 
+@set_location
+Angi ditt omtrentlige sted for automatisk aktivering ved solnedgang
+
+@longitude
+Lengdegrad
+
+@latitude
+Breddegrad
+
 @page_protected
 Denne siden er beskyttet
 av nettleseren
diff --git a/src/_locales/pt_BR.config b/src/_locales/pt_BR.config
index 49c6e2c6..28e30987 100644
--- a/src/_locales/pt_BR.config
+++ b/src/_locales/pt_BR.config
@@ -33,6 +33,15 @@ Configurações de tempo
 @set_active_hours
 Defina o horário ativo
 
+@set_location
+Insira sua localização aproximada para ativação automática ao pôr do sol
+
+@longitude
+Longitude
+
+@latitude
+Latitude
+
 @page_protected
 Esta página é protegida
 pelo navegador
diff --git a/src/_locales/pt_PT.config b/src/_locales/pt_PT.config
index 2bd10abf..fd9efeaf 100644
--- a/src/_locales/pt_PT.config
+++ b/src/_locales/pt_PT.config
@@ -33,6 +33,15 @@ Configurações de tempo
 @set_active_hours
 Defina as horas de atividade
 
+@set_location
+Insira sua localização aproximada para ativação automática ao pôr do sol
+
+@longitude
+Longitude
+
+@latitude
+Latitude
+
 @page_protected
 Esta página é protegida
 pelo navegador
diff --git a/src/_locales/ro.config b/src/_locales/ro.config
index 2b5b5233..ff51a4a8 100755
--- a/src/_locales/ro.config
+++ b/src/_locales/ro.config
@@ -33,6 +33,15 @@ Setări de timp
 @set_active_hours
 Setați orele de activitate
 
+@set_location
+Introduceți locația dvs. aproximativă pentru activarea automată la apusul soarelui
+
+@longitude
+Longitudine
+
+@latitude
+Latitudine
+
 @page_protected
 Această pagină este protejată
 de navigator
diff --git a/src/_locales/ru.config b/src/_locales/ru.config
index 5247f3de..effeb72f 100644
--- a/src/_locales/ru.config
+++ b/src/_locales/ru.config
@@ -33,6 +33,15 @@
 @set_active_hours
 Установите период активности
 
+@set_location
+Введите приблизительное местоположение для автоматической активации на закате
+
+@longitude
+Долгота
+
+@latitude
+широта
+
 @page_protected
 Эта страница защищена
 браузером
diff --git a/src/_locales/si.config b/src/_locales/si.config
index 7878d74f..b96e84b6 100644
--- a/src/_locales/si.config
+++ b/src/_locales/si.config
@@ -33,6 +33,15 @@ Extension එක සකසන්න
 @set_active_hours
 ක්‍රියාකාරී වේලාවන් සකසන්න
 
+@set_location
+හිරු බැස යෑමේදී ස්වයංක්‍රීයව ක්‍රියාත්මක කිරීම සඳහා ඔබගේ ආසන්න ස්ථානය ඇතුළත් කරන්න
+
+@longitude
+දේශාංශ
+
+@latitude
+අක්ෂාංශ
+
 @page_protected
 මෙම පිටුව ආරක්‍ෂා කර ඇත
 බ්‍රව්සරය මඟින්
diff --git a/src/_locales/th.config b/src/_locales/th.config
index 0c10d25f..5b9abebd 100644
--- a/src/_locales/th.config
+++ b/src/_locales/th.config
@@ -33,6 +33,15 @@
 @set_active_hours
 ตั้งเวลาที่เริ่มทำงาน
 
+@set_location
+ป้อนตำแหน่งโดยประมาณของคุณสำหรับการเปิดใช้งานอัตโนมัติเมื่อพระอาทิตย์ตก
+
+@longitude
+ลองจิจูด
+
+@latitude
+ละติจูด
+
 @page_protected
 หน้านี้ถูกป้องกัน
 โดยเว็บเบราเซอร์
diff --git a/src/_locales/tr.config b/src/_locales/tr.config
index c6b204a6..e227d3e6 100644
--- a/src/_locales/tr.config
+++ b/src/_locales/tr.config
@@ -33,6 +33,15 @@ Zaman ayarları
 @set_active_hours
 Etkin saatleri ayarla
 
+@set_location
+Günbatımında otomatik aktivasyon için yaklaşık konumunuzu girin
+
+@longitude
+Boylam
+
+@latitude
+Enlem
+
 @page_protected
 Bu sayfa tarayıcı
 tarafından korunuyor
diff --git a/src/_locales/uk.config b/src/_locales/uk.config
index fc1f76a4..16f40df1 100644
--- a/src/_locales/uk.config
+++ b/src/_locales/uk.config
@@ -33,6 +33,15 @@
 @set_active_hours
 Встановити робочий час
 
+@set_location
+Введіть своє приблизне місце для автоматичної активації на заході сонця
+
+@longitude
+Довгота
+
+@latitude
+Широта
+
 @page_protected
 Ця сторінка захищена
 браузером
diff --git a/src/_locales/zh_CN.config b/src/_locales/zh_CN.config
index 148e186c..c1071bc4 100644
--- a/src/_locales/zh_CN.config
+++ b/src/_locales/zh_CN.config
@@ -33,6 +33,15 @@
 @set_active_hours
 设置使用时段
 
+@set_location
+输入您在日落时自动激活的大致位置
+
+@longitude
+经度
+
+@latitude
+纬度
+
 @page_protected
 此页面受
 浏览器保护
diff --git a/src/_locales/zh_TW.config b/src/_locales/zh_TW.config
index 61a81b84..c92c6dab 100644
--- a/src/_locales/zh_TW.config
+++ b/src/_locales/zh_TW.config
@@ -33,6 +33,15 @@
 @set_active_hours
 設定使用時間
 
+@set_location
+輸入您在日落時自動激活的大致位置
+
+@longitude
+經度
+
+@latitude
+緯度
+
 @page_protected
 此頁面受
 瀏覽器保護
diff --git a/src/background/extension.ts b/src/background/extension.ts
index d7fe3e77..a6d61d35 100644
--- a/src/background/extension.ts
+++ b/src/background/extension.ts
@@ -8,7 +8,7 @@ import UserStorage from './user-storage';
 import {setWindowTheme, resetWindowTheme} from './window-theme';
 import {getFontList, getCommands, setShortcut, canInjectScript} from './utils/extension-api';
 import {isFirefox} from '../utils/platform';
-import {isInTimeInterval, getDuration} from '../utils/time';
+import {isInTimeInterval, getDuration, isNightAtLocation} from '../utils/time';
 import {isURLInList, getURLHost, isURLEnabled} from '../utils/url';
 import ThemeEngines from '../generators/theme-engines';
 import createCSSFilterStylesheet from '../generators/css-filter';
@@ -50,7 +50,16 @@ export class Extension {
         if (this.user.settings.automation === 'time') {
             const now = new Date();
             return isInTimeInterval(now, this.user.settings.time.activation, this.user.settings.time.deactivation);
+        } else if (this.user.settings.automation === 'location') {
+            const latitude = this.user.settings.location.latitude;
+            const longitude = this.user.settings.location.longitude;
+
+            if (latitude != null && longitude != null) {
+                const now = new Date();
+                return isNightAtLocation(now, latitude, longitude);
+            }
         }
+
         return this.user.settings.enabled;
     }
 
@@ -190,7 +199,7 @@ export class Extension {
 
     private startAutoTimeCheck() {
         setInterval(() => {
-            if (!this.ready || this.user.settings.automation !== 'time') {
+            if (!this.ready || this.user.settings.automation === '') {
                 return;
             }
             const isEnabled = this.isEnabled();
@@ -212,7 +221,9 @@ export class Extension {
             (prev.enabled !== this.user.settings.enabled) ||
             (prev.automation !== this.user.settings.automation) ||
             (prev.time.activation !== this.user.settings.time.activation) ||
-            (prev.time.deactivation !== this.user.settings.time.deactivation)
+            (prev.time.deactivation !== this.user.settings.time.deactivation) ||
+            (prev.location.latitude !== this.user.settings.location.latitude) ||
+            (prev.location.longitude !== this.user.settings.location.longitude)
         ) {
             this.onAppToggle();
         }
diff --git a/src/background/user-storage.ts b/src/background/user-storage.ts
index 7106b660..e7ef61cd 100644
--- a/src/background/user-storage.ts
+++ b/src/background/user-storage.ts
@@ -34,6 +34,10 @@ export default class UserStorage {
                 activation: '18:00',
                 deactivation: '9:00',
             },
+            location: {
+                latitude: null,
+                longitude: null,
+            }
         };
         this.settings = null;
     }
diff --git a/src/definitions.d.ts b/src/definitions.d.ts
index 88ed028f..7f442ac7 100644
--- a/src/definitions.d.ts
+++ b/src/definitions.d.ts
@@ -65,6 +65,7 @@ export interface UserSettings {
     syncSettings: boolean;
     automation: string;
     time: TimeSettings;
+    location: LocationSettings;
 }
 
 export interface TimeSettings {
@@ -72,6 +73,11 @@ export interface TimeSettings {
     deactivation: string;
 }
 
+export interface LocationSettings {
+    latitude: number;
+    longitude: number;
+}
+
 export interface TabInfo {
     url: string;
     isProtected: boolean;
diff --git a/src/ui/connect/mock.ts b/src/ui/connect/mock.ts
index 5f84cfcd..083fd64e 100644
--- a/src/ui/connect/mock.ts
+++ b/src/ui/connect/mock.ts
@@ -29,6 +29,10 @@ export function getMockData(override = {}): ExtensionData {
                 activation: '18:00',
                 deactivation: '9:00',
             },
+            location: {
+                latitude: 52.4237178,
+                longitude: 31.021786,
+            },
         } as UserSettings,
         fonts: [
             'serif',
diff --git a/src/ui/popup/components/header/index.tsx b/src/ui/popup/components/header/index.tsx
index ef380e57..bc869980 100644
--- a/src/ui/popup/components/header/index.tsx
+++ b/src/ui/popup/components/header/index.tsx
@@ -2,11 +2,12 @@ import {m} from 'malevic';
 import SiteToggle from '../site-toggle';
 import MoreToggleSettings from './more-toggle-settings';
 import WatchIcon from './watch-icon';
+import SunMoonIcon from './sun-moon-icon';
 import {Shortcut, Toggle} from '../../../controls';
 import {getLocalMessage} from '../../../../utils/locales';
 import {ExtWrapper, TabInfo} from '../../../../definitions';
 
-function multiline(...lines) {
+function multiline(...lines: string[]) {
     return lines.join('\n');
 }
 
@@ -24,7 +25,9 @@ function Header({data, actions, tab, onMoreToggleSettingsClick}: HeaderProps) {
         });
     }
 
+    const isAutomation = Boolean(data.settings.automation);
     const isTimeAutomation = data.settings.automation === 'time';
+    const isLocationAutomation = data.settings.automation === 'location';
     const now = new Date();
 
     return (
@@ -74,12 +77,14 @@ function Header({data, actions, tab, onMoreToggleSettingsClick}: HeaderProps) {
                 <span
                     class={{
                         'header__app-toggle__time': true,
-                        'header__app-toggle__time--active': isTimeAutomation,
+                        'header__app-toggle__time--active': isAutomation,
                     }}
                 >
                     {(isTimeAutomation
                         ? <WatchIcon hours={now.getHours()} minutes={now.getMinutes()} />
-                        : null)}
+                        : (isLocationAutomation
+                            ? (<SunMoonIcon date={now} latitude={data.settings.location.latitude} longitude={data.settings.location.longitude} />)
+                            : null))}
                 </span>
             </div>
         </header>
diff --git a/src/ui/popup/components/header/more-toggle-settings.tsx b/src/ui/popup/components/header/more-toggle-settings.tsx
index df80f51a..b1feff0b 100644
--- a/src/ui/popup/components/header/more-toggle-settings.tsx
+++ b/src/ui/popup/components/header/more-toggle-settings.tsx
@@ -1,5 +1,5 @@
 import {m} from 'malevic';
-import {CheckBox, TimeRangePicker} from '../../../controls';
+import {CheckBox, TextBox, TimeRangePicker} from '../../../controls';
 import {getLocalMessage} from '../../../../utils/locales';
 import {ExtWrapper} from '../../../../definitions';
 
@@ -9,6 +9,64 @@ type MoreToggleSettingsProps = ExtWrapper & {
 };
 
 export default function MoreToggleSettings({data, actions, isExpanded, onClose}: MoreToggleSettingsProps) {
+    const locationSettings = data.settings.location;
+    const values = {
+        'latitude': {
+            min: -90,
+            max: 90
+        },
+        'longitude': {
+            min: -180,
+            max: 180,
+        },
+    };
+
+    function getLocationString(location: number) {
+        if (location == null) {
+            return '';
+        }
+
+        return `${location}°`;
+    }
+
+    function locationChanged(inputElement: HTMLInputElement, newValue: string, type: string) {
+        if (newValue.trim() === '') {
+            inputElement.value = '';
+
+            actions.changeSettings({
+                location: {
+                    ...locationSettings,
+                    [type]: null,
+                },
+            });
+
+            return;
+        }
+
+        const min: number = values[type].min;
+        const max: number = values[type].max;
+
+        newValue = newValue.replace(',', '.').replace('°', '');
+
+        let num = Number(newValue);
+        if (isNaN(num)) {
+            num = 0;
+        } else if (num > max) {
+            num = max;
+        } else if (num < min) {
+            num = min;
+        }
+
+        inputElement.value = getLocationString(num);
+
+        actions.changeSettings({
+            location: {
+                ...locationSettings,
+                [type]: num,
+            },
+        });
+    }
+
     return (
         <div
             class={{
@@ -35,6 +93,37 @@ export default function MoreToggleSettings({data, actions, isExpanded, onClose}:
                 <p class="header__app-toggle__more-settings__description">
                     {getLocalMessage('set_active_hours')}
                 </p>
+                <div class="header__app-toggle__more-settings__line">
+                    <CheckBox
+                        checked={data.settings.automation === 'location'}
+                        onchange={(e) => actions.changeSettings({automation: e.target.checked ? 'location' : ''})}
+                    />
+                    <TextBox
+                        class="header__app-toggle__more-settings__location__latitude"
+                        placeholder={getLocalMessage('latitude')}
+                        onchange={(e) => locationChanged(e.target, e.target.value, 'latitude')}
+                        attached={(node: HTMLInputElement) => node.value = getLocationString(locationSettings.latitude)}
+                        onkeypress={(e) => {
+                            if (e.key === 'Enter') {
+                                (e.target as HTMLInputElement).blur();
+                            }
+                        }}
+                    />
+                    <TextBox
+                        class="header__app-toggle__more-settings__location__longitude"
+                        placeholder={getLocalMessage('longitude')}
+                        onchange={(e) => locationChanged(e.target, e.target.value, 'longitude')}
+                        attached={(node: HTMLInputElement) => node.value = getLocationString(locationSettings.longitude)}
+                        onkeypress={(e) => {
+                            if (e.key === 'Enter') {
+                                (e.target as HTMLInputElement).blur();
+                            }
+                        }}
+                    />
+                </div>
+                <p class="header__app-toggle__more-settings__sunset-description">
+                    {getLocalMessage('set_location')}
+                </p>
             </div>
         </div>
     );
diff --git a/src/ui/popup/components/header/style.less b/src/ui/popup/components/header/style.less
index 06ca1c46..0a640db9 100644
--- a/src/ui/popup/components/header/style.less
+++ b/src/ui/popup/components/header/style.less
@@ -45,7 +45,7 @@
             display: inline-block;
             height: @size-control-inner + 2 * @size-border;
             left: 100%;
-            opacity: 0;
+            opacity: 0.5;
             position: absolute;
             top: 0;
             transition: opacity @time-slow, background-color @time-slow, color @time-slow;
@@ -106,12 +106,15 @@
                 + @indent-large
                 + 2 * @size-border + @size-control-inner
                 + @size-text-small-height
+                + @indent-large
+                + 2 * @size-border + @size-control-inner
+                + @size-text-small-height * 2
                 + @indent-large;
             left: 0;
             position: absolute;
             top: @popup-content-padding + @size-control-inner * 2 + @size-border + @indent-small;
             transition: all @time-slow;
-            z-index: 2;
+            z-index: 999;
 
             &:not(&--expanded) {
                 border-bottom-width: 0;
@@ -186,11 +189,18 @@
             &__description {
                 font-size: @size-text-small;
                 line-height: @size-text-small-height;
-                margin: 0;
+                margin: 0 0 @size-control-description 0;
                 text-align: center;
                 white-space: nowrap;
             }
 
+            &__sunset-description {
+                font-size: @size-text-small;
+                line-height: @size-text-small-height;
+                margin: 0;
+                text-align: center;
+            }
+
             &::after {
                 background-image: linear-gradient(to bottom, fadeout(@color-back, 10%), fadeout(@color-back, 100%));
                 content: "";
@@ -204,6 +214,24 @@
                 transition: height @time-slow;
                 width: 100%;
             }
+
+            &__location {
+                &__latitude,
+                &__longitude {
+                    flex: auto;
+                    text-align: center;
+                    text-indent: 0;
+                    width: 100%;
+                }
+
+                &__latitude {
+                    border-right: none;
+                }
+
+                &__longitude {
+                    border-left: @size-border-inner solid @color-border;
+                }
+            }
         }
     }
 
diff --git a/src/ui/popup/components/header/sun-moon-icon.tsx b/src/ui/popup/components/header/sun-moon-icon.tsx
new file mode 100644
index 00000000..2fbca698
--- /dev/null
+++ b/src/ui/popup/components/header/sun-moon-icon.tsx
@@ -0,0 +1,42 @@
+import {m} from 'malevic';
+import {isNightAtLocation} from '../../../../utils/time';
+
+export default function SunMoonIcon({date, latitude, longitude}) {
+    if (latitude == null || longitude == null) {
+        // question mark icon
+        return (
+            <svg viewBox="-2.5 -2.5 21 21">
+                <path fill="white" stroke="none" d="M7.78 0C10.55 0 13.28 1.27 13.28 4.32C13.28 7.14 10.05 8.21 9.36 9.23C8.84 9.99 9.02 11.05 7.59 11.05C6.66 11.05 6.21 10.29 6.21 9.6C6.21 7.03 9.99 6.45 9.99 4.32C9.99 3.16 9.21 2.46 7.91 2.46C5.14 2.46 6.23 5.32 4.13 5.32C3.38 5.32 2.73 4.86 2.73 4C2.72 1.88 5.14 0 7.79 0Z M7.68 12.45C8.65 12.45 9.45 13.25 9.45 14.23C9.45 15.2 8.65 16 7.68 16C6.7 16 5.9 15.2 5.9 14.23C5.9 13.25 6.7 12.45 7.68 12.45Z" />
+            </svg>
+        );
+    }
+
+    if (isNightAtLocation(date, latitude, longitude)) {
+        // moon icon
+        return (
+            <svg viewBox="0 0 16 16">
+                <path fill="white" stroke="none" d="M 6 3 Q 10 8 6 13 Q 12 13 12 8 Q 12 3 6 3" />
+            </svg>
+        );
+    }
+
+    // sun icon
+    return (
+        <svg viewBox="-4.5 -4.5 41 41">
+            <circle fill="white" stroke="none" cx="16" cy="16" r="8" />
+            {...(Array.from({length: 8}).map((_, i) => (
+                <line
+                    x1="2"
+                    y1="16"
+                    x2="4"
+                    y2="16"
+                    stroke="white"
+                    stroke-width="5"
+                    stroke-linecap="round"
+                    transform-origin="16 16"
+                    transform={`rotate(${i * 45})`}
+                />
+            )))}
+        </svg>
+    );
+}
diff --git a/src/utils/time.ts b/src/utils/time.ts
index 3599d447..03fc40ec 100644
--- a/src/utils/time.ts
+++ b/src/utils/time.ts
@@ -70,3 +70,139 @@ export function getDuration(time: Duration) {
     }
     return duration;
 }
+
+function getSunsetSunriseUTCTime(
+    date: Date,
+    latitude: number,
+    longitude: number,
+) {
+    const dec31 = new Date(date.getUTCFullYear(), 0, 0);
+    const oneDay = getDuration({days: 1});
+    const dayOfYear = Math.floor((Number(date) - Number(dec31)) / oneDay);
+
+    const zenith = 90.83333333333333;
+    const D2R = Math.PI / 180;
+    const R2D = 180 / Math.PI;
+
+    // convert the longitude to hour value and calculate an approximate time
+    const lnHour = longitude / 15;
+
+    function getTime(isSunrise: boolean) {
+        const t = dayOfYear + (((isSunrise ? 6 : 18) - lnHour) / 24);
+
+        // calculate the Sun's mean anomaly
+        const M = (0.9856 * t) - 3.289;
+
+        // calculate the Sun's true longitude
+        let L = M + (1.916 * Math.sin(M * D2R)) + (0.020 * Math.sin(2 * M * D2R)) + 282.634;
+        if (L > 360) {
+            L = L - 360;
+        } else if (L < 0) {
+            L = L + 360;
+        }
+
+        // calculate the Sun's right ascension
+        let RA = R2D * Math.atan(0.91764 * Math.tan(L * D2R));
+        if (RA > 360) {
+            RA = RA - 360;
+        } else if (RA < 0) {
+            RA = RA + 360;
+        }
+
+        // right ascension value needs to be in the same qua
+        const Lquadrant = (Math.floor(L / (90))) * 90;
+        const RAquadrant = (Math.floor(RA / 90)) * 90;
+        RA = RA + (Lquadrant - RAquadrant);
+
+        // right ascension value needs to be converted into hours
+        RA = RA / 15;
+
+        // calculate the Sun's declination
+        const sinDec = 0.39782 * Math.sin(L * D2R);
+        const cosDec = Math.cos(Math.asin(sinDec));
+
+        // calculate the Sun's local hour angle
+        const cosH = (Math.cos(zenith * D2R) - (sinDec * Math.sin(latitude * D2R))) / (cosDec * Math.cos(latitude * D2R));
+        if (cosH > 1) {
+            // always night
+            return {
+                alwaysDay: false,
+                alwaysNight: true,
+                time: 0,
+            };
+        } else if (cosH < -1) {
+            // always day
+            return {
+                alwaysDay: true,
+                alwaysNight: false,
+                time: 0,
+            };
+        }
+
+        const H = (isSunrise ? (360 - R2D * Math.acos(cosH)) : (R2D * Math.acos(cosH))) / 15;
+
+        // calculate local mean time of rising/setting
+        const T = H + RA - (0.06571 * t) - 6.622;
+
+        // adjust back to UTC
+        let UT = T - lnHour;
+        if (UT > 24) {
+            UT = UT - 24;
+        } else if (UT < 0) {
+            UT = UT + 24;
+        }
+
+        // convert to milliseconds
+        return {
+            alwaysDay: false,
+            alwaysNight: false,
+            time: UT * getDuration({hours: 1}),
+        };
+    }
+
+    const sunriseTime = getTime(true);
+    const sunsetTime = getTime(false);
+
+    if (sunriseTime.alwaysDay || sunsetTime.alwaysDay) {
+        return {
+            alwaysDay: true
+        };
+    } else if (sunriseTime.alwaysNight || sunsetTime.alwaysNight) {
+        return {
+            alwaysNight: true
+        };
+    }
+
+    return {
+        sunriseTime: sunriseTime.time,
+        sunsetTime: sunsetTime.time
+    };
+}
+
+export function isNightAtLocation(
+    date: Date,
+    latitude: number,
+    longitude: number,
+) {
+    const time = getSunsetSunriseUTCTime(date, latitude, longitude);
+
+    if (time.alwaysDay) {
+        return false;
+    } else if (time.alwaysNight) {
+        return true;
+    }
+
+    const sunriseTime = time.sunriseTime;
+    const sunsetTime = time.sunsetTime;
+    const currentTime = (
+        date.getUTCHours() * getDuration({hours: 1}) +
+        date.getUTCMinutes() * getDuration({minutes: 1}) +
+        date.getUTCSeconds() * getDuration({seconds: 1})
+    );
+
+    if (sunsetTime > sunriseTime) {
+        return (currentTime > sunsetTime) || (currentTime < sunriseTime);
+    } else {
+        return (currentTime > sunsetTime) && (currentTime < sunriseTime);
+    }
+}


Test patch:
diff --git a/tests/time.tests.ts b/tests/time.tests.ts
index 95d1acd3..ff1c21bc 100644
--- a/tests/time.tests.ts
+++ b/tests/time.tests.ts
@@ -1,4 +1,4 @@
-import {isInTimeInterval, parseTime, getDuration} from '../src/utils/time';
+import {isInTimeInterval, isNightAtLocation, parseTime, getDuration} from '../src/utils/time';
 
 test('Time interval', () => {
     expect(isInTimeInterval(new Date(2018, 11, 4, 10), '9:00', '12:00')).toBe(true);
@@ -32,3 +32,31 @@ test('Duration', () => {
         days: 3
     })).toEqual(289488000);
 });
+
+test('Sunrize/sunset', () => {
+    const utcDate = (y, m, d, hh, mm) => new Date(Date.UTC(y, m, d, hh, mm));
+
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 0, 0), 52, 0)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 5, 0), 52, 0)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 7, 0), 52, 0)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 12, 0), 52, 0)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 18, 0), 52, 0)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 20, 0), 52, 0)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 23, 59), 52, 0)).toBe(true);
+
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 0, 0), 52, 30)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 3, 0), 52, 30)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 5, 0), 52, 30)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 10, 0), 52, 30)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 16, 0), 52, 30)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 18, 0), 52, 30)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 23, 59), 52, 30)).toBe(true);
+
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 0, 0), 52, -30)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 7, 0), 52, -30)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 9, 0), 52, -30)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 14, 0), 52, -30)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 20, 0), 52, -30)).toBe(false);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 22, 0), 52, -30)).toBe(true);
+    expect(isNightAtLocation(utcDate(2019, 8, 9, 23, 59), 52, -30)).toBe(true);
+});