diff --git a/.github/PULL_REQUEST_TEMPLATE/1-translation.md b/.github/PULL_REQUEST_TEMPLATE/1-translation.md deleted file mode 100644 index 5e06777..0000000 --- a/.github/PULL_REQUEST_TEMPLATE/1-translation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Translation -about: Translation using the *.po files -title: 'Update Translation' -labels: translation -assignees: '' - ---- - -Tip: A test will be done on this pull request when you submit it. -Common issues are missing format specifiers (if a "%s" is present ensure that -the same amount are present in the translation) or forgetting quotes ("). - -You can run a test on your local copy with: `make`. diff --git a/.github/PULL_REQUEST_TEMPLATE/2-bug-fix.md b/.github/PULL_REQUEST_TEMPLATE/2-bug-fix.md deleted file mode 100644 index f1ea69b..0000000 --- a/.github/PULL_REQUEST_TEMPLATE/2-bug-fix.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Bug Fix -about: Fixes a bug -title: '' -labels: bug -assignees: '' - ---- - -Tip: Read `docs/building.md` and `docs/source.md`. diff --git a/.github/PULL_REQUEST_TEMPLATE/3-feature.md b/.github/PULL_REQUEST_TEMPLATE/3-feature.md deleted file mode 100644 index 06cd4a6..0000000 --- a/.github/PULL_REQUEST_TEMPLATE/3-feature.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Feature -about: Adds a feature -title: '' -labels: enhancement -assignees: '' - ---- - -Tip: Read `docs/building.md` and `docs/source.md`. diff --git a/.github/PULL_REQUEST_TEMPLATE/pr.md b/.github/PULL_REQUEST_TEMPLATE/pr.md new file mode 100644 index 0000000..55b8c14 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pr.md @@ -0,0 +1,2 @@ + +🚫 Do NOT open PRs to `master`. Use `development` branch instead. diff --git a/.github/workflows/BlockMasterPRs.yml b/.github/workflows/BlockMasterPRs.yml new file mode 100644 index 0000000..7bc2d84 --- /dev/null +++ b/.github/workflows/BlockMasterPRs.yml @@ -0,0 +1,17 @@ +name: Block PRs to master + +on: + pull_request: + branches: + - master + +jobs: + block-master-prs: + runs-on: ubuntu-latest + steps: + - name: Block PRs to master + run: | + if [ "${{ github.actor }}" != "${{ github.repository_owner }}" ]; then + echo 'Please open PRs to the development branch instead.' + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index da3eb25..d312ef8 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -1,10 +1,10 @@ -name: 'Translation Build' +name: 'Build' on: workflow_dispatch: pull_request: types: [ 'opened' ] - branches: [ 'master' ] + branches: [ 'master', 'development' ] paths: - '**.po' - '**.ts' @@ -15,10 +15,10 @@ on: - 'Makefile' push: branches: - - 'master' + - 'development' jobs: - build_translations: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..b8416be --- /dev/null +++ b/AUTHORS @@ -0,0 +1,5 @@ + +Maintainer/Programmer: Roman Lefler + +German (Deutsch): Ahmet Ala +Turkish (Türkçe): Ahmet Ala diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a00d187 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ + +# v48.1.0 + +## Features + +- Support for GNOME 46 +- Themes (choose between default, Light, Afterdark, and Immersive) +- Label in panel can show any weather detail +- Second available label in panel +- Show or hide the condition icon in panel +- Current weather details can be configured +- Configure where panel is shown in top bar +- Configure the location provider + +## Improvements + +- Change cloudy night icon to be more clear +- Credits dialog in About +- Use the word "Today" in the forecast +- Weather data copyright always shows current year +- Improve keyboard and mouse shortcuts in locations menus +- Pop-up shows which city it thinks you're in when set to My Location +- Better make script for packaging + +## Bug Fixes + +- Fix Mutter crash on some machines if Mutter couldn't find cursor when hovering the panel + +## Translations + +- German (thanks Ahmet Ala) +- Turkish (thanks Ahmet Ala) + diff --git a/Makefile b/Makefile index cf835fe..ad63a6b 100644 --- a/Makefile +++ b/Makefile @@ -11,28 +11,40 @@ BUILD := $(DIST)/build SCHEMAOUTDIR := $(BUILD)/schemas PO := ./po ICONS := ./icons +THEMES := ./themes +AUTHORS := ./AUTHORS -METADATA := $(STATIC)/metadata.json -STYLESHEET := $(STATIC)/stylesheet.css +STATICSRCS := $(wildcard $(STATIC)/*) SCHEMASRC := $(SCHEMAS)/org.gnome.shell.extensions.$(NAME).gschema.xml # This excludes .d.ts files SRCS := $(shell find $(SRC) -type f -name '*.ts' ! -name '*.d.ts') POFILES := $(wildcard $(PO)/*.po) # This intentionally includes the license file ICONSSRCS := $(wildcard $(ICONS)/*) +CSSSRCS := $(wildcard $(THEMES)/*.css) SCHEMAOUT := $(SCHEMAOUTDIR)/gschemas.compiled SCHEMACP := $(SCHEMAOUTDIR)/org.gnome.shell.extensions.$(NAME).gschema.xml -METADATACP := $(BUILD)/metadata.json -STYLESHEETCP := $(BUILD)/stylesheet.css +STATICOUT := $(STATICSRCS:$(STATIC)/%=$(BUILD)/%) ZIP := $(DIST)/$(NAME)-v$(VERSION).zip POT := $(PO)/$(UUID).pot ICONSOUT := $(ICONSSRCS:$(ICONS)/%=$(BUILD)/icons/%) +CSSOUT := $(BUILD)/stylesheet.css MOS := $(POFILES:$(PO)/%.po=$(BUILD)/locale/%/LC_MESSAGES/$(UUID).mo) +# Packages should use make DESTDIR=... for packaging +ifeq ($(strip $(DESTDIR)),) + INSTALLTYPE = local + INSTALLBASE = $(HOME)/.local/share/gnome-shell/extensions +else + INSTALLTYPE = system + SHARE_PREFIX = $(DESTDIR)/usr/share + INSTALLBASE = $(SHARE_PREFIX)/gnome-shell/extensions +endif + .PHONY: out pack install clean copyicons ts -out: $(POT) ts $(SCHEMAOUT) $(SCHEMACP) $(METADATACP) $(STYLESHEETCP) $(ICONSOUT) $(MOS) +out: $(POT) ts $(SCHEMAOUT) $(SCHEMACP) $(STATICOUT) $(ICONSOUT) $(MOS) $(CSSOUT) pack: $(ZIP) @@ -42,20 +54,40 @@ install: out rm -rf ~/.local/share/gnome-shell/extensions/$(UUID) mkdir -p ~/.local/share/gnome-shell/extensions cp -r $(BUILD) ~/.local/share/gnome-shell/extensions/$(UUID) +ifeq ($(INSTALLTYPE),system) + rm -r $(addprefix $(INSTALLBASE)/$(UUID)/, schemas locale LICENSE) + mkdir -p $(SHARE_PREFIX)/glib-2.0/schemas \ + $(SHARE_PREFIX)/locale \ + $(SHARE_PREFIX)/licenses/$(UUID) + cp -r $(BUILD)/schemas/*gschema.xml $(SHARE_PREFIX)/glib-2.0/schemas + cp -r $(BUILD)/locale/* $(SHARE_PREFIX)/locale + cp -r $(BUILD)/LICENSE $(SHARE_PREFIX)/licenses/$(UUID) +endif clean: rm -rf $(DIST) rm -f $(POT) -./node_modules: package.json +./node_modules/.package-lock.json: package.json printf -- 'NEEDED: npm\n' npm install ts: $(BUILD)/extension.js -$(BUILD)/extension.js: $(SRCS) ./node_modules +# Build files with tsc +# Also inserts "const authors=FILE" into resources.js +$(BUILD)/extension.js $(BUILD)/resource.js: $(SRCS) $(AUTHORS) ./node_modules/.package-lock.json printf -- 'NEEDED: tsc\n' tsc + @touch $(BUILD)/extension.js + + @if ! grep -q '// Inserted' $(BUILD)/resource.js; then \ + printf '// Inserted\n\nconst authors = `' >> $(BUILD)/resource.js; \ + cat $(AUTHORS) >> $(BUILD)/resource.js; \ + printf '`;' >> $(BUILD)/resource.js; \ + else \ + touch $(BUILD)/resource.js; \ + fi $(SCHEMAOUT): $(SCHEMASRC) printf -- 'NEEDED: glib-compile-schemas\n' @@ -66,13 +98,9 @@ $(SCHEMACP): $(SCHEMASRC) mkdir -p $(SCHEMAOUTDIR) cp $(SCHEMASRC) $(SCHEMACP) -$(METADATACP): $(METADATA) - mkdir -p $(BUILD) - cp $(METADATA) $(METADATACP) - -$(STYLESHEETCP): $(STYLESHEET) +$(STATICOUT): $(BUILD)/%: $(STATIC)/% mkdir -p $(BUILD) - cp $(STYLESHEET) $(STYLESHEETCP) + cp $< $@ $(POT): $(SRCS) printf -- 'NEEDED: xgettext\n' @@ -93,6 +121,11 @@ $(BUILD)/icons: $(BUILD)/icons/%: $(ICONS)/% $(BUILD)/icons cp $< $@ +# Explicitly putting stylesheet.css here makes it +# first in the outputted file +$(CSSOUT): $(THEMES)/stylesheet.css $(CSSSRCS) + cat $^ > $@ + $(ZIP): out printf -- 'NEEDED: zip\n' mkdir -p $(DIST) diff --git a/README.md b/README.md index c61ddf2..d3eba03 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ A highly configurable GNOME shell extension for viewing the weather. ![Screenshot](./docs/screenshot.png) -Also has support for translations (albeit translations are probably poor). +Support for themes, the following screenshot uses the *Immersive* theme: -![International Screenshot](./docs/intlscreenshot.png) +![Immersive Theme Screenshot](./docs/immersivescreenshot.png) + +## Installation + +[![Get on GNOME Extensions](./docs/ego.png)](https://extensions.gnome.org/extension/8261/simpleweather/) ## Features diff --git a/docs/ego.png b/docs/ego.png new file mode 100644 index 0000000..1fa2534 Binary files /dev/null and b/docs/ego.png differ diff --git a/docs/immersivescreenshot.png b/docs/immersivescreenshot.png new file mode 100644 index 0000000..bb3c1a5 Binary files /dev/null and b/docs/immersivescreenshot.png differ diff --git a/docs/intlscreenshot.png b/docs/intlscreenshot.png deleted file mode 100644 index e0b004b..0000000 Binary files a/docs/intlscreenshot.png and /dev/null differ diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..d6bed48 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,13 @@ + +# Set Up for Development + +Note that working on this project requires the GNOME shell, +the GNU gettext utilities, and npm. + +If you are just translating you can use any text editor or +a specialized translation editor. + +If you want to write code then preferably use an editor that supports +TypeScript/LSPs like VS Code, Neovim, or Emacs. + + diff --git a/docs/themes.md b/docs/themes.md new file mode 100644 index 0000000..dab2010 --- /dev/null +++ b/docs/themes.md @@ -0,0 +1,68 @@ + +# Themes + +Themes are done by dynamically adding the correct classes onto widgets for the chosen theme. + +## Files and Naming + +Themes are stored in the `themes/` directory as `.css`. + +In order for them to appear in the settings, they must be added to +`src/preferences/generalPage.ts` in the themes model and array. + +Each class takes the name of `sw-style--`. + +Example of a "sky" theme in `themes/sky.css`: + +```css +.sw-style-sky-bg { + background: #C2DAE6; +} + +.sw-style-sky-forecast-box:hover { + background: #DAEBF2; +} +``` + +> **Note** +> `light` is a good example of a theme. + +### Notes + +Prefer `background` over `background-color` in case a GTK style uses a background image. + +### Classes + +``` +menu +|--- bg +| |--- left-box +| |--- forecast-box +| |--- faded +``` + +Additional clases: + +``` +button +``` + +### "Attributes" + +There are "attribute" classes which show some kind of logic in the program. +They look like `swa-`. +Attributes should be in selectors in conjunction with the normal classes. + +The `menu` can have the following: + +- `open` when pop-up is open +- any of the following for weather conditions: `clear`, `cloudy`, `rainy`, `snowy`, `stormy`, `windy` +- either `day` or `night` + +For example, this selector makes the faded text yellow on a sunny day: + +```css +.sw-style--menu.swa-clear.swa-day .sw-style--faded { + color: yellow; +} +``` diff --git a/icons/weather-few-clouds-night-symbolic.svg b/icons/weather-few-clouds-night-symbolic.svg index 175e56b..a98e395 100644 --- a/icons/weather-few-clouds-night-symbolic.svg +++ b/icons/weather-few-clouds-night-symbolic.svg @@ -1,15 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/po/de.po b/po/de.po index 69c8bae..9e137dc 100644 --- a/po/de.po +++ b/po/de.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: simpleweather\n" "Report-Msgid-Bugs-To: simpleweather-gnome@proton.me\n" "POT-Creation-Date: 2025-06-23 19:40-0500\n" -"PO-Revision-Date: 2025-06-24 00:49\n" -"Last-Translator: \n" +"PO-Revision-Date: 2025-07-04 10:49\n" +"Last-Translator: Ahmet Ala\n" "Language-Team: German\n" "Language: de_DE\n" "MIME-Version: 1.0\n" @@ -35,7 +35,7 @@ msgstr "%f°S" #: src/location.ts:75 #, javascript-format msgid "%f°E" -msgstr "%f°E" +msgstr "%f°O" #: src/location.ts:75 #, javascript-format @@ -58,17 +58,17 @@ msgstr "H: %s" #: src/popup.ts:379 #, javascript-format msgid "L: %s" -msgstr "L: %s" +msgstr "T: %s" #: src/popup.ts:400 #, javascript-format msgid "Temp: %s" -msgstr "Tempo: %s" +msgstr "Temperatur: %s" #: src/popup.ts:401 #, javascript-format msgid "Feels Like: %s" -msgstr "Gefühl: %s" +msgstr "Gefühlte Temp.: %s" #: src/popup.ts:402 #, javascript-format @@ -78,22 +78,22 @@ msgstr "Wind: %s, %s" #: src/popup.ts:406 #, javascript-format msgid "Gusts: %s" -msgstr "Preise: %s" +msgstr "Windböen: %s" #: src/popup.ts:407 #, javascript-format msgid "Humidity: %s" -msgstr "Luftfeucht: %s" +msgstr "Luftfeuchtigkeit: %s" #: src/popup.ts:408 #, javascript-format msgid "Pressure: %s" -msgstr "Druck: %s" +msgstr "Luftdruck: %s" #: src/popup.ts:409 #, javascript-format msgid "UV High: %s" -msgstr "UV-Hoch: %s" +msgstr "UV-Index hoch: %s" #: src/popup.ts:410 #, javascript-format @@ -110,7 +110,7 @@ msgstr "GitHub Repository" #: src/preferences/aboutPage.ts:61 msgid "SimpleWeather Version" -msgstr "Einfaches Wetter Version" +msgstr "SimpleWeather Version" #: src/preferences/aboutPage.ts:64 msgid "Unknown" @@ -119,7 +119,7 @@ msgstr "Unbekannt" #: src/preferences/aboutPage.ts:75 #, javascript-format msgid "This extension is a rewrite of the %s project." -msgstr "Diese Erweiterung ist ein Umschreiben des %s Projekts." +msgstr "Diese Erweiterung ist ein Neufassung des %s Projekts." #: src/preferences/editLocation.ts:33 #, javascript-format @@ -164,7 +164,7 @@ msgstr "Maßeinheiten konfigurieren" #: src/preferences/generalPage.ts:48 msgid "Custom" -msgstr "Eigene" +msgstr "Benutzerdefiniert" #: src/preferences/generalPage.ts:48 msgid "Metric" @@ -172,11 +172,11 @@ msgstr "Metrisch" #: src/preferences/generalPage.ts:48 msgid "UK" -msgstr "TN" +msgstr "UK" #: src/preferences/generalPage.ts:48 msgid "US" -msgstr "MN" +msgstr "US" #: src/preferences/generalPage.ts:61 msgid "Fahrenheit" @@ -200,11 +200,11 @@ msgstr "Druck" #: src/preferences/generalPage.ts:106 msgid "Rain Measurement" -msgstr "Regenmessung" +msgstr "Niederschlagsmessung" #: src/preferences/generalPage.ts:120 msgid "Distance" -msgstr "Distanz" +msgstr "Entfernung" #: src/preferences/generalPage.ts:144 msgid "Degrees" @@ -212,7 +212,7 @@ msgstr "Grad" #: src/preferences/generalPage.ts:144 msgid "Eight-Point Compass" -msgstr "Acht-Punkte-Kompass" +msgstr "Acht-Punkte-Kompassrose" #: src/preferences/generalPage.ts:147 msgid "Direction" @@ -224,7 +224,7 @@ msgstr "Wetterdienst" #: src/preferences/generalPage.ts:160 msgid "Configure how the weather is attained" -msgstr "Konfiguriere wie das Wetter erreicht wird" +msgstr "Konfigurieren, wie Wetterdaten abgerufen werden" #: src/preferences/generalPage.ts:167 msgid "Weather Provider" @@ -232,7 +232,7 @@ msgstr "Wetteranbieter" #: src/preferences/generalPage.ts:180 msgid "Configure how your location is found" -msgstr "Konfigurieren Sie, wie Ihr Standort gefunden wird" +msgstr "Konfigurieren, wie Ihr Standort gefunden wird" #: src/preferences/generalPage.ts:184 msgid "Online" @@ -256,11 +256,11 @@ msgstr "Aktualisierungsintervall (Minuten)" #: src/preferences/generalPage.ts:217 msgid "Accessibility" -msgstr "Bedienungshilfen" +msgstr "Barrierefreiheit" #: src/preferences/generalPage.ts:218 msgid "Configure accessibility features" -msgstr "Bedienungshilfen konfigurieren" +msgstr "Barrierefreiheitsfunktionen konfigurieren" #: src/preferences/generalPage.ts:221 msgid "High Contrast" @@ -268,7 +268,7 @@ msgstr "Hoher Kontrast" #: src/preferences/generalPage.ts:232 msgid "Panel" -msgstr "Platte" +msgstr "Bedienfeld" #: src/preferences/generalPage.ts:233 msgid "Configure the panel and pop-up" @@ -276,7 +276,7 @@ msgstr "Leiste und Pop-up konfigurieren" #: src/preferences/generalPage.ts:236 msgid "Show Sunrise/Sunset" -msgstr "Sonnenaufgang/Sonnenuntergang anzeigen" +msgstr "Sonnenauf-/untergang anzeigen" #: src/preferences/locationsPage.ts:57 src/preferences/locationsPage.ts:80 msgid "Locations" @@ -284,7 +284,7 @@ msgstr "Standorte" #: src/preferences/locationsPage.ts:70 msgid "Add" -msgstr "Neu" +msgstr "Hinzufügen" #: src/preferences/locationsPage.ts:97 msgid "Move Up" @@ -301,7 +301,7 @@ msgstr "Hier hinzufügen" #: src/preferences/locationsPage.ts:193 #, javascript-format msgid "Are you sure you want delete %s?" -msgstr "Sind Sie sicher, dass Sie %s löschen wollen?" +msgstr "Möchten Sie %s wirklich löschen?" #: src/preferences/locationsPage.ts:194 msgid "Cancel" @@ -326,11 +326,11 @@ msgstr "Etwas anderes hat die Standorte geändert." #: src/preferences/search.ts:45 msgid "Search Location" -msgstr "Suchort" +msgstr "Standort suchen" #: src/preferences/search.ts:54 msgid "City, Neighborhood, etc." -msgstr "Stadt, Nachbarschaft, etc." +msgstr "Stadt, Stadtteil, etc." #: src/preferences/search.ts:59 msgid "Search" @@ -346,16 +346,16 @@ msgstr "Keine Ergebnisse." #: src/preferences/search.ts:187 msgid "No copyright information available." -msgstr "Keine Copyright-Informationen verfügbar." +msgstr "Keine Urheberrechtsinformationen verfügbar." #: src/prefs.ts:60 #, javascript-format msgid "SimpleWeather doesn't know how to handle your locale.\n" "\tError - %s\n" "Please consider submitting a bug report on GitHub." -msgstr "SimpleWeather weiß nicht, wie Sie mit Ihrer Sprache umgehen sollen.\n" +msgstr "SimpleWeather kann Ihr Gebietsschema nicht verarbeiten.\n" "\tFehler - %s\n" -"Bitte überlegen Sie einen Fehlerbericht auf GitHub einzureichen." +"Bitte erwägen Sie, einen Fehlerbericht auf GitHub einzureichen." #: src/prefs.ts:63 msgid "Don't Show Again" @@ -367,7 +367,7 @@ msgstr "Ignorieren" #: src/prefs.ts:63 msgid "Open GitHub" -msgstr "Open GitHub" +msgstr "GitHub öffnen" #: src/welcome.ts:53 #, javascript-format @@ -380,10 +380,10 @@ msgid "%s occasionally connects to the selected weather service. By default, it " • %s, an %s service for weather\n" " • %s, optional for resolving the current location\n" " • %s, for searching locations by name\n\n" -msgstr "%s verbindet sich gelegentlich mit dem ausgewählten Wetterdienst. Standardmäßig wird es das Internet nutzen, um sich zu verbinden:\n" -" • %s, ein %s Service für das Wetter\n" -" • %s, optional für die Lösung des aktuellen Standortes\n" -" • %s, für die Suche nach Orten mit dem Namen\n\n" +msgstr "%s verbindet sich gelegentlich mit dem ausgewählten Wetterdienst. Standardmäßig wird die Internetverbindung genutzt, um sich mit folgenden Diensten zu verbinden:\n" +" • %s, einem %s Dienst für das Wetter\n" +" • %s, optional zur Ermittlung des aktuellen Standorts\n" +" • %s, zur Suche nach Orten per Namen\n\n" #: src/welcome.ts:83 #, javascript-format diff --git a/po/tr.po b/po/tr.po index 1f7be55..7e5f0c8 100644 --- a/po/tr.po +++ b/po/tr.po @@ -3,8 +3,8 @@ msgstr "" "Project-Id-Version: simpleweather\n" "Report-Msgid-Bugs-To: simpleweather-gnome@proton.me\n" "POT-Creation-Date: 2025-06-23 19:40-0500\n" -"PO-Revision-Date: 2025-06-24 00:49\n" -"Last-Translator: \n" +"PO-Revision-Date: 2025-07-04 10:49\n" +"Last-Translator: Ahmet Ala\n" "Language-Team: Turkish\n" "Language: tr_TR\n" "MIME-Version: 1.0\n" @@ -20,372 +20,376 @@ msgstr "" #: src/autoConfig.ts:49 src/location.ts:48 src/location.ts:56 #: src/preferences/generalPage.ts:179 msgid "My Location" -msgstr "" +msgstr "Konumum" #: src/location.ts:74 #, javascript-format msgid "%f°N" -msgstr "" +msgstr "%f°K" #: src/location.ts:74 #, javascript-format msgid "%f°S" -msgstr "" +msgstr "%f°G" #: src/location.ts:75 #, javascript-format msgid "%f°E" -msgstr "" +msgstr "%f°D" #: src/location.ts:75 #, javascript-format msgid "%f°W" -msgstr "" +msgstr "%f°B" #: src/popup.ts:163 msgid "Weather Data" -msgstr "" +msgstr "Hava Durumu Verileri" #: src/popup.ts:307 msgid "Settings" -msgstr "" +msgstr "Ayarlar" #: src/popup.ts:378 #, javascript-format msgid "H: %s" -msgstr "" +msgstr "Y: %s" #: src/popup.ts:379 #, javascript-format msgid "L: %s" -msgstr "" +msgstr "D: %s" #: src/popup.ts:400 #, javascript-format msgid "Temp: %s" -msgstr "" +msgstr "Sıcaklık: %s" #: src/popup.ts:401 #, javascript-format msgid "Feels Like: %s" -msgstr "" +msgstr "Hissedilen: %s" #: src/popup.ts:402 #, javascript-format msgid "Wind: %s, %s" -msgstr "" +msgstr "Rüzgar: %s, %s" #: src/popup.ts:406 #, javascript-format msgid "Gusts: %s" -msgstr "" +msgstr "Bora: %s" #: src/popup.ts:407 #, javascript-format msgid "Humidity: %s" -msgstr "" +msgstr "Nem: %s" #: src/popup.ts:408 #, javascript-format msgid "Pressure: %s" -msgstr "" +msgstr "Basınç: %s" #: src/popup.ts:409 #, javascript-format msgid "UV High: %s" -msgstr "" +msgstr "Yüksek UV: %s" #: src/popup.ts:410 #, javascript-format msgid "Precipitation: %s" -msgstr "" +msgstr "Yağış: %s" #: src/preferences/aboutPage.ts:43 msgid "About" -msgstr "" +msgstr "Hakkında" #: src/preferences/aboutPage.ts:54 msgid "GitHub Repository" -msgstr "" +msgstr "GitHub Deposu" #: src/preferences/aboutPage.ts:61 msgid "SimpleWeather Version" -msgstr "" +msgstr "SimpleWeather Sürümü" #: src/preferences/aboutPage.ts:64 msgid "Unknown" -msgstr "" +msgstr "Bilinmiyor" #: src/preferences/aboutPage.ts:75 #, javascript-format msgid "This extension is a rewrite of the %s project." -msgstr "" +msgstr "Bu eklenti, %s projesinin yeniden yazılmış halidir." #: src/preferences/editLocation.ts:33 #, javascript-format msgid "Edit %s" -msgstr "" +msgstr "%s'yi Düzenle" #: src/preferences/editLocation.ts:33 msgid "New Location" -msgstr "" +msgstr "Yeni Konum" #: src/preferences/editLocation.ts:41 msgid "Name" -msgstr "" +msgstr "Ad" #: src/preferences/editLocation.ts:56 msgid "Coordinates" -msgstr "" +msgstr "Koordinatlar" #: src/preferences/editLocation.ts:66 msgid "Save" -msgstr "" +msgstr "Kaydet" #: src/preferences/editLocation.ts:87 msgid "Name is required." -msgstr "" +msgstr "Ad gerekli." #: src/preferences/editLocation.ts:92 msgid "Invalid coordinates entry." -msgstr "" +msgstr "Geçersiz koordinat girişi." #: src/preferences/generalPage.ts:38 msgid "General" -msgstr "" +msgstr "Genel" #: src/preferences/generalPage.ts:43 src/preferences/generalPage.ts:53 msgid "Units" -msgstr "" +msgstr "Birimler" #: src/preferences/generalPage.ts:44 msgid "Configure units of measurement" -msgstr "" +msgstr "Ölçü birimlerini yapılandırın" #: src/preferences/generalPage.ts:48 msgid "Custom" -msgstr "" +msgstr "Özel" #: src/preferences/generalPage.ts:48 msgid "Metric" -msgstr "" +msgstr "Metrik" #: src/preferences/generalPage.ts:48 msgid "UK" -msgstr "" +msgstr "İngiltere" #: src/preferences/generalPage.ts:48 msgid "US" -msgstr "" +msgstr "ABD" #: src/preferences/generalPage.ts:61 msgid "Fahrenheit" -msgstr "" +msgstr "Fahrenheit" #: src/preferences/generalPage.ts:62 msgid "Celsius" -msgstr "" +msgstr "Celsius" #: src/preferences/generalPage.ts:64 msgid "Temperature" -msgstr "" +msgstr "Sıcaklık" #: src/preferences/generalPage.ts:78 msgid "Speed" -msgstr "" +msgstr "Hız" #: src/preferences/generalPage.ts:92 msgid "Pressure" -msgstr "" +msgstr "Basınç" #: src/preferences/generalPage.ts:106 msgid "Rain Measurement" -msgstr "" +msgstr "Yağmur Ölçümü" #: src/preferences/generalPage.ts:120 msgid "Distance" -msgstr "" +msgstr "Mesafe" #: src/preferences/generalPage.ts:144 msgid "Degrees" -msgstr "" +msgstr "Derece" #: src/preferences/generalPage.ts:144 msgid "Eight-Point Compass" -msgstr "" +msgstr "Sekiz Yönlü Pusula" #: src/preferences/generalPage.ts:147 msgid "Direction" -msgstr "" +msgstr "Yön" #: src/preferences/generalPage.ts:159 msgid "Weather Service" -msgstr "" +msgstr "Hava Durumu Servisi" #: src/preferences/generalPage.ts:160 msgid "Configure how the weather is attained" -msgstr "" +msgstr "Hava durumunun nasıl alındığını yapılandırın" #: src/preferences/generalPage.ts:167 msgid "Weather Provider" -msgstr "" +msgstr "Hava Durumu Sağlayıcısı" #: src/preferences/generalPage.ts:180 msgid "Configure how your location is found" -msgstr "" +msgstr "Konumunuzun nasıl bulunduğunu yapılandırın" #: src/preferences/generalPage.ts:184 msgid "Online" -msgstr "" +msgstr "Çevrimiçi" #: src/preferences/generalPage.ts:185 msgid "System" -msgstr "" +msgstr "Sistem" #: src/preferences/generalPage.ts:186 msgid "Disable" -msgstr "" +msgstr "Devre Dışı Bırak" #: src/preferences/generalPage.ts:188 msgid "Provider" -msgstr "" +msgstr "Sağlayıcı" #: src/preferences/generalPage.ts:199 msgid "Refresh Interval (Minutes)" -msgstr "" +msgstr "Yenileme Aralığı (Dakika)" #: src/preferences/generalPage.ts:217 msgid "Accessibility" -msgstr "" +msgstr "Erişilebilirlik" #: src/preferences/generalPage.ts:218 msgid "Configure accessibility features" -msgstr "" +msgstr "Erişilebilirlik özelliklerini yapılandırın" #: src/preferences/generalPage.ts:221 msgid "High Contrast" -msgstr "" +msgstr "Yüksek Kontrast" #: src/preferences/generalPage.ts:232 msgid "Panel" -msgstr "" +msgstr "Panel" #: src/preferences/generalPage.ts:233 msgid "Configure the panel and pop-up" -msgstr "" +msgstr "Paneli ve açılır pencereyi yapılandırın" #: src/preferences/generalPage.ts:236 msgid "Show Sunrise/Sunset" -msgstr "" +msgstr "Gün Doğumu/Batımı Göster" #: src/preferences/locationsPage.ts:57 src/preferences/locationsPage.ts:80 msgid "Locations" -msgstr "" +msgstr "Konumlar" #: src/preferences/locationsPage.ts:70 msgid "Add" -msgstr "" +msgstr "Ekle" #: src/preferences/locationsPage.ts:97 msgid "Move Up" -msgstr "" +msgstr "Yukarı Taşı" #: src/preferences/locationsPage.ts:112 msgid "Move Down" -msgstr "" +msgstr "Aşağı Taşı" #: src/preferences/locationsPage.ts:128 msgid "Add Here" -msgstr "" +msgstr "Buraya Ekle" #: src/preferences/locationsPage.ts:193 #, javascript-format msgid "Are you sure you want delete %s?" -msgstr "" +msgstr "%s'yi silmek istediğinize emin misiniz?" #: src/preferences/locationsPage.ts:194 msgid "Cancel" -msgstr "" +msgstr "İptal" #: src/preferences/locationsPage.ts:194 msgid "Delete" -msgstr "" +msgstr "Sil" #: src/preferences/locationsPage.ts:235 #, javascript-format msgid "Internal Error: %s" -msgstr "" +msgstr "Dahili Hata: %s" #: src/preferences/locationsPage.ts:236 msgid "Internal Error" -msgstr "" +msgstr "Dahili Hata" #: src/preferences/locationsPage.ts:261 msgid "Something else edited the locations." -msgstr "" +msgstr "Başka bir şey konumları düzenledi." #: src/preferences/search.ts:45 msgid "Search Location" -msgstr "" +msgstr "Konum Ara" #: src/preferences/search.ts:54 msgid "City, Neighborhood, etc." -msgstr "" +msgstr "Şehir, Mahalle, vb." #: src/preferences/search.ts:59 msgid "Search" -msgstr "" +msgstr "Ara" #: src/preferences/search.ts:143 msgid "No Internet" -msgstr "" +msgstr "İnternet Yok" #: src/preferences/search.ts:182 msgid "No results." -msgstr "" +msgstr "Sonuç bulunamadı." #: src/preferences/search.ts:187 msgid "No copyright information available." -msgstr "" +msgstr "Telif hakkı bilgisi mevcut değil." #: src/prefs.ts:60 #, javascript-format msgid "SimpleWeather doesn't know how to handle your locale.\n" "\tError - %s\n" "Please consider submitting a bug report on GitHub." -msgstr "" +msgstr "SimpleWeather yerel ayarınızı işleyemiyor.\n" +"\tHata - %s\n" +"Lütfen GitHub'da bir hata raporu göndermeyi düşünün." #: src/prefs.ts:63 msgid "Don't Show Again" -msgstr "" +msgstr "Tekrar Gösterme" #: src/prefs.ts:63 msgid "Ignore" -msgstr "" +msgstr "Yoksay" #: src/prefs.ts:63 msgid "Open GitHub" -msgstr "" +msgstr "GitHub'ı Aç" #: src/welcome.ts:53 #, javascript-format msgid "Welcome to %s" -msgstr "" +msgstr "%s'e Hoş Geldiniz" #: src/welcome.ts:74 #, javascript-format msgid "%s occasionally connects to the selected weather service. By default, it will use the Internet to connect to:\n" -" • %s, an %s service for weather\n" -" • %s, optional for resolving the current location\n" -" • %s, for searching locations by name\n\n" -msgstr "" +"  •  %s, an %s service for weather\n" +"  •  %s, optional for resolving the current location\n" +"  •  %s, for searching locations by name\n\n" +msgstr "%s zaman zaman seçilen hava durumu servisine bağlanır. Varsayılan olarak, interneti kullanarak şunlara bağlanır:\n" +"  •  %s, hava durumu için bir %s servisi\n" +"  •  %s, mevcut konumu çözmek için (isteğe bağlı)\n" +"  •  %s, konumu ada göre aramak için\n\n" #: src/welcome.ts:83 #, javascript-format msgid "Thank you for installing %s!" -msgstr "" +msgstr "%s'i yüklediğiniz için teşekkür ederiz!" #: src/welcome.ts:99 msgid "Abort" -msgstr "" - +msgstr "İptal Et" diff --git a/schemas/org.gnome.shell.extensions.simple-weather.gschema.xml b/schemas/org.gnome.shell.extensions.simple-weather.gschema.xml index b743505..f2b2602 100644 --- a/schemas/org.gnome.shell.extensions.simple-weather.gschema.xml +++ b/schemas/org.gnome.shell.extensions.simple-weather.gschema.xml @@ -48,6 +48,11 @@ + + + + + @@ -134,5 +139,40 @@ Show sunrise/sunset in panel + + [ 'temp', 'windSpeedAndDir', 'gusts', 'pressure', 'feelsLike', 'humidity', 'uvIndex', 'precipitation' ] + Current weather details in pop-up + + + + 'right' + Which box to put the button into in the panel + + + + 1 + Priority of button in panel box + + + + 'temp' + Detail to show in the panel + + + + '' + Second detail to show in the panel + + + + true + Show condition icon in the panel + + + + '' + The theme or a blank string + + diff --git a/src/config.ts b/src/config.ts index 853fcd1..d80af67 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,7 @@ import { DirectionUnits, DistanceUnits, PressureUnits, RainMeasurementUnits, Spe import { Location } from "./location.js"; import { MyLocationProvider } from "./myLocation.js"; import { WeatherProviderNames } from "./providers/provider.js"; +import { Details } from "./details.js"; export enum UnitPreset { Custom = 0, @@ -29,9 +30,15 @@ export enum UnitPreset { Metric = 3 } +export type PanelBox = "right" | "center" | "left"; +export interface PanelPosition { + box: PanelBox; + priority: number; +} + export class Config { - #settings? : Gio.Settings; + #settings : Gio.Settings; #handlerIds : number[]; constructor(settings : Gio.Settings) { @@ -44,6 +51,7 @@ export class Config { const id = this.#handlerIds.pop()!; this.#settings?.disconnect(id); } + // @ts-ignore this.#settings = undefined; } @@ -55,14 +63,14 @@ export class Config { } onTempUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "temp-unit" || key === "unit-preset") callback(); }); this.#handlerIds.push(id); } getLocations() : Location[] { - const gVariant = this.#settings!.get_value("locations"); + const gVariant = this.#settings.get_value("locations"); const stringArr = readGTypeAS(gVariant); const locArr = stringArr.map(k => Location.parse(k)); @@ -75,14 +83,14 @@ export class Config { } onLocationsChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "locations") callback(); }); this.#handlerIds.push(id); } getMainLocation() : Location { - const inx = this.#settings!.get_int64("main-location-index"); + const inx = this.#settings.get_int64("main-location-index"); const arr = this.getLocations(); return arr[inx] ?? arr[0]; } @@ -91,7 +99,7 @@ export class Config { // Using change-event instead of changed makes sure // that the callback isn't double-fired since either // key causes a change - const id = this.#settings!.connect("change-event", (_, quarks) => { + const id = this.#settings.connect("change-event", (_, quarks) => { // Returning false continues to call changed events if(!quarks) return false; @@ -109,47 +117,47 @@ export class Config { } getMainLocationIndex() : number { - return this.#settings!.get_int64("main-location-index"); + return this.#settings.get_int64("main-location-index"); } onMainLocationIndexChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "main-location-index") callback(); }); this.#handlerIds.push(id); } getMyLocationProvider() : MyLocationProvider { - const val = this.#settings!.get_enum("my-loc-provider"); + const val = this.#settings.get_enum("my-loc-provider"); if(val > 2 || val < 1) return 1; else return val; } onMyLocationProviderChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "my-loc-provider") callback(); }); this.#handlerIds.push(id); } getMyLocationRefreshMin() : number { - const val = this.#settings!.get_double("my-loc-refresh-min"); + const val = this.#settings.get_double("my-loc-refresh-min"); if(val < 10.0) return 10.0; else return val; } getDontCheckLocales() : boolean { - return this.#settings!.get_boolean("dont-check-locales"); + return this.#settings.get_boolean("dont-check-locales"); } getWeatherProvider() : number { - const val = this.#settings!.get_enum("weather-provider"); + const val = this.#settings.get_enum("weather-provider"); if(val < 1 || val > WeatherProviderNames.length) return 1; else return val; } onWeatherProviderChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "weather-provider") callback(); }); this.#handlerIds.push(id); @@ -163,18 +171,18 @@ export class Config { } onSpeedUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "speed-unit" || key === "unit-preset") callback(); }); this.#handlerIds.push(id); } getDirectionUnit(): DirectionUnits { - return this.#settings!.get_enum("direction-unit"); + return this.#settings.get_enum("direction-unit"); } onDirectionUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "direction-unit" || key === "unit-preset") callback(); }); this.#handlerIds.push(id); @@ -188,7 +196,7 @@ export class Config { } onPressureUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "pressure-unit" || key === "unit-preset") callback(); }); this.#handlerIds.push(id); @@ -202,7 +210,7 @@ export class Config { } onRainMeasurementUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "rain-measurement-unit" || key === "unit-preset") callback(); }); this.#handlerIds.push(id); @@ -216,44 +224,138 @@ export class Config { } onDistanceUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "distance-unit" || key === "unit-preset") callback(); }); this.#handlerIds.push(id); } getHighContrast() : boolean { - return this.#settings!.get_boolean("high-contrast"); + return this.#settings.get_boolean("high-contrast"); } onHighContrastChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "high-contrast") callback(); }); this.#handlerIds.push(id); } getShowSunTime() : boolean { - return this.#settings!.get_boolean("show-suntime"); + return this.#settings.get_boolean("show-suntime"); } onShowSunTimeChanged(callback : (val : boolean) => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { if(key === "show-suntime") { - callback(this.#settings!.get_boolean("show-suntime")); + callback(this.#settings.get_boolean("show-suntime")); + } + }); + this.#handlerIds.push(id); + } + + getSecondaryPanelDetail() : Details | null { + const detail = this.#settings.get_string("secondary-panel-detail"); + if(!Object.values(Details).includes(detail as Details)) return null; + else return detail as Details; + } + + onSecondaryPanelDetailChanged(callback : () => void) : void { + const id = this.#settings.connect("changed", (_, key) => { + if(key === "secondary-panel-detail") callback(); + }); + this.#handlerIds.push(id); + } + + getShowPanelIcon() : boolean { + return this.#settings.get_boolean("show-panel-icon"); + } + + onShowPanelIconChanged(callback : () => void) : void { + const id = this.#settings.connect("changed", (_, key) => { + if(key === "show-panel-icon") callback(); + }); + this.#handlerIds.push(id); + } + + /** + * Gets the details list. + * Items are not sanitized and may not be in Details. + * If value is severely malformed a string full of + * "invalid" will be returned. + * @returns Guaranteed to be an 8 item string array. + */ + getDetailsList() : string[] { + const gval = this.#settings.get_value("details-list"); + const strarr = readGTypeAS(gval); + if(strarr.length !== 8) { + const defVal = this.#settings.get_default_value("details-list"); + if(!defVal) return new Array(8).fill("invalid"); + const defStrarr = readGTypeAS(defVal); + if(defStrarr.length !== 8) return new Array(8).fill("invalid"); + return defStrarr; + } + else return strarr; + } + + onDetailsListChanged(callback : () => void) : void { + const id = this.#settings.connect("changed", (_, key) => { + if(key === "details-list") { + callback(); } }); this.#handlerIds.push(id); } + getPanelPosition() : PanelPosition { + const boxNum = this.#settings.get_enum("panel-box"); + const box = (["right", "center", "left"])[boxNum] ?? "right"; + const priority = this.#settings.get_int64("panel-priority"); + return { + box: box as PanelBox, + priority + }; + } + + onPanelPositionChanged(callback : () => void) : void { + const id = this.#settings.connect("changed", (_, key) => { + if(key === "panel-box" || key === "panel-priority") callback(); + }); + this.#handlerIds.push(id); + } + + getPanelDetail() : Details | null { + const detail = this.#settings.get_string("panel-detail"); + if(!Object.values(Details).includes(detail as Details)) return null; + else return detail as Details; + } + + onPanelDetailChanged(callback : () => void) : void { + const id = this.#settings.connect("changed", (_, key) => { + if(key === "panel-detail") callback(); + }); + this.#handlerIds.push(id); + } + + getTheme() : string { + return this.#settings.get_string("theme"); + } + + onThemeChanged(callback : () => void) : void { + const id = this.#settings.connect("changed", (_, key) => { + if(key === "theme") callback(); + }); + this.#handlerIds.push(id); + } + getUnitPreset() : UnitPreset { - return this.#settings!.get_enum("unit-preset"); + return this.#settings.get_enum("unit-preset"); } onAnyUnitChanged(callback : () => void) { - const id = this.#settings!.connect("changed", (_, key) => { + const id = this.#settings.connect("changed", (_, key) => { const unitKeys = [ "unit-preset", "temp-unit", "speed-unit", "pressure-unit", "rain-measurement-unit", "distance-unit", "direction-unit" @@ -289,7 +391,7 @@ export class Config { if(args.metric !== undefined) return args.metric; else break; } - return this.#settings!.get_enum(getEnumKey); + return this.#settings.get_enum(getEnumKey); } } diff --git a/src/details.ts b/src/details.ts new file mode 100644 index 0000000..d5d8470 --- /dev/null +++ b/src/details.ts @@ -0,0 +1,89 @@ +/* + Copyright 2025 Roman Lefler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import { Config } from "./config.js"; +import { displayTime } from "./lang.js"; +import { Displayable } from "./units.js"; +import { Weather } from "./weather.js"; + +export enum Details { + TEMP = "temp", + CONDITION_TEXT = "conditionText", + FEELS_LIKE = "feelsLike", + WIND_SPEED_AND_DIR = "windSpeedAndDir", + HUMIDITY = "humidity", + GUSTS = "gusts", + UV_INDEX = "uvIndex", + PRESSURE = "pressure", + PRECIPITATION = "precipitation", + SUNRISE = "sunrise", + SUNSET = "sunset", + CLOUD_COVER = "cloudCover" +} + +/** + * This interface includes a property named for every *value* + * in the Details enum. + */ +export type IDetails = { + [K in `${Details}`] : unknown; +}; + +// Fake gettext to trick xgettext into +// translating these strings +function _g(s : string) : string { + return s; +} + +/** + * Gets a string that should be passed into gettext. + * THE CALLER MUST TRANSLATE THE VALUE. + * THESE ARE NOT PASSED INTO GETTEXT. + */ +export const detailName : IDetails = { + temp: _g("Temperature"), + conditionText: _g("Condition"), + feelsLike: _g("Feels Like"), + windSpeedAndDir: _g("Wind"), + humidity: _g("Humidity"), + gusts: _g("Gusts"), + uvIndex: _g("UV High"), + pressure: _g("Pressure"), + precipitation: _g("Precipitation"), + sunrise: _g("Sunrise"), + sunset: _g("Sunset"), + cloudCover: _g("Cloud Cover"), +}; + +export function displayDetail(w : Weather, detail : Details, gettext : (s : string) => string, + cfg : Config, onlyValue = false) { + if(detail as string === "invalid") return _g("Invalid"); + + const value = w[detail]; + let fmt: string; + if (typeof (value as any).display === "function") { + fmt = (value as Displayable).display(cfg); + } else if(value instanceof Date) { + fmt = displayTime(value, cfg); + } else if (typeof value === "number") { + fmt = `${Math.round(value)}`; + } else throw new Error("Detail must implement Displayable or be a Date or number."); + + if(onlyValue) return fmt; + const name = detailName[detail] as string; + return `${_g(name)}: ${fmt}`; +} diff --git a/src/extension.ts b/src/extension.ts index f240b58..c88067c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ */ import Clutter from "gi://Clutter"; +import Cogl from "gi://Cogl"; import Gio from "gi://Gio"; import GLib from "gi://GLib"; import St from 'gi://St'; @@ -30,18 +31,21 @@ import { Weather } from "./weather.js"; import { delayTask, removeSourceIfTruthy } from "./utils.js"; import { displayTemp, displayTime, initLocales } from "./lang.js"; import { freeMyLocation, setUpMyLocation } from "./myLocation.js"; -import { setUpGettext } from "./gettext.js"; +import { setUpGettext, gettext as _g } from "./gettext.js"; import { gettext as shellGettext } from "resource:///org/gnome/shell/extensions/extension.js"; import { Popup } from "./popup.js"; import { PopupMenu } from "resource:///org/gnome/shell/ui/popupMenu.js"; import { showWelcome } from "./welcome.js"; import { setFirstTimeConfig } from "./autoConfig.js"; +import { displayDetail } from "./details.js"; +import { theme, themeInitAll, themeRemoveAll } from "./theme.js"; export default class SimpleWeatherExtension extends Extension { #gsettings? : Gio.Settings; #indicator? : PanelMenu.Button; #panelLabel? : St.Label; + #secondPanelLabel? : St.Label; #panelIcon? : St.Icon; #popup? : Popup; #hasAddedIndicator : boolean = false; @@ -129,31 +133,39 @@ export default class SimpleWeatherExtension extends Extension { setUpMyLocation(this.#libsoup, this.#config); } - #enablePastWelcome() { - // This is normal extension enabling now - - // Add the menu into the top bar - this.#indicator = new PanelMenu.Button(0, "Weather", false); + #createIndicator() : PanelMenu.Button { + const indic = new PanelMenu.Button(0, "Weather", false); this.#popup = new Popup( this.#config!, this.metadata, this.openPreferences.bind(this), - this.#indicator.menu as PopupMenu, + indic.menu as PopupMenu, this.#gsettings! ); const layout = new St.BoxLayout({ - orientation: Clutter.Orientation.HORIZONTAL + vertical: false }); - this.#panelLabel = new St.Label({ + + const hasDetail1 = this.#config!.getPanelDetail() != null; + const hasDetail2 = this.#config!.getSecondaryPanelDetail() !== null; + const showIcon = this.#config!.getShowPanelIcon(); + + this.#panelLabel = hasDetail1 ? new St.Label({ text: "...", y_align: Clutter.ActorAlign.CENTER, y_expand: true - }); - this.#panelIcon = new St.Icon({ + }) : undefined; + this.#secondPanelLabel = hasDetail2 ? new St.Label({ + text: "...", + y_align: Clutter.ActorAlign.CENTER, + y_expand: true, + style_class: "simpleweather-second-panel-label" + }) : undefined; + this.#panelIcon = showIcon ? new St.Icon({ icon_name: "view-refresh-symbolic", style_class: "system-status-icon" - }); + }) : undefined; this.#sunTimeLabel = new St.Label({ text: "...", y_align: Clutter.ActorAlign.CENTER, @@ -164,13 +176,35 @@ export default class SimpleWeatherExtension extends Extension { icon_name: "daytime-sunset-symbolic", style_class: "system-status-icon" }); - layout.add_child(this.#panelLabel); - layout.add_child(this.#panelIcon); + + if(this.#panelLabel) layout.add_child(this.#panelLabel); + if(this.#secondPanelLabel) layout.add_child(this.#secondPanelLabel); + if(this.#panelIcon) layout.add_child(this.#panelIcon); if(this.#config!.getShowSunTime()) { layout.add_child(this.#sunTimeLabel); layout.add_child(this.#sunTimeIcon); } - this.#indicator.add_child(layout); + indic.add_child(layout); + + const actor = (indic.menu as PopupMenu).box; + theme(actor, "menu"); + // @ts-ignore + indic.menu.connect("open-state-changed", (_, isOpen : boolean) => { + if(isOpen) actor.add_style_class_name("swa-open"); + else actor.remove_style_class_name("swa-open"); + }); + + const themeName = this.#config!.getTheme(); + if(themeName) themeInitAll(indic.menu.actor, themeName); + return indic; + } + + #enablePastWelcome() { + // This is normal extension enabling now + + // Add the menu into the top bar + this.#indicator = this.#createIndicator(); + // #indicator is added to panel in #updateGui // Set up a timer to refresh the weather on repeat this.#fetchLoopId = GLib.timeout_add_seconds( @@ -188,8 +222,11 @@ export default class SimpleWeatherExtension extends Extension { }); // Some settings just require a GUI update this.#config!.onAnyUnitChanged(this.#updateGui.bind(this)); - this.#config!.onHighContrastChanged(this.#updateGui.bind(this)); + this.#config!.onDetailsListChanged(this.#updateGui.bind(this)); + // Some require extra stuff this.#config!.onShowSunTimeChanged(b => { + if(!this.#indicator) return; + const layout = this.#indicator!.get_first_child()!; if (b) { layout.add_child(this.#sunTimeLabel!); layout.add_child(this.#sunTimeIcon!); @@ -199,11 +236,24 @@ export default class SimpleWeatherExtension extends Extension { layout.remove_child(this.#sunTimeIcon!); } }); + this.#config!.onPanelDetailChanged(this.#rebuildIndicator.bind(this)); + this.#config!.onSecondaryPanelDetailChanged(this.#rebuildIndicator.bind(this)); + this.#config!.onShowPanelIconChanged(this.#rebuildIndicator.bind(this)); + this.#config!.onPanelPositionChanged(this.#rebuildIndicator.bind(this)); + this.#config!.onThemeChanged(this.#rebuildIndicator.bind(this)); + this.#config!.onHighContrastChanged(this.#rebuildIndicator.bind(this)); // First weather fetch this.#updateWeather(); } + #rebuildIndicator() { + this.#indicator?.destroy(); + this.#indicator = this.#createIndicator(); + this.#hasAddedIndicator = false; + this.#updateGui(); + } + /** * Called by GNOME Extensions when this extension is disabled. * Everything must be manually freed since this class may not be @@ -223,6 +273,7 @@ export default class SimpleWeatherExtension extends Extension { this.#hasAddedIndicator = false; this.#panelIcon = undefined; this.#panelLabel = undefined; + this.#secondPanelLabel = undefined; this.#indicator?.destroy(); this.#indicator = undefined; @@ -238,13 +289,7 @@ export default class SimpleWeatherExtension extends Extension { } #updateWeather() { - this.#updateWeatherAsync().then(() => { - if(!this.#hasAddedIndicator) { - this.#hasAddedIndicator = true; - Main.panel.addToStatusArea(this.uuid, this.#indicator!); - } - - }).catch(err => { + this.#updateWeatherAsync().catch(err => { console.error(err); // This happens on boot presumably when things are loaded // out of order, try max 10 times @@ -275,17 +320,33 @@ export default class SimpleWeatherExtension extends Extension { const w = this.#cachedWeather; if(!w) return; - this.#panelLabel!.text = displayTemp(w.temp, this.#config!); + const panelDetail = this.#config!.getPanelDetail(); + if(panelDetail !== null && this.#panelLabel) { + const panelText = displayDetail(w, panelDetail, _g, this.#config!, true); + this.#panelLabel.text = panelText; + } + + const secondPanelDetail = this.#config!.getSecondaryPanelDetail(); + if(secondPanelDetail !== null && this.#secondPanelLabel) { + const secondPanelText = displayDetail(w, secondPanelDetail, _g, this.#config!, true); + this.#secondPanelLabel.text = secondPanelText; + } - this.#panelIcon!.icon_name = w.gIconName; + if(this.#panelIcon) this.#panelIcon.icon_name = w.gIconName; const showSunset = w.sunset < w.sunrise; const sunTime = showSunset ? w.sunset : w.sunrise; - this.#sunTimeLabel!.text = displayTime(sunTime, this.#config!); - this.#sunTimeIcon!.icon_name = `daytime-${showSunset ? "sunset" : "sunrise"}-symbolic`; + if(this.#sunTimeLabel) this.#sunTimeLabel.text = displayTime(sunTime, this.#config!); + if(this.#sunTimeIcon) this.#sunTimeIcon.icon_name = `daytime-${showSunset ? "sunset" : "sunrise"}-symbolic`; this.#popup!.updateGui(w); + + if (!this.#hasAddedIndicator) { + this.#hasAddedIndicator = true; + const pos = this.#config!.getPanelPosition(); + Main.panel.addToStatusArea(this.uuid, this.#indicator!, pos.priority, pos.box); + } } } diff --git a/src/lang.ts b/src/lang.ts index 0812adc..ee34564 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -19,6 +19,8 @@ import GLib from "gi://GLib"; import { Weather } from "./weather.js"; import { Config } from "./config.js"; import { Direction, DirectionUnits, Pressure, RainMeasurement, RainMeasurementUnits, Speed, Temp } from "./units.js"; +import { sameDate } from "./utils.js"; +import { gettext as _g } from "./gettext.js" let locales : string[] | undefined; @@ -102,11 +104,15 @@ export function displayTime(d : Date, cfg : Config, showAmPm : boolean = true) : return str; } -export function displayDayOfWeek(d : Date) : string { - const locales = getLocales(); - return d.toLocaleDateString(locales, { - weekday: "long" - }); +export function displayDayOfWeek(d : Date, useToday = false) : string { + if(useToday && sameDate(new Date(), d)) return _g("Today"); + + const map = [ + _g("Sunday"), _g("Monday"), _g("Tuesday"), _g("Wednesday"), + _g("Thursday"), _g("Friday"), _g("Saturday") + ]; + const idx = d.getDay(); + return map[idx]; }; export function displaySpeed(s : Speed, cfg : Config) { diff --git a/src/popup.ts b/src/popup.ts index f0a4514..a249f72 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -23,9 +23,11 @@ import { ExtensionMetadata, gettext as _ } from "resource:///org/gnome/shell/ext import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PopupMenu from "resource:///org/gnome/shell/ui/popupMenu.js"; import { Config } from "./config.js"; -import { Forecast, Weather } from "./weather.js"; -import { displayDayOfWeek, displayDirection, displayPressure, displayRainMeasurement, displaySpeed, displayTemp, displayTime } from "./lang.js"; +import { Condition, Forecast, Weather } from "./weather.js"; +import { displayDayOfWeek, displayTime } from "./lang.js"; import { gettext as _g } from "./gettext.js"; +import { Details, displayDetail } from "./details.js"; +import { theme, themeInitAll } from "./theme.js"; interface ForecastCard { card : St.BoxLayout; @@ -52,7 +54,7 @@ function createForecastCard() : ForecastCard { }); const day = new St.Label({ - text: displayDayOfWeek(new Date()), + text: _g("Today"), x_align: Clutter.ActorAlign.CENTER }); @@ -92,17 +94,6 @@ function createForecastCard() : ForecastCard { }; } -interface CurInfo { - temp : St.Label; - feelsLike : St.Label; - wind : St.Label, - gusts: St.Label, - humidity: St.Label; - pressure: St.Label; - uvIndex : St.Label; - precipitation : St.Label; -} - function addChildren(parent : Clutter.Actor, ...children : Clutter.Actor[]) { children.forEach(m => parent.add_child(m)); } @@ -112,7 +103,7 @@ function getTextColor() : `rgba(${number}, ${number}, ${number}, ${number})` { return `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha / 255})`; } -function evenLabel(opts : Partial = {}) { +function evenLabel(cfg : Config, opts : Partial = {}) { const label = new St.Label({ x_expand: true, y_align: Clutter.ActorAlign.CENTER, @@ -120,6 +111,11 @@ function evenLabel(opts : Partial = {}) { style_class: "simpleweather-current-item", ...opts }); + + if(cfg.getHighContrast()) { + if(cfg.getTheme() === "") label.style = `color:${getTextColor()}`; + } else theme(label, "faded"); + const box = new St.BoxLayout({ x_expand: true, x_align: Clutter.ActorAlign.FILL, @@ -128,49 +124,35 @@ function evenLabel(opts : Partial = {}) { return { label, box }; } -function createCurInfo(parent : Clutter.Actor) : CurInfo { +function createCurInfo(cfg : Config, parent : Clutter.Actor) : St.Label[] { const cols = new St.BoxLayout({ vertical: true, x_expand: true }); const row1 = new St.BoxLayout({ vertical: false, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL }); const row2 = new St.BoxLayout({ vertical: false, x_expand: true, y_expand: true, x_align: Clutter.ActorAlign.FILL }); addChildren(cols, row1, row2); - const temp = evenLabel(); - const feelsLike = evenLabel(); - const wind = evenLabel(); - const gusts = evenLabel(); - const humidity = evenLabel(); - const pressure = evenLabel(); - const uvIndex = evenLabel(); - const precipitation = evenLabel(); - const c : CurInfo = { - temp: temp.label, - feelsLike: feelsLike.label, - wind: wind.label, - gusts: gusts.label, - humidity: humidity.label, - pressure: pressure.label, - uvIndex: uvIndex.label, - precipitation: precipitation.label - }; - addChildren(row1, temp.box, wind.box, gusts.box, pressure.box); - addChildren(row2, feelsLike.box, humidity.box, uvIndex.box, precipitation.box); + const list = Array.from({ length: 8 }, evenLabel.bind(null, cfg)); + const boxes = list.map(l => l.box); + addChildren(row1, ...boxes.slice(0, 4)); + addChildren(row2, ...boxes.slice(4, 8)); parent.add_child(cols); - return c; + return list.map(l => l.label); } function copyrightText(provName : string) : string { - return `${_g("Weather Data")} \u00A9 ${provName} 2025`; + return `${_g("Weather Data")} \u00A9 ${provName} ${new Date().getFullYear()}`; } // Widget must have reactive and track_hover true function setPointer(widget : Clutter.Actor) : void { - widget.connect("enter-event", () => { - global.display.set_cursor(Meta.Cursor.POINTER); - }); - widget.connect("leave-event", () => { - global.display.set_cursor(Meta.Cursor.DEFAULT); - }); + if(Meta.Cursor.POINTER && Meta.Cursor.DEFAULT) { + widget.connect("enter-event", () => { + global.display.set_cursor(Meta.Cursor.POINTER); + }); + widget.connect("leave-event", () => { + global.display.set_cursor(Meta.Cursor.DEFAULT); + }); + } } export class Popup { @@ -182,15 +164,15 @@ export class Popup { readonly #temp : St.Label; readonly #forecastCards : ForecastCard[]; readonly #copyright : St.Label; - readonly #curInfo : CurInfo; + readonly #currentLabels : St.Label[]; readonly #placeLabel : St.Label; readonly #placeBtn : St.Button; readonly #menuItems : PopupMenu.PopupBaseMenuItem[]; + readonly #menuBox : St.BoxLayout; #foreMode : ForecastMode; #cachedWeather? : Weather; - #wasHiContrast : boolean = false; constructor( config : Config, @@ -219,8 +201,10 @@ export class Popup { const leftVBox = new St.BoxLayout({ vertical: true, - style_class: "modal-dialog simpleweather-current" + style_class: "simpleweather-current" }); + if(!this.#config.getTheme()) leftVBox.add_style_class_name("modal-dialog"); + theme(leftVBox, "left-box"); leftVBox.add_child(this.#condition); leftVBox.add_child(this.#temp); @@ -237,6 +221,7 @@ export class Popup { reactive: true, style_class: "button simpleweather-card-row" }); + theme(forecasts, "forecast-box button"); this.#forecastCards = [ ]; for(let i = 0; i < 7; i++) { const c = createForecastCard(); @@ -244,7 +229,8 @@ export class Popup { this.#forecastCards.push(c); } rightVBox.add_child(forecasts); - this.#curInfo = createCurInfo(rightVBox); + this.#currentLabels = createCurInfo(this.#config, rightVBox); + if(this.#currentLabels.length !== 8) throw new Error("Incorrect cur len."); hbox.add_child(rightVBox); forecasts.connect("button-press-event", () => { @@ -256,6 +242,7 @@ export class Popup { }); const childItem = new PopupMenu.PopupBaseMenuItem({ reactive: false }); + theme(childItem, "bg"); childItem.actor.add_child(hbox); const textRect = new St.BoxLayout({ @@ -270,6 +257,7 @@ export class Popup { textRect.add_child(this.#copyright); const baseText = new PopupMenu.PopupBaseMenuItem({ reactive: false }); + theme(baseText, "bg"); baseText.actor.add_child(textRect); this.#placeLabel = new St.Label(); @@ -282,13 +270,15 @@ export class Popup { opacity: 255, x_expand: true }); + theme(this.#placeBtn, "button"); this.#placeBtn.connect("clicked", () => { + const placeCount = config.getLocations().length; + if(placeCount === 1) return; // These will be restored in the #updateGUI method this.#placeBtn.reactive = false; this.#placeBtn.opacity = 127; const index = config.getMainLocationIndex(); - const placeCount = config.getLocations().length; let newIndex; if(index === placeCount - 1) newIndex = 0; else newIndex = index + 1; @@ -310,6 +300,7 @@ export class Popup { y_align: Clutter.ActorAlign.CENTER, style_class: "message-list-clear-button button", }); + theme(configBtn, "button"); configBtn.connect("clicked", () => { menu.toggle(); openPreferences(); @@ -321,6 +312,7 @@ export class Popup { setPointer(configBtn); this.#menuItems = [ childItem, baseText ]; + this.#menuBox = menu.box; menu.addMenuItem(childItem); menu.addMenuItem(baseText); } @@ -336,10 +328,23 @@ export class Popup { } updateGui(w : Weather) { + const old = this.#cachedWeather; + this.#condition.gicon = this.#createIcon(w.gIconName); - this.#temp.text = displayTemp(w.temp, this.#config); + this.#temp.text = w.temp.display(this.#config); this.#copyright.text = copyrightText(w.providerName); + if(old) this.#menuBox.remove_style_class_name(`swa-${old.condit}`); + this.#menuBox.add_style_class_name(`swa-${w.condit}`); + + if(old) { + if(old.isNight !== w.isNight) { + const names = w.isNight ? [ "day", "night" ] : [ "night", "day" ]; + this.#menuBox.remove_style_class_name(`swa-${names[0]}`); + this.#menuBox.add_style_class_name(`swa-${names[1]}`); + } + } else this.#menuBox.add_style_class_name(`swa-${w.isNight ? "night" : "day"}`); + this.#updateForecast(w); } @@ -359,7 +364,7 @@ export class Popup { const c = this.#forecastCards[i]; let dateText : string; - if(this.#foreMode === ForecastMode.Week) dateText = displayDayOfWeek(fore[i].date); + if(this.#foreMode === ForecastMode.Week) dateText = displayDayOfWeek(fore[i].date, true); else dateText = displayTime(fore[i].date, this.#config, true); c.day.text = dateText; @@ -372,11 +377,11 @@ export class Popup { const tempMin = fore[i].tempMin; const tempMax = fore[i].tempMax; if(temp !== undefined) { - text.push(displayTemp(temp, this.#config)); + text.push(temp.display(this.#config)); } else if(tempMax !== undefined && tempMin !== undefined) { - text.push(_g("H: %s").format(displayTemp(tempMax, this.#config))); - text.push(_g("L: %s").format(displayTemp(tempMin, this.#config))); + text.push(_g("H: %s").format(tempMax.display(this.#config))); + text.push(_g("L: %s").format(tempMin.display(this.#config))); } const rainChance = fore[i].precipChancePercent; @@ -396,40 +401,16 @@ export class Popup { this.#placeLabel.text = w.loc.getName(); - const inf = this.#curInfo; - inf.temp.text = _g("Temp: %s").format(displayTemp(w.temp, this.#config)); - inf.feelsLike.text = _g("Feels Like: %s").format(displayTemp(w.feelsLike, this.#config)); - inf.wind.text = _g("Wind: %s, %s").format( - displayDirection(w.windDir, this.#config), - displaySpeed(w.wind, this.#config) - ); - inf.gusts.text = _g("Gusts: %s").format(displaySpeed(w.gusts, this.#config)); - inf.humidity.text = _g("Humidity: %s").format(`${Math.round(w.humidity)}%`); - inf.pressure.text = _g("Pressure: %s").format(displayPressure(w.pressure, this.#config)); - inf.uvIndex.text = _g("UV High: %s").format(String(Math.round(w.uvIndex))); - inf.precipitation.text = _g("Precipitation: %s").format( - displayRainMeasurement(w.precipitation, this.#config) - ); - - // This only performs the updates if necessary - if(this.#config.getHighContrast()) { - if(!this.#wasHiContrast) { - this.#wasHiContrast = true; - const color = getTextColor(); - const affected = [ this.#copyright, ...Object.values(inf) ]; - for(const w of affected) { - if(w instanceof St.Widget) w.style = `color:${color};`; - } - } - } - else { - if(this.#wasHiContrast) { - this.#wasHiContrast = false; - const affected = [ this.#copyright, ...Object.values(inf) ]; - for(const w of affected) { - if(w instanceof St.Widget) w.style = ""; - } + const details = this.#config.getDetailsList(); + const detailPossibilities = Object.values(details); + for(let i = 0; i < 8; i++) { + const label = this.#currentLabels[i]; + if(!detailPossibilities.includes(details[i])) { + label.text = _g("Invalid"); + continue; } + const deet = details[i] as Details; + label.text = displayDetail(w, deet, _g, this.#config); } this.#placeBtn.reactive = true; diff --git a/src/preferences/aboutPage.ts b/src/preferences/aboutPage.ts index 695f678..cfb9054 100644 --- a/src/preferences/aboutPage.ts +++ b/src/preferences/aboutPage.ts @@ -24,11 +24,13 @@ import { gettext as _g } from "resource:///org/gnome/Shell/Extensions/js/extensi // @ts-ignore import { PACKAGE_VERSION } from "resource:///org/gnome/Shell/Extensions/js/misc/config.js"; import { getLocales } from "../lang.js"; +import { AUTHORS } from "../resource.js"; function md(s : string, classes? : string[]) : Gtk.Label { const props : Partial = { label: s, use_markup: true, + css_classes: [ "simpleweather-margin" ] }; if(classes) props.css_classes = classes; return new Gtk.Label(props); @@ -54,7 +56,9 @@ export class AboutPage extends Adw.PreferencesPage { topBox.append(md("SimpleWeather for GNOME", [ "simpleweather-h1" ])); topBox.append(md("Roman Lefler", [ "simpleweather-h2" ])); topBox.append(md( - `${_g("GitHub Repository")}`, + `${_g("GitHub Repository")}` + + " | " + + `${_g("Support Me")}`, )); topGroup.add(topBox); this.add(topGroup); @@ -107,10 +111,36 @@ export class AboutPage extends Adw.PreferencesPage { const bottomBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL }); - const owrLink = "OpenWeather Refined"; + const repoLink = "GitHub"; bottomBox.append(md( - _g("This extension is a rewrite of the %s project.").format(owrLink) + _g("Contributions and translations are welcome! Read how on %s.").format(repoLink) )); + bottomBox.append(md( + _g("If you like this extension, consider starring it on %s.").format("GitHub") + )); + const issuesLink = "" + + _g("here") + ""; + bottomBox.append(md( + _g("Report bugs or request new features %s.").format(issuesLink) + )); + const credits = new Gtk.Button({ + child: new Gtk.Label({ + label: "Credits", + // This effectively is the padding on the button + css_classes: [ "simpleweather-margin-wide" ] + }), + hexpand: false, + halign: Gtk.Align.CENTER, + css_classes: [ "simpleweather-margin" ] + }); + credits.connect("clicked", () => { + const dialog = new Gtk.AlertDialog({ + message: _g("Credits"), + detail: AUTHORS() + }) + dialog.show(window); + }); + bottomBox.append(credits); bottomGroup.add(bottomBox); this.add(bottomGroup); } diff --git a/src/preferences/detailsPage.ts b/src/preferences/detailsPage.ts new file mode 100644 index 0000000..991f4be --- /dev/null +++ b/src/preferences/detailsPage.ts @@ -0,0 +1,262 @@ +/* + Copyright 2025 Roman Lefler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import GObject from "gi://GObject"; +import Gdk from "gi://Gdk"; +import Gtk from "gi://Gtk"; +import Gio from "gi://Gio"; +import Adw from "gi://Adw"; +import { gettext as _g } from "../gettext.js"; +import { detailName, Details, displayDetail } from "../details.js"; +import { Condition, Weather, gettextCondit } from "../weather.js"; +import { Direction, Percentage, Pressure, RainMeasurement, Speed, SpeedAndDir, Temp } from "../units.js"; +import { Location } from "../location.js"; +import { Config, writeGTypeAS } from "../config.js"; + +const MOCK_WEATHER : Weather = { + condit: Condition.CLEAR, + temp: new Temp(71), + gIconName: "weather-clear-symbolic", + isNight: false, + sunset: new Date(), + sunrise: new Date(), + forecast: [ ], + hourForecast: [ ], + feelsLike: new Temp(77), + wind: new Speed(8), + gusts: new Speed(14), + windDir: new Direction(0), + humidity: new Percentage(87), + pressure: new Pressure(24), + uvIndex: 7, + precipitation: new RainMeasurement(0.0), + providerName: "Open-Meteo", + loc: Location.newCoords("Dallas", 32.7792, -96.8089), + windSpeedAndDir: new SpeedAndDir(new Speed(8), new Direction(0)), + cloudCover: new Percentage(44), + conditionText: gettextCondit(Condition.CLEAR, false) +}; + +export class DetailsPage extends Adw.PreferencesPage { + + readonly #settings : Gio.Settings; + readonly #config : Config; + #clickedDeet? : Details; + #clickedWidget? : Gtk.Widget; + + static { + GObject.registerClass(this); + } + + constructor(settings : Gio.Settings) { + + super({ + title: _g("Details"), + icon_name: "view-list-symbolic" + }); + this.#settings = settings; + + const curGroup = new Adw.PreferencesGroup({ + title: _g("Pop-Up"), + description: _g("Drag-and-drop from bottom to configure the pop-up") + }); + + const stringFmt = Gdk.ContentFormats.new_for_gtype(GObject.TYPE_STRING); + this.#config = new Config(settings); + + // Selected + const curBox = new Gtk.FlowBox({ + orientation: Gtk.Orientation.HORIZONTAL, + selection_mode: Gtk.SelectionMode.NONE + }); + const initialDetails = this.#config.getDetailsList(); + for(let i = 0; i < 8; i++) { + const selection = new Gtk.Frame({ + receives_default: true, + can_focus: true + }); + let initialDeet = initialDetails[i] as Details; + if(!Object.values(Details).includes(initialDeet)) initialDeet = "invalid" as Details; + const selLabel = new Gtk.Label({ + label: displayDetail(MOCK_WEATHER, initialDeet, _g, this.#config) + }); + selection.child = selLabel; + + const dropTarget = new Gtk.DropTarget({ + formats: stringFmt, + actions: Gdk.DragAction.COPY + }); + dropTarget.connect("drop", (_s, value, _x, _y) => { + // The types here for value are wrong, it is just a JS string + if(typeof value !== "string") throw new Error("Drop received unknown type."); + if(!Object.values(Details).includes(value)) return false; + const deet = value as Details; + this.#setDetail(selLabel, i, deet); + return true; + }); + selection.add_controller(dropTarget); + selection.add_controller(new Gtk.DropControllerMotion()); + + // This is for + const gesture = new Gtk.GestureClick(); + gesture.connect("pressed", (_s, _n, _x, _y) => { + if(!this.#clickedDeet) return; + + this.#setDetail(selLabel, i, this.#clickedDeet); + this.#unsetClickedDetail(); + }); + selection.add_controller(gesture); + + curBox.append(selection); + } + + const pool = new Gtk.FlowBox({ + orientation: Gtk.Orientation.HORIZONTAL, + selection_mode: Gtk.SelectionMode.NONE + }); + const items = Object.values(Details); + for(const d of items) { + const btn = new Gtk.Button({ + label: displayDetail(MOCK_WEATHER, d, _g, this.#config), + can_focus: true, + }); + // get_data/set_data not supported in GJS + (btn as any)["simpleweather-detail"] = d; + + const dragSrc = new Gtk.DragSource({ + actions: Gdk.DragAction.COPY + }); + dragSrc.connect("prepare", (_s, _x, _y) => { + const gval = new GObject.Value(); + gval.init(GObject.TYPE_STRING); + gval.set_string(d); + return Gdk.ContentProvider.new_for_value(gval); + }); + btn.add_controller(dragSrc); + + btn.connect("clicked", () => { + this.#setClickedDetail(d, btn); + // HACK: Fixes no longer draggable after click + btn.remove_controller(dragSrc); + btn.add_controller(dragSrc); + }); + pool.append(btn); + } + + const vbox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + spacing: 16, + margin_top: 16 + }); + vbox.append(curBox); + vbox.append(pool); + + curGroup.add(vbox); + this.add(curGroup); + + + const panelGroup = new Adw.PreferencesGroup({ + title: _g("Panel") + }); + const detailsArr = Object.values(Details); + const detailsNames = [ _g("None") ]; + for(let d of detailsArr) { + detailsNames.push(_g(detailName[d] as string)); + } + detailsArr.unshift("" as Details); + + const detailsModel = new Gtk.StringList({ + strings: detailsNames + }); + + const panelDetailSel = detailsArr.indexOf(this.#settings.get_string("panel-detail") as Details); + const panelDetailRow = new Adw.ComboRow({ + title: _g("Panel Detail"), + model: detailsModel, + selected: Math.max(0, panelDetailSel) + }); + panelDetailRow.connect("notify::selected", (widget : Adw.ComboRow) => { + settings.set_string("panel-detail", detailsArr[widget.selected]); + settings.apply(); + + if(widget.selected === 0 && secondPanelDetailRow.selected !== 0) { + widget.selected = secondPanelDetailRow.selected; + secondPanelDetailRow.selected = 0; + } + secondPanelDetailRow.sensitive = widget.selected !== 0; + }); + panelGroup.add(panelDetailRow); + + const secondPanelDetailSel = detailsArr.indexOf(this.#settings.get_string("secondary-panel-detail") as Details); + const secondPanelDetailRow = new Adw.ComboRow({ + title: _g("Secondary Panel Detail"), + model: detailsModel, + selected: Math.max(0, secondPanelDetailSel), + sensitive: panelDetailSel !== 0 + }); + secondPanelDetailRow.connect("notify::selected", (widget : Adw.ComboRow) => { + settings.set_string("secondary-panel-detail", detailsArr[widget.selected]); + settings.apply(); + }); + panelGroup.add(secondPanelDetailRow); + + const showIconRow = new Adw.SwitchRow({ + title: _g("Show Condition Icon"), + active: settings.get_boolean("show-panel-icon") + }); + showIconRow.connect("notify::active", () => { + settings.set_boolean("show-panel-icon", showIconRow.active); + settings.apply(); + }); + panelGroup.add(showIconRow); + + const showSunTimeRow = new Adw.SwitchRow({ + title: _g("Show Sunrise/Sunset"), + active: settings.get_boolean("show-suntime") + }); + showSunTimeRow.connect("notify::active", () => { + settings.set_boolean("show-suntime", showSunTimeRow.active); + settings.apply(); + }); + panelGroup.add(showSunTimeRow); + + this.add(panelGroup); + } + + #setDetail(lbl : Gtk.Label, idx : number, detail : Details) : void { + lbl.label = displayDetail(MOCK_WEATHER, detail, _g, this.#config); + + const arr = this.#config.getDetailsList(); + arr[idx] = detail; + this.#settings.set_value("details-list", writeGTypeAS(arr)); + this.#settings.apply(); + } + + #setClickedDetail(deet : Details, widget : Gtk.Widget) : void { + this.#unsetClickedDetail(); + + this.#clickedDeet = deet; + widget.add_css_class("simpleweather-selected"); + this.#clickedWidget = widget; + } + + #unsetClickedDetail() : void { + this.#clickedDeet = undefined; + this.#clickedWidget?.remove_css_class("simpleweather-selected"); + this.#clickedWidget = undefined; + } +} diff --git a/src/preferences/editLocation.ts b/src/preferences/editLocation.ts index 5f78c73..9f90caa 100644 --- a/src/preferences/editLocation.ts +++ b/src/preferences/editLocation.ts @@ -72,6 +72,13 @@ export async function editLocation(parent : Gtk.Window, loc? : Location) : Promi dialog.set_child(page); dialog.set_default_widget(save); + nameRow.connect("entry-activated", () => { + save.emit("clicked"); + }); + coordsRow.connect("entry-activated", () => { + save.emit("clicked"); + }); + const prom = new Promise((resolve, reject) => { save.connect("clicked", () => { const name = nameRow.text; diff --git a/src/preferences/generalPage.ts b/src/preferences/generalPage.ts index 639081c..2c9b5e2 100644 --- a/src/preferences/generalPage.ts +++ b/src/preferences/generalPage.ts @@ -206,7 +206,7 @@ export class GeneralPage extends Adw.PreferencesPage { step_increment: 5.0, page_increment: 30.0, value: settings.get_double("my-loc-refresh-min") - }), + }) }); myLocRefresh.connect("notify::value", () => { settings.set_double("my-loc-refresh-min", myLocRefresh.value); @@ -235,15 +235,58 @@ export class GeneralPage extends Adw.PreferencesPage { title: _g("Panel"), description: _g("Configure the panel and pop-up") }); - const showSunTimeRow = new Adw.SwitchRow({ - title: _g("Show Sunrise/Sunset"), - active: settings.get_boolean("show-suntime") + const themes = [ + "", + "light", + "afterdark", + "immersive" + ]; + const themeModel = new Gtk.StringList({ strings: [ + _g("System"), + _g("Light"), + _g("Afterdark"), + _g("Immersive") + ]}); + const themeRow = new Adw.ComboRow({ + title: _g("Theme"), + model: themeModel, + selected: Math.max(themes.indexOf(settings.get_string("theme")), 0) + }); + themeRow.connect("notify::selected", (w : Adw.ComboRow) => { + settings.set_string("theme", themes[w.selected]); + settings.apply(); }); - showSunTimeRow.connect("notify::active", () => { - settings.set_boolean("show-suntime", showSunTimeRow.active); + panelGroup.add(themeRow); + const panelBoxModel = new Gtk.StringList({ strings: [ + _g("Right"), _g("Center"), _g("Left") + ]}); + const panelBoxRow = new Adw.ComboRow({ + title: _g("Side of Panel"), + model: panelBoxModel, + selected: settings.get_enum("panel-box") + }); + panelBoxRow.connect("notify::selected", () => { + settings.set_enum("panel-box", panelBoxRow.selected); settings.apply(); }); - panelGroup.add(showSunTimeRow); + panelGroup.add(panelBoxRow); + const panelPriorityRow = new Adw.SpinRow({ + title: _g("Order in Panel"), + adjustment: new Gtk.Adjustment({ + lower: -10000, + upper: 10000, + step_increment: 1, + page_increment: 3, + value: settings.get_int64("panel-priority") + }) + }); + panelPriorityRow.connect("notify::value", () => { + const int64 = Math.round(panelPriorityRow.value); + settings.set_int64("panel-priority", int64); + settings.apply(); + }); + panelGroup.add(panelPriorityRow); + this.add(panelGroup); } diff --git a/src/preferences/search.ts b/src/preferences/search.ts index 1047b60..caea8dc 100644 --- a/src/preferences/search.ts +++ b/src/preferences/search.ts @@ -59,6 +59,9 @@ export async function searchDialog(parent : Gtk.Window, soup : LibSoup, cfg : Co label: _g("Search") }); group.add(searchButton); + searchField.connect("activate", () => { + searchButton.emit("clicked"); + }) const resultsLocList : SelLoc[] = [ ]; const stringList = new Gtk.StringList(); @@ -66,29 +69,39 @@ export async function searchDialog(parent : Gtk.Window, soup : LibSoup, cfg : Co can_unselect: false, model: stringList }); + + // Added later + const addBtn = new Gtk.Button({ + label: _g("Add") + }); + const resultsView = new Gtk.ListView({ orientation: Gtk.Orientation.VERTICAL, model: selModel, - factory: setupListFactory(), + factory: setupListFactory(addBtn), margin_top: 20, margin_bottom: 20 }); - group.add(resultsView); + const resultsScroll = new Gtk.ScrolledWindow({ + child: resultsView, + vexpand: true, + hexpand: true + }); + group.add(resultsScroll); const licenseLabel = new Gtk.Label({ wrap: true, - wrap_mode: Pango.WrapMode.WORD_CHAR + wrap_mode: Pango.WrapMode.WORD_CHAR, + css_classes: [ "simpleweather-small", "simpleweather-center" ] }); group.add(licenseLabel); - const addBtn = new Gtk.Button({ - label: _g("Add") - }); group.add(addBtn); return new Promise((resolve, reject) => { searchButton.connect("clicked", () => { + searchButton.sensitive = false; const a : SearchArgs = { search: searchField.text, licenseLabel, @@ -100,10 +113,12 @@ export async function searchDialog(parent : Gtk.Window, soup : LibSoup, cfg : Co const oldLen = resultsLocList.length; resultsLocList.splice(0, oldLen, ...locArr); populateList(stringList, locArr); + searchButton.sensitive = true; }).catch(e => { if(e instanceof Gio.ResolverError) { console.error(e); showNoInternetDialog(dialog); + searchButton.sensitive = true; } else reject(e); }); @@ -145,7 +160,7 @@ function showNoInternetDialog(parent : Gtk.Window) { alert.show(parent); } -function setupListFactory() : Gtk.SignalListItemFactory { +function setupListFactory(addBtn : Gtk.Button) : Gtk.SignalListItemFactory { const f = new Gtk.SignalListItemFactory(); f.connect("setup", (_, item : Gtk.ListItem) => { const label = new Gtk.Label({ @@ -153,6 +168,15 @@ function setupListFactory() : Gtk.SignalListItemFactory { margin_bottom: 5 }); item.set_child(label); + + const dblClick = new Gtk.GestureClick(); + dblClick.connect("pressed", (_g, nClicks, _x, _y) => { + if(nClicks === 2) { + // Double-clicking is same as clicking add + addBtn.emit("clicked"); + } + }); + label.add_controller(dblClick); }); f.connect("bind", (_, item : Gtk.ListItem) => { const label = item.get_child() as Gtk.Label; diff --git a/src/prefs.ts b/src/prefs.ts index b6c48bc..7f014dd 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -17,6 +17,7 @@ import Adw from "gi://Adw"; import Gio from "gi://Gio"; +import Gdk from "gi://Gdk"; import Gtk from "gi://Gtk"; import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; @@ -28,6 +29,7 @@ import { setUpGettext } from "./gettext.js"; import { gettext as prefsGettext } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; import { initLocales } from "./lang.js"; import { gettext as _g } from "./gettext.js"; +import { DetailsPage } from "./preferences/detailsPage.js"; export default class SimpleWeatherPreferences extends ExtensionPreferences { @@ -44,8 +46,20 @@ export default class SimpleWeatherPreferences extends ExtensionPreferences { settings.delay(); this.checkLocales(window, settings); + const gdkDisplay = Gdk.Display.get_default(); + if(!gdkDisplay) throw new Error("No GDK display detected."); + const cssProv = new Gtk.CssProvider(); + const cssFile = this.#metadata.dir.get_child("prefs.css"); + cssProv.load_from_file(cssFile); + Gtk.StyleContext.add_provider_for_display( + gdkDisplay, + cssProv, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + window.add(new GeneralPage(settings)); window.add(new LocationsPage(settings, window)); + window.add(new DetailsPage(settings)); window.add(new AboutPage(settings, this.#metadata, window)); } diff --git a/src/providers/openmeteo.ts b/src/providers/openmeteo.ts index fc58b97..f6ce55e 100644 --- a/src/providers/openmeteo.ts +++ b/src/providers/openmeteo.ts @@ -17,8 +17,8 @@ import { Config } from "../config.js"; import { LibSoup } from "../libsoup.js"; -import { Direction, Pressure, RainMeasurement, RainMeasurementUnits, Speed, Temp } from "../units.js"; -import { Forecast, Weather } from "../weather.js"; +import { Direction, Percentage, Pressure, RainMeasurement, RainMeasurementUnits, Speed, SpeedAndDir, Temp } from "../units.js"; +import { Condition, Forecast, Weather, gettextCondit } from "../weather.js"; import { getGIconName, Icons } from "../icons.js" import { Provider } from "./provider.js"; import { getTimezoneName } from "../utils.js"; @@ -83,15 +83,16 @@ export class OpenMeteo implements Provider { const wind = new Speed(cur.wind_speed_10m); const gusts = new Speed(cur.wind_gusts_10m); const windDir = new Direction(cur.wind_direction_10m); - const humidity : number = cur.relative_humidity_2m; + const humidity = new Percentage(cur.relative_humidity_2m); // hPa to inHg const pressure = new Pressure(cur.surface_pressure * 0.02953); const uvIndex = daily.uv_index_max[0]; const isNight = cur.is_day === 0; const precipitation = new RainMeasurement(cur.precipitation); + const cloudCover = new Percentage(cur.cloud_cover); - const weatherCode = fixWeatherCode(cur.weather_code, cur.cloud_cover, precipitation); - const icon = codeToIcon[weatherCode]; + const weatherCode = fixWeatherCode(cur.weather_code, cloudCover, precipitation); + const { c: condit, i: icon } = codeToIcon[weatherCode]; const gIconName = getGIconName(icon, isNight); // If sunrise/sunset have already happened, take the next day's @@ -106,12 +107,12 @@ export class OpenMeteo implements Provider { for(let i = 0; i < dayCount; i++) { const fDateStr = daily.time[i]; const fPrecipitation = new RainMeasurement(daily.precipitation_sum[i]); - const fCloudCover = daily.cloud_cover_mean[i]; + const fCloudCover = new Percentage(daily.cloud_cover_mean[i]); // We always want day icons for day forecast const fWeatherCode = fixWeatherCode(daily.weather_code[i], fCloudCover, fPrecipitation); const fIcon = codeToIcon[fWeatherCode]; - const fIconName = getGIconName(fIcon, false); + const fIconName = getGIconName(fIcon.i, false); dayForecast.push({ // This T00 thing tells the parser to assume local time (which we must do) date: new Date(`${fDateStr}T00:00:00`), @@ -127,12 +128,12 @@ export class OpenMeteo implements Provider { for(let i = 0; i < hourCount; i++) { const fDateStr = hourly.time[i]; const fPrecipitation = new RainMeasurement(hourly.precipitation[i]); - const fCloudCover = hourly.cloud_cover[i]; + const fCloudCover = new Percentage(hourly.cloud_cover[i]); const fIsNight = hourly.is_day[i] === 0; const fWeatherCode = fixWeatherCode(hourly.weather_code[i], fCloudCover, fPrecipitation); const fIcon = codeToIcon[fWeatherCode]; - const fIconName = getGIconName(fIcon, fIsNight); + const fIconName = getGIconName(fIcon.i, fIsNight); hourForecast.push({ date: new Date(fDateStr), gIconName: fIconName, @@ -142,6 +143,7 @@ export class OpenMeteo implements Provider { } return { + condit, temp, gIconName, isNight, @@ -157,6 +159,9 @@ export class OpenMeteo implements Provider { pressure, uvIndex, precipitation, + cloudCover, + conditionText: gettextCondit(condit, isNight), + windSpeedAndDir: new SpeedAndDir(wind, windDir), providerName: this.nameKey, loc }; @@ -164,9 +169,10 @@ export class OpenMeteo implements Provider { } -function fixWeatherCode(code : number, cloudPercent : number, precip : RainMeasurement) : number { +function fixWeatherCode(code : number, cloudCover : Percentage, precip : RainMeasurement) : number { // Compensate for https://github.com/open-meteo/open-meteo/issues/812 // Often the CAPE might be over 3000 J/kg but it's completely sunny outside + const cloudPercent = cloudCover.get(); if(code === 95) { if(cloudPercent < 40 && precip.get(RainMeasurementUnits.In) < 0.1) { // In Open-Meteo's WeatherCode.swift calculate function @@ -180,45 +186,45 @@ function fixWeatherCode(code : number, cloudPercent : number, precip : RainMeasu } // https://open-meteo.com/en/docs#weather_variable_documentation -const codeToIcon : Record = { - 0: Icons.Clear, +const codeToIcon : Record = { + 0: { c: Condition.CLEAR, i: Icons.Clear }, - 1: Icons.Clear, - 2: Icons.Cloudy, - 3: Icons.Overcast, + 1: { c : Condition.CLEAR, i: Icons.Clear }, + 2: { c : Condition.CLOUDY, i: Icons.Cloudy }, + 3: { c : Condition.CLOUDY, i: Icons.Overcast }, - 45: Icons.Foggy, - 48: Icons.Foggy, + 45: { c : Condition.CLOUDY, i: Icons.Foggy }, + 48: { c : Condition.CLOUDY, i: Icons.Foggy }, - 51: Icons.RainScattered, - 53: Icons.Rainy, - 55: Icons.Rainy, + 51: { c : Condition.RAINY, i: Icons.RainScattered }, + 53: { c : Condition.RAINY, i: Icons.Rainy }, + 55: { c : Condition.RAINY, i: Icons.Rainy }, - 56: Icons.FreezingRain, - 57: Icons.FreezingRain, + 56: { c : Condition.RAINY, i: Icons.FreezingRain }, + 57: { c : Condition.RAINY, i: Icons.FreezingRain }, - 61: Icons.RainScattered, - 63: Icons.Rainy, - 65: Icons.Rainy, + 61: { c : Condition.RAINY, i: Icons.RainScattered }, + 63: { c : Condition.RAINY, i: Icons.Rainy }, + 65: { c : Condition.RAINY, i: Icons.Rainy }, - 66: Icons.FreezingRain, - 67: Icons.FreezingRain, + 66: { c : Condition.RAINY, i: Icons.FreezingRain }, + 67: { c : Condition.RAINY, i: Icons.FreezingRain }, - 71: Icons.Snowy, - 73: Icons.Snowy, - 75: Icons.Snowy, + 71: { c : Condition.SNOWY, i: Icons.Snowy }, + 73: { c : Condition.SNOWY, i: Icons.Snowy }, + 75: { c : Condition.SNOWY, i: Icons.Snowy }, - 77: Icons.Snowy, + 77: { c : Condition.SNOWY, i: Icons.Snowy }, - 80: Icons.RainScattered, - 81: Icons.Rainy, - 82: Icons.Rainy, + 80: { c : Condition.RAINY, i: Icons.RainScattered }, + 81: { c : Condition.RAINY, i: Icons.Rainy }, + 82: { c : Condition.RAINY, i: Icons.Rainy }, - 85: Icons.Snowy, - 86: Icons.Snowy, + 85: { c : Condition.SNOWY, i: Icons.Snowy }, + 86: { c : Condition.SNOWY, i: Icons.Snowy }, - 95: Icons.Stormy, + 95: { c : Condition.STORMY, i: Icons.Stormy }, - 96: Icons.Hail, - 99: Icons.Hail + 96: { c : Condition.SNOWY, i: Icons.Hail }, + 99: { c : Condition.SNOWY, i: Icons.Hail } }; diff --git a/src/resource.ts b/src/resource.ts new file mode 100644 index 0000000..e141508 --- /dev/null +++ b/src/resource.ts @@ -0,0 +1,23 @@ +/* + Copyright 2025 Roman Lefler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +// This file will get appended with variabels in the build step + +export function AUTHORS() : string { + // @ts-ignore + return authors; +} diff --git a/src/theme.ts b/src/theme.ts new file mode 100644 index 0000000..050cd1b --- /dev/null +++ b/src/theme.ts @@ -0,0 +1,53 @@ +/* + Copyright 2025 Roman Lefler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import Clutter from "gi://Clutter"; +import St from "gi://St"; + +export function theme(widget : St.Widget, klassName : string) { + (widget as any).dataSwClass = klassName; +} + +export function themeInitAll(parent : Clutter.Actor, theme : string) { + if(parent instanceof St.Widget) { + const classStr : string | undefined = (parent as any).dataSwClass; + const classes = classStr ? classStr.split(" ") : [ ]; + for(const klass of classes) { + parent.add_style_class_name(`sw-style-${theme}-${klass}`); + } + } + + for(const child of parent.get_children()) { + themeInitAll(child, theme); + } +} + +export function themeRemoveAll(parent : Clutter.Actor) { + if(parent instanceof St.Widget && parent.style_class) { + const classes : string[] = parent.style_class.split(" "); + const removeList : string[] = [ ]; + for(const klass of classes) { + if(!klass.startsWith("sw-style-")) continue; + removeList.push(klass); + } + for(const c of removeList) parent.remove_style_class_name(c); + } + + for(const child of parent.get_children()) { + themeRemoveAll(child); + } +} diff --git a/src/units.ts b/src/units.ts index b58459e..be5097b 100644 --- a/src/units.ts +++ b/src/units.ts @@ -15,19 +15,27 @@ along with this program. If not, see . */ +import { Config } from "./config.js"; import { UnitError } from "./errors.js"; +import { displayDirection, displayPressure, displayRainMeasurement, displaySpeed, displayTemp } from "./lang.js"; +import { gettext as _g } from "./gettext.js"; /* The measures are classes. This is to make it harder to make unit mistakes. + There is also a Displayable interface to abstract displaying them. */ +export interface Displayable { + display : (cfg : Config) => string; +} + export enum TempUnits { Fahrenheit = 1, Celsius = 2 } -export class Temp { +export class Temp implements Displayable { #fahrenheit : number; @@ -45,6 +53,10 @@ export class Temp { throw new UnitError("Temperature unit invalid."); } } + + display(cfg : Config) : string { + return displayTemp(this, cfg); + } } export enum SpeedUnits { @@ -56,7 +68,7 @@ export enum SpeedUnits { Beaufort = 6 } -export class Speed { +export class Speed implements Displayable { #mph : number; @@ -89,6 +101,10 @@ export class Speed { throw new UnitError("Speed unit invalid."); } } + + display(cfg : Config) : string { + return displaySpeed(this, cfg); + } } export enum DirectionUnits { @@ -96,7 +112,7 @@ export enum DirectionUnits { EightPoint = 2 } -export class Direction { +export class Direction implements Displayable { #degrees : number; @@ -120,6 +136,10 @@ export class Direction { throw new UnitError("Direction unit invalid."); } } + + display(cfg : Config) : string { + return displayDirection(this, cfg); + } } export enum PressureUnits { @@ -128,7 +148,7 @@ export enum PressureUnits { MmHg = 3 } -export class Pressure { +export class Pressure implements Displayable { #inHg : number; @@ -148,6 +168,10 @@ export class Pressure { throw new UnitError("Pressure unit invalid."); } } + + display(cfg : Config) : string { + return displayPressure(this, cfg); + } } export enum RainMeasurementUnits { @@ -157,7 +181,7 @@ export enum RainMeasurementUnits { Pt = 4 } -export class RainMeasurement { +export class RainMeasurement implements Displayable { #inches : number; @@ -180,6 +204,9 @@ export class RainMeasurement { } } + display(cfg : Config) : string { + return displayRainMeasurement(this, cfg); + } } export enum DistanceUnits { @@ -189,7 +216,7 @@ export enum DistanceUnits { M = 4 } -export class Distance { +export class Distance implements Displayable { #miles : number; @@ -211,4 +238,52 @@ export class Distance { throw new UnitError("Distance unit invalid."); } } + + display(cfg : Config) : string { + const suffices = [ "mi", "km", "ft", "m" ]; + const unit = cfg.getDistanceUnit(); + return `${this.get(unit)} ${suffices[unit]}`; + } +} + + +export class SpeedAndDir implements Displayable { + + #speed : Speed; + #dir : Direction; + + constructor(speed : Speed, dir : Direction) { + this.#speed = speed; + this.#dir = dir; + } + + display(cfg : Config) : string { + return displayDirection(this.#dir, cfg) + ", " + + displaySpeed(this.#speed, cfg); + } + +} + +export class Percentage implements Displayable { + #percentage : number; + constructor(zeroToOneHundred : number) { + this.#percentage = zeroToOneHundred; + } + get() : number { + return this.#percentage; + } + display(_cfg : Config) : string { + return `${Math.round(this.#percentage)}%`; + } +} + +export class GettextKey implements Displayable { + #key : string; + constructor(key : string) { + this.#key = key; + } + display(_cfg : Config) : string { + return _g(this.#key); + } } + diff --git a/src/utils.ts b/src/utils.ts index b659d9a..1152653 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,3 +37,15 @@ export function removeSourceIfTruthy(id : number | null | undefined) : undefined export function getTimezoneName() : string { return Intl.DateTimeFormat(getLocales()).resolvedOptions().timeZone; } + +export function noTime(d : Date) : Date { + const dup = new Date(d); + dup.setHours(0, 0, 0, 0); + return dup; +} + +export function sameDate(d1 : Date, d2 : Date) : boolean { + const dup1 = noTime(d1); + const dup2 = noTime(d2); + return dup1.getTime() === dup2.getTime(); +} diff --git a/src/weather.ts b/src/weather.ts index 4de506d..096ff4f 100644 --- a/src/weather.ts +++ b/src/weather.ts @@ -15,10 +15,13 @@ along with this program. If not, see . */ +import { IDetails } from "./details.js"; import { Location } from "./location.js"; -import { Direction, Pressure, RainMeasurement, Speed, Temp } from "./units.js"; +import { Direction, Percentage, Pressure, RainMeasurement, Speed, SpeedAndDir, Temp, GettextKey } from "./units.js"; -export interface Weather { +export interface Weather extends IDetails { + + condit : Condition; temp : Temp; @@ -44,7 +47,7 @@ export interface Weather { windDir : Direction, - humidity: number; + humidity: Percentage; pressure : Pressure; @@ -56,6 +59,12 @@ export interface Weather { loc : Location; + windSpeedAndDir : SpeedAndDir; + + cloudCover : Percentage; + + conditionText : GettextKey; + } export interface Forecast { @@ -72,5 +81,38 @@ export interface Forecast { // Should be 0 - 100 precipChancePercent : number; +} +export enum Condition { + CLEAR = "clear", + CLOUDY = "cloudy", + RAINY = "rainy", + SNOWY = "snowy", + STORMY = "stormy", + WINDY = "windy" } + +// Fake gettext to trick xgettext +export function _g(s : string) : string { + return s; +} +// This is done like this so that xgettext understands it +export function gettextCondit(condit : Condition, isNight : boolean) : GettextKey { + switch(condit) { + case Condition.CLEAR: + return new GettextKey(_g(isNight ? "Clear" : "Sunny")); + case Condition.CLOUDY: + return new GettextKey(_g("Cloudy")); + case Condition.RAINY: + return new GettextKey(_g("Rainy")); + case Condition.SNOWY: + return new GettextKey(_g("Snowy")); + case Condition.STORMY: + return new GettextKey(_g("Stormy")); + case Condition.WINDY: + return new GettextKey(_g("Windy")); + default: + throw new Error(`Unknown condition: ${condit}`); + } +} + diff --git a/static/clear-night-pexels.jpg b/static/clear-night-pexels.jpg new file mode 100644 index 0000000..dc18f9c Binary files /dev/null and b/static/clear-night-pexels.jpg differ diff --git a/static/cloudy-pexels.jpg b/static/cloudy-pexels.jpg new file mode 100644 index 0000000..4dd2295 Binary files /dev/null and b/static/cloudy-pexels.jpg differ diff --git a/static/metadata.json b/static/metadata.json index f3c132a..4e82034 100644 --- a/static/metadata.json +++ b/static/metadata.json @@ -1,12 +1,12 @@ { "name": "SimpleWeather", - "description": "Shows the weather on the top bar. Doesn't use GNOME weather.", + "description": "A highly configurable weather indicator. Doesn't use GNOME Weather.", "uuid": "simple-weather@romanlefler.com", "url": "https://github.com/romanlefler/SimpleWeather", "settings-schema": "org.gnome.shell.extensions.simple-weather", "gettext-domain": "simple-weather@romanlefler.com", "shell-version": [ - "48" + "46", "48" ], - "version-name": "48.0.0" + "version-name": "48.1.0" } diff --git a/static/prefs.css b/static/prefs.css new file mode 100644 index 0000000..79f36ae --- /dev/null +++ b/static/prefs.css @@ -0,0 +1,30 @@ + +.simpleweather-h1 { + font-size: 1.75em; + margin: 20px; +} + +.simpleweather-h2 { + font-size: 1.5em; + margin: 15px; +} + +.simpleweather-small { + font-size: 0.85em; +} + +.simpleweather-center { + text-align: center; +} + +.simpleweather-selected { + font-style: italic; +} + +.simpleweather-margin { + margin: 8px; +} + +.simpleweather-margin-wide { + margin: 8px 16px; +} diff --git a/static/rainy-gam-ol.jpg b/static/rainy-gam-ol.jpg new file mode 100644 index 0000000..abfd631 Binary files /dev/null and b/static/rainy-gam-ol.jpg differ diff --git a/static/snowy-public-domain-pictures.jpg b/static/snowy-public-domain-pictures.jpg new file mode 100644 index 0000000..74fa545 Binary files /dev/null and b/static/snowy-public-domain-pictures.jpg differ diff --git a/static/stormy-aiac-pl.jpg b/static/stormy-aiac-pl.jpg new file mode 100644 index 0000000..05d9cfb Binary files /dev/null and b/static/stormy-aiac-pl.jpg differ diff --git a/static/sunny-moinzon.jpg b/static/sunny-moinzon.jpg new file mode 100644 index 0000000..ad582af Binary files /dev/null and b/static/sunny-moinzon.jpg differ diff --git a/themes/afterdark.css b/themes/afterdark.css new file mode 100644 index 0000000..f6413cf --- /dev/null +++ b/themes/afterdark.css @@ -0,0 +1,33 @@ + +/* begin theme afterdark */ + +.sw-style-afterdark-menu { + border: 4px solid #35332D; +} + +.sw-style-afterdark-left-box { + background: #35332D; + border-radius: 20px; +} + +.sw-style-afterdark-bg, .sw-style-afterdark-menu, .sw-style-afterdark-forecast-box { + background-color: #2D2B25; +} + +.sw-style-afterdark-menu * { + color: #EFEDDC; +} + +.sw-style-afterdark-faded { + color: #D7D5C5; +} + +.sw-style-immersive-button { + background: transparent; +} + +.sw-style-afterdark-button:hover { + background: #35332D; +} + +/* end theme afterdark */ diff --git a/themes/immersive.css b/themes/immersive.css new file mode 100644 index 0000000..4a5a21a --- /dev/null +++ b/themes/immersive.css @@ -0,0 +1,80 @@ + +/* begin theme immersive */ + + +.sw-style-immersive-menu { + border: none; + box-shadow: none; + background: orange; +} + +.sw-style-immersive-left-box { + background: transparent; + border-radius: 20px; +} + +.sw-style-immersive-bg, .sw-style-immersive-forecast-box { + background: transparent; +} + +.sw-style-immersive-menu * { + color: #DCEDEF; +} + +.sw-style-immersive-menu.swa-clear.swa-day *, +.sw-style-immersive-menu.swa-windy.swa-day *, +.sw-style-immersive-menu.swa-cloudy *, +.sw-style-immersive-menu.swa-snowy * { + color: #101223; +} + +.sw-style-immersive-faded { + color: #C5D5D7; +} + +.swa-clear.swa-day .sw-style-immersive-faded, +.swa-windy.swa-day .sw-style-immersive-faded, +.swa-cloudy .sw-style-immersive-faded, +.swa-snowy .sw-style-immserive-faded { + color: #282A3A; +} + +.sw-style-immersive-button { + background: transparent; +} + +.sw-style-immersive-button:hover { + background: rgba(0, 10, 0, 0.15); +} + +.sw-style-immersive-menu.swa-clear.swa-day, .sw-style-immersive-menu.swa-windy.swa-day { + background: url("./sunny-moinzon.jpg"); + background-size: cover; +} + +.sw-style-immersive-menu.swa-clear.swa-night, .sw-style-immersive-menu.swa-windy.swa-night { + background: url("./clear-night-pexels.jpg"); + background-size: cover; +} + +.sw-style-immersive-menu.swa-cloudy { + background: url("./cloudy-pexels.jpg"); + background-size: cover; +} + +.sw-style-immersive-menu.swa-rainy { + background: url("./rainy-gam-ol.jpg"); + background-size: cover; +} + +.sw-style-immersive-menu.swa-snowy { + background: url("./snowy-public-domain-pictures.jpg"); + background-size: cover; +} + +.sw-style-immersive-menu.swa-stormy { + background: url("./stormy-aiac-pl.jpg"); + background-size: cover; +} + +/* end theme immersive */ diff --git a/themes/light.css b/themes/light.css new file mode 100644 index 0000000..8f34068 --- /dev/null +++ b/themes/light.css @@ -0,0 +1,31 @@ + +/* light theme */ + +.sw-style-light-menu { + border: 4px solid #CACCD2; +} + +.sw-style-light-left-box { + background: #CACCD2; + border-radius: 20px; +} + +.sw-style-light-bg, .sw-style-light-menu, .sw-style-light-forecast-box { + background-color: #D2D4DA; +} + +.sw-style-light-menu * { + color: #101223; +} + +.sw-style-light-faded { + color: #282A3A; +} + +.sw-style-immersive-button { + background: transparent; +} + +.sw-style-light-button:hover { + background: #CACCD2; +} diff --git a/static/stylesheet.css b/themes/stylesheet.css similarity index 82% rename from static/stylesheet.css rename to themes/stylesheet.css index c9fe4dd..6191205 100644 --- a/static/stylesheet.css +++ b/themes/stylesheet.css @@ -29,10 +29,6 @@ icon-size: 16px; } -.simpleweather-h1 { - font-size: 1.75em; -} - -.simpleweather-h2 { - font-size: 1.5em; +.simpleweather-second-panel-label { + margin-left: 8px; } diff --git a/tsconfig.json b/tsconfig.json index 47a7b35..38d9085 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,8 @@ "sourceMap": false, "strict": true, "target": "ES2022", + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", "lib": [ "ES2022" ]