├── .gitignore ├── Changelog.md ├── ReadMe.md ├── build ├── install ├── package ├── contents │ ├── config │ │ ├── config.qml │ │ └── main.xml │ ├── fonts │ │ └── weathericons-regular-webfont.ttf │ ├── icons │ │ ├── google_calendar_96px.png │ │ ├── google_tasks_96px.png │ │ └── hangouts.svg │ ├── images │ │ ├── singlecolumn.svg │ │ └── twocolumns.svg │ ├── scripts │ │ ├── icsjson.py │ │ ├── konsolekalendar.py │ │ └── notification.py │ └── ui │ │ ├── AgendaEventItem.qml │ │ ├── AgendaListItem.qml │ │ ├── AgendaModel.qml │ │ ├── AgendaTaskItem.qml │ │ ├── AgendaView.qml │ │ ├── AppletConfig.qml │ │ ├── CalendarSelector.qml │ │ ├── ClockView.qml │ │ ├── ConfigMigration.qml │ │ ├── DateSelector.qml │ │ ├── DateTimeSelector.qml │ │ ├── DayDelegate.qml │ │ ├── DaysCalendar.qml │ │ ├── DurationSelector.qml │ │ ├── EditEventForm.qml │ │ ├── EditTaskForm.qml │ │ ├── ErrorType.js │ │ ├── EventModel.qml │ │ ├── EventPropertyIcon.qml │ │ ├── FontIcon.qml │ │ ├── LinkRect.qml │ │ ├── LinkText.qml │ │ ├── LocaleFuncs.js │ │ ├── Logic.qml │ │ ├── MeteogramView.qml │ │ ├── MonthView.qml │ │ ├── NetworkMonitor.qml │ │ ├── NetworkMonitorPlasmaNM.qml │ │ ├── NewEventForm.qml │ │ ├── NotificationManager.qml │ │ ├── PopupView.qml │ │ ├── Shared.js │ │ ├── TimeFormatSizeHelper.qml │ │ ├── TimeModel.qml │ │ ├── TimeSelector.qml │ │ ├── TimerInputView.qml │ │ ├── TimerModel.qml │ │ ├── TimerPresetButton.qml │ │ ├── TimerTextField.qml │ │ ├── TimerView.qml │ │ ├── TooltipView.qml │ │ ├── UpcomingEvents.qml │ │ ├── badges │ │ ├── DotsBadge.qml │ │ ├── EventColorsBarBadge.qml │ │ ├── EventCountBadge.qml │ │ └── HighlightBarBadge.qml │ │ ├── calendars │ │ ├── CalendarManager.qml │ │ ├── DebugCalendarManager.qml │ │ ├── DebugGoogleCalendarManager.qml │ │ ├── GoogleApiSession.qml │ │ ├── GoogleCalendarManager.qml │ │ ├── GoogleCalendarTests.js │ │ ├── GoogleTasksManager.qml │ │ ├── ICalManager.qml │ │ ├── PlasmaCalendarManager.qml │ │ └── PlasmaCalendarUtils.js │ │ ├── code │ │ ├── ColorIdMap.js │ │ └── DebugFixtures.js │ │ ├── config │ │ ├── ColorTextButton.qml │ │ ├── ConfigAgenda.qml │ │ ├── ConfigCalendar.qml │ │ ├── ConfigEvents.qml │ │ ├── ConfigGeneral.qml │ │ ├── ConfigGoogleCalendar.qml │ │ ├── ConfigICal.qml │ │ ├── ConfigLayout.qml │ │ ├── ConfigSerializedString.qml │ │ ├── ConfigTimezones.qml │ │ ├── ConfigWeather.qml │ │ ├── GoogleLoginManager.qml │ │ ├── HeaderText.qml │ │ ├── LockIcon.qml │ │ ├── OpenWeatherMapCityDialog.qml │ │ └── WeatherCanadaCityDialog.qml │ │ ├── lib │ │ ├── AppletVersion.qml │ │ ├── Async.js │ │ ├── Base64Json.qml │ │ ├── Base64JsonListModel.qml │ │ ├── ColorGrid.qml │ │ ├── ColorUtil.js │ │ ├── ConfigAdvanced.qml │ │ ├── ConfigCheckBox.qml │ │ ├── ConfigColor.qml │ │ ├── ConfigComboBox.qml │ │ ├── ConfigDimension.qml │ │ ├── ConfigFontFamily.qml │ │ ├── ConfigNotification.qml │ │ ├── ConfigPage.qml │ │ ├── ConfigRadioButtonGroup.qml │ │ ├── ConfigSection.qml │ │ ├── ConfigSlider.qml │ │ ├── ConfigSound.qml │ │ ├── ConfigSpinBox.qml │ │ ├── ConfigString.qml │ │ ├── ContextMenu.qml │ │ ├── ExecUtil.qml │ │ ├── Logger.qml │ │ ├── MenuItem.qml │ │ ├── MessageWidget.qml │ │ └── Requests.js │ │ ├── main.qml │ │ └── weather │ │ ├── OpenWeatherMap.js │ │ ├── WeatherApi.js │ │ └── WeatherCanada.js ├── metadata.desktop └── translate │ ├── ReadMe.md │ ├── build │ ├── da.po │ ├── de.po │ ├── el.po │ ├── es.po │ ├── fi.po │ ├── fr.po │ ├── he.po │ ├── it.po │ ├── ja.po │ ├── ko.po │ ├── merge │ ├── nl.po │ ├── pl.po │ ├── plasmoidlocaletest │ ├── pt_BR.po │ ├── pt_PT.po │ ├── ru.po │ ├── sl.po │ ├── sv.po │ ├── template.pot │ ├── tr.po │ ├── uk.po │ └── zh_CN.po ├── uninstall └── update /.gitignore: -------------------------------------------------------------------------------- 1 | *.plasmoid 2 | *.qmlc 3 | *.jsc 4 | *.mo 5 | /dist 6 | /package/metadata.json 7 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Event Calendar 2 | 3 | https://store.kde.org/p/998901/ 4 | 5 | Plasmoid for a calendar+agenda with weather that syncs to Google Calendar. 6 | 7 | ## Screenshots 8 | 9 | ![](https://i.imgur.com/qdJ71sb.jpg) 10 | ![](https://i.imgur.com/Ow8UlFj.jpg) 11 | 12 | 13 | ## A) Install via KDE 14 | 15 | 1. Right Click Panel > Panel Options > Add Widgets 16 | 2. Get New Widgets > Download New Widgets 17 | 3. Search: Event Calendar 18 | 4. Install 19 | 5. Right Click your current calendar widget > Alternatives 20 | 6. Select Event Calendar 21 | 22 | ## B) Install via GitHub 23 | 24 | ``` 25 | git clone https://github.com/Zren/plasma-applet-eventcalendar.git eventcalendar 26 | cd eventcalendar 27 | sh ./install 28 | ``` 29 | 30 | To update, run the `sh ./update` script. It will run a `git pull` then reinstall the applet. Please note this script will restart plasmashell (so you don't have to relog)! 31 | 32 | ## C) Install via Package Manager 33 | 34 | Some awesome users seemed to have packaged this applet under `plasma5-applets-eventcalendar`. 35 | 36 | * Arch: https://aur.archlinux.org/packages/plasma5-applets-eventcalendar/ 37 | * Chakra: https://chakralinux.org/ccr/packages.php?ID=7656 38 | 39 | (Old) There's also a russian who's patched the widget with russian translations. It's out of date though, and we now bundle russian translations with the rest. 40 | 41 | * ABF: https://abf.rosalinux.ru/victorr2007/plasma5-applet-eventcalendar 42 | 43 | ## Update to GitHub master 44 | 45 | If you're asked to test something, you can do so by installing the latest unreleased code. 46 | 47 | Beforehand, uninstall the AUR version if you are running Arch (you can reinstall after testing). 48 | 49 | Then install pen the Terminal and run the following commands. Please note the install script will restart plasmashell so that you don't have to relog. 50 | 51 | ``` 52 | sudo apt install git 53 | git clone https://github.com/Zren/plasma-applet-eventcalendar.git eventcalendar 54 | cd eventcalendar 55 | sh ./install --restart 56 | ``` 57 | 58 | When you've finished testing, you may wish to reinstall the KDE Store or AUR version. First uninstall the widget with the following command, then reinstall your desired version of the widget. 59 | 60 | ``` 61 | sh ./uninstall 62 | ``` 63 | 64 | ## Configure 65 | 66 | 1. Right click the Calendar > Event Calendar Settings > Google Calendar 67 | 2. Copy the Code and enter it at the given link. Keep the settings window open. 68 | 3. After the settings window says it's synched, click apply. 69 | 4. Go to the Weather Tab > Enter your city id for OpenWeatherMap. If their search can't find your city, try googling it with [site:openweathermap.org/city](https://www.google.ca/search?q=site%3Aopenweathermap.org%2Fcity+toronto). 70 | 71 | 72 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 18 3 | 4 | ### User Variables 5 | qtMinVer="5.12" 6 | kfMinVer="5.68" 7 | plasmaMinVer="5.18" 8 | filenameTag="-plasma${plasmaMinVer}" 9 | packageExt="plasmoid" 10 | 11 | 12 | ### Misc 13 | startDir=$PWD 14 | 15 | ### Colors 16 | # https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux 17 | # https://stackoverflow.com/questions/911168/how-can-i-detect-if-my-shell-script-is-running-through-a-pipe 18 | TC_Red='\033[31m'; TC_Orange='\033[33m'; 19 | TC_LightGray='\033[90m'; TC_LightRed='\033[91m'; TC_LightGreen='\033[92m'; TC_Yellow='\033[93m'; TC_LightBlue='\033[94m'; 20 | TC_Reset='\033[0m'; TC_Bold='\033[1m'; 21 | if [ ! -t 1 ]; then 22 | TC_Red=''; TC_Orange=''; 23 | TC_LightGray=''; TC_LightRed=''; TC_LightGreen=''; TC_Yellow=''; TC_LightBlue=''; 24 | TC_Bold=''; TC_Reset=''; 25 | fi 26 | function echoTC() { 27 | text="$1" 28 | textColor="$2" 29 | echo -e "${textColor}${text}${TC_Reset}" 30 | } 31 | function echoGray { echoTC "$1" "$TC_LightGray"; } 32 | function echoRed { echoTC "$1" "$TC_Red"; } 33 | function echoGreen { echoTC "$1" "$TC_LightGreen"; } 34 | 35 | 36 | ### Check QML Versions 37 | # See https://zren.github.io/kde/versions/ for distro versions 38 | if [ -f checkimports.py ]; then 39 | python3 checkimports.py --qt="$qtMinVer" --kf="$kfMinVer" --plasma="$plasmaMinVer" 40 | if [ $? == 1 ]; then 41 | exit 1 42 | fi 43 | fi 44 | 45 | ### Translations 46 | if [ -d "package/translate" ]; then 47 | echoGray "[build] translate dir found, running merge." 48 | (cd package/translate && sh ./merge) 49 | (cd package/translate && sh ./build) 50 | if type "git" > /dev/null; then 51 | # echo "[build] Git found, running translation diff check." 52 | if [ "$(git diff --stat package/translate)" != "" ]; then 53 | echoRed "[build] Changed detected. Cancelling build." 54 | git diff --stat . 55 | exit 1 56 | fi 57 | else 58 | echoGray "[build] Git not found, skipping translation diff check." 59 | fi 60 | fi 61 | 62 | 63 | ### Variables 64 | packageNamespace=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 65 | packageName="${packageNamespace##*.}" # Strip namespace (Eg: "org.kde.plasma.") 66 | packageVersion=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Version"` 67 | packageAuthor=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Author"` 68 | packageAuthorEmail=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Email"` 69 | packageWebsite=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Website"` 70 | packageComment=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="Comment"` 71 | 72 | ### metadata.desktop => metadata.json 73 | if command -v desktoptojson &> /dev/null ; then 74 | genOutput=`desktoptojson --serviceType="plasma-applet.desktop" -i "$PWD/package/metadata.desktop" -o "$PWD/package/metadata.json"` 75 | if [ "$?" != "0" ]; then 76 | exit 1 77 | fi 78 | # Tabify metadata.json 79 | sed -i '{s/ \{4\}/\t/g}' "$PWD/package/metadata.json" 80 | fi 81 | 82 | 83 | ### *.plasmoid 84 | 85 | if ! type "zip" > /dev/null; then 86 | echoRed "[error] 'zip' command not found." 87 | if type "zypper" > /dev/null; then 88 | echoRed "[error] Opensuse detected, please run: ${TC_Bold}sudo zypper install zip" 89 | fi 90 | exit 1 91 | fi 92 | filename="${packageName}-v${packageVersion}${filenameTag}.${packageExt}" 93 | rm ${packageName}-v*.${packageExt} # Cleanup 94 | echoGray "[${packageExt}] Zipping '${filename}'" 95 | (cd package \ 96 | && zip -r $filename * \ 97 | && mv $filename $startDir/$filename \ 98 | ) 99 | cd $startDir 100 | echoGray "[${packageExt}] md5: $(md5sum $filename | awk '{ print $1 }')" 101 | echoGray "[${packageExt}] sha256: $(sha256sum $filename | awk '{ print $1 }')" 102 | 103 | 104 | ### Done 105 | cd $startDir 106 | 107 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 5 3 | 4 | # This script detects if the widget is already installed. 5 | # If it is, it will use --upgrade instead and restart plasmashell. 6 | 7 | packageNamespace=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 8 | packageServiceType=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-ServiceTypes"` 9 | restartPlasmashell=false 10 | 11 | for arg in "$@"; do 12 | case "$arg" in 13 | -r) restartPlasmashell=true;; 14 | --restart) restartPlasmashell=true;; 15 | *) ;; 16 | esac 17 | done 18 | 19 | isAlreadyInstalled=false 20 | kpackagetool5 --type="${packageServiceType}" --show="$packageNamespace" &> /dev/null 21 | if [ $? == 0 ]; then 22 | isAlreadyInstalled=true 23 | fi 24 | 25 | ### metadata.desktop => metadata.json 26 | if command -v desktoptojson &> /dev/null ; then 27 | genOutput=`desktoptojson --serviceType="plasma-applet.desktop" -i "$PWD/package/metadata.desktop" -o "$PWD/package/metadata.json"` 28 | if [ "$?" != "0" ]; then 29 | exit 1 30 | fi 31 | # Tabify metadata.json 32 | sed -i '{s/ \{4\}/\t/g}' "$PWD/package/metadata.json" 33 | fi 34 | 35 | if $isAlreadyInstalled; then 36 | # Eg: kpackagetool5 -t "Plasma/Applet" -u package 37 | kpackagetool5 -t "${packageServiceType}" -u package 38 | restartPlasmashell=true 39 | else 40 | # Eg: kpackagetool5 -t "Plasma/Applet" -i package 41 | kpackagetool5 -t "${packageServiceType}" -i package 42 | fi 43 | 44 | if $restartPlasmashell; then 45 | killall plasmashell 46 | kstart5 plasmashell 47 | fi 48 | -------------------------------------------------------------------------------- /package/contents/config/config.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import org.kde.plasma.configuration 2.0 3 | import org.kde.plasma.calendar 2.0 as PlasmaCalendar 4 | 5 | import "../ui/calendars/PlasmaCalendarUtils.js" as PlasmaCalendarUtils 6 | 7 | ConfigModel { 8 | id: configModel 9 | 10 | ConfigCategory { 11 | name: i18n("General") 12 | icon: "clock" 13 | source: "config/ConfigGeneral.qml" 14 | } 15 | // ConfigCategory { 16 | // name: i18n("Clock") 17 | // icon: "clock" 18 | // source: "config/ConfigClock.qml" 19 | // } 20 | ConfigCategory { 21 | name: i18n("Layout") 22 | icon: "grid-rectangular" 23 | source: "config/ConfigLayout.qml" 24 | } 25 | ConfigCategory { 26 | name: i18n("Timezones") 27 | icon: "preferences-system-time" 28 | source: "config/ConfigTimezones.qml" 29 | } 30 | ConfigCategory { 31 | name: i18n("Calendar") 32 | icon: "view-calendar" 33 | source: "config/ConfigCalendar.qml" 34 | } 35 | ConfigCategory { 36 | name: i18n("Agenda") 37 | icon: "view-calendar-agenda" 38 | source: "config/ConfigAgenda.qml" 39 | } 40 | ConfigCategory { 41 | name: i18n("Events") 42 | icon: "view-calendar-week" 43 | source: "config/ConfigEvents.qml" 44 | } 45 | ConfigCategory { 46 | name: i18n("ICalendar (.ics)") 47 | icon: "text-calendar" 48 | source: "config/ConfigICal.qml" 49 | visible: plasmoid.configuration.debugging 50 | } 51 | ConfigCategory { 52 | name: i18n("Google Calendar") 53 | icon: plasmoid.file("", "icons/google_calendar_96px.png") 54 | source: "config/ConfigGoogleCalendar.qml" 55 | } 56 | ConfigCategory { 57 | name: i18n("Weather") 58 | icon: "weather-clear" 59 | source: "config/ConfigWeather.qml" 60 | } 61 | ConfigCategory { 62 | name: i18n("Advanced") 63 | icon: "applications-development" 64 | source: "lib/ConfigAdvanced.qml" 65 | visible: plasmoid.configuration.debugging 66 | } 67 | 68 | property Instantiator __eventPlugins: Instantiator { 69 | model: PlasmaCalendar.EventPluginsManager.model 70 | delegate: ConfigCategory { 71 | name: model.display 72 | icon: model.decoration 73 | source: model.configUi 74 | readonly property string pluginFilename: PlasmaCalendarUtils.getPluginFilename(model.pluginPath) 75 | visible: plasmoid.configuration.enabledCalendarPlugins.indexOf(pluginFilename) > -1 76 | } 77 | 78 | onObjectAdded: configModel.appendCategory(object) 79 | onObjectRemoved: configModel.removeCategory(object) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package/contents/fonts/weathericons-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zren/plasma-applet-eventcalendar/564348e5033129d87d2f5ea076f8e1505333877b/package/contents/fonts/weathericons-regular-webfont.ttf -------------------------------------------------------------------------------- /package/contents/icons/google_calendar_96px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zren/plasma-applet-eventcalendar/564348e5033129d87d2f5ea076f8e1505333877b/package/contents/icons/google_calendar_96px.png -------------------------------------------------------------------------------- /package/contents/icons/google_tasks_96px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zren/plasma-applet-eventcalendar/564348e5033129d87d2f5ea076f8e1505333877b/package/contents/icons/google_tasks_96px.png -------------------------------------------------------------------------------- /package/contents/icons/hangouts.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | Hangouts icon 16 | 18 | 20 | 24 | 28 | 29 | 38 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | Hangouts icon 48 | 04/04/2018 49 | 50 | 51 | Google Inc 52 | 53 | 54 | 55 | 56 | CMetalCore 57 | 58 | 59 | https://commons.wikimedia.org/wiki/File:Hangouts_Icon.png 60 | 61 | 62 | 63 | 66 | 68 | 72 | 76 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /package/contents/scripts/icsjson.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import datetime 3 | from icalendar import Calendar 4 | import json 5 | import urllib.parse 6 | import urllib.request 7 | 8 | debugging=False 9 | def debug(*args): 10 | if debugging: 11 | print(*args) 12 | 13 | def dateToJson(dateObj): 14 | if isinstance(dateObj.dt, datetime.datetime): 15 | # { "dateTime": "2010-08-04T02:44:20.063Z" } 16 | dateTimeStr = dateObj.dt.isoformat() # 2014-10-02T18:00:00+00:00 17 | return { 'dateTime': dateTimeStr } 18 | else: # datetime 19 | # { "date": "2010-08-04" } 20 | dateStr = dateObj.dt.isoformat() 21 | return { 'date': dateStr } 22 | 23 | def eventsToJson(eventList=None, indent=4): 24 | if eventList is None: 25 | eventList = list(self.cal.walk('vevent')) 26 | 27 | data = {} 28 | data['items'] = [] 29 | for event in eventList: 30 | item = {} 31 | 32 | item['kind'] = 'calendar#event' 33 | item['etag'] = '\"0123456789012345\"' 34 | item['iCalUID'] = event['UID'] 35 | item['id'] = "ics_{}_{}_{}".format(item['iCalUID'], 36 | event['DTSTART'].dt.isoformat(), 37 | event['DTEND'].dt.isoformat() 38 | ) 39 | 40 | item['status'] = 'confirmed' # TODO: event['STATUS'] 41 | item['htmlLink'] = '' 42 | if 'CREATED' in event: 43 | item['created'] = event['CREATED'].dt.isoformat() 44 | if 'LAST-MODIFIED' in event: 45 | item['updated'] = event['LAST-MODIFIED'].dt.isoformat() 46 | 47 | item['summary'] = event['SUMMARY'] 48 | if 'LOCATION' in event: 49 | item['location'] = event['LOCATION'] 50 | 51 | item['start'] = dateToJson(event['DTSTART']) 52 | item['end'] = dateToJson(event['DTEND']) 53 | 54 | # item['transparency'] = event['TRANSP'] # 'transparent' 55 | # item['recurringEventId'] = '' 56 | 57 | data['items'].append(item) 58 | 59 | return json.dumps(data, indent=indent) 60 | 61 | def ensureDateTime(dt): 62 | if isinstance(dt, datetime.date): 63 | return datetime.datetime.combine(dt, datetime.time.min) 64 | else: 65 | return dt 66 | 67 | def eventWithin(event, startTime, endTime): 68 | eventStart = ensureDateTime(event['DTSTART'].dt) 69 | eventEnd = ensureDateTime(event['DTEND'].dt) 70 | startTime = ensureDateTime(startTime) 71 | endTime = ensureDateTime(endTime) 72 | # If it starts before endTime and it ends after startTime 73 | return eventStart <= endTime and eventEnd >= startTime 74 | 75 | class CalendarManager: 76 | def __init__(self, url): 77 | self.url = url 78 | self.cal = None 79 | 80 | def read(self): 81 | with urllib.request.urlopen(self.url) as sock: 82 | text = sock.read() 83 | self.cal = Calendar.from_ical(text) 84 | 85 | @property 86 | def events(self): 87 | return self.cal.walk('vevent') 88 | 89 | def query(self, startTime, endTime): 90 | for event in self.events: 91 | if eventWithin(event, startTime, endTime): 92 | debug("within", event['DTSTART'].dt, event['DTEND'].dt) 93 | yield event 94 | else: 95 | debug("out", event['DTSTART'].dt, event['DTEND'].dt) 96 | 97 | 98 | def toJson(self): 99 | return eventsToJson(self.events) 100 | 101 | 102 | def parseDate(dateStr): 103 | return datetime.datetime.strptime(dateStr, '%Y-%m-%d') 104 | 105 | def argparse_date(s): 106 | try: 107 | return parseDate(s) 108 | except ValueError: 109 | msg = "Not a valid date: '{0}'.".format(s) 110 | raise argparse.ArgumentTypeError(msg) 111 | 112 | if __name__ == '__main__': 113 | import argparse 114 | 115 | parser = argparse.ArgumentParser(description="calculate X to the power of Y") 116 | parser.add_argument("--url", type=str, required=True, help="The .ics file to read/write") 117 | subparsers = parser.add_subparsers(help='Commands', dest='subcommand') 118 | 119 | query = subparsers.add_parser('query') 120 | query.add_argument("startTime", type=argparse_date, help="Inclusive starting date in YYYY-MM-DD format") 121 | query.add_argument("endTime", type=argparse_date, help="Inclusive ending date in YYYY-MM-DD format") 122 | 123 | add = subparsers.add_parser('add') 124 | 125 | delete = subparsers.add_parser('delete') 126 | 127 | # debugging = True 128 | if debugging: 129 | args = parser.parse_args(['--url', 'basic.ics', 'query', '2016-09-15', '2016-09-16']) 130 | else: 131 | args = parser.parse_args() 132 | 133 | url = urllib.parse.urlparse(args.url, scheme='file').geturl() 134 | 135 | manager = CalendarManager(url) 136 | if args.subcommand == 'query': 137 | manager.read() 138 | eventList = manager.query(args.startTime, args.endTime) 139 | print(eventsToJson(eventList)) 140 | 141 | elif args.subcommand == 'add': 142 | pass 143 | elif args.subcommand == 'delete': 144 | pass 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /package/contents/scripts/notification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os, sys 3 | import argparse 4 | import subprocess 5 | from enum import Enum, IntEnum 6 | from ctypes import * 7 | 8 | import gi 9 | gi.require_version('GLib', '2.0') 10 | gi.require_version('Notify', '0.7') 11 | from gi.repository import GLib, Notify 12 | 13 | 14 | 15 | #--- 16 | # Recreate canberra-gtk-play in Python 17 | # https://git.0pointer.net/libcanberra.git/tree/src/canberra-gtk-play.c 18 | # Based on Dave Barry's pycanberra under LGPL 2.1 19 | # https://github.com/totdb/pycanberra/blob/master/pycanberra.py 20 | libcanberra = None 21 | try: 22 | libcanberra = CDLL("libcanberra.so.0") 23 | except: 24 | sys.stderr.write('libcanberra not found\n') 25 | 26 | def convertArgs(args): 27 | return ( 28 | arg.encode("utf-8") if isinstance(arg, str) else arg 29 | for arg in args 30 | ) 31 | 32 | ca_context = c_void_p 33 | 34 | class Canberra: 35 | @staticmethod 36 | def installed(): 37 | return libcanberra is not None 38 | 39 | class Code(IntEnum): 40 | SUCCESS = 0 41 | ERROR_NOTSUPPORTED = -1 42 | ERROR_INVALID = -2 43 | ERROR_STATE = -3 44 | ERROR_OOM = -4 45 | ERROR_NODRIVER = -5 46 | ERROR_SYSTEM = -6 47 | ERROR_CORRUPT = -7 48 | ERROR_TOOBIG = -8 49 | ERROR_NOTFOUND = -9 50 | ERROR_DESTROYED = -10 51 | ERROR_CANCELED = -11 52 | ERROR_NOTAVAILABLE = -12 53 | ERROR_ACCESS = -13 54 | ERROR_IO = -14 55 | ERROR_INTERNAL = -15 56 | ERROR_DISABLED = -16 57 | ERROR_FORKED = -17 58 | ERROR_DISCONNECTED = -18 59 | ERROR_MAX = -19 60 | 61 | class Prop(bytes, Enum): 62 | EVENT_ID = b'event.id' 63 | EVENT_DESCRIPTION = b'event.description' 64 | MEDIA_FILENAME = b'media.filename' 65 | APPLICATION_NAME = b'application.name' 66 | 67 | def __init__(self): 68 | self.context = ca_context() 69 | libcanberra.ca_context_create(byref(self.context)) 70 | 71 | def play(self, *props): 72 | playId = 0 73 | res = libcanberra.ca_context_play( 74 | self.context, 75 | playId, 76 | *convertArgs(props), 77 | None, # Must end with NULL to mark end of props 78 | ) 79 | if res != Canberra.Code.SUCCESS: 80 | raise Exception(Canberra.Code(res), "Error playing", props) 81 | 82 | def playEvent(self, eventId, *props): 83 | props += (Canberra.Prop.EVENT_ID, eventId) 84 | self.play(*props) 85 | 86 | def playFile(self, filename, *props): 87 | props += (Canberra.Prop.MEDIA_FILENAME, filename) 88 | self.play(*props) 89 | 90 | 91 | #--- 92 | # Plasma's Notification server doesn't support sounds, 93 | # the KNotify manually plays sounds instead. So we manually 94 | # play them with libcanberra. We can't use canberra-gtk-play since 95 | # it requires the gnome-session-canberra package in Ubuntu, 96 | # which is not installed by default. 97 | def playSound(args): 98 | if not Canberra.installed(): 99 | sys.stderr.write('skipping playing sound\n') 100 | return 101 | 102 | canberra = Canberra() 103 | props = [ 104 | Canberra.Prop.EVENT_DESCRIPTION, args.appName, 105 | Canberra.Prop.APPLICATION_NAME, args.appName, 106 | ] 107 | 108 | if args.sound.startswith('file://'): 109 | args.sound = args.sound[len('file://'):] 110 | 111 | if args.sound.startswith('/'): 112 | canberra.playFile(args.sound, *props) 113 | else: 114 | canberra.playEvent(args.sound, *props) 115 | 116 | 117 | if args.loop: 118 | for i in range(args.loop): 119 | # TODO: wait for playEffect to end. 120 | # TODO: wrap playEffect in a function, and call it here. 121 | pass 122 | 123 | 124 | #--- 125 | def notify(args): 126 | sfxProc = None 127 | 128 | #--- Notification 129 | # https://notify2.readthedocs.io/en/latest/ 130 | loop = GLib.MainLoop() 131 | Notify.init(args.appName) 132 | # print(Notify.get_server_caps()) 133 | 134 | n = Notify.Notification.new( 135 | args.summary, 136 | args.message, 137 | icon=args.icon, 138 | ) 139 | 140 | # Note: EXPIRES_DEFAULT = -1, EXPIRES_NEVER = 0 141 | n.set_timeout(args.timeout) 142 | 143 | def on_action(notification, action, *user_data): 144 | sys.stdout.write(' '.join([action, *user_data]) + '\n') 145 | if sfxProc: 146 | sfxProc.terminate() 147 | loop.quit() 148 | 149 | def closed(notification): 150 | on_action(notification, "closed") 151 | 152 | n.connect("closed", closed) 153 | n.add_action("default", "default", on_action) 154 | if args.actions: 155 | for action in args.actions: 156 | actionId, actionLabel = action.split(',', 1) 157 | n.add_action(actionId, actionLabel, on_action) 158 | n.show() 159 | 160 | #--- Sound 161 | if args.sound: 162 | playSound(args) 163 | 164 | loop.run() 165 | 166 | def main(): 167 | parser = argparse.ArgumentParser(prog='notification.py', description='Notifications with sound effects and actions.') 168 | parser.add_argument('summary') 169 | parser.add_argument('message') 170 | parser.add_argument('--icon', default='') 171 | parser.add_argument('--app-name', dest='appName', default='Event Calendar') 172 | parser.add_argument('--sound') 173 | parser.add_argument('--loop') 174 | parser.add_argument('--timeout', type=int, default=Notify.EXPIRES_DEFAULT) 175 | parser.add_argument('--action', dest='actions', action='append') 176 | parser.add_argument('--metadata') 177 | 178 | 179 | try: 180 | args = parser.parse_args() 181 | notify(args) 182 | except KeyboardInterrupt: 183 | pass 184 | except Exception as e: 185 | sys.stderr.write('{}\n'.format(e)) 186 | parser.print_help() 187 | 188 | def test(): 189 | notify(argparse.Namespace( 190 | summary='Summary', 191 | message='Message', 192 | icon='plasma', 193 | appName='Plasma', 194 | sound='complete', 195 | loop=False, 196 | actions=[ 197 | 'ok,Ok', 198 | 'cancel,Cancel', 199 | ], 200 | )) 201 | 202 | if __name__ == '__main__': 203 | main() 204 | # test() 205 | 206 | 207 | -------------------------------------------------------------------------------- /package/contents/ui/AppletConfig.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | import "lib" 5 | import "lib/ColorUtil.js" as ColorUtil 6 | 7 | QtObject { 8 | id: config 9 | 10 | property bool showIconOutline: plasmoid.configuration.showOutlines 11 | 12 | property color alternateBackgroundColor: { 13 | var textColor = PlasmaCore.ColorScope.textColor 14 | var bgColor = theme.buttonBackgroundColor 15 | if (ColorUtil.hasEnoughContrast(textColor, bgColor)) { 16 | return bgColor 17 | } else { 18 | // 10% of Text color should be a large enough contrast 19 | return ColorUtil.setAlpha(textColor, 0.1) 20 | } 21 | } 22 | 23 | property color meteogramTextColorDefault: theme.textColor 24 | property color meteogramScaleColorDefault: ColorUtil.lerp(theme.backgroundColor, theme.textColor, 0.9) 25 | property color meteogramPrecipitationRawColorDefault: "#acd" 26 | property color meteogramPositiveTempColorDefault: "#900" 27 | property color meteogramNegativeTempColorDefault: "#369" 28 | property color meteogramIconColorDefault: theme.textColor 29 | 30 | property color meteogramTextColor: plasmoid.configuration.meteogramTextColor || meteogramTextColorDefault 31 | property color meteogramScaleColor: plasmoid.configuration.meteogramGridColor || meteogramScaleColorDefault 32 | property color meteogramPrecipitationRawColor: plasmoid.configuration.meteogramRainColor || meteogramPrecipitationRawColorDefault 33 | property color meteogramPrecipitationColor: ColorUtil.setAlpha(meteogramPrecipitationRawColor, 0.6) 34 | property color meteogramPrecipitationTextColor: Qt.tint(meteogramTextColor, ColorUtil.setAlpha(meteogramPrecipitationRawColor, 0.3)) 35 | property color meteogramPrecipitationTextOutlineColor: showIconOutline ? theme.backgroundColor : "transparent" 36 | property color meteogramPositiveTempColor: plasmoid.configuration.meteogramPositiveTempColor || meteogramPositiveTempColorDefault 37 | property color meteogramNegativeTempColor: plasmoid.configuration.meteogramNegativeTempColor || meteogramNegativeTempColorDefault 38 | property color meteogramIconColor: plasmoid.configuration.meteogramIconColor || meteogramIconColorDefault 39 | 40 | property color agendaHoverBackground: alternateBackgroundColor 41 | property color agendaInProgressColorDefault: theme.highlightColor 42 | property color agendaInProgressColor: plasmoid.configuration.agendaInProgressColor || agendaInProgressColorDefault 43 | 44 | property int agendaColumnSpacing: 10 * units.devicePixelRatio 45 | property int agendaDaySpacing: plasmoid.configuration.agendaDaySpacing * units.devicePixelRatio 46 | property int agendaEventSpacing: plasmoid.configuration.agendaEventSpacing * units.devicePixelRatio 47 | property int agendaWeatherColumnWidth: 60 * units.devicePixelRatio 48 | property int agendaWeatherIconSize: plasmoid.configuration.agendaWeatherIconHeight * units.devicePixelRatio 49 | property int agendaDateColumnWidth: 50 * units.devicePixelRatio + agendaColumnSpacing * 2 50 | property int eventIndicatorWidth: 2 * units.devicePixelRatio 51 | 52 | property int agendaFontSize: plasmoid.configuration.agendaFontSize === 0 ? theme.defaultFont.pixelSize : plasmoid.configuration.agendaFontSize * units.devicePixelRatio 53 | 54 | property int timerClockFontHeight: 40 * units.devicePixelRatio 55 | property int timerButtonWidth: 48 * units.devicePixelRatio 56 | 57 | property int meteogramIconSize: 24 * units.devicePixelRatio 58 | property int meteogramColumnWidth: 32 * units.devicePixelRatio // weatherIconSize = 32px (height = 24px but most icons are landscape) 59 | 60 | property QtObject icalCalendarList: Base64Json { 61 | configKey: 'icalCalendarList' 62 | } 63 | 64 | property ListModel icalCalendarListModel: Base64JsonListModel { 65 | configKey: 'icalCalendarList' 66 | } 67 | 68 | readonly property string clockFontFamily: plasmoid.configuration.clockFontFamily || theme.defaultFont.family 69 | 70 | readonly property int lineWeight1: plasmoid.configuration.clockLineBold1 ? Font.Bold : Font.Normal 71 | readonly property int lineWeight2: plasmoid.configuration.clockLineBold2 ? Font.Bold : Font.Normal 72 | 73 | readonly property string localeTimeFormat: Qt.locale().timeFormat(Locale.ShortFormat) 74 | readonly property string localeDateFormat: Qt.locale().dateFormat(Locale.ShortFormat) 75 | readonly property string line1TimeFormat: plasmoid.configuration.clockTimeFormat1 || localeTimeFormat 76 | readonly property string line2TimeFormat: plasmoid.configuration.clockTimeFormat2 || localeDateFormat 77 | readonly property string combinedFormat: { 78 | if (plasmoid.configuration.clockShowLine2) { 79 | return line1TimeFormat + '\n' + line2TimeFormat 80 | } else { 81 | return line1TimeFormat 82 | } 83 | } 84 | readonly property bool clock24h: { 85 | var is12hour = combinedFormat.toLowerCase().indexOf('ap') >= 0 86 | return !is12hour 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /package/contents/ui/CalendarSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | import org.kde.plasma.components 3.0 as PlasmaComponents3 4 | 5 | PlasmaComponents3.ComboBox { 6 | id: calendarSelector 7 | model: [ 8 | { text: i18n("[No Calendars]") } 9 | ] 10 | textRole: "text" 11 | 12 | readonly property var selectedCalendar: currentIndex >= 0 ? model[currentIndex] : null 13 | readonly property var selectedCalendarId: selectedCalendar ? selectedCalendar.id : null 14 | readonly property bool selectedIsTasklist: selectedCalendar ? selectedCalendar.isTasklist : false 15 | 16 | function populate(calendarList, initialCalendarId) { 17 | // logger.debug('CalendarSelector.populate') 18 | // logger.debugJSON('calendarList', calendarList) 19 | var list = [] 20 | var selectedIndex = 0 21 | calendarList.forEach(function(calendar){ 22 | var canEditCalendar = calendar.accessRole === 'writer' || calendar.accessRole === 'owner' 23 | var isSelected = calendar.id === initialCalendarId 24 | 25 | if (isSelected) { 26 | selectedIndex = list.length // set index after insertion 27 | } 28 | 29 | if (canEditCalendar || isSelected) { 30 | list.push({ 31 | 'calendarId': calendar.id, 32 | 'text': calendar.summary, 33 | 'backgroundColor': calendar.backgroundColor, 34 | 'isTasklist': calendar.isTasklist, 35 | }) 36 | } 37 | }) 38 | if (list.length === 0) { 39 | list.push({ text: i18n("[No Calendars]") }) 40 | } 41 | calendarSelector.model = list 42 | calendarSelector.currentIndex = selectedIndex 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package/contents/ui/ConfigMigration.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "./calendars/PlasmaCalendarUtils.js" as PlasmaCalendarUtils 4 | 5 | QtObject { 6 | signal migrate() 7 | 8 | function copy(oldKey, newKey) { 9 | if (typeof plasmoid.configuration[oldKey] === 'undefined') return 10 | if (typeof plasmoid.configuration[newKey] === 'undefined') return 11 | if (plasmoid.configuration[oldKey] === plasmoid.configuration[newKey]) return 12 | plasmoid.configuration[newKey] = plasmoid.configuration[oldKey] 13 | console.log('[eventcalendar:migrate] copy ' + oldKey + ' => ' + newKey + ' (value: ' + plasmoid.configuration[oldKey] + ')') 14 | } 15 | 16 | Component.onCompleted: migrate() 17 | onMigrate: { 18 | // Modified in: v72 19 | if (!plasmoid.configuration.v72Migration) { 20 | var oldValue = plasmoid.configuration.enabledCalendarPlugins 21 | var newValue = PlasmaCalendarUtils.pluginPathToFilenameList(plasmoid.configuration.enabledCalendarPlugins) 22 | plasmoid.configuration.enabledCalendarPlugins = newValue 23 | console.log('[eventcalendar:migrate] convert enabledCalendarPlugins (' + oldValue + ' => ' + newValue + ')') 24 | 25 | plasmoid.configuration.v72Migration = true 26 | } 27 | 28 | // Renamed in: v71 29 | if (!plasmoid.configuration.v71Migration) { 30 | copy('widget_show_meteogram', 'widgetShowMeteogram') 31 | copy('widget_show_timer', 'widgetShowTimer') 32 | copy('widget_show_agenda', 'widgetShowAgenda') 33 | copy('widget_show_calendar', 'widgetShowCalendar') 34 | copy('timer_sfx_enabled', 'timerSfxEnabled') 35 | copy('timer_sfx_filepath', 'timerSfxFilepath') 36 | copy('timer_repeats', 'timerRepeats') 37 | copy('clock_fontfamily', 'clockFontFamily') 38 | copy('clock_timeformat', 'clockTimeFormat1') 39 | copy('clock_timeformat_2', 'clockTimeFormat2') 40 | copy('clock_line_2', 'clockShowLine2') 41 | copy('clock_line_2_height_ratio', 'clockLine2HeightRatio') 42 | copy('clock_line_1_bold', 'clockLineBold1') 43 | copy('clock_line_2_bold', 'clockLineBold2') 44 | copy('clock_maxheight', 'clockMaxHeight') 45 | copy('clock_mousewheel_up', 'clockMouseWheelUp') 46 | copy('clock_mousewheel_down', 'clockMouseWheelDown') 47 | copy('show_outlines', 'showOutlines') 48 | 49 | copy('month_show_border', 'monthShowBorder') 50 | copy('month_show_weeknumbers', 'monthShowWeekNumbers') 51 | copy('month_eventbadge_type', 'monthEventBadgeType') 52 | copy('month_today_style', 'monthTodayStyle') 53 | copy('month_cell_radius', 'monthCellRadius') 54 | 55 | copy('agenda_newevent_remember_calendar', 'agendaNewEventRememberCalendar') 56 | copy('agenda_newevent_last_calendar_id', 'agendaNewEventLastCalendarId') 57 | copy('agenda_weather_show_icon', 'agendaWeatherShowIcon') 58 | copy('agenda_weather_icon_height', 'agendaWeatherIconHeight') 59 | copy('agenda_weather_show_text', 'agendaWeatherShowText') 60 | copy('agenda_breakup_multiday_events', 'agendaBreakupMultiDayEvents') 61 | copy('agenda_inProgressColor', 'agendaInProgressColor') 62 | copy('agenda_fontSize', 'agendaFontSize') 63 | 64 | copy('events_pollinterval', 'eventsPollInterval') 65 | 66 | copy('weather_app_id', 'openWeatherMapAppId') 67 | copy('weather_city_id', 'openWeatherMapCityId') 68 | copy('weather_canada_city_id', 'weatherCanadaCityId') 69 | copy('weather_service', 'weatherService') 70 | copy('weather_units', 'weatherUnits') 71 | copy('meteogram_hours', 'meteogramHours') 72 | copy('meteogram_textColor', 'meteogramTextColor') 73 | copy('meteogram_gridColor', 'meteogramGridColor') 74 | copy('meteogram_rainColor', 'meteogramRainColor') 75 | copy('meteogram_positiveTempColor', 'meteogramPositiveTempColor') 76 | copy('meteogram_negativeTempColor', 'meteogramNegativeTempColor') 77 | copy('meteogram_iconColor', 'meteogramIconColor') 78 | 79 | plasmoid.configuration.v71Migration = true 80 | } 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /package/contents/ui/DateSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Window 2.2 3 | 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.components 3.0 as PlasmaComponents3 6 | 7 | import QtQuick.Templates 2.1 as T 8 | import QtQuick.Controls 2.1 as Controls 9 | import QtGraphicalEffects 1.0 // DropShadow 10 | 11 | // Based on: 12 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/plasmacomponents3/ComboBox.qml 13 | // https://doc.qt.io/archives/qt-5.11/qml-qtquick-controls2-combobox.html 14 | // https://github.com/qt/qtquickcontrols2/blob/dev/src/quicktemplates2/qquickcombobox.cpp 15 | 16 | PlasmaComponents3.TextField { 17 | id: dateSelector 18 | readonly property Item control: dateSelector 19 | 20 | property int defaultMinimumWidth: 80 * units.devicePixelRatio 21 | readonly property int implicitContentWidth: contentWidth + leftPadding + rightPadding 22 | implicitWidth: Math.max(defaultMinimumWidth, implicitContentWidth) 23 | 24 | property var dateTime: new Date() 25 | property var dateFormat: Locale.ShortFormat 26 | 27 | signal dateTimeShifted(date oldDateTime, int deltaDateTime, date newDateTime) 28 | signal dateSelected(date newDateTime) 29 | 30 | function setDateTime(dt) { 31 | var oldDateTime = new Date(dateTime) 32 | 33 | var newDateTime = new Date(dt) 34 | newDateTime.setHours(oldDateTime.getHours()) 35 | newDateTime.setMinutes(oldDateTime.getMinutes()) 36 | 37 | var deltaDateTime = newDateTime.valueOf() - oldDateTime.valueOf() 38 | dateTimeShifted(oldDateTime, deltaDateTime, newDateTime) 39 | } 40 | function updateText() { 41 | text = Qt.binding(function(){ 42 | return dateSelector.dateTime.toLocaleDateString(Qt.locale(), dateSelector.dateFormat) 43 | }) 44 | } 45 | 46 | onPressed: popup.open() 47 | 48 | onDateSelected: { 49 | setDateTime(newDateTime) 50 | } 51 | 52 | onTextEdited: { 53 | var dt = Date.fromLocaleDateString(Qt.locale(), text, dateSelector.dateFormat) 54 | // console.log('onTextEdited', text, dt) 55 | if (!isNaN(dt)) { 56 | setDateTime(dt) 57 | } 58 | } 59 | 60 | onEditingFinished: updateText() 61 | Component.onCompleted: updateText() 62 | 63 | property T.Popup popup: T.Popup { 64 | x: control.mirrored ? control.width - width : 0 65 | y: control.height 66 | 67 | implicitWidth: contentItem.implicitWidth 68 | implicitHeight: contentItem.implicitHeight 69 | 70 | topMargin: 6 * units.devicePixelRatio 71 | bottomMargin: 6 * units.devicePixelRatio 72 | 73 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/calendar/qml/MonthView.qml 74 | contentItem: MonthView { 75 | id: dateSelectorMonthView 76 | 77 | implicitWidth: 280 * units.devicePixelRatio 78 | implicitHeight: 280 * units.devicePixelRatio 79 | 80 | today: new Date() 81 | currentDate: dateSelector.dateTime 82 | displayedDate: dateSelector.dateTime 83 | 84 | showTooltips: false 85 | showTodaysDate: false 86 | headingFontLevel: 3 87 | 88 | onDateClicked: { 89 | // console.log('onDateSelected', currentDate, '(popup.visible: ', popup.visible, ')') 90 | dateSelector.dateSelected(clickedDate) 91 | popup.close() 92 | } 93 | } 94 | 95 | background: Rectangle { 96 | anchors { 97 | fill: parent 98 | margins: -1 99 | } 100 | radius: 2 101 | color: theme.viewBackgroundColor 102 | border.color: Qt.rgba(theme.textColor.r, theme.textColor.g, theme.textColor.b, 0.3) 103 | layer.enabled: true 104 | 105 | layer.effect: DropShadow { 106 | transparentBorder: true 107 | radius: 4 108 | samples: 8 109 | horizontalOffset: 2 110 | verticalOffset: 2 111 | color: Qt.rgba(0, 0, 0, 0.3) 112 | } 113 | } 114 | } // Popup 115 | } 116 | -------------------------------------------------------------------------------- /package/contents/ui/DateTimeSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.4 3 | import QtQuick.Layouts 1.1 4 | import QtQuick.Window 2.2 5 | 6 | import org.kde.plasma.core 2.0 as PlasmaCore 7 | 8 | GridLayout { 9 | id: dateTimeSelector 10 | property var dateTime: new Date() 11 | property bool enabled: true 12 | property bool showTime: true 13 | property alias dateFormat: dateSelector.dateFormat 14 | property alias timeFormat: timeSelector.timeFormat 15 | property bool dateFirst: true 16 | columns: 2 17 | columnSpacing: units.smallSpacing 18 | readonly property int minimumWidth: dateSelector.implicitWidth + columnSpacing + timeSelector.implicitWidth 19 | 20 | signal dateTimeShifted(date oldDateTime, int deltaDateTime, date newDateTime) 21 | onDateTimeShifted: { 22 | dateTimeSelector.dateTime = newDateTime 23 | } 24 | 25 | DateSelector { 26 | id: dateSelector 27 | enabled: dateTimeSelector.enabled 28 | // opacity: 1 // Override disabled opacity effect. 29 | Layout.column: dateTimeSelector.dateFirst ? 0 : 1 30 | 31 | dateTime: dateTimeSelector.dateTime 32 | dateFormat: i18nc("event editor date format", "d MMM, yyyy") 33 | 34 | onDateTimeShifted: { 35 | dateTimeSelector.dateTimeShifted(oldDateTime, deltaDateTime, newDateTime) 36 | } 37 | } 38 | 39 | TimeSelector { 40 | id: timeSelector 41 | enabled: dateTimeSelector.enabled && dateTimeSelector.showTime 42 | // opacity: 1 // Override disabled opacity effect. 43 | visible: dateTimeSelector.showTime 44 | Layout.column: dateTimeSelector.dateFirst ? 1 : 0 45 | 46 | dateTime: dateTimeSelector.dateTime 47 | 48 | onDateTimeShifted: { 49 | dateTimeSelector.dateTimeShifted(oldDateTime, deltaDateTime, newDateTime) 50 | } 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /package/contents/ui/DurationSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.4 3 | import QtQuick.Layouts 1.1 4 | 5 | import org.kde.plasma.core 2.0 as PlasmaCore 6 | import org.kde.plasma.components 3.0 as PlasmaComponents3 7 | 8 | Flow { 9 | id: durationSelector 10 | 11 | property alias startTimeSelector: startTimeSelector 12 | property alias endTimeSelector: endTimeSelector 13 | 14 | property alias startDateTime: startTimeSelector.dateTime 15 | property alias endDateTime: endTimeSelector.dateTime 16 | 17 | property bool enabled: true 18 | property bool showTime: false 19 | 20 | spacing: 0 21 | // Layout.minimumWidth: startTimeSelector.minimumWidth + seperatorLabel.implicitWidth + endTimeSelector.minimumWidth 22 | 23 | DateTimeSelector { 24 | id: startTimeSelector 25 | enabled: durationSelector.enabled 26 | showTime: durationSelector.showTime 27 | dateFirst: true 28 | 29 | onDateTimeShifted: { 30 | logger.debug('onDateTimeShifted') 31 | logger.debug(' dt1', oldDateTime) 32 | logger.debug(' dt2', dateTime) 33 | logger.debug(' delta', deltaDateTime) 34 | 35 | var shiftedEndDate = new Date(endTimeSelector.dateTime.valueOf() + deltaDateTime) 36 | logger.debug(' t3', shiftedEndDate) 37 | endTimeSelector.dateTime = shiftedEndDate 38 | } 39 | } 40 | PlasmaComponents3.Label { 41 | id: seperatorLabel 42 | text: ' ' + i18n("to") + ' ' 43 | font.weight: Font.Bold 44 | verticalAlignment: Text.AlignVCenter 45 | height: startTimeSelector.implicitHeight 46 | } 47 | DateTimeSelector { 48 | id: endTimeSelector 49 | enabled: durationSelector.enabled 50 | showTime: durationSelector.showTime 51 | dateFirst: false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package/contents/ui/ErrorType.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | // Since we use the ErrorType enum outside a single class and it's 4 | // dependencies, we can't use QML's enums. 5 | 6 | var NoError = 0 7 | var NetworkError = 1 8 | var ClientError = 2 9 | var ServerError = 3 10 | var UnknownError = 4 11 | -------------------------------------------------------------------------------- /package/contents/ui/EventPropertyIcon.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.1 3 | import org.kde.plasma.core 2.0 as PlasmaCore 4 | 5 | ColumnLayout { 6 | id: eventDialogIcon 7 | Layout.fillHeight: true 8 | 9 | property alias source: iconItem.source 10 | property int size: units.iconSizes.smallMedium 11 | 12 | PlasmaCore.IconItem { 13 | id: iconItem 14 | Layout.alignment: Qt.AlignVCenter 15 | 16 | implicitWidth: eventDialogIcon.size 17 | implicitHeight: eventDialogIcon.size 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package/contents/ui/FontIcon.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.2 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | import org.kde.plasma.components 3.0 as PlasmaComponents3 4 | 5 | // Technique based on plasma-applet-weather-widget 6 | // https://github.com/kotelnik/plasma-applet-weather-widget/blob/320ed5661475f176116e1785476dc51710494b86/package/contents/code/icons.js 7 | Item { 8 | width: 16 9 | height: 16 10 | property string source: "" 11 | property alias color: iconText.color 12 | property bool showOutline: true 13 | 14 | // FontLoader { 15 | // source: "../fonts/weathericons-regular-webfont.ttf" 16 | // } 17 | 18 | PlasmaComponents3.Label { 19 | id: iconText 20 | text: '' 21 | color: PlasmaCore.ColorScope.textColor 22 | style: showOutline ? Text.Outline : Text.Normal 23 | styleColor: PlasmaCore.ColorScope.backgroundColor 24 | 25 | font.family: "weathericons" 26 | font.pointSize: -1 27 | font.pixelSize: parent.height 28 | anchors.centerIn: parent 29 | } 30 | 31 | // https://erikflowers.github.io/weather-icons/ 32 | function getIconCode(name) { 33 | var codeByName = { 34 | 'question': '?', 35 | 'weather-clear': '\uf00d', 36 | 'weather-clear-night': '\uf02e', // wi-day-sunny 37 | 'weather-clouds': '\uf041', // wi-cloud 38 | 'weather-clouds-night': '\uf041', // wi-cloud 39 | 'weather-few-clouds': '\uf002', // wi-day-cloudy 40 | 'weather-few-clouds-night': '\uf086', // wi-night-alt-cloudy 41 | 'weather-fog': '\uf014', // wi-fog 42 | 'weather-freezing-rain': '\uf0b5', // wi-sleet 43 | 'weather-hail': '\uf015', // wi-hail 44 | 'weather-overcast': '\uf013', // wi-cloudy 45 | 'weather-showers': '\uf019', // wi-rain 46 | 'weather-showers-night': '\uf019', // wi-rain 47 | 'weather-showers-scattered': '\uf009', // wi-day-showers 48 | 'weather-showers-scattered-night': '\uf029', // wi-night-alt-showers 49 | 'weather-snow': '\uf01b', // wi-snow 50 | 'weather-snow-rain': '\uf006', // wi-day-rain-mix 51 | 'weather-snow-rain-night': '\uf034', // wi-night-rain-mix 52 | 'weather-snow-scattered-day': '\uf00a', // wi-day-snow 53 | 'weather-snow-scattered-night': '\uf038', // wi-night-snow 54 | 'weather-storm': '\uf01e', // wi-thunderstorm 55 | 'weather-storm-night': '\uf025', // wi-night-alt-lightning 56 | 'wi-dust': '\uf063', // wi-dust 57 | 'wi-sandstorm': '\uf082', // wi-sandstorm 58 | 'wi-smoke': '\uf062', // wi-smoke 59 | 'wi-tornado': '\uf056', // wi-tornado 60 | 'wi-windy': '\uf021', // wi-windy 61 | } 62 | return codeByName[name] 63 | } 64 | 65 | function setIcon() { 66 | if (!source) { 67 | return 68 | } 69 | 70 | var code = getIconCode(source); 71 | iconText.text = code ? code : '' 72 | if (!code) { 73 | console.log('missing fontIcon', source) 74 | } 75 | } 76 | 77 | onSourceChanged: { 78 | setIcon() 79 | } 80 | 81 | Component.onCompleted: { 82 | setIcon() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /package/contents/ui/LinkRect.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | import "lib" 5 | 6 | Rectangle { 7 | id: linkRect 8 | width: implicitWidth 9 | height: implicitHeight 10 | implicitWidth: childrenRect.width 11 | implicitHeight: childrenRect.height 12 | property color backgroundColor: "transparent" 13 | property color backgroundHoverColor: appletConfig.agendaHoverBackground 14 | color: enabled && hovered ? backgroundHoverColor : backgroundColor 15 | property string tooltipMainText 16 | property string tooltipSubText 17 | property alias acceptedButtons: mouseArea.acceptedButtons 18 | property bool enabled: true 19 | readonly property alias hovered: mouseArea.containsMouse 20 | 21 | signal clicked(var mouse) 22 | signal leftClicked(var mouse) 23 | signal doubleClicked(var mouse) 24 | signal loadContextMenu(var contextMenu) 25 | 26 | PlasmaCore.ToolTipArea { 27 | id: tooltip 28 | anchors.fill: parent 29 | mainText: linkRect.tooltipMainText 30 | subText: linkRect.tooltipSubText 31 | 32 | MouseArea { 33 | id: mouseArea 34 | anchors.fill: parent 35 | hoverEnabled: true 36 | acceptedButtons: Qt.LeftButton | Qt.RightButton 37 | cursorShape: linkRect.enabled && containsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor 38 | enabled: linkRect.enabled 39 | onClicked: { 40 | mouse.accepted = false 41 | linkRect.clicked(mouse) 42 | if (!mouse.accepted) { 43 | if (mouse.button == Qt.LeftButton) { 44 | linkRect.leftClicked(mouse) 45 | } else if (mouse.button == Qt.RightButton) { 46 | contextMenu.show(mouse.x, mouse.y) 47 | mouse.accepted = true 48 | } 49 | } 50 | } 51 | onDoubleClicked: linkRect.doubleClicked(mouse) 52 | } 53 | } 54 | 55 | ContextMenu { 56 | id: contextMenu 57 | onPopulate: linkRect.loadContextMenu(contextMenu) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package/contents/ui/LinkText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import org.kde.plasma.core 2.0 as PlasmaCore 4 | 5 | Label { 6 | linkColor: PlasmaCore.ColorScope.highlightColor 7 | onLinkActivated: Qt.openUrlExternally(link) 8 | MouseArea { 9 | anchors.fill: parent 10 | acceptedButtons: Qt.NoButton // we don't want to eat clicks on the Text 11 | cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package/contents/ui/LocaleFuncs.js: -------------------------------------------------------------------------------- 1 | .import "Shared.js" as Shared 2 | 3 | function formatEventTime(dateTime, args) { 4 | var clock24h = args && args.clock24h 5 | var timeFormat 6 | if (clock24h) { 7 | if (dateTime.getMinutes() === 0) { 8 | timeFormat = i18nc("event time on the hour (24 hour clock)", "h") 9 | } else { 10 | timeFormat = i18nc("event time (24 hour clock)", "h:mm") 11 | } 12 | } else { // 12h 13 | if (dateTime.getMinutes() === 0) { 14 | timeFormat = i18nc("event time on the hour (12 hour clock)", "h AP") 15 | } else { 16 | timeFormat = i18nc("event time (12 hour clock)", "h:mm AP") 17 | } 18 | } 19 | return Qt.formatDateTime(dateTime, timeFormat) 20 | } 21 | 22 | function formatEventDateTime(dateTime, args) { 23 | var shortDateFormat = i18nc("short month+date format", "MMM d") 24 | var dateStr = Qt.formatDateTime(dateTime, shortDateFormat) 25 | var timeStr = formatEventTime(dateTime, args) 26 | return i18nc("date (%1) with time (%2)", "%1, %2", dateStr, timeStr) 27 | } 28 | 29 | function formatEventDuration(event, args) { 30 | var relativeDate = args && args.relativeDate 31 | var clock24h = args && args.clock24h 32 | var startTime = event.startDateTime 33 | var endTime = event.endDateTime 34 | var shortDateFormat = i18nc("short month+date format", "MMM d") 35 | 36 | if (event.start.date) { 37 | // GCal ends all day events at midnight, which is technically the next day. 38 | // Humans consider the event to end at 23:59 the day before though. 39 | var dayBefore = new Date(endTime) 40 | dayBefore.setDate(dayBefore.getDate() - 1) 41 | if (Shared.isSameDate(startTime, dayBefore)) { 42 | return i18n("All Day") 43 | } else { 44 | var startStr = Qt.formatDateTime(startTime, shortDateFormat) 45 | var endStr = Qt.formatDateTime(dayBefore, shortDateFormat) 46 | return i18nc("from date/time %1 until date/time %2", "%1 - %2", startStr, endStr) 47 | } 48 | } else { 49 | var startStr 50 | if (!relativeDate || !Shared.isSameDate(startTime, relativeDate)) { 51 | startStr = formatEventDateTime(startTime, args) // MMM d, h:mm AP 52 | } else { 53 | startStr = formatEventTime(startTime, args) // h:mm AP 54 | } 55 | 56 | if (startTime.valueOf() === endTime.valueOf()) { 57 | return startStr // Don't need the end time 58 | } 59 | 60 | var endStr 61 | if (Shared.isSameDate(startTime, endTime)) { 62 | endStr = formatEventTime(endTime, args) // MMM d, h:mm AP - h:mm AP 63 | } else { 64 | // !isSameDate, so we need to add the date 65 | endStr = formatEventDateTime(endTime, args) // MMM d, h:mm AP - MMM d, h:mm AP 66 | } 67 | return i18nc("from date/time %1 until date/time %2", "%1 - %2", startStr, endStr) 68 | } 69 | } 70 | 71 | function getHours(t) { 72 | var hours = Math.floor(t / (60 * 60 * 1000)) 73 | return hours 74 | } 75 | function getMinutes(t) { 76 | var millisLeftInHour = t % (60 * 60 * 1000) 77 | var minutes = millisLeftInHour / (60 * 1000) 78 | return minutes 79 | } 80 | function getSeconds(t) { 81 | var millisLeftInMinute = t % (60 * 1000) 82 | var seconds = millisLeftInMinute / 1000 83 | return seconds 84 | } 85 | function durationShortFormat(nSeconds) { 86 | var t = nSeconds * 1000 87 | var str = '' 88 | var hours = Math.floor(getHours(t)) 89 | if (hours > 0) { 90 | str += i18nc("short form for %1 hours", "%1h", hours) 91 | } 92 | var minutes = Math.floor(getMinutes(t)) 93 | if (minutes > 0) { 94 | str += i18nc("short form for %1 minutes", "%1m", minutes) 95 | } 96 | var seconds = Math.floor(getSeconds(t)) 97 | if (seconds > 0) { 98 | str += i18nc("short form for %1 seconds", "%1s", seconds) 99 | } 100 | return str 101 | } 102 | -------------------------------------------------------------------------------- /package/contents/ui/NetworkMonitor.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | // import org.kde.plasma.networkmanagement 0.2 as PlasmaNM 3 | 4 | QtObject { 5 | id: networkMonitor 6 | 7 | // https://invent.kde.org/plasma/plasma-nm 8 | // readonly property var plasmaNMStatus: PlasmaNM.NetworkStatus { 9 | // id: plasmaNMStatus 10 | // // onActiveConnectionsChanged: logger.debug('NetworkStatus.activeConnections', activeConnections) 11 | // onNetworkStatusChanged: logger.debug('NetworkStatus.networkStatus', networkStatus) 12 | // Component.onCompleted: { 13 | // // logger.debug('NetworkStatus.activeConnections', activeConnections) 14 | // logger.debug('NetworkStatus.networkStatus', networkStatus) 15 | // } 16 | // } 17 | // readonly property var plasmaNMIcon: PlasmaNM.ConnectionIcon { 18 | // id: plasmaNMIcon 19 | // onConnectingChanged: logger.debug('ConnectionIcon.connecting', connecting) 20 | // onConnectionIconChanged: logger.debug('ConnectionIcon.connectionIcon', connectionIcon) 21 | // onConnectionTooltipIconChanged: logger.debug('ConnectionIcon.connectionTooltipIcon', connectionTooltipIcon) 22 | // onNeedsPortalChanged: logger.debug('ConnectionIcon.needsPortal', needsPortal) 23 | // Component.onCompleted: { 24 | // logger.debug('ConnectionIcon.connecting', connecting) 25 | // logger.debug('ConnectionIcon.connectionIcon', connectionIcon) 26 | // logger.debug('ConnectionIcon.connectionTooltipIcon', connectionTooltipIcon) 27 | // logger.debug('ConnectionIcon.needsPortal', needsPortal) 28 | // } 29 | // } 30 | // readonly property var plasmaNMAvailableDevices: PlasmaNM.AvailableDevices { 31 | // id: plasmaNMAvailableDevices 32 | // onWiredDeviceAvailableChanged: logger.debug('AvailableDevices.wiredDeviceAvailable', wiredDeviceAvailable) 33 | // onWirelessDeviceAvailableChanged: logger.debug('AvailableDevices.wirelessDeviceAvailable', wirelessDeviceAvailable) 34 | // onModemDeviceAvailableChanged: logger.debug('AvailableDevices.modemDeviceAvailable', modemDeviceAvailable) 35 | // onBluetoothDeviceAvailableChanged: logger.debug('AvailableDevices.bluetoothDeviceAvailable', bluetoothDeviceAvailable) 36 | // Component.onCompleted: { 37 | // logger.debug('AvailableDevices.wiredDeviceAvailable', wiredDeviceAvailable) 38 | // logger.debug('AvailableDevices.wirelessDeviceAvailable', wirelessDeviceAvailable) 39 | // logger.debug('AvailableDevices.modemDeviceAvailable', modemDeviceAvailable) 40 | // logger.debug('AvailableDevices.bluetoothDeviceAvailable', bluetoothDeviceAvailable) 41 | // } 42 | // } 43 | 44 | 45 | 46 | // We need to dynamically import PlasmaNM since it's not preinstalled on every distro (Issue #212) 47 | // readonly property var plasmaNMStatus: Qt.createQmlObject("import org.kde.plasma.networkmanagement 0.2 as PlasmaNM; PlasmaNM.NetworkStatus {}", networkMonitor) 48 | readonly property Loader plasmaNMStatusLoader: Loader { 49 | id: plasmaNMStatusLoader 50 | source: "NetworkMonitorPlasmaNM.qml" 51 | } 52 | 53 | 54 | // Since the network status state isn't exposed, we need to either parse the icon or user message to know the state. 55 | // We could compare the icon, however it has a number of network types (wired/wireless) with different wireless strengths 56 | // like network-wireless-connected-80 for 80% signal. There's a ton of disconnected types too. 57 | // (network-flightmode-on/network-unavailable/network-wired-available/network-mobile-available) 58 | // While comparing the i18n messages could be buggy in certain locales, at least we have a simple complete list of states. 59 | 60 | 61 | // https://invent.kde.org/plasma/plasma-nm/-/blame/master/libs/declarative/networkstatus.cpp#L115 62 | readonly property var connectedMessages: [ 63 | i18ndc("plasmanetworkmanagement-libs", "A network device is connected, but there is only link-local connectivity", "Connected"), 64 | i18ndc("plasmanetworkmanagement-libs", "A network device is connected, but there is only site-local connectivity", "Connected"), 65 | i18ndc("plasmanetworkmanagement-libs", "A network device is connected, with global network connectivity", "Connected"), 66 | ] 67 | // readonly property var disconnectedMessages: [ 68 | // i18ndc("plasmanetworkmanagement-libs", "Networking is inactive and all devices are disabled", "Inactive"), 69 | // i18ndc("plasmanetworkmanagement-libs", "There is no active network connection", "Disconnected"), 70 | // i18ndc("plasmanetworkmanagement-libs", "Network connections are being cleaned up", "Disconnecting"), 71 | // i18ndc("plasmanetworkmanagement-libs", "A network device is connecting to a network and there is no other available network connection", "Connecting"), 72 | // ] 73 | 74 | readonly property string networkStatus: { 75 | if (plasmaNMStatusLoader.status == Loader.Ready) { 76 | return plasmaNMStatusLoader.item.networkStatus 77 | } else { 78 | return '' 79 | } 80 | } 81 | readonly property bool isConnected: { 82 | if (plasmaNMStatusLoader.status == Loader.Error) { 83 | // Failed to load PlasmaNM, so treat it as connected. 84 | return true 85 | } else { 86 | return connectedMessages.indexOf(networkStatus) >= 0 87 | } 88 | } 89 | 90 | onIsConnectedChanged: logger.debug('NetworkMonitor.isConnected', isConnected) 91 | Component.onCompleted: { 92 | logger.debug('NetworkMonitor.isConnected', isConnected) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /package/contents/ui/NetworkMonitorPlasmaNM.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.networkmanagement 0.2 as PlasmaNM 3 | 4 | PlasmaNM.NetworkStatus { 5 | id: plasmaNMStatus 6 | // onActiveConnectionsChanged: logger.debug('NetworkStatus.activeConnections', activeConnections) 7 | onNetworkStatusChanged: logger.debug('NetworkStatus.networkStatus', networkStatus) 8 | Component.onCompleted: { 9 | // logger.debug('NetworkStatus.activeConnections', activeConnections) 10 | logger.debug('NetworkStatus.networkStatus', networkStatus) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package/contents/ui/NewEventForm.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.1 3 | import QtQuick.Layouts 1.1 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.components 3.0 as PlasmaComponents3 6 | 7 | Loader { 8 | id: newEventForm 9 | active: false 10 | visible: active 11 | 12 | sourceComponent: Component { 13 | RowLayout { 14 | spacing: 4 * units.devicePixelRatio 15 | 16 | PlasmaComponents3.CheckBox { 17 | Layout.alignment: Qt.AlignHCenter | Qt.AlignTop 18 | Layout.preferredHeight: calendarSelector.implicitHeight 19 | enabled: false 20 | visible: calendarSelector.selectedIsTasklist 21 | } 22 | 23 | Rectangle { 24 | Layout.preferredWidth: appletConfig.eventIndicatorWidth 25 | Layout.fillHeight: true 26 | color: calendarSelector.selectedCalendar && calendarSelector.selectedCalendar.backgroundColor || theme.textColor 27 | } 28 | 29 | ColumnLayout { 30 | spacing: 10 * units.devicePixelRatio 31 | 32 | Component.onCompleted: { 33 | newEventText.forceActiveFocus() 34 | newEventFormOpened(model, calendarSelector) 35 | } 36 | CalendarSelector { 37 | id: calendarSelector 38 | Layout.fillWidth: true 39 | } 40 | 41 | RowLayout { 42 | PlasmaComponents3.TextField { 43 | id: newEventText 44 | Layout.fillWidth: true 45 | placeholderText: i18n("Eg: 9am-5pm Work") 46 | onAccepted: { 47 | var calendarEntry = calendarSelector.model[calendarSelector.currentIndex] 48 | // calendarId = calendarId.calendarId ? calendarId.calendarId : calendarId 49 | var calendarId = calendarEntry.calendarId 50 | if (calendarId && date && text) { 51 | submitNewEventForm(calendarId, date, text) 52 | text = '' 53 | } 54 | } 55 | Keys.onEscapePressed: newEventForm.active = false 56 | } 57 | } 58 | } 59 | 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /package/contents/ui/NotificationManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | import "./lib" 5 | 6 | QtObject { 7 | id: notificationManager 8 | 9 | property var executable: ExecUtil { id: executable } 10 | 11 | function notify(args, callback) { 12 | logger.debugJSON('NotificationMananger.notify', args) 13 | args.sound = args.sound || args.soundFile 14 | 15 | var cmd = [ 16 | 'python3', 17 | plasmoid.file("", "scripts/notification.py"), 18 | ] 19 | if (args.appName) { 20 | cmd.push('--app-name', args.appName) 21 | } 22 | if (args.appIcon) { 23 | cmd.push('--icon', args.appIcon) 24 | } 25 | if (args.sound) { 26 | cmd.push('--sound', args.sound) 27 | if (args.loop) { 28 | cmd.push('--loop', args.loop) 29 | } 30 | } 31 | if (typeof args.expireTimeout !== 'undefined') { 32 | cmd.push('--timeout', args.expireTimeout) 33 | } 34 | if (args.actions) { 35 | for (var i = 0; i < args.actions.length; i++) { 36 | var action = args.actions[i] 37 | cmd.push('--action', action) 38 | } 39 | } 40 | cmd.push('--metadata', '' + Date.now()) 41 | var sanitizedSummary = executable.sanitizeString(args.summary) 42 | var sanitizedBody = executable.sanitizeString(args.body) 43 | cmd.push(sanitizedSummary) 44 | cmd.push(sanitizedBody) 45 | executable.exec(cmd, function(cmd, exitCode, exitStatus, stdout, stderr) { 46 | var actionId = stdout.replace('\n', ' ').trim() 47 | if (typeof callback === 'function') { 48 | callback(actionId) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package/contents/ui/Shared.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | function openGoogleCalendarNewEventUrl(date) { 4 | function dateString(year, month, day) { 5 | var s = '' + year 6 | s += (month < 10 ? '0' : '') + month 7 | s += (day < 10 ? '0' : '') + day 8 | return s 9 | } 10 | 11 | var nextDay = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1) 12 | 13 | var url = 'https://calendar.google.com/calendar/render?action=TEMPLATE' 14 | var startDate = dateString(date.getFullYear(), date.getMonth() + 1, date.getDate()) 15 | var endDate = dateString(nextDay.getFullYear(), nextDay.getMonth() + 1, nextDay.getDate()) 16 | url += '&dates=' + startDate + '/' + endDate 17 | Qt.openUrlExternally(url) 18 | } 19 | 20 | function isSameDate(a, b) { 21 | // console.log('isSameDate', a, b) 22 | return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() 23 | } 24 | function isDateEarlier(a, b) { 25 | var c = new Date(b.getFullYear(), b.getMonth(), b.getDate()) // midnight of date b 26 | return a < c 27 | } 28 | function isDateAfter(a, b) { 29 | var c = new Date(b.getFullYear(), b.getMonth(), b.getDate() + 1) // midnight of next day after b 30 | return a >= c 31 | } 32 | function dateTimeString(d) { 33 | return d.toISOString() 34 | } 35 | function dateString(d) { 36 | return d.toISOString().substr(0, 10) 37 | } 38 | function localeDateString(d) { 39 | return Qt.formatDateTime(d, 'yyyy-MM-dd') 40 | } 41 | function isValidDate(d) { 42 | if (d === null) { 43 | return false 44 | } else if (isNaN(d)) { 45 | return false 46 | } else { 47 | return true 48 | } 49 | } 50 | 51 | function renderText(text) { 52 | // console.log('renderText') 53 | if (typeof text === 'undefined') { 54 | return '' 55 | } 56 | var out = text 57 | // text && console.log('renderText', text) 58 | 59 | // Render links 60 | // Google doesn't auto-convert links to anchor tags when you paste a link in the description. 61 | // However, we should treat it as a link. This simple regex replacement works when we're not 62 | // dealing with HTML. So if we see an HTML anchor tag, skip it and assume the link has been 63 | // formatted. 64 | if (out.indexOf('' + encodedUrl + '' + ' ' 75 | }) 76 | } 77 | // text && console.log(' Links', out) 78 | 79 | // Render new lines 80 | // out = out.replace(/\n/g, '
') 81 | // text && console.log(' Newlines', out) 82 | 83 | // Remove leading new line, as Google sometimes adds them. 84 | out = out.replace(/^(\)+/, '') 85 | // text && console.log(' LeadingBR', out) 86 | 87 | return out 88 | } 89 | 90 | // Merge values of objB into objA 91 | function merge(objA, objB) { 92 | var keys = Object.keys(objB) 93 | for (var i = 0; i < keys.length; i++) { 94 | var key = keys[i] 95 | objA[key] = objB[key] 96 | } 97 | } 98 | 99 | // Remove keys from objA that are missing in objB 100 | function removeMissingKeys(objA, objB) { 101 | var keys = Object.keys(objA) 102 | for (var i = 0; i < keys.length; i++) { 103 | var key = keys[i] 104 | if (typeof objB[key] === 'undefined') { 105 | delete objA[key] 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /package/contents/ui/TimeFormatSizeHelper.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.4 2 | import org.kde.plasma.components 3.0 as PlasmaComponents3 3 | 4 | Item { 5 | id: timeFormatSizeHelper 6 | visible: false 7 | 8 | property Text timeLabel 9 | 10 | FontMetrics { 11 | id: fontMetrics 12 | 13 | font.pointSize: -1 14 | font.pixelSize: timeLabel.font.pixelSize 15 | font.family: timeLabel.font.family 16 | font.weight: timeLabel.font.weight 17 | font.italic: timeLabel.font.italic 18 | } 19 | 20 | function getWidestNumber(fontMetrics) { 21 | // find widest character between 0 and 9 22 | var maximumWidthNumber = 0 23 | var maximumAdvanceWidth = 0 24 | for (var i = 0; i <= 9; i++) { 25 | var advanceWidth = fontMetrics.advanceWidth(i) 26 | if (advanceWidth > maximumAdvanceWidth) { 27 | maximumAdvanceWidth = advanceWidth 28 | maximumWidthNumber = i 29 | } 30 | } 31 | // console.log('getWidestNumber', maximumWidthNumber) 32 | return maximumWidthNumber 33 | } 34 | 35 | readonly property string widestTimeFormat: { 36 | var maximumWidthNumber = getWidestNumber(fontMetrics) 37 | // replace all placeholders with the widest number (two digits) 38 | var format = timeLabel.timeFormat.replace(/(h+|m+|s+)/g, "" + maximumWidthNumber + maximumWidthNumber) // make sure maximumWidthNumber is formatted as string 39 | return format 40 | } 41 | 42 | readonly property real minWidth: formattedSizeHelper.paintedWidth 43 | function updateMinWidth() { 44 | var now = new Date(timeModel.currentTime) 45 | var date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 1, 0, 0) 46 | var timeAm = Qt.formatDateTime(date, widestTimeFormat) 47 | var advanceWidthAm = fontMetrics.advanceWidth(timeAm) 48 | date.setHours(13) 49 | var timePm = Qt.formatDateTime(date, widestTimeFormat) 50 | var advanceWidthPm = fontMetrics.advanceWidth(timePm) 51 | 52 | if (advanceWidthAm > advanceWidthPm) { 53 | formattedSizeHelper.text = timeAm 54 | } else { 55 | formattedSizeHelper.text = timePm 56 | } 57 | // console.log('updateMinWidth', minWidth) 58 | // console.log('\t', 'timeAm', timeAm, advanceWidthAm) 59 | // console.log('\t', 'timePm', timePm, advanceWidthPm) 60 | } 61 | 62 | PlasmaComponents3.Label { 63 | id: formattedSizeHelper 64 | 65 | font.pointSize: -1 66 | font.pixelSize: timeLabel.font.pixelSize 67 | font.family: timeLabel.font.family 68 | font.weight: timeLabel.font.weight 69 | font.italic: timeLabel.font.italic 70 | wrapMode: timeLabel.wrapMode 71 | fontSizeMode: Text.FixedSize 72 | } 73 | 74 | Connections { 75 | target: clock 76 | onWidthChanged: timeFormatSizeHelper.updateMinWidth() 77 | onHeightChanged: timeFormatSizeHelper.updateMinWidth() 78 | } 79 | Connections { 80 | target: timeLabel 81 | onHeightChanged: timeFormatSizeHelper.updateMinWidth() 82 | onTimeFormatChanged: timeFormatSizeHelper.updateMinWidth() 83 | } 84 | Connections { 85 | target: timeModel 86 | onDateChanged: timeFormatSizeHelper.updateMinWidth() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /package/contents/ui/TimeModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.1 3 | 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | 6 | Item { 7 | id: timeModel 8 | property string timezone: "Local" 9 | property var currentTime: dataSource.data[timezone]["DateTime"] 10 | property alias dataSource: dataSource 11 | property var allTimezones: { 12 | var timezones = plasmoid.configuration.selectedTimeZones.toString() 13 | if (timezones.length > 0) { 14 | timezones = timezones.split(',') 15 | } else { 16 | timezones = [] 17 | } 18 | if (timezones.indexOf('Local') === -1) { 19 | timezones.push('Local') 20 | } 21 | return timezones 22 | } 23 | 24 | signal secondChanged() 25 | signal minuteChanged() 26 | signal dateChanged() 27 | signal loaded() 28 | 29 | PlasmaCore.DataSource { 30 | id: dataSource 31 | engine: "time" 32 | connectedSources: timeModel.allTimezones 33 | interval: 1000 34 | intervalAlignment: PlasmaCore.Types.NoAlignment 35 | onNewData: { 36 | if (sourceName === 'Local') { 37 | timeModel.tick() 38 | } 39 | } 40 | } 41 | 42 | property bool ready: false 43 | property int lastMinute: -1 44 | property int lastDate: -1 45 | function tick() { 46 | if (!ready) { 47 | ready = true 48 | loaded() 49 | } 50 | secondChanged() 51 | var currentMinute = currentTime.getMinutes() 52 | if (currentMinute != lastMinute) { 53 | minuteChanged() 54 | var currentDate = currentTime.getDate() 55 | if (currentDate != lastDate) { 56 | dateChanged() 57 | lastDate = currentDate 58 | } 59 | lastMinute = currentMinute 60 | } 61 | } 62 | 63 | 64 | property bool testing: false 65 | Component.onCompleted: { 66 | if (testing) { 67 | currentTime = new Date(2016, 1, 2, 23, 59, 55) 68 | timeModel.loaded() 69 | } 70 | } 71 | 72 | Timer { 73 | running: testing 74 | repeat: true 75 | interval: 1000 76 | onTriggered: { 77 | currentTime.setSeconds(currentTime.getSeconds() + 1) 78 | timeModel.currentTimeChanged() 79 | timeModel.tick() 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package/contents/ui/TimeSelector.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Window 2.2 3 | 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.components 3.0 as PlasmaComponents3 6 | 7 | import QtQuick.Templates 2.1 as T 8 | import QtQuick.Controls 2.1 as Controls 9 | import QtGraphicalEffects 1.0 // DropShadow 10 | 11 | // Based on: 12 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/plasmacomponents3/ComboBox.qml 13 | // https://doc.qt.io/archives/qt-5.11/qml-qtquick-controls2-combobox.html 14 | // https://github.com/qt/qtquickcontrols2/blob/dev/src/quicktemplates2/qquickcombobox.cpp 15 | 16 | PlasmaComponents3.TextField { 17 | id: timeSelector 18 | readonly property Item control: timeSelector 19 | 20 | property int defaultMinimumWidth: 80 * units.devicePixelRatio 21 | readonly property int implicitContentWidth: contentWidth + leftPadding + rightPadding 22 | implicitWidth: Math.max(defaultMinimumWidth, implicitContentWidth) 23 | 24 | property var dateTime: new Date() 25 | property var timeFormat: Locale.ShortFormat 26 | 27 | signal dateTimeShifted(date oldDateTime, int deltaDateTime, date newDateTime) 28 | signal entryActivated(int index) 29 | 30 | function setDateTime(newDateTime) { 31 | var oldDateTime = new Date(dateTime) 32 | var deltaDateTime = newDateTime.valueOf() - oldDateTime.valueOf() 33 | dateTimeShifted(oldDateTime, deltaDateTime, newDateTime) 34 | } 35 | function updateText() { 36 | text = Qt.binding(function(){ 37 | return timeSelector.dateTime.toLocaleTimeString(Qt.locale(), timeSelector.timeFormat) 38 | }) 39 | } 40 | 41 | property string valueRole: "dt" 42 | property string textRole: "label" 43 | property var model: { 44 | var dt = dateTime 45 | var midnight = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0, 0, 0) 46 | var interval = 30 // minutes 47 | var intervalMillis = interval*60*1000 48 | var numEntries = Math.ceil(24*60 / interval) // 30min intervals = 48 entries 49 | var l = [] 50 | for (var i = 0; i < numEntries; i++) { 51 | var deltaT = i * intervalMillis 52 | var entryDateTime = new Date(midnight.valueOf() + deltaT) 53 | var entry = { 54 | dt: entryDateTime, 55 | label: entryDateTime.toLocaleTimeString(Qt.locale(), timeSelector.timeFormat) 56 | } 57 | l.push(entry) 58 | } 59 | return l 60 | } 61 | 62 | onPressed: { 63 | popup.open() 64 | highlightDateTime(dateTime) 65 | } 66 | 67 | onEntryActivated: { 68 | if (0 <= index && index < model.length) { 69 | var entry = model[index] 70 | setDateTime(entry[control.valueRole]) 71 | } 72 | } 73 | 74 | onTextEdited: { 75 | var dt = Date.fromLocaleTimeString(Qt.locale(), text, timeSelector.timeFormat) 76 | // console.log('onTextEdited', text, dt) 77 | if (!isNaN(dt)) { 78 | setDateTime(dt) 79 | highlightDateTime(dt) 80 | } 81 | } 82 | 83 | function highlightDateTime(dt) { 84 | for (var i = 0; i < model.length; i++) { 85 | var entry = model[i] 86 | var eDT = entry[valueRole] 87 | if (dt.getHours() === eDT.getHours() && dt.getMinutes() === eDT.getMinutes()) { 88 | listView.currentIndex = i 89 | listView.positionViewAtIndex(i, ListView.Contain) 90 | return 91 | } 92 | } 93 | listView.currentIndex = -1 // Unselect 94 | } 95 | 96 | onEditingFinished: updateText() 97 | Component.onCompleted: updateText() 98 | 99 | property Component delegate: PlasmaComponents3.ItemDelegate { 100 | width: control.popup.width 101 | text: control.textRole ? (Array.isArray(control.model) ? modelData[control.textRole] : model[control.textRole]) : modelData 102 | property bool separatorVisible: false 103 | highlighted: listView.currentIndex === index 104 | 105 | onClicked: { 106 | listView.currentIndex = index 107 | control.entryActivated(listView.currentIndex) 108 | popup.close() 109 | } 110 | } 111 | 112 | property T.Popup popup: T.Popup { 113 | x: control.mirrored ? control.width - width : 0 114 | y: control.height 115 | property int minWidth: 120 * units.devicePixelRatio 116 | property int maxHeight: 150 * units.devicePixelRatio 117 | width: Math.max(control.width, minWidth) 118 | implicitHeight: Math.min(contentItem.implicitHeight, maxHeight) 119 | topMargin: 6 * units.devicePixelRatio 120 | bottomMargin: 6 * units.devicePixelRatio 121 | 122 | contentItem: ListView { 123 | id: listView 124 | clip: true 125 | implicitHeight: contentHeight 126 | highlightRangeMode: ListView.ApplyRange 127 | highlightMoveDuration: 0 128 | // HACK: When the ComboBox is not inside a top-level Window, it's Popup does not inherit 129 | // the LayoutMirroring options. This is a workaround to fix this by enforcing 130 | // the LayoutMirroring options properly. 131 | // QTBUG: https://bugreports.qt.io/browse/QTBUG-66446 132 | LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft 133 | LayoutMirroring.childrenInherit: true 134 | T.ScrollBar.vertical: Controls.ScrollBar { } 135 | 136 | model: control.popup.visible ? control.model : null 137 | delegate: control.delegate 138 | } 139 | background: Rectangle { 140 | anchors { 141 | fill: parent 142 | margins: -1 143 | } 144 | radius: 2 145 | color: theme.viewBackgroundColor 146 | border.color: Qt.rgba(theme.textColor.r, theme.textColor.g, theme.textColor.b, 0.3) 147 | layer.enabled: true 148 | 149 | layer.effect: DropShadow { 150 | transparentBorder: true 151 | radius: 4 152 | samples: 8 153 | horizontalOffset: 2 154 | verticalOffset: 2 155 | color: Qt.rgba(0, 0, 0, 0.3) 156 | } 157 | } 158 | } // Popup 159 | } 160 | -------------------------------------------------------------------------------- /package/contents/ui/TimerInputView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.1 3 | import QtQuick.Layouts 1.1 4 | import org.kde.plasma.components 3.0 as PlasmaComponents3 5 | 6 | ColumnLayout { 7 | id: timerInputView 8 | 9 | readonly property int totalSeconds: { 10 | var h = parseInt(hoursTextField.text || "0", 10) 11 | var m = parseInt(minutesTextField.text || "0", 10) 12 | var s = parseInt(secondsTextField.text || "0", 10) 13 | 14 | return (h*60*60) + (m*60) + (s) 15 | } 16 | 17 | function reset() { 18 | hoursTextField.text = "0" 19 | minutesTextField.text = "00" 20 | secondsTextField.text = "00" 21 | } 22 | 23 | function cancel() { 24 | timerInputView.reset() 25 | timerView.isSetTimerViewVisible = false 26 | } 27 | 28 | function start() { 29 | // console.log('timerInputView.totalSeconds', timerInputView.totalSeconds) 30 | timerModel.setDurationAndStart(timerInputView.totalSeconds) 31 | timerView.isSetTimerViewVisible = false 32 | } 33 | 34 | RowLayout { 35 | id: textFieldRow 36 | Layout.fillHeight: true 37 | spacing: 0 38 | 39 | property int fontPixelSize: height/2 40 | 41 | TimerTextField { 42 | id: hoursTextField 43 | defaultText: "0" 44 | validator: IntValidator { bottom: 0 } 45 | } 46 | 47 | PlasmaComponents3.Label { 48 | Layout.fillHeight: true 49 | font.pointSize: -1 50 | font.pixelSize: textFieldRow.fontPixelSize 51 | text: ":" 52 | } 53 | 54 | TimerTextField { 55 | id: minutesTextField 56 | } 57 | 58 | PlasmaComponents3.Label { 59 | Layout.fillHeight: true 60 | font.pointSize: -1 61 | font.pixelSize: textFieldRow.fontPixelSize 62 | text: ":" 63 | } 64 | 65 | TimerTextField { 66 | id: secondsTextField 67 | } 68 | } 69 | 70 | RowLayout { 71 | Item { 72 | Layout.fillWidth: true 73 | } 74 | PlasmaComponents3.Button { 75 | icon.name: 'chronometer-start' 76 | text: i18n("&Start") 77 | onClicked: timerInputView.start() 78 | } 79 | PlasmaComponents3.Button { 80 | icon.name: 'dialog-cancel' 81 | text: i18n("&Cancel") 82 | onClicked: timerInputView.cancel() 83 | } 84 | } 85 | 86 | Component.onCompleted: { 87 | minutesTextField.forceActiveFocus() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package/contents/ui/TimerModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | id: timerModel 5 | 6 | property int secondsLeft: 0 7 | property int duration: 0 8 | readonly property bool timerRepeats: plasmoid.configuration.timerRepeats 9 | readonly property bool timerSfxEnabled: plasmoid.configuration.timerSfxEnabled 10 | readonly property string timerSfxFilepath: plasmoid.configuration.timerSfxFilepath 11 | property alias running: timerTicker.running 12 | property date finished: new Date() 13 | 14 | signal timerFinished() 15 | 16 | property var defaultTimers: [ 17 | { seconds: 30 }, 18 | { seconds: 60 }, 19 | { seconds: 5 * 60 }, 20 | { seconds: 10 * 60 }, 21 | { seconds: 15 * 60 }, 22 | { seconds: 20 * 60 }, 23 | { seconds: 30 * 60 }, 24 | { seconds: 45 * 60 }, 25 | { seconds: 60 * 60 }, 26 | ] 27 | 28 | // Note that QML Timer intervals are shorter when the refresh rate is faster, 29 | // so we can't rely on it to tick exactly every 1000ms. See Issue #129. 30 | property Timer timerTicker: Timer { 31 | id: timerTicker 32 | interval: 1000 33 | running: false 34 | repeat: true 35 | 36 | onTriggered: { 37 | timerModel.tick() 38 | } 39 | } 40 | 41 | function setDuration(newDuration) { 42 | if (newDuration <= 0) { 43 | return 44 | } 45 | timerModel.duration = newDuration 46 | timerModel.secondsLeft = newDuration 47 | } 48 | 49 | function setDurationAndStart(newDuration) { 50 | setDuration(newDuration) 51 | if (newDuration > 0) { 52 | timerModel.runTimer() 53 | } 54 | } 55 | 56 | function getIncrementFor(oldDuration, multiplier) { 57 | if (oldDuration >= 15 * 60) { // 15m 58 | return 5 * 60 // +5m 59 | } else if (oldDuration >= 60) { // 1m 60 | if (multiplier < 0 && oldDuration < 120) { // 1-2m -5sec 61 | return 5 // -5sec 62 | } else { 63 | return 60 // +1m 64 | } 65 | } else if (oldDuration >= 15) { // 15sec 66 | return 5 // +5sec 67 | } else { 68 | if (multiplier < 0 && oldDuration <= 1) { // 0-1sec 69 | return 0 // +0 70 | } else { // 2-14sec 71 | return 1 // +5sec 72 | } 73 | } 74 | } 75 | function deltaDuration(multiplier) { 76 | var delta = getIncrementFor(duration, multiplier) 77 | var newDuration = Math.max(0, timerModel.duration + (delta * multiplier)) 78 | // console.log(timerModel.duration, multiplier, delta, newDuration) 79 | setDuration(newDuration) 80 | } 81 | function increaseDuration() { 82 | deltaDuration(1) 83 | } 84 | function decreaseDuration() { 85 | deltaDuration(-1) 86 | } 87 | 88 | onDurationChanged: { 89 | secondsLeft = duration 90 | } 91 | 92 | onSecondsLeftChanged: { 93 | // console.log('onSecondsLeftChanged', secondsLeft) 94 | if (secondsLeft <= 0) { 95 | timerFinished() 96 | } 97 | } 98 | 99 | function formatTimer(nSeconds) { 100 | // returns "1:00:00" or "10:00" or "0:01" 101 | var hours = Math.floor(nSeconds / 3600) 102 | var minutes = Math.floor((nSeconds - hours*3600) / 60) 103 | var seconds = nSeconds - hours*3600 - minutes*60 104 | var s = "" + (seconds < 10 ? "0" : "") + seconds 105 | s = minutes + ":" + s 106 | if (hours > 0) { 107 | s = hours + ":" + (minutes < 10 ? "0" : "") + s 108 | } 109 | return s 110 | } 111 | 112 | function tick() { 113 | var now = new Date() 114 | var deltaMillis = finished.valueOf() - now.valueOf() 115 | timerModel.secondsLeft = Math.max(0, Math.ceil(deltaMillis / 1000)) 116 | // console.log('tick', timerModel.secondsLeft, timerModel.duration) 117 | } 118 | 119 | function repeatTimer() { 120 | timerModel.secondsLeft = timerModel.duration 121 | timerModel.runTimer() 122 | } 123 | 124 | function runTimer() { 125 | var now = new Date() 126 | timerModel.finished = new Date(now.valueOf() + timerModel.secondsLeft * 1000) 127 | // console.log('finished', now.valueOf(), timerModel.secondsLeft * 1000, timerModel.finished) 128 | timerTicker.restart() 129 | } 130 | 131 | function pause() { 132 | timerTicker.stop() 133 | } 134 | 135 | onTimerFinished: { 136 | timerModel.pause() 137 | timerModel.createNotification() 138 | 139 | if (timerModel.timerRepeats) { 140 | timerModel.repeatTimer() 141 | } 142 | } 143 | 144 | function createNotification() { 145 | var args = { 146 | appName: i18n("Timer"), 147 | appIcon: "chronometer", 148 | summary: i18n("Timer finished"), 149 | body: i18n("%1 has passed", formatTimer(timerModel.duration)), 150 | // expireTimeout: 2000, 151 | } 152 | if (timerModel.timerSfxEnabled) { 153 | args.soundFile = timerModel.timerSfxFilepath 154 | } 155 | 156 | args.actions = [] 157 | if (!timerModel.timerRepeats) { 158 | var action = 'repeat' + ',' + i18n("Repeat") 159 | args.actions.push(action) 160 | } 161 | notificationManager.notify(args, function(actionId){ 162 | if (actionId === 'repeat') { 163 | repeatTimer() 164 | } 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /package/contents/ui/TimerPresetButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.0 3 | import org.kde.plasma.components 3.0 as PlasmaComponents3 4 | 5 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/plasmacomponents3/Button.qml#L35 6 | PlasmaComponents3.Button { 7 | // PlasmaComponents3.Button already sets Layout.minimumWidth since KF5 v5.68 8 | Layout.preferredWidth: appletConfig.timerButtonWidth 9 | } 10 | -------------------------------------------------------------------------------- /package/contents/ui/TimerTextField.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.1 3 | import QtQuick.Layouts 1.1 4 | import org.kde.plasma.components 3.0 as PlasmaComponents3 5 | 6 | PlasmaComponents3.TextField { 7 | id: timerTextField 8 | Layout.fillWidth: true 9 | Layout.fillHeight: true 10 | font.pointSize: -1 11 | font.pixelSize: textFieldRow.fontPixelSize 12 | horizontalAlignment: TextInput.AlignHCenter 13 | property string defaultText: "00" 14 | text: defaultText 15 | validator: IntValidator { bottom: 0; top: 59 } 16 | onFocusChanged: { 17 | if (focus) { 18 | selectAll() 19 | } else { 20 | if (text === "") { 21 | text = defaultText 22 | } 23 | } 24 | } 25 | onAccepted: timerInputView.start() 26 | Keys.onEscapePressed: timerInputView.cancel() 27 | } 28 | -------------------------------------------------------------------------------- /package/contents/ui/TooltipView.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.1 3 | import org.kde.plasma.core 2.0 as PlasmaCore 4 | import org.kde.plasma.components 3.0 as PlasmaComponents3 5 | import org.kde.plasma.extras 2.0 as PlasmaExtras 6 | import org.kde.plasma.private.digitalclock 1.0 as DigitalClock 7 | 8 | Item { 9 | id: tooltipContentItem 10 | 11 | property int preferredTextWidth: units.gridUnit * 20 12 | 13 | width: childrenRect.width + units.gridUnit 14 | height: childrenRect.height + units.gridUnit 15 | 16 | LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft 17 | LayoutMirroring.childrenInherit: true 18 | 19 | property var dataSource: timeModel.dataSource 20 | readonly property string timezoneTimeFormat: Qt.locale().timeFormat(Locale.ShortFormat) 21 | 22 | function timeForZone(zone) { 23 | var compactRepresentationItem = plasmoid.compactRepresentationItem 24 | if (!compactRepresentationItem) { 25 | return "" 26 | } 27 | 28 | // get the time for the given timezone from the dataengine 29 | var now = dataSource.data[zone]["DateTime"] 30 | // get current UTC time 31 | var msUTC = now.getTime() + (now.getTimezoneOffset() * 60000) 32 | // add the dataengine TZ offset to it 33 | var dateTime = new Date(msUTC + (dataSource.data[zone]["Offset"] * 1000)) 34 | 35 | var formattedTime = Qt.formatTime(dateTime, timezoneTimeFormat) 36 | 37 | if (dateTime.getDay() != dataSource.data["Local"]["DateTime"].getDay()) { 38 | formattedTime += " (" + Qt.formatDate(dateTime, Locale.ShortFormat) + ")" 39 | } 40 | 41 | return formattedTime 42 | } 43 | 44 | function nameForZone(zone) { 45 | if (plasmoid.configuration.displayTimezoneAsCode) { 46 | return dataSource.data[zone]["Timezone Abbreviation"] 47 | } else { 48 | return DigitalClock.TimezonesI18n.i18nCity(dataSource.data[zone]["Timezone City"]) 49 | } 50 | } 51 | 52 | ColumnLayout { 53 | id: columnLayout 54 | anchors { 55 | left: parent.left 56 | top: parent.top 57 | margins: units.gridUnit / 2 58 | } 59 | spacing: units.largeSpacing 60 | 61 | RowLayout { 62 | spacing: units.largeSpacing 63 | 64 | PlasmaCore.IconItem { 65 | id: tooltipIcon 66 | source: "preferences-system-time" 67 | Layout.alignment: Qt.AlignTop 68 | visible: true 69 | implicitWidth: units.iconSizes.medium 70 | Layout.preferredWidth: implicitWidth 71 | Layout.preferredHeight: implicitWidth 72 | } 73 | 74 | ColumnLayout { 75 | spacing: 0 76 | 77 | PlasmaExtras.Heading { 78 | id: tooltipMaintext 79 | level: 3 80 | Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) 81 | Layout.maximumWidth: preferredTextWidth 82 | elide: Text.ElideRight 83 | text: Qt.formatTime(timeModel.currentTime, Qt.locale().timeFormat(Locale.LongFormat)) 84 | } 85 | 86 | PlasmaComponents3.Label { 87 | id: tooltipSubtext 88 | Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) 89 | Layout.maximumWidth: preferredTextWidth 90 | text: Qt.formatDate(timeModel.currentTime, Qt.locale().dateFormat(Locale.LongFormat)) 91 | opacity: 0.6 92 | } 93 | } 94 | } 95 | 96 | 97 | GridLayout { 98 | id: timezoneLayout 99 | Layout.minimumWidth: Math.min(implicitWidth, preferredTextWidth) 100 | Layout.maximumWidth: preferredTextWidth 101 | // Layout.maximumHeight: childrenRect.height // Causes binding loop 102 | columns: 2 103 | visible: timezoneRepeater.count > 0 104 | 105 | Repeater { 106 | id: timezoneRepeater 107 | model: { 108 | // The timezones need to be duplicated in the array 109 | // because we need their data twice - once for the name 110 | // and once for the time and the Repeater delegate cannot 111 | // be one Item with two Labels because that wouldn't work 112 | // in a grid then 113 | var timezones = [] 114 | for (var i = 0; i < plasmoid.configuration.selectedTimeZones.length; i++) { 115 | var timezone = plasmoid.configuration.selectedTimeZones[i] 116 | if (timezone != 'Local') { 117 | timezones.push(timezone) 118 | timezones.push(timezone) 119 | } 120 | } 121 | 122 | return timezones 123 | } 124 | 125 | PlasmaComponents3.Label { 126 | id: timezone 127 | Layout.alignment: index % 2 === 0 ? Qt.AlignRight : Qt.AlignLeft 128 | 129 | wrapMode: Text.NoWrap 130 | text: index % 2 == 0 ? nameForZone(modelData) : timeForZone(modelData) 131 | font.weight: index % 2 == 0 ? Font.Bold : Font.Normal 132 | elide: Text.ElideNone 133 | opacity: 0.6 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /package/contents/ui/badges/DotsBadge.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | Item { 5 | id: dotsBadge 6 | property int dotSize: (height / 8) + dotBorderWidth*2 7 | property color dotColor: theme.highlightColor 8 | property int dotBorderWidth: plasmoid.configuration.showOutlines ? 1 : 0 9 | property color dotBorderColor: theme.backgroundColor 10 | 11 | Row { 12 | anchors.horizontalCenter: dotsBadge.horizontalCenter 13 | anchors.bottom: dotsBadge.bottom 14 | anchors.margins: dotsBadge.height / 8 15 | spacing: units.smallSpacing 16 | 17 | Rectangle { 18 | visible: modelEventsCount >= 1 19 | width: dotsBadge.dotSize 20 | height: dotsBadge.dotSize 21 | radius: width / 2 22 | color: dotsBadge.dotColor 23 | border.width: dotsBadge.dotBorderWidth 24 | border.color: dotsBadge.dotBorderColor 25 | } 26 | Rectangle { 27 | visible: modelEventsCount >= 2 28 | width: dotsBadge.dotSize 29 | height: dotsBadge.dotSize 30 | radius: width / 2 31 | color: dotsBadge.dotColor 32 | border.width: dotsBadge.dotBorderWidth 33 | border.color: dotsBadge.dotBorderColor 34 | } 35 | Rectangle { 36 | visible: modelEventsCount >= 3 37 | width: dotsBadge.dotSize 38 | height: dotsBadge.dotSize 39 | radius: width / 2 40 | color: dotsBadge.dotColor 41 | border.width: dotsBadge.dotBorderWidth 42 | border.color: dotsBadge.dotBorderColor 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /package/contents/ui/badges/EventColorsBarBadge.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.0 3 | import org.kde.plasma.core 2.0 as PlasmaCore 4 | 5 | Item { 6 | id: eventColorsBarColor 7 | 8 | Item { 9 | anchors.left: eventColorsBarColor.left 10 | anchors.right: eventColorsBarColor.right 11 | anchors.bottom: eventColorsBarColor.bottom 12 | height: parent.height / 8 13 | 14 | property bool usePadding: !plasmoid.configuration.monthShowBorder 15 | anchors.leftMargin: usePadding ? parent.width/8 : 0 16 | anchors.rightMargin: usePadding ? parent.width/8 : 0 17 | anchors.bottomMargin: usePadding ? parent.height/16 : 0 18 | 19 | RowLayout { 20 | anchors.fill: parent 21 | spacing: 0 22 | 23 | Repeater { 24 | model: dayStyle.useHightlightColor ? [theme.highlightColor] : dayStyle.eventColors 25 | 26 | Rectangle { 27 | Layout.fillHeight: true 28 | Layout.fillWidth: true 29 | color: modelData 30 | 31 | Rectangle { 32 | anchors.fill: parent 33 | color: "transparent" 34 | border.width: 1 35 | border.color: theme.backgroundColor 36 | opacity: 0.5 37 | } 38 | } 39 | 40 | } 41 | } 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /package/contents/ui/badges/EventCountBadge.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | import org.kde.plasma.components 3.0 as PlasmaComponents3 4 | 5 | Item { 6 | id: eventBadgeCount 7 | 8 | Rectangle { 9 | // This spams "TypeError: Cannot read property of null" when month is changed... 10 | // anchors.right: parent.right 11 | // anchors.bottom: parent.bottom 12 | 13 | // This doesn't ... why?! 14 | anchors.right: eventBadgeCount.right 15 | anchors.bottom: eventBadgeCount.bottom 16 | 17 | height: parent.height / 3 18 | width: eventBadgeCountText.width 19 | color: { 20 | if (plasmoid.configuration.showOutlines) { 21 | var c = Qt.darker(PlasmaCore.ColorScope.backgroundColor, 1) // Cast to color 22 | c.a = 0.6 // 60% 23 | return c 24 | } else { 25 | return "transparent" 26 | } 27 | } 28 | 29 | PlasmaComponents3.Label { 30 | id: eventBadgeCountText 31 | height: parent.height 32 | width: Math.max(paintedWidth, height) 33 | anchors.centerIn: parent 34 | 35 | color: PlasmaCore.ColorScope.highlightColor 36 | text: modelEventsCount 37 | font.weight: Font.Bold 38 | font.pointSize: 1024 39 | fontSizeMode: Text.VerticalFit 40 | wrapMode: Text.NoWrap 41 | 42 | horizontalAlignment: Text.AlignHCenter 43 | verticalAlignment: Text.AlignVCenter 44 | smooth: true 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /package/contents/ui/badges/HighlightBarBadge.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | Item { 4 | id: highlightBarBadge 5 | 6 | Rectangle { 7 | anchors.left: highlightBarBadge.left 8 | anchors.right: highlightBarBadge.right 9 | anchors.bottom: parent.bottom 10 | height: parent.height / 8 11 | opacity: 0.6 12 | color: theme.highlightColor 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/CalendarManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | Item { 5 | id: calendarManager 6 | 7 | property string calendarManagerId: "" 8 | property var eventsByCalendar: ({}) // { "": { "items": [] } } 9 | 10 | property date dateMin: new Date() 11 | property date dateMax: new Date() 12 | 13 | property bool clearingData: false 14 | property int asyncRequests: 0 15 | property int asyncRequestsDone: 0 16 | signal refresh() 17 | signal dataCleared() 18 | signal fetchingData() 19 | signal calendarFetched(string calendarId, var data) 20 | signal allDataFetched() 21 | signal eventAdded(string calendarId, var data) 22 | signal eventCreated(string calendarId, var data) 23 | signal eventRemoved(string calendarId, string eventId, var data) 24 | signal eventDeleted(string calendarId, string eventId, var data) 25 | signal eventUpdated(string calendarId, string eventId, var data) 26 | signal error(string msg, int errorType) 27 | 28 | 29 | onAsyncRequestsDoneChanged: checkIfDone() 30 | 31 | function checkIfDone() { 32 | if (clearingData) { 33 | return 34 | } 35 | if (asyncRequestsDone >= asyncRequests) { 36 | allDataFetched() 37 | } 38 | } 39 | 40 | //--- Calendar 41 | function getCalendarList() { 42 | return [] // Function is overloaded 43 | } 44 | 45 | function getCalendar(calendarId) { 46 | var calendarList = getCalendarList() 47 | for (var i = 0; i < calendarList.length; i++) { 48 | var calendar = calendarList[i] 49 | if (calendarId === calendar.id) { 50 | return calendar 51 | } 52 | } 53 | return null 54 | } 55 | 56 | //--- Calendar data 57 | function setCalendarData(calendarId, data) { 58 | calendarParsing(calendarId, data) 59 | eventsByCalendar[calendarId] = data 60 | calendarFetched(calendarId, data) 61 | } 62 | 63 | function clear() { 64 | logger.debug(calendarManager, 'clear()') 65 | calendarManager.clearingData = true 66 | calendarManager.asyncRequests = 0 67 | calendarManager.asyncRequestsDone = 0 68 | calendarManager.eventsByCalendar = {} 69 | calendarManager.clearingData = false 70 | dataCleared() 71 | } 72 | 73 | //--- Event 74 | function getEvent(calendarId, eventId) { 75 | var events = calendarManager.eventsByCalendar[calendarId].items 76 | for (var i = 0; i < events.length; i++) { 77 | if (events[i].id == eventId) { 78 | return events[i] 79 | } 80 | } 81 | } 82 | 83 | // Add to model only 84 | function addEvent(calendarId, data) { 85 | calendarManager.eventsByCalendar[calendarId].items.push(data) 86 | eventAdded(calendarId, data) 87 | } 88 | 89 | // Remove from model only 90 | function removeEvent(calendarId, eventId) { 91 | logger.debug(calendarManager, 'removeEvent', calendarId, eventId) 92 | var events = calendarManager.eventsByCalendar[calendarId].items 93 | for (var i = 0; i < events.length; i++) { 94 | if (events[i].id == eventId) { 95 | var data = events[i] 96 | events.splice(i, 1) // Remove item at index 97 | eventRemoved(calendarId, eventId, data) 98 | return 99 | } 100 | } 101 | logger.log(calendarManager, 'removeEvent', 'event didn\'t exist') 102 | } 103 | 104 | //--- 105 | function fetchAll(dateMin, dateMax) { 106 | logger.debug(calendarManager, 'fetchAllEvents', dateMin, dateMax) 107 | fetchingData() 108 | clear() 109 | if (typeof dateMin !== "undefined") { 110 | calendarManager.dateMin = dateMin 111 | calendarManager.dateMax = dateMax 112 | } 113 | fetchAllCalendars() 114 | checkIfDone() 115 | } 116 | 117 | // Implementation 118 | signal fetchAllCalendars() 119 | signal calendarParsing(string calendarId, var data) 120 | signal eventParsing(string calendarId, var event) 121 | 122 | // Parsing order: 123 | // CalendarManager.onCalendarParsing 124 | // CalendarManager.onEventParsing 125 | // SubClass.onEventParsing 126 | // CalendarManager.defaultEventParsing 127 | // SubClass.onCalendarParsing 128 | onCalendarParsing: { 129 | // logger.debug('CalendarManager.calendarParsing(', calendarManager, ')', calendarId) 130 | data.items.forEach(function(event) { 131 | eventParsing(calendarId, event) 132 | defaultEventParsing(calendarId, event) 133 | }) 134 | } 135 | onEventParsing: { 136 | // logger.debug('CalendarManager.eventParsing(', calendarManager, ')', calendarId) 137 | } 138 | 139 | // To simplify repeated code amongst implementations, 140 | // we'll put the reused code here. 141 | function defaultEventParsing(calendarId, event) { 142 | // logger.debug('CalendarManager.defaultEventParsing') 143 | event.calendarManagerId = calendarManagerId 144 | event.calendarId = calendarId 145 | 146 | event._summary = event.summary 147 | event.summary = event.summary || i18nc("event with no summary", "(No title)") 148 | 149 | if (event.start.date) { 150 | event.startDateTime = new Date(event.start.date + ' 00:00:00') 151 | } else { 152 | event.startDateTime = new Date(event.start.dateTime) 153 | } 154 | 155 | if (event.end.date) { 156 | event.endDateTime = new Date(event.end.date + ' 00:00:00') 157 | } else { 158 | event.endDateTime = new Date(event.end.dateTime) 159 | } 160 | } 161 | 162 | function parseSingleEvent(calendarId, event) { 163 | calendarParsing(calendarId, { 164 | items: [event], 165 | }) 166 | } 167 | 168 | //--- 169 | function createEvent(calendarId, date, text) { 170 | logger.log(calendarManager, 'createEvent(', date, text, ') is not implemented') 171 | } 172 | 173 | function deleteEvent(calendarId, eventId) { 174 | logger.log(calendarManager, 'deleteEvent(', calendarId, eventId, ') is not implemented') 175 | } 176 | 177 | function setEventProperty(calendarId, eventId, key, value) { 178 | logger.log(calendarManager, 'setEventProperty(', calendarId, eventId, key, value, ') is not implemented') 179 | } 180 | 181 | function setEventProperties(calendarId, eventId, args) { 182 | logger.log(calendarManager, 'setEventProperties(', calendarId, eventId, args, ') is not implemented') 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/DebugCalendarManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../Shared.js" as Shared 4 | import "../lib/Requests.js" as Requests 5 | import "../code/DebugFixtures.js" as DebugFixtures 6 | 7 | CalendarManager { 8 | id: debugCalendarManager 9 | 10 | calendarManagerId: "debug" 11 | property var debugCalendar: null 12 | 13 | function fetchDebugEvents() { 14 | plasmoid.configuration.debugging = true 15 | debugCalendar = DebugFixtures.getCalendar() 16 | var debugEventData = DebugFixtures.getEventData() 17 | setCalendarData(debugCalendar.id, debugEventData) 18 | } 19 | 20 | // Note: Not in use 21 | // Used to load dumped json events found in debug logs from file. 22 | // fetchJsonEventsFile(plasmoid.file('', 'testevents.json'), 'testevents@gmail.com') // .../contents/testevents.json 23 | function fetchJsonEventsFile(filename, calendarId) { 24 | logger.debug('fetchJsonEventsFile', calendarId) 25 | debugCalendarManager.asyncRequests += 1 26 | Requests.getFile(filename, function(err, data) { 27 | if (err) { 28 | return callback(err) 29 | } 30 | 31 | var obj = JSON.parse(data) 32 | setCalendarData(calendarId, obj) 33 | debugCalendarManager.asyncRequestsDone += 1 34 | }) 35 | } 36 | 37 | function getCalendarList() { 38 | if (debugCalendar) { 39 | return [ debugCalendar ] 40 | } else { 41 | return [] 42 | } 43 | } 44 | 45 | function createEvent(calendarId, date, text) { 46 | var summary = text 47 | var start = { 48 | date: Shared.dateString(date), 49 | dateTime: date, 50 | } 51 | var endDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, 0, 0, 0) 52 | var end = { 53 | date: Shared.dateString(endDate), 54 | dateTime: endDate, 55 | } 56 | var description = '' 57 | var data = DebugFixtures.createEvent(summary, start, end, description) 58 | parseSingleEvent(calendarId, data) 59 | addEvent(calendarId, data) 60 | eventCreated(calendarId, data) 61 | } 62 | 63 | function deleteEvent(calendarId, eventId) { 64 | var data = getEvent(calendarId, eventId) 65 | removeEvent(calendarId, eventId) 66 | eventDeleted(calendarId, eventId, data) 67 | } 68 | 69 | 70 | onFetchAllCalendars: { 71 | fetchDebugEvents() 72 | } 73 | 74 | onCalendarParsing: { 75 | parseEventList(debugCalendar, data.items) 76 | } 77 | 78 | function parseEvent(calendar, event) { 79 | event.description = event.description || "" 80 | event.backgroundColor = calendar.backgroundColor 81 | event.canEdit = true 82 | } 83 | 84 | function parseEventList(calendar, eventList) { 85 | eventList.forEach(function(event) { 86 | parseEvent(calendar, event) 87 | }) 88 | } 89 | 90 | function setEventProperty(calendarId, eventId, key, value) { 91 | logger.log('debugCalendarManager.setEventProperty', calendarId, eventId, key, value) 92 | var event = getEvent(calendarId, eventId) 93 | if (!event) { 94 | logger.log('error, trying to update event that doesn\'t exist') 95 | return 96 | } 97 | event[key] = value 98 | eventUpdated(calendarId, eventId, event) 99 | } 100 | 101 | function setEventProperties(calendarId, eventId, args) { 102 | logger.debugJSON('debugCalendarManager.setEventProperties', calendarId, eventId, args) 103 | var keys = Object.keys(args) 104 | for (var i = 0; i < keys.length; i++) { 105 | var key = keys[i] 106 | var value = args[key] 107 | setEventProperty(calendarId, eventId, key, value) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/DebugGoogleCalendarManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../lib/Requests.js" as Requests 4 | 5 | CalendarManager { 6 | id: debugCalendarManager 7 | 8 | calendarManagerId: "DebugGoogleCalendar" 9 | 10 | function fetchDebugGoogleSession() { 11 | if (plasmoid.configuration.accessToken) { 12 | return 13 | } 14 | // Steal accessToken from our current user's config. 15 | fetchCurrentUserConfig(function(err, metadata) { 16 | plasmoid.configuration.sessionClientId = metadata['sessionClientId'] 17 | plasmoid.configuration.sessionClientSecret = metadata['sessionClientSecret'] 18 | plasmoid.configuration.accessToken = metadata['accessToken'] 19 | plasmoid.configuration.refreshToken = metadata['refreshToken'] 20 | plasmoid.configuration.accessToken = metadata['accessToken'] 21 | plasmoid.configuration.accessTokenType = metadata['accessTokenType'] 22 | plasmoid.configuration.accessTokenExpiresAt = metadata['accessTokenExpiresAt'] 23 | plasmoid.configuration.calendarIdList = metadata['calendarIdList'] 24 | plasmoid.configuration.calendarList = metadata['calendarList'] 25 | plasmoid.configuration.tasklistIdList = metadata['tasklistIdList'] 26 | plasmoid.configuration.tasklistList = metadata['tasklistList'] 27 | plasmoid.configuration.agendaNewEventLastCalendarId = metadata['agendaNewEventLastCalendarId'] 28 | }) 29 | } 30 | 31 | function fetchCurrentUserConfig(callback) { 32 | var url = 'file:///home/chris/.config/plasma-org.kde.plasma.desktop-appletsrc' 33 | Requests.getFile(url, function(err, data) { 34 | if (err) { 35 | return callback(err) 36 | } 37 | 38 | var metadata = Requests.parseMetadata(data) 39 | callback(null, metadata) 40 | }) 41 | } 42 | 43 | onFetchAllCalendars: { 44 | fetchDebugGoogleSession() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/GoogleApiSession.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | import "../lib/Requests.js" as Requests 4 | 5 | QtObject { 6 | id: googleApiSession 7 | 8 | readonly property string accessToken: plasmoid.configuration.accessToken 9 | 10 | //--- Refresh Credentials 11 | function checkAccessToken(callback) { 12 | logger.debug('checkAccessToken') 13 | if (plasmoid.configuration.accessTokenExpiresAt < Date.now() + 5000) { 14 | updateAccessToken(callback) 15 | } else { 16 | callback(null) 17 | } 18 | } 19 | 20 | function updateAccessToken(callback) { 21 | // logger.debug('accessTokenExpiresAt', plasmoid.configuration.accessTokenExpiresAt) 22 | // logger.debug(' now', Date.now()) 23 | // logger.debug('refreshToken', plasmoid.configuration.refreshToken) 24 | if (plasmoid.configuration.refreshToken) { 25 | logger.debug('updateAccessToken') 26 | fetchNewAccessToken(function(err, data, xhr) { 27 | if (err || (!err && data && data.error)) { 28 | logger.log('Error when using refreshToken:', err, data) 29 | return callback(err) 30 | } 31 | logger.debug('onAccessToken', data) 32 | data = JSON.parse(data) 33 | 34 | googleApiSession.applyAccessToken(data) 35 | 36 | callback(null) 37 | }) 38 | } else { 39 | callback('No refresh token. Cannot update access token.') 40 | } 41 | } 42 | 43 | signal accessTokenError(string msg) 44 | signal newAccessToken() 45 | signal transactionError(string msg) 46 | 47 | onTransactionError: logger.log(msg) 48 | 49 | function applyAccessToken(data) { 50 | plasmoid.configuration.accessToken = data.access_token 51 | plasmoid.configuration.accessTokenType = data.token_type 52 | plasmoid.configuration.accessTokenExpiresAt = Date.now() + data.expires_in * 1000 53 | newAccessToken() 54 | } 55 | 56 | function fetchNewAccessToken(callback) { 57 | logger.debug('fetchNewAccessToken') 58 | var url = 'https://www.googleapis.com/oauth2/v4/token' 59 | Requests.post({ 60 | url: url, 61 | data: { 62 | client_id: plasmoid.configuration.sessionClientId, 63 | client_secret: plasmoid.configuration.sessionClientSecret, 64 | refresh_token: plasmoid.configuration.refreshToken, 65 | grant_type: 'refresh_token', 66 | }, 67 | }, callback) 68 | } 69 | 70 | 71 | //--- 72 | property int errorCount: 0 73 | function getErrorTimeout(n) { 74 | // Exponential Backoff 75 | // 43200 seconds is 12 hours, which is a reasonable polling limit when the API is down. 76 | // After 6 errors, we wait an entire minute. 77 | // After 11 errors, we wait an entire hour. 78 | // After 15 errors, we will have waited 9 hours. 79 | // 16 errors and above uses the upper limit of 12 hour intervals. 80 | return 1000 * Math.min(43200, Math.pow(2, n)) 81 | } 82 | // https://stackoverflow.com/questions/28507619/how-to-create-delay-function-in-qml 83 | function delay(delayTime, callback) { 84 | var timer = Qt.createQmlObject("import QtQuick 2.0; Timer {}", googleCalendarManager) 85 | timer.interval = delayTime 86 | timer.repeat = false 87 | timer.triggered.connect(callback) 88 | timer.triggered.connect(function release(){ 89 | timer.triggered.disconnect(callback) 90 | timer.triggered.disconnect(release) 91 | timer.destroy() 92 | }) 93 | timer.start() 94 | } 95 | function waitForErrorTimeout(callback) { 96 | errorCount += 1 97 | var timeout = getErrorTimeout(errorCount) 98 | delay(timeout, function(){ 99 | callback() 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/GoogleCalendarTests.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | // Google Calendar Errors are documented at: 4 | // https://developers.google.com/calendar/v3/errors 5 | 6 | function testCouldNotConnect(callback) { 7 | var err = 'HTTP 0' 8 | var data = null 9 | var xhr = { status: 0 } 10 | return callback(err, data, xhr) 11 | } 12 | 13 | function testInvalidCredentials(callback) { 14 | var err = { 15 | "error": { 16 | "errors": [ 17 | { 18 | "domain": "global", 19 | "reason": "authError", 20 | "message": "Invalid Credentials", 21 | "locationType": "header", 22 | "location": "Authorization", 23 | } 24 | ], 25 | "code": 401, 26 | "message": "Invalid Credentials" 27 | } 28 | } 29 | var data = null 30 | var xhr = { status: err.error.code } 31 | return callback(err, data, xhr) 32 | } 33 | 34 | function testDailyLimitExceeded(callback) { 35 | var err = { 36 | "error": { 37 | "errors": [ 38 | { 39 | "domain": "usageLimits", 40 | "reason": "dailyLimitExceeded", 41 | "message": "Daily Limit Exceeded" 42 | } 43 | ], 44 | "code": 403, 45 | "message": "Daily Limit Exceeded" 46 | } 47 | } 48 | var data = null 49 | var xhr = { status: err.error.code } 50 | return callback(err, data, xhr) 51 | } 52 | 53 | function testUserRateLimitExceeded(callback) { 54 | var err = { 55 | "error": { 56 | "errors": [ 57 | { 58 | "domain": "usageLimits", 59 | "reason": "userRateLimitExceeded", 60 | "message": "User Rate Limit Exceeded" 61 | } 62 | ], 63 | "code": 403, 64 | "message": "User Rate Limit Exceeded" 65 | } 66 | } 67 | var data = null 68 | var xhr = { status: err.error.code } 69 | return callback(err, data, xhr) 70 | } 71 | 72 | function testRateLimitExceeded(callback) { 73 | var err = { 74 | "error": { 75 | "errors": [ 76 | { 77 | "domain": "usageLimits", 78 | "reason": "rateLimitExceeded", 79 | "message": "Rate Limit Exceeded" 80 | } 81 | ], 82 | "code": 403, 83 | "message": "Rate Limit Exceeded" 84 | } 85 | } 86 | var data = null 87 | var xhr = { status: err.error.code } 88 | return callback(err, data, xhr) 89 | } 90 | 91 | function testBackendError(callback) { 92 | var err = { 93 | "error": { 94 | "errors": [ 95 | { 96 | "domain": "global", 97 | "reason": "backendError", 98 | "message": "Backend Error", 99 | } 100 | ], 101 | "code": 500, 102 | "message": "Backend Error" 103 | } 104 | } 105 | var data = null 106 | var xhr = { status: err.error.code } 107 | return callback(err, data, xhr) 108 | } 109 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/ICalManager.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.core 2.0 as PlasmaCore 3 | 4 | import "../lib" 5 | 6 | CalendarManager { 7 | id: icalManager 8 | 9 | calendarManagerId: "ical" 10 | ExecUtil { id: executable } 11 | 12 | // property var eventsData: { "items": [] } 13 | 14 | property var calendarList: [ 15 | { 16 | url: "/home/chris/Code/icsjson/basic.ics", 17 | backgroundColor: '#ff0', 18 | isTasklist: false, 19 | } 20 | ] 21 | 22 | function getCalendar(calendarId) { 23 | for (var i = 0; i < calendarList.length; i++) { 24 | var calendarData = calendarList[i] 25 | if (calendarData.url == calendarId) { 26 | return calendarData 27 | } 28 | } 29 | return null 30 | } 31 | 32 | function fetchEvents(calendarData, startTime, endTime, callback) { 33 | logger.debug('ical.fetchEvents', calendarData.url) 34 | var cmd = 'python3 ' + plasmoid.file("", "scripts/icsjson.py") 35 | cmd += ' --url "' + calendarData.url + '"' // TODO proper argument wrapping 36 | cmd += ' query' 37 | cmd += ' ' + startTime.getFullYear() + '-' + (startTime.getMonth()+1) + '-' + startTime.getDate() 38 | cmd += ' ' + endTime.getFullYear() + '-' + (endTime.getMonth()+1) + '-' + endTime.getDate() 39 | executable.exec(cmd, function(cmd, exitCode, exitStatus, stdout, stderr) { 40 | if (exitCode) { 41 | logger.log('ical.stderr', stderr) 42 | return callback(stderr) 43 | } 44 | var data = JSON.parse(stdout) 45 | // console.log(cmd) 46 | // console.log(str) 47 | callback(null, data) 48 | }) 49 | } 50 | 51 | function fetchCalendar(calendarData) { 52 | icalManager.asyncRequests += 0 53 | fetchEvents(calendarData, dateMin, dateMax, function(err, data) { 54 | parseEventList(calendarData, data.items) 55 | setCalendarData(calendarData.url, data) 56 | icalManager.asyncRequestsDone += 1 57 | }) 58 | } 59 | 60 | onFetchAllCalendars: { 61 | for (var i = 0; i < calendarList.length; i++) { 62 | var calendarData = calendarList[i] 63 | fetchCalendar(calendarData) 64 | } 65 | } 66 | 67 | onCalendarParsing: { 68 | var calendar = getCalendar(calendarId) 69 | parseEventList(calendar, data.items) 70 | } 71 | 72 | function parseEvent(calendar, event) { 73 | event.backgroundColor = calendar.backgroundColor 74 | event.canEdit = false 75 | } 76 | 77 | function parseEventList(calendar, eventList) { 78 | eventList.forEach(function(event) { 79 | parseEvent(calendar, event) 80 | }) 81 | } 82 | 83 | // onCalendarFetched: { 84 | // console.log(calendarId, data) 85 | // } 86 | 87 | // Component.onCompleted: { 88 | // var startTime = new Date(2017, 07-1, 01) 89 | // var endTime = new Date(2017, 07-1, 31) 90 | // dateMin = startTime 91 | // dateMax = endTime 92 | // fetchAllCalendars() 93 | // } 94 | } 95 | -------------------------------------------------------------------------------- /package/contents/ui/calendars/PlasmaCalendarUtils.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/calendar/eventpluginsmanager.h 4 | // https://github.com/KDE/plasma-framework/blob/master/src/declarativeimports/calendar/eventpluginsmanager.cpp 5 | 6 | function getPluginFilename(pluginPath) { 7 | return pluginPath.substr(pluginPath.lastIndexOf('/') + 1) 8 | } 9 | 10 | function pluginPathToFilenameList(pluginPathList) { 11 | var pluginFilenameList = [] 12 | for (var i = 0; i < pluginPathList.length; i++) { 13 | var pluginFilename = getPluginFilename(pluginPathList[i]) 14 | if (pluginFilenameList.indexOf(pluginFilename) == -1) { 15 | pluginFilenameList.push(pluginFilename) 16 | } 17 | } 18 | return pluginFilenameList 19 | } 20 | 21 | function getPluginPath(eventPluginsManager, pluginFilenameA) { 22 | for (var i = 0; i < eventPluginsManager.model.rowCount(); i++) { 23 | var pluginPath = eventPluginsManager.model.get(i, 'pluginPath') 24 | // console.log('\t\t', i, pluginPath) 25 | var pluginFilenameB = getPluginFilename(pluginPath) 26 | if (pluginFilenameA == pluginFilenameB) { 27 | return pluginPath 28 | } 29 | } 30 | 31 | // Plugin not installed 32 | return null 33 | } 34 | 35 | function pluginFilenameToPathList(eventPluginsManager, pluginFilenameList) { 36 | // console.log('eventPluginsManager', eventPluginsManager) 37 | // console.log('eventPluginsManager.model', eventPluginsManager.model) 38 | // console.log('eventPluginsManager.model.rowCount', eventPluginsManager.model.rowCount()) 39 | var pluginPathList = [] 40 | for (var i = 0; i < pluginFilenameList.length; i++) { 41 | var pluginFilename = pluginFilenameList[i] 42 | // console.log('\t\t', i, pluginFilename) 43 | var pluginPath = getPluginPath(eventPluginsManager, pluginFilename) 44 | if (!pluginPath) { 45 | console.log('[eventcalendar] Tried to load ', pluginFilename, ' however the plasma calendar plugin is not installed.') 46 | continue 47 | } 48 | if (pluginPathList.indexOf(pluginPath) == -1) { 49 | pluginPathList.push(pluginPath) 50 | } 51 | } 52 | // console.log('pluginFilenameList', pluginFilenameList) 53 | // console.log('pluginPathList', pluginPathList) 54 | return pluginPathList 55 | } 56 | 57 | function populateEnabledPluginsByFilename(eventPluginsManager, pluginFilenameList) { 58 | var pluginPathList = pluginFilenameToPathList(eventPluginsManager, pluginFilenameList) 59 | eventPluginsManager.populateEnabledPluginsList(pluginPathList) 60 | } 61 | 62 | function setEnabledPluginsByFilename(eventPluginsManager, pluginFilenameList) { 63 | var pluginPathList = pluginFilenameToPathList(eventPluginsManager, pluginFilenameList) 64 | eventPluginsManager.enabledPlugins = pluginPathList 65 | } 66 | 67 | -------------------------------------------------------------------------------- /package/contents/ui/code/ColorIdMap.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | var colorIdMap = { 4 | "calendar": { 5 | "1": { 6 | "background": "#ac725e", 7 | "foreground": "#1d1d1d" 8 | }, 9 | "2": { 10 | "background": "#d06b64", 11 | "foreground": "#1d1d1d" 12 | }, 13 | "3": { 14 | "background": "#f83a22", 15 | "foreground": "#1d1d1d" 16 | }, 17 | "4": { 18 | "background": "#fa573c", 19 | "foreground": "#1d1d1d" 20 | }, 21 | "5": { 22 | "background": "#ff7537", 23 | "foreground": "#1d1d1d" 24 | }, 25 | "6": { 26 | "background": "#ffad46", 27 | "foreground": "#1d1d1d" 28 | }, 29 | "7": { 30 | "background": "#42d692", 31 | "foreground": "#1d1d1d" 32 | }, 33 | "8": { 34 | "background": "#16a765", 35 | "foreground": "#1d1d1d" 36 | }, 37 | "9": { 38 | "background": "#7bd148", 39 | "foreground": "#1d1d1d" 40 | }, 41 | "10": { 42 | "background": "#b3dc6c", 43 | "foreground": "#1d1d1d" 44 | }, 45 | "11": { 46 | "background": "#fbe983", 47 | "foreground": "#1d1d1d" 48 | }, 49 | "12": { 50 | "background": "#fad165", 51 | "foreground": "#1d1d1d" 52 | }, 53 | "13": { 54 | "background": "#92e1c0", 55 | "foreground": "#1d1d1d" 56 | }, 57 | "14": { 58 | "background": "#9fe1e7", 59 | "foreground": "#1d1d1d" 60 | }, 61 | "15": { 62 | "background": "#9fc6e7", 63 | "foreground": "#1d1d1d" 64 | }, 65 | "16": { 66 | "background": "#4986e7", 67 | "foreground": "#1d1d1d" 68 | }, 69 | "17": { 70 | "background": "#9a9cff", 71 | "foreground": "#1d1d1d" 72 | }, 73 | "18": { 74 | "background": "#b99aff", 75 | "foreground": "#1d1d1d" 76 | }, 77 | "19": { 78 | "background": "#c2c2c2", 79 | "foreground": "#1d1d1d" 80 | }, 81 | "20": { 82 | "background": "#cabdbf", 83 | "foreground": "#1d1d1d" 84 | }, 85 | "21": { 86 | "background": "#cca6ac", 87 | "foreground": "#1d1d1d" 88 | }, 89 | "22": { 90 | "background": "#f691b2", 91 | "foreground": "#1d1d1d" 92 | }, 93 | "23": { 94 | "background": "#cd74e6", 95 | "foreground": "#1d1d1d" 96 | }, 97 | "24": { 98 | "background": "#a47ae2", 99 | "foreground": "#1d1d1d" 100 | } 101 | }, 102 | "event": { 103 | "1": { 104 | "background": "#a4bdfc", 105 | "foreground": "#1d1d1d" 106 | }, 107 | "2": { 108 | "background": "#7ae7bf", 109 | "foreground": "#1d1d1d" 110 | }, 111 | "3": { 112 | "background": "#dbadff", 113 | "foreground": "#1d1d1d" 114 | }, 115 | "4": { 116 | "background": "#ff887c", 117 | "foreground": "#1d1d1d" 118 | }, 119 | "5": { 120 | "background": "#fbd75b", 121 | "foreground": "#1d1d1d" 122 | }, 123 | "6": { 124 | "background": "#ffb878", 125 | "foreground": "#1d1d1d" 126 | }, 127 | "7": { 128 | "background": "#46d6db", 129 | "foreground": "#1d1d1d" 130 | }, 131 | "8": { 132 | "background": "#e1e1e1", 133 | "foreground": "#1d1d1d" 134 | }, 135 | "9": { 136 | "background": "#5484ed", 137 | "foreground": "#1d1d1d" 138 | }, 139 | "10": { 140 | "background": "#51b749", 141 | "foreground": "#1d1d1d" 142 | }, 143 | "11": { 144 | "background": "#dc2127", 145 | "foreground": "#1d1d1d" 146 | }, 147 | }, 148 | } 149 | -------------------------------------------------------------------------------- /package/contents/ui/config/ColorTextButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import org.kde.kirigami 2.0 as Kirigami 4 | 5 | Button { 6 | id: colorTextButton 7 | property int padding: Kirigami.Units.smallSpacing 8 | implicitWidth: padding + colorTextLabel.implicitWidth + padding 9 | implicitHeight: padding + colorTextLabel.implicitHeight + padding 10 | 11 | property alias label: colorTextLabel.text 12 | 13 | Label { 14 | id: colorTextLabel 15 | anchors.centerIn: parent 16 | color: Kirigami.Theme.buttonTextColor 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigAgenda.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.kirigami 2.0 as Kirigami 5 | 6 | import ".." 7 | import "../lib" 8 | 9 | ConfigPage { 10 | id: page 11 | 12 | property int indentWidth: 24 * Kirigami.Units.devicePixelRatio 13 | 14 | ConfigCheckBox { 15 | configKey: 'widgetShowAgenda' 16 | text: i18n("Show agenda") 17 | } 18 | 19 | ConfigSection { 20 | ConfigSpinBox { 21 | configKey: 'agendaFontSize' 22 | before: i18n("Font Size:") 23 | suffix: i18n("px") 24 | after: i18n(" (0px = System Settings > Fonts > General)") 25 | } 26 | } 27 | 28 | ConfigSection { 29 | RowLayout { 30 | ConfigCheckBox { 31 | configKey: 'agendaWeatherShowIcon' 32 | checked: true 33 | text: i18n("Weather Icon") 34 | } 35 | ConfigSlider { 36 | configKey: 'agendaWeatherIconHeight' 37 | minimumValue: 12 38 | maximumValue: 48 39 | stepSize: 1 40 | after: '' + value + i18n("px") 41 | Layout.fillWidth: false 42 | } 43 | } 44 | 45 | RowLayout { 46 | Text { width: indentWidth } // Indent 47 | ConfigCheckBox { 48 | configKey: 'showOutlines' 49 | text: i18n("Icon Outline") 50 | } 51 | } 52 | 53 | ConfigCheckBox { 54 | configKey: 'agendaWeatherShowText' 55 | text: i18n("Weather Text") 56 | } 57 | 58 | ConfigRadioButtonGroup { 59 | configKey: 'agendaWeatherOnRight' 60 | label: i18n("Position:") 61 | model: [ 62 | { value: false, text: i18n("Left") }, 63 | { value: true, text: i18n("Right") }, 64 | ] 65 | } 66 | 67 | ConfigRadioButtonGroup { 68 | id: clickWeatherGroup 69 | label: i18n("Click Weather:") 70 | RadioButton { 71 | text: i18n("Open City Forecast In Browser") 72 | exclusiveGroup: clickWeatherGroup.exclusiveGroup 73 | checked: true 74 | } 75 | } 76 | } 77 | 78 | ConfigSection { 79 | ConfigRadioButtonGroup { 80 | id: clickDateGroup 81 | label: i18n("Click Date:") 82 | RadioButton { 83 | text: i18n("Open New Event In Browser") 84 | exclusiveGroup: clickDateGroup.exclusiveGroup 85 | enabled: false 86 | } 87 | RadioButton { 88 | text: i18n("Open New Event Form") 89 | exclusiveGroup: clickDateGroup.exclusiveGroup 90 | checked: true 91 | } 92 | } 93 | } 94 | 95 | ConfigSection { 96 | RowLayout { 97 | ConfigCheckBox { 98 | configKey: 'agendaShowEventDescription' 99 | text: i18n("Event description") 100 | } 101 | // ConfigSpinBox { 102 | // configKey: 'agendaMaxDescriptionLines' 103 | // after: i18n("lines") 104 | // } 105 | } 106 | ConfigCheckBox { 107 | configKey: 'agendaCondensedAllDayEvent' 108 | text: i18n("Hide 'All Day' text") 109 | } 110 | ConfigCheckBox { 111 | configKey: 'agendaShowEventHangoutLink' 112 | text: i18n("Google Hangouts link") 113 | } 114 | ConfigRadioButtonGroup { 115 | id: clickEventGroup 116 | label: i18n("Click Event:") 117 | RadioButton { 118 | text: i18n("Open Event In Browser") 119 | exclusiveGroup: clickEventGroup.exclusiveGroup 120 | checked: true 121 | } 122 | } 123 | } 124 | 125 | 126 | ConfigSection { 127 | ConfigRadioButtonGroup { 128 | configKey: 'agendaBreakupMultiDayEvents' 129 | label: i18n("Show multi-day events:") 130 | model: [ 131 | { value: true, text: i18n("On all days") }, 132 | { value: false, text: i18n("Only on the first and current day") }, 133 | ] 134 | } 135 | } 136 | 137 | ConfigSection { 138 | ConfigCheckBox { 139 | configKey: 'agendaNewEventRememberCalendar' 140 | text: i18n("Remember selected calendar in New Event Form") 141 | } 142 | } 143 | 144 | ConfigSection { 145 | title: i18n("Current Month") 146 | 147 | CheckBox { 148 | enabled: false 149 | checked: true 150 | text: i18n("Always show next 14 days") 151 | } 152 | CheckBox { 153 | enabled: false 154 | checked: false 155 | text: i18n("Hide completed events") 156 | } 157 | CheckBox { 158 | enabled: false 159 | checked: true 160 | text: i18n("Show all events of the current day (including completed events)") 161 | } 162 | } 163 | 164 | AppletConfig { id: config } 165 | ColorGrid { 166 | title: i18n("Colors") 167 | 168 | ConfigColor { 169 | configKey: 'agendaInProgressColor' 170 | label: i18n("In Progress") 171 | defaultColor: config.agendaInProgressColorDefault 172 | } 173 | } 174 | 175 | ConfigSection { 176 | ConfigSpinBox { 177 | configKey: 'agendaDaySpacing' 178 | before: i18n("Day Spacing:") 179 | suffix: i18n("px") 180 | } 181 | ConfigSpinBox { 182 | configKey: 'agendaEventSpacing' 183 | before: i18n("Event Spacing:") 184 | suffix: i18n("px") 185 | } 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigCalendar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | 5 | import "../lib" 6 | 7 | ConfigPage { 8 | id: page 9 | 10 | ConfigCheckBox { 11 | configKey: 'widgetShowCalendar' 12 | text: i18n("Show calendar") 13 | } 14 | 15 | ConfigSection { 16 | ConfigRadioButtonGroup { 17 | id: clickDateGroup 18 | label: i18n("Click Date:") 19 | RadioButton { 20 | text: i18n("Scroll to event in Agenda") 21 | exclusiveGroup: clickDateGroup.exclusiveGroup 22 | checked: true 23 | } 24 | } 25 | } 26 | 27 | ConfigSection { 28 | ConfigRadioButtonGroup { 29 | id: doubleClickDateGroup 30 | label: i18n("Double-click on Date:") 31 | configKey: 'monthDayDoubleClick' 32 | model: [ 33 | { value: 'GoogleCalWeb', text: i18n("New Google Calendar Event (Web Browser)") }, 34 | { value: 'DoNothing', text: i18n("Do Nothing") }, 35 | ] 36 | } 37 | } 38 | 39 | HeaderText { 40 | text: i18n("Style") 41 | } 42 | ConfigSection { 43 | RowLayout { 44 | Layout.fillWidth: true 45 | Label { 46 | text: i18n("Current Month Title:") 47 | } 48 | ConfigString { 49 | id: monthCurrentCustomTitleFormat 50 | configKey: 'monthCurrentCustomTitleFormat' 51 | placeholderText: i18nc("calendar title format for current month", "MMMM d, yyyy") 52 | } 53 | Label { 54 | text: Qt.formatDateTime(new Date(), monthCurrentCustomTitleFormat.value) 55 | } 56 | } 57 | 58 | ConfigCheckBox { 59 | configKey: 'monthShowBorder' 60 | text: i18n("Show Borders") 61 | } 62 | ConfigCheckBox { 63 | configKey: 'monthShowWeekNumbers' 64 | text: i18n("Show Week Numbers") 65 | } 66 | ConfigCheckBox { 67 | configKey: 'monthHighlightCurrentDayWeek' 68 | text: i18n("Highlight Current Day / Week") 69 | } 70 | RowLayout { 71 | Label { 72 | text: i18n("First day of week:") 73 | } 74 | ComboBox { 75 | // [-1, 0, 1, 2, 3, 4, 5, 6] // Default = -1, 0..6 = Sun..Sat 76 | model: ListModel {} 77 | textRole: "text" 78 | 79 | Component.onCompleted: { 80 | model.append({ 81 | text: i18n("Default"), 82 | value: -1, 83 | }) 84 | for (var i = 0; i < 7; i++) { 85 | model.append({ 86 | text: Qt.locale().dayName(i), 87 | value: i, 88 | }) 89 | } 90 | 91 | // The firstDayOfWeek enum starts at -1 instead of 0 92 | currentIndex = plasmoid.configuration.firstDayOfWeek + 1 93 | currentIndexChanged.connect(function(){ 94 | plasmoid.configuration.firstDayOfWeek = currentIndex - 1 95 | }) 96 | } 97 | } 98 | } 99 | ConfigRadioButtonGroup { 100 | configKey: 'monthEventBadgeType' 101 | label: i18n("Event Badge:") 102 | model: [ 103 | { value: 'theme', text: i18n("Theme") }, 104 | { value: 'dots', text: i18n("Dots (3 Maximum)") }, 105 | { value: 'bottomBar', text: i18n("Bottom Bar (Event Color)") }, 106 | { value: 'bottomBarHighlight', text: i18n("Bottom Bar (Highlight)") }, 107 | { value: 'count', text: i18n("Count") }, 108 | ] 109 | } 110 | 111 | ConfigSlider { 112 | configKey: 'monthCellRadius' 113 | minimumValue: 0 114 | maximumValue: 1 115 | before: i18n("Radius:") 116 | after: "" + Math.round(value*100) + "%" 117 | Layout.fillWidth: false 118 | } 119 | 120 | ConfigRadioButtonGroup { 121 | id: selectedStyleGroup 122 | label: i18n("Selected:") 123 | RadioButton { 124 | text: i18n("Solid Color (Highlight)") 125 | exclusiveGroup: selectedStyleGroup.exclusiveGroup 126 | checked: true 127 | } 128 | } 129 | 130 | ConfigRadioButtonGroup { 131 | configKey: 'monthTodayStyle' 132 | label: i18n("Today:") 133 | model: [ 134 | { value: 'theme', text: i18n("Solid Color (Inverted)") }, 135 | { value: 'bigNumber', text: i18n("Big Number") }, 136 | ] 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigEvents.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | 5 | import org.kde.plasma.calendar 2.0 as PlasmaCalendar 6 | 7 | import "../lib" 8 | import "../calendars/PlasmaCalendarUtils.js" as PlasmaCalendarUtils 9 | 10 | ConfigPage { 11 | id: page 12 | 13 | HeaderText { 14 | text: i18n("Event Calendar Plugins") 15 | } 16 | 17 | ConfigSection { 18 | CheckBox { 19 | text: i18n("ICalendar (.ics)") 20 | checked: true 21 | enabled: false 22 | visible: plasmoid.configuration.debugging 23 | } 24 | CheckBox { 25 | text: i18n("Google Calendar") 26 | checked: true 27 | enabled: false 28 | } 29 | } 30 | 31 | 32 | HeaderText { 33 | text: i18n("Plasma Calendar Plugins") 34 | } 35 | 36 | // From digitalclock's configCalendar.qml 37 | signal configurationChanged() 38 | ConfigSection { 39 | Repeater { 40 | id: calendarPluginsRepeater 41 | model: PlasmaCalendar.EventPluginsManager.model 42 | delegate: CheckBox { 43 | text: model.display 44 | checked: model.checked 45 | onClicked: { 46 | model.checked = checked // needed for model's setData to be called 47 | // page.configurationChanged() 48 | page.saveConfig() 49 | } 50 | } 51 | } 52 | } 53 | function saveConfig() { 54 | plasmoid.configuration.enabledCalendarPlugins = PlasmaCalendarUtils.pluginPathToFilenameList(PlasmaCalendar.EventPluginsManager.enabledPlugins) 55 | } 56 | Component.onCompleted: { 57 | PlasmaCalendarUtils.populateEnabledPluginsByFilename(PlasmaCalendar.EventPluginsManager, plasmoid.configuration.enabledCalendarPlugins) 58 | } 59 | 60 | HeaderText { 61 | text: i18n("Misc") 62 | } 63 | ColumnLayout { 64 | 65 | ConfigSpinBox { 66 | configKey: 'eventsPollInterval' 67 | before: i18n("Refresh events every: ") 68 | suffix: i18nc("Polling interval in minutes", "min") 69 | minimumValue: 5 70 | maximumValue: 90 71 | } 72 | } 73 | 74 | HeaderText { 75 | text: i18n("Notifications") 76 | } 77 | 78 | ConfigSection { 79 | ConfigNotification { 80 | label: i18n("Event Reminder") 81 | notificationEnabledKey: 'eventReminderNotificationEnabled' 82 | sfxEnabledKey: 'eventReminderSfxEnabled' 83 | sfxPathKey: 'eventReminderSfxPath' 84 | sfxPathDefaultValue: '/usr/share/sounds/Oxygen-Im-Nudge.ogg' 85 | 86 | RowLayout { 87 | spacing: 0 88 | Item { implicitWidth: parent.parent.indentWidth } // indent 89 | ConfigSpinBox { 90 | configKey: 'eventReminderMinutesBefore' 91 | suffix: i18nc("Polling interval in minutes", "min") 92 | minimumValue: 1 93 | } 94 | } 95 | } 96 | } 97 | 98 | ConfigSection { 99 | ConfigNotification { 100 | label: i18n("Event Starting") 101 | notificationEnabledKey: 'eventStartingNotificationEnabled' 102 | sfxEnabledKey: 'eventStartingSfxEnabled' 103 | sfxPathKey: 'eventStartingSfxPath' 104 | sfxPathDefaultValue: '/usr/share/sounds/Oxygen-Im-Nudge.ogg' 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigICal.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Controls.Styles 1.1 4 | import QtQuick.Dialogs 1.2 5 | import QtQuick.Layouts 1.0 6 | import org.kde.kirigami 2.0 as Kirigami 7 | 8 | import org.kde.kcoreaddons 1.0 as KCoreAddons 9 | 10 | import ".." 11 | import "../lib" 12 | 13 | ConfigPage { 14 | id: page 15 | 16 | KCoreAddons.KUser { 17 | id: kuser 18 | } 19 | 20 | Base64JsonListModel { 21 | id: calendarsModel 22 | configKey: 'icalCalendarList' 23 | 24 | function addCalendar() { 25 | addItem({ 26 | url: '', 27 | name: 'Label', 28 | backgroundColor: '' + Kirigami.Theme.highlightColor, 29 | show: true, 30 | isReadOnly: true, 31 | }) 32 | } 33 | 34 | function addNewCalendar() { 35 | var dirPath = '/home/' + kuser.loginName + '/.local/share/plasma_org.kde.plasma.eventcalendar' 36 | var icsPath = dirPath + '/calendar.ics' 37 | addItem({ 38 | url: icsPath, 39 | name: 'Label', 40 | backgroundColor: '' + Kirigami.Theme.highlightColor, 41 | show: true, 42 | isReadOnly: true, 43 | }) 44 | } 45 | } 46 | 47 | RowLayout { 48 | HeaderText { 49 | text: i18n("Calendars") 50 | } 51 | Button { 52 | iconName: "resource-calendar-insert" 53 | text: i18n("Add Calendar") 54 | onClicked: calendarsModel.addCalendar() 55 | } 56 | Button { 57 | iconName: "resource-calendar-insert" 58 | text: i18n("New Calendar") 59 | onClicked: calendarsModel.addNewCalendar() 60 | } 61 | } 62 | 63 | ColumnLayout { 64 | Layout.fillWidth: true 65 | spacing: 20 * Kirigami.Units.devicePixelRatio // x4 the default spacing (5px) 66 | 67 | Repeater { 68 | model: calendarsModel 69 | delegate: RowLayout { 70 | spacing: 0 71 | 72 | CheckBox { 73 | Layout.preferredHeight: labelTextField.height 74 | Layout.preferredWidth: height 75 | Layout.alignment: Qt.AlignTop 76 | checked: show 77 | style: CheckBoxStyle {} 78 | 79 | onClicked: { 80 | calendarsModel.setProperty(index, 'show', checked) 81 | } 82 | } 83 | ColumnLayout { 84 | RowLayout { 85 | Rectangle { 86 | Layout.preferredHeight: labelTextField.height 87 | Layout.preferredWidth: height 88 | color: model.backgroundColor 89 | } 90 | TextField { 91 | id: labelTextField 92 | Layout.fillWidth: true 93 | text: model.name 94 | placeholderText: i18n("Calendar Label") 95 | } 96 | Button { 97 | iconName: "trash-empty" 98 | onClicked: calendarsModel.removeIndex(index) 99 | } 100 | } 101 | RowLayout { 102 | TextField { 103 | id: calendarUrlField 104 | Layout.fillWidth: true 105 | text: model.url 106 | onTextChanged: calendarsModel.setItemProperty(index, 'url', text) 107 | } 108 | 109 | Button { 110 | iconName: "folder-open" 111 | text: i18n("Browse") 112 | onClicked: { 113 | filePicker.open() 114 | } 115 | 116 | FileDialog { 117 | id: filePicker 118 | 119 | nameFilters: [ i18n("iCalendar (*.ics)") ] 120 | 121 | onFileUrlChanged: { 122 | calendarUrlField.text = fileUrl 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigLayout.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.kirigami 2.0 as Kirigami 5 | import QtGraphicalEffects 1.0 // Colorize 6 | 7 | import ".." 8 | import "../lib" 9 | 10 | ConfigPage { 11 | id: page 12 | 13 | SystemPalette { 14 | id: syspal 15 | } 16 | 17 | //--- 18 | ExclusiveGroup { id: layoutGroup } 19 | RadioButton { 20 | text: i18n("Calendar to the left of the Agenda (Two Columns)") 21 | exclusiveGroup: layoutGroup 22 | checked: plasmoid.configuration.twoColumns 23 | onClicked: plasmoid.configuration.twoColumns = true 24 | Layout.fillWidth: false 25 | Layout.alignment: Qt.AlignHCenter 26 | } 27 | GridLayout { 28 | Layout.fillWidth: false 29 | Layout.alignment: Qt.AlignHCenter 30 | Layout.preferredWidth: 400 * Kirigami.Units.devicePixelRatio 31 | columns: 3 32 | 33 | //--- Row1 34 | ConfigDimension { 35 | configKey: 'leftColumnWidth' 36 | suffix: i18n("px") 37 | orientation: Qt.Horizontal 38 | lineColor: syspal.text 39 | Layout.column: 1 40 | Layout.row: 0 41 | } 42 | 43 | ConfigDimension { 44 | configKey: 'rightColumnWidth' 45 | suffix: i18n("px") 46 | orientation: Qt.Horizontal 47 | lineColor: syspal.text 48 | Layout.column: 2 49 | Layout.row: 0 50 | } 51 | 52 | //--- Row2 53 | ConfigDimension { 54 | configKey: 'topRowHeight' 55 | suffix: i18n("px") 56 | orientation: Qt.Vertical 57 | lineColor: syspal.text 58 | Layout.column: 0 59 | Layout.row: 1 60 | } 61 | 62 | //--- Row3 63 | ConfigDimension { 64 | configKey: 'bottomRowHeight' 65 | suffix: i18n("px") 66 | orientation: Qt.Vertical 67 | lineColor: syspal.text 68 | Layout.column: 0 69 | Layout.row: 2 70 | } 71 | 72 | //--- Center 73 | Item { 74 | Layout.column: 1 75 | Layout.row: 1 76 | Layout.columnSpan: 2 77 | Layout.rowSpan: 2 78 | 79 | implicitWidth: 300 * Kirigami.Units.devicePixelRatio 80 | implicitHeight: 300 * Kirigami.Units.devicePixelRatio 81 | 82 | Layout.fillWidth: true 83 | Layout.fillHeight: true 84 | 85 | Image { 86 | id: twoColumnsImage 87 | anchors.fill: parent 88 | source: plasmoid.file("", "images/twocolumns.svg") 89 | smooth: true 90 | visible: false 91 | } 92 | 93 | ColorOverlay { 94 | anchors.fill: parent 95 | source: twoColumnsImage 96 | color: syspal.text 97 | opacity: 0.8 98 | } 99 | } 100 | } 101 | 102 | //--- 103 | Item { 104 | implicitHeight: Kirigami.Units.largeSpacing * 2 105 | } 106 | 107 | //--- 108 | RadioButton { 109 | text: i18n("Agenda below the Calendar (Single Column)") 110 | exclusiveGroup: layoutGroup 111 | checked: !plasmoid.configuration.twoColumns 112 | onClicked: plasmoid.configuration.twoColumns = false 113 | Layout.fillWidth: false 114 | Layout.alignment: Qt.AlignHCenter 115 | } 116 | 117 | GridLayout { 118 | Layout.fillWidth: false 119 | Layout.alignment: Qt.AlignHCenter 120 | Layout.preferredWidth: 400 * Kirigami.Units.devicePixelRatio 121 | columns: 3 122 | 123 | //--- Row1 124 | Item { 125 | implicitWidth: 150 * Kirigami.Units.devicePixelRatio 126 | Layout.fillWidth: true 127 | Layout.column: 0 128 | Layout.row: 0 129 | } 130 | ConfigDimension { 131 | configKey: 'leftColumnWidth' 132 | suffix: i18n("px") 133 | orientation: Qt.Horizontal 134 | lineColor: syspal.text 135 | Layout.column: 1 136 | Layout.row: 0 137 | } 138 | 139 | //--- Row2 140 | ConfigDimension { 141 | configKey: 'monthHeightSingleColumn' 142 | suffix: i18n("px") 143 | orientation: Qt.Vertical 144 | lineColor: syspal.text 145 | Layout.column: 2 146 | Layout.row: 1 147 | } 148 | 149 | //--- Row3 150 | Item { 151 | implicitHeight: 150 * Kirigami.Units.devicePixelRatio 152 | Layout.column: 2 153 | Layout.row: 2 154 | } 155 | 156 | //--- Center 157 | Item { 158 | Layout.column: 0 159 | Layout.row: 1 160 | Layout.columnSpan: 2 161 | Layout.rowSpan: 2 162 | 163 | implicitWidth: 300 * Kirigami.Units.devicePixelRatio 164 | implicitHeight: 300 * Kirigami.Units.devicePixelRatio 165 | 166 | Layout.fillWidth: true 167 | Layout.fillHeight: true 168 | 169 | Image { 170 | id: singleColumnImage 171 | anchors.fill: parent 172 | source: plasmoid.file("", "images/singlecolumn.svg") 173 | smooth: true 174 | visible: false 175 | } 176 | 177 | ColorOverlay { 178 | anchors.fill: parent 179 | source: singleColumnImage 180 | color: syspal.text 181 | opacity: 0.8 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigSerializedString.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | id: obj 5 | property string configKey: '' 6 | readonly property string configValue: configKey ? plasmoid.configuration[configKey] : '' 7 | property var value: null 8 | property var defaultValue: ({}) // Empty Map 9 | 10 | function serialize() { 11 | plasmoid.configuration[configKey] = Qt.btoa(JSON.stringify(value)) 12 | } 13 | 14 | function deserialize() { 15 | value = configValue ? JSON.parse(Qt.atob(configValue)) : defaultValue 16 | } 17 | 18 | onConfigKeyChanged: deserialize() 19 | onConfigValueChanged: deserialize() 20 | onValueChanged: { 21 | if (value === null) { 22 | return // 99% of the time this is unintended 23 | } 24 | serialize() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigTimezones.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | 5 | import org.kde.plasma.private.digitalclock 1.0 as DigitalClock 6 | 7 | import ".." 8 | import "../lib" 9 | 10 | // Mostly copied from digitalclock 11 | ColumnLayout { // ConfigPage creates a binding loop when a child uses fillHeight 12 | id: page 13 | 14 | function digitalclock_i18n(message) { 15 | return i18nd("plasma_applet_org.kde.plasma.digitalclock", message) 16 | } 17 | 18 | DigitalClock.TimeZoneModel { 19 | id: timeZoneModel 20 | 21 | selectedTimeZones: plasmoid.configuration.selectedTimeZones 22 | onSelectedTimeZonesChanged: plasmoid.configuration.selectedTimeZones = selectedTimeZones 23 | } 24 | 25 | MessageWidget { 26 | id: messageWidget 27 | } 28 | 29 | TextField { 30 | id: filter 31 | Layout.fillWidth: true 32 | placeholderText: digitalclock_i18n("Search Time Zones") 33 | } 34 | 35 | TableView { 36 | id: timeZoneView 37 | Layout.fillWidth: true 38 | Layout.fillHeight: true 39 | 40 | signal toggleCurrent 41 | 42 | Keys.onSpacePressed: toggleCurrent() 43 | 44 | model: DigitalClock.TimeZoneFilterProxy { 45 | sourceModel: timeZoneModel 46 | filterString: filter.text 47 | } 48 | 49 | TableViewColumn { 50 | role: "city" 51 | title: digitalclock_i18n("City") 52 | } 53 | TableViewColumn { 54 | role: "region" 55 | title: digitalclock_i18n("Region") 56 | } 57 | TableViewColumn { 58 | role: "comment" 59 | title: digitalclock_i18n("Comment") 60 | } 61 | TableViewColumn { 62 | role: "checked" 63 | title: i18n("Tooltip") 64 | delegate: CheckBox { 65 | id: checkBox 66 | anchors.centerIn: parent 67 | checked: styleData.value 68 | activeFocusOnTab: false // only let the TableView as a whole get focus 69 | 70 | function setValue(checked) { 71 | if (!checked && model.region == "Local") { 72 | messageWidget.warn(i18n("Cannot deselect Local time from the tooltip")) 73 | } else { 74 | model.checked = checked // needed for model's setData to be called 75 | } 76 | checkBox.checked = Qt.binding(function(){ return styleData.value }) 77 | } 78 | 79 | onClicked: checkBox.setValue(checked) 80 | 81 | Connections { 82 | target: timeZoneView 83 | onToggleCurrent: { 84 | if (styleData.row === timeZoneView.currentRow) { 85 | checkBox.setValue(!checkBox.checked) 86 | } 87 | } 88 | } 89 | } 90 | 91 | resizable: false 92 | movable: false 93 | } 94 | } 95 | 96 | 97 | ExclusiveGroup { id: timezoneDisplayType } 98 | RowLayout { 99 | Label { 100 | text: digitalclock_i18n("Display time zone as:") 101 | } 102 | 103 | RadioButton { 104 | id: timezoneCityRadio 105 | text: digitalclock_i18n("Time zone city") 106 | exclusiveGroup: timezoneDisplayType 107 | checked: !plasmoid.configuration.displayTimezoneAsCode 108 | onClicked: plasmoid.configuration.displayTimezoneAsCode = false 109 | } 110 | 111 | RadioButton { 112 | id: timezoneCodeRadio 113 | text: digitalclock_i18n("Time zone code") 114 | exclusiveGroup: timezoneDisplayType 115 | checked: plasmoid.configuration.displayTimezoneAsCode 116 | onClicked: plasmoid.configuration.displayTimezoneAsCode = true 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /package/contents/ui/config/ConfigWeather.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.plasma.extras 2.0 as PlasmaExtras 5 | 6 | import ".." 7 | import "../lib" 8 | 9 | ConfigPage { 10 | id: page 11 | 12 | HeaderText { 13 | text: i18n("Data") 14 | } 15 | 16 | ConfigComboBox { 17 | id: weatherService 18 | configKey: 'weatherService' 19 | 20 | model: [ 21 | { value: 'OpenWeatherMap', text: 'OpenWeatherMap' }, 22 | { value: 'WeatherCanada', text: 'WeatherCanada' }, 23 | ] 24 | } 25 | 26 | ConfigSection { 27 | RowLayout { 28 | visible: plasmoid.configuration.debugging && weatherService.value === 'OpenWeatherMap' 29 | Label { 30 | text: i18n("API App Id:") 31 | } 32 | ConfigString { 33 | configKey: 'openWeatherMapAppId' 34 | } 35 | } 36 | 37 | RowLayout { 38 | visible: weatherService.value === 'OpenWeatherMap' 39 | Label { 40 | text: i18n("City Id:") 41 | } 42 | ConfigString { 43 | id: weatherCityId 44 | configKey: 'openWeatherMapCityId' 45 | placeholderText: i18n("Eg: 5983720") 46 | } 47 | Button { 48 | text: i18n("Find City") 49 | onClicked: openWeatherMapCityDialog.open() 50 | } 51 | 52 | OpenWeatherMapCityDialog { 53 | id: openWeatherMapCityDialog 54 | onAccepted: { 55 | weatherCityId.value = selectedCityId 56 | } 57 | } 58 | } 59 | 60 | RowLayout { 61 | visible: weatherService.value === 'WeatherCanada' 62 | Label { 63 | text: i18n("City Id:") 64 | } 65 | ConfigString { 66 | id: weatherCanadaCityId 67 | configKey: 'weatherCanadaCityId' 68 | placeholderText: i18n("Eg: on-14") 69 | } 70 | Button { 71 | text: i18n("Find City") 72 | onClicked: weatherCanadaCityDialog.open() 73 | } 74 | 75 | WeatherCanadaCityDialog { 76 | id: weatherCanadaCityDialog 77 | onAccepted: { 78 | weatherCanadaCityId.value = selectedCityId 79 | } 80 | } 81 | } 82 | } 83 | 84 | HeaderText { 85 | text: i18n("Settings") 86 | } 87 | 88 | ConfigSection { 89 | ConfigSpinBox { 90 | // configKey: 'weatherPollInterval' 91 | before: i18n("Update forecast every: ") 92 | enabled: false 93 | value: 60 94 | suffix: i18nc("Polling interval in minutes", "min") 95 | minimumValue: 60 96 | maximumValue: 90 97 | } 98 | } 99 | 100 | HeaderText { 101 | text: i18n("Style") 102 | } 103 | 104 | ConfigSection { 105 | ConfigSpinBox { 106 | configKey: 'meteogramHours' 107 | before: i18n("Show next ") 108 | suffix: i18np(" hour", " hours", value) 109 | after: i18n(" in the meteogram.") 110 | minimumValue: 9 111 | maximumValue: 48 112 | stepSize: 3 113 | } 114 | } 115 | 116 | ConfigSection { 117 | ConfigRadioButtonGroup { 118 | configKey: 'weatherUnits' 119 | label: i18n("Units:") 120 | model: [ 121 | { value: 'metric', text: i18n("Celsius") }, 122 | { value: 'imperial', text: i18n("Fahrenheit") }, 123 | { value: 'kelvin', text: i18n("Kelvin") }, 124 | ] 125 | } 126 | } 127 | 128 | HeaderText { 129 | text: i18n("Colors") 130 | } 131 | 132 | AppletConfig { id: config } 133 | ColorGrid { 134 | ConfigColor { 135 | configKey: 'meteogramTextColor' 136 | label: i18n("Text") 137 | defaultColor: config.meteogramTextColorDefault 138 | } 139 | ConfigColor { 140 | configKey: 'meteogramGridColor' 141 | label: i18n("Grid") 142 | defaultColor: config.meteogramScaleColorDefault 143 | } 144 | ConfigColor { 145 | configKey: 'meteogramRainColor' 146 | label: i18n("Rain") 147 | defaultColor: config.meteogramPrecipitationRawColorDefault 148 | } 149 | ConfigColor { 150 | configKey: 'meteogramPositiveTempColor' 151 | label: i18n("Positive Temp") 152 | defaultColor: config.meteogramPositiveTempColorDefault 153 | } 154 | ConfigColor { 155 | configKey: 'meteogramNegativeTempColor' 156 | label: i18n("Negative Temp") 157 | defaultColor: config.meteogramNegativeTempColorDefault 158 | } 159 | ConfigColor { 160 | configKey: 'meteogramIconColor' 161 | label: i18n("Icons") 162 | defaultColor: config.meteogramIconColorDefault 163 | } 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /package/contents/ui/config/HeaderText.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Layouts 1.1 3 | import org.kde.plasma.extras 2.0 as PlasmaExtras 4 | import org.kde.kirigami 2.0 as Kirigami 5 | 6 | PlasmaExtras.Heading { 7 | id: heading 8 | text: "Heading" 9 | level: 2 10 | color: Kirigami.Theme.textColor 11 | Layout.fillWidth: true 12 | property bool showUnderline: level <= 2 13 | 14 | Rectangle { 15 | visible: heading.showUnderline 16 | anchors.bottom: heading.bottom 17 | width: heading.width 18 | height: 1 19 | color: heading.color 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package/contents/ui/config/LockIcon.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.kirigami 2.0 as Kirigami 3 | 4 | // We need to use this pattern in order to use the SystemPalette colors 5 | Canvas { 6 | contextType: "2d" 7 | property real size: Math.min(width, height) 8 | property color fillColor: Kirigami.Theme.textColor 9 | property int iconSize: 22 // Used for scaling the icon 10 | readonly property real scale: size/iconSize // Math.floor(size/iconSize) 11 | property alias path: iconPathSvg.path 12 | 13 | Path { 14 | id: iconPath 15 | PathSvg { 16 | id: iconPathSvg 17 | // Breeze 22px lock.svg (which symlinks to document-encrypted.svg) 18 | path: "M 11,3 C 8.784,3 7,4.784 7,7 l 0,4 -2,0 c 0,2.666667 0,5.333333 0,8 4,0 8,0 12,0 l 0,-8 c -0.666667,0 -1.333333,0 -2,0 L 15,7 C 15,4.784 13.216,3 11,3 m 0,1 c 1.662,0 3,1.561 3,3.5 L 14,11 8,11 8,7.5 C 8,5.561 9.338,4 11,4" 19 | } 20 | } 21 | 22 | onFillColorChanged: requestPaint() 23 | 24 | onPaint: { 25 | context.reset() 26 | context.translate(width/2-size/2, height/2-size/2) 27 | context.fillStyle = fillColor 28 | context.scale(scale, scale) 29 | context.path = iconPath 30 | context.fill() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package/contents/ui/config/OpenWeatherMapCityDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import QtQuick.Dialogs 1.2 3 | import QtQuick.Layouts 1.2 4 | import QtQuick.Controls 1.4 5 | import org.kde.plasma.core 2.0 as PlasmaCore 6 | 7 | import ".." 8 | import "../lib" 9 | import "../lib/Requests.js" as Requests 10 | 11 | Dialog { 12 | id: chooseCityDialog 13 | title: i18n("Select city") 14 | 15 | width: 500 16 | height: 600 17 | property bool loadingCityList: false 18 | 19 | Logger { 20 | id: logger 21 | showDebug: plasmoid.configuration.debugging 22 | } 23 | 24 | ListModel { id: cityListModel } 25 | PlasmaCore.SortFilterModel { 26 | id: filteredCityListModel 27 | // sourceModel: cityListModel // Link after populating cityListModel so the UI doesn't freeze. 28 | filterRole: 'name' 29 | sortRole: 'name' 30 | sortCaseSensitivity: Qt.CaseInsensitive 31 | } 32 | 33 | property string selectedCityId: '' 34 | Connections { 35 | target: tableView.selection 36 | 37 | onSelectionChanged: { 38 | tableView.selection.forEach(function(row) { 39 | var city = filteredCityListModel.get(row) 40 | chooseCityDialog.selectedCityId = city.id 41 | // console.log('selectedCityId', city.id, city.name) 42 | }) 43 | } 44 | } 45 | Connections { 46 | target: filteredCityListModel 47 | 48 | onFilterRegExpChanged: { 49 | tableView.selection.clear() 50 | chooseCityDialog.selectedCityId = '' 51 | } 52 | } 53 | 54 | Timer { 55 | id: debouceApplyFilter 56 | interval: 1000 57 | onTriggered: chooseCityDialog.applyCityListSearch() 58 | } 59 | 60 | 61 | ColumnLayout { 62 | anchors.fill: parent 63 | LinkText { 64 | text: i18n("Fetched from %1", "https://openweathermap.org/find") 65 | } 66 | TextField { 67 | id: cityNameInput 68 | Layout.fillWidth: true 69 | text: '' 70 | placeholderText: i18n("Search") 71 | onTextChanged: debouceApplyFilter.restart() 72 | } 73 | TableView { 74 | id: tableView 75 | Layout.fillWidth: true 76 | Layout.fillHeight: true 77 | Layout.minimumHeight: 200 78 | model: filteredCityListModel 79 | 80 | TableViewColumn { 81 | width: 240 82 | role: 'name' 83 | title: i18n("Name") 84 | } 85 | TableViewColumn { 86 | width: 100 87 | role: 'id' 88 | title: i18n("Id") 89 | } 90 | TableViewColumn { 91 | width: 100 92 | role: 'id' 93 | title: i18n("City Webpage") 94 | delegate: LinkText { 95 | text: '' + i18n("Open Link") + '' 96 | linkColor: styleData.selected ? theme.textColor : theme.highlightColor 97 | } 98 | } 99 | 100 | BusyIndicator { 101 | anchors.centerIn: parent 102 | running: visible 103 | visible: chooseCityDialog.loadingCityList 104 | } 105 | } 106 | } 107 | 108 | function clearCityList() { 109 | // clear list so that each append() doesn't rebuild the UI 110 | filteredCityListModel.sourceModel = null 111 | cityListModel.clear() 112 | } 113 | 114 | function parseCityList(data) { 115 | for (var i = 0; i < data.list.length; i++) { 116 | var item = data.list[i] 117 | var city = { 118 | id: item.id, 119 | name: item.name + ', ' + item.sys.country, 120 | } 121 | cityListModel.append(city) 122 | } 123 | } 124 | 125 | function applyCityListSearch() { 126 | searchCityList(cityNameInput.text) 127 | } 128 | 129 | function searchCityList(q) { 130 | logger.debug('searchCityList', q) 131 | clearCityList() 132 | if (q) { 133 | chooseCityDialog.loadingCityList = true 134 | fetchCityList({ 135 | appId: plasmoid.configuration.openWeatherMapAppId, 136 | q: q, 137 | }, function(err, data, xhr) { 138 | if (err) return console.log('searchCityList.err', err, xhr && xhr.status, data) 139 | logger.debug('searchCityList.response') 140 | logger.debugJSON('searchCityList.response', data) 141 | 142 | parseCityList(data) 143 | 144 | // link after populating so that each append() doesn't attempt to rebuild the UI. 145 | filteredCityListModel.sourceModel = cityListModel 146 | 147 | chooseCityDialog.loadingCityList = false 148 | }) 149 | } 150 | } 151 | 152 | function fetchCityList(args, callback) { 153 | if (!args.appId) return callback('OpenWeatherMap AppId not set') 154 | 155 | var url = 'https://api.openweathermap.org/data/2.5/' 156 | url += 'find?q=' + encodeURIComponent(args.q) 157 | url += '&type=like' 158 | url += '&sort=population' 159 | url += '&cnt=30' 160 | url += '&appid=' + args.appId 161 | Requests.getJSON(url, callback) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /package/contents/ui/config/WeatherCanadaCityDialog.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.1 2 | import QtQuick.Dialogs 1.2 3 | import QtQuick.Layouts 1.2 4 | import QtQuick.Controls 1.4 5 | import org.kde.plasma.core 2.0 as PlasmaCore 6 | 7 | import "../lib/Requests.js" as Requests 8 | import ".." 9 | import "../weather/WeatherCanada.js" as WeatherCanada 10 | 11 | Dialog { 12 | id: chooseCityDialog 13 | title: i18n("Select city") 14 | 15 | width: 500 16 | height: 600 17 | property bool loadingCityList: false 18 | property bool cityListLoaded: false 19 | 20 | ListModel { id: emptyListModel } 21 | ListModel { id: cityListModel } 22 | PlasmaCore.SortFilterModel { 23 | id: filteredCityListModel 24 | // sourceModel: cityListModel // Link after populating cityListModel so the UI doesn't freeze. 25 | sourceModel: emptyListModel 26 | filterRole: 'name' 27 | sortRole: 'name' 28 | sortCaseSensitivity: Qt.CaseInsensitive 29 | } 30 | 31 | property string selectedCityId: '' 32 | Connections { 33 | target: tableView.selection 34 | 35 | onSelectionChanged: { 36 | tableView.selection.forEach(function(row) { 37 | var city = filteredCityListModel.get(row) 38 | chooseCityDialog.selectedCityId = city.id 39 | // console.log('selectedCityId', city.id, city.name) 40 | }) 41 | } 42 | } 43 | Connections { 44 | target: filteredCityListModel 45 | 46 | onFilterRegExpChanged: { 47 | tableView.selection.clear() 48 | chooseCityDialog.selectedCityId = '' 49 | } 50 | } 51 | 52 | Timer { 53 | id: debouceApplyFilter 54 | interval: 1000 55 | onTriggered: filteredCityListModel.filterRegExp = cityNameInput.text 56 | } 57 | 58 | onVisibleChanged: { 59 | if (visible && !cityListLoaded && !loadingCityList) { 60 | loadProvinceCityList() 61 | } 62 | } 63 | 64 | 65 | ColumnLayout { 66 | anchors.fill: parent 67 | LinkText { 68 | text: i18n("Fetched from %1", "https://weather.gc.ca/canada_e.html") 69 | } 70 | 71 | Item { 72 | height: 21 73 | Layout.fillWidth: true 74 | TabView { 75 | id: provinceTabView 76 | width: parent.width 77 | frameVisible: false 78 | Repeater { 79 | id: provinceRepeater 80 | model: ['AB', 'BC', 'MB', 'NB', 'NL', 'NS', 'NT', 'NU', 'ON', 'PE', 'QC', 'SK', 'YT'] 81 | Tab { title: modelData } 82 | } 83 | onCurrentIndexChanged: loadProvinceCityList() 84 | } 85 | } 86 | 87 | TextField { 88 | id: cityNameInput 89 | Layout.fillWidth: true 90 | text: '' 91 | placeholderText: i18n("Search") 92 | onTextChanged: debouceApplyFilter.restart() 93 | } 94 | TableView { 95 | id: tableView 96 | Layout.fillWidth: true 97 | Layout.fillHeight: true 98 | Layout.minimumHeight: 200 99 | model: filteredCityListModel 100 | 101 | TableViewColumn { 102 | width: 240 103 | role: 'name' 104 | title: i18n("Name") 105 | } 106 | TableViewColumn { 107 | width: 100 108 | role: 'id' 109 | title: i18n("Id") 110 | } 111 | TableViewColumn { 112 | width: 100 113 | role: 'id' 114 | title: i18n("City Webpage") 115 | delegate: LinkText { 116 | text: '' + i18n("Open Link") + '' 117 | linkColor: styleData.selected ? theme.textColor : theme.highlightColor 118 | } 119 | } 120 | 121 | BusyIndicator { 122 | anchors.centerIn: parent 123 | running: visible 124 | visible: chooseCityDialog.loadingCityList 125 | } 126 | } 127 | } 128 | 129 | 130 | function loadCityList(provinceUrl) { 131 | chooseCityDialog.loadingCityList = true 132 | filteredCityListModel.sourceModel = emptyListModel 133 | cityListModel.clear() 134 | 135 | Requests.request(provinceUrl, function(err, data) { 136 | if (err) { 137 | console.log('[eventcalendar]', 'loadCityList.err', err, data) 138 | chooseCityDialog.loadingCityList = false 139 | return 140 | } 141 | var cityList = WeatherCanada.parseProvincePage(data) 142 | for (var i = 0; i < cityList.length; i++) { 143 | cityListModel.append(cityList[i]) 144 | } 145 | 146 | // link after populating so that each append() doesn't attempt to rebuild the UI. 147 | filteredCityListModel.sourceModel = cityListModel 148 | 149 | chooseCityDialog.cityListLoaded = true 150 | chooseCityDialog.loadingCityList = false 151 | }) 152 | } 153 | 154 | property alias provinceIdList: provinceRepeater.model 155 | function loadProvinceCityList() { 156 | var provinceId = provinceIdList[0] 157 | if (provinceTabView.currentIndex >= 0) { 158 | provinceId = provinceIdList[provinceTabView.currentIndex] 159 | } 160 | 161 | var provinceUrl = 'https://weather.gc.ca/forecast/canada/index_e.html?id=' + provinceId 162 | loadCityList(provinceUrl) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /package/contents/ui/lib/AppletVersion.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | import org.kde.plasma.plasmoid 2.0 6 | 7 | Item { 8 | implicitWidth: label.implicitWidth 9 | implicitHeight: label.implicitHeight 10 | 11 | property string version: "?" 12 | property string metadataFilepath: plasmoid.file("", "../metadata.desktop") 13 | 14 | PlasmaCore.DataSource { 15 | id: executable 16 | engine: "executable" 17 | connectedSources: [] 18 | onNewData: { 19 | var exitCode = data["exit code"] 20 | var exitStatus = data["exit status"] 21 | var stdout = data["stdout"] 22 | var stderr = data["stderr"] 23 | exited(exitCode, exitStatus, stdout, stderr) 24 | disconnectSource(sourceName) // cmd finished 25 | } 26 | function exec(cmd) { 27 | connectSource(cmd) 28 | } 29 | signal exited(int exitCode, int exitStatus, string stdout, string stderr) 30 | } 31 | 32 | Connections { 33 | target: executable 34 | onExited: { 35 | version = stdout.replace('\n', ' ').trim() 36 | } 37 | } 38 | 39 | Label { 40 | id: label 41 | text: i18n("Version: %1", version) 42 | } 43 | 44 | Component.onCompleted: { 45 | var cmd = 'kreadconfig5 --file "' + metadataFilepath + '" --group "Desktop Entry" --key "X-KDE-PluginInfo-Version"' 46 | executable.exec(cmd) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Async.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | // Version 1 3 | 4 | function _taskCallback(asyncObj, key, err, taskResult) { 5 | asyncObj.numCompleted += 1 6 | if (err) { 7 | asyncObj.err = err 8 | asyncObj.finalCallback(err) 9 | } else if (asyncObj.err) { 10 | // Skip 11 | } else { 12 | asyncObj.results[key] = taskResult 13 | if (asyncObj.numCompleted >= asyncObj.numTasks) { 14 | asyncObj.finalCallback(null, asyncObj.results) 15 | } 16 | } 17 | } 18 | 19 | // http://caolan.github.io/async/docs.html#parallel 20 | function parallel(tasks, finalCallback) { 21 | if (tasks.length == 0) { 22 | finalCallback(null, []) 23 | } else { 24 | var asyncObj = {} 25 | asyncObj.numTasks = tasks.length // Serialize in case the array changes size 26 | asyncObj.numCompleted = 0 27 | asyncObj.err = null 28 | asyncObj.results = [] 29 | asyncObj.finalCallback = finalCallback 30 | 31 | for (var i = 0; i < tasks.length; i++) { 32 | var task = tasks[i] 33 | var taskCallback = _taskCallback.bind(null, asyncObj, i) 34 | task(taskCallback) 35 | } 36 | } 37 | } 38 | 39 | /* 40 | ** Example 41 | */ 42 | // parallel([ 43 | // function(callback) { 44 | // setTimeout(function() { 45 | // callback(null, 'one'); 46 | // }, 200); 47 | // }, 48 | // function(callback) { 49 | // setTimeout(function() { 50 | // callback(null, 'two'); 51 | // }, 100); 52 | // }, 53 | // ], function(err, results) { 54 | // console.log('done', results) 55 | // }) 56 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Base64Json.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | QtObject { 4 | property string configKey 5 | readonly property string configValue: plasmoid.configuration[configKey] 6 | property var value: null 7 | 8 | onConfigValueChanged: deserialize() 9 | 10 | function deserialize() { 11 | var s = JSON.parse(Qt.atob(configValue)) 12 | value = s 13 | } 14 | 15 | function serialize() { 16 | var v = Qt.btoa(JSON.stringify(value)) 17 | plasmoid.configuration[configKey] = v 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Base64JsonListModel.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | 3 | ListModel { 4 | id: listModel 5 | property alias configKey: base64Json.configKey 6 | 7 | property int oldCount: count 8 | property QtObject base64Json: Base64Json { 9 | id: base64Json 10 | value: [] 11 | onValueChanged: { 12 | listModel.clear() 13 | if (value !== null) { 14 | for (var i = 0; i < value.length; i++) { 15 | var item = value[i] 16 | listModel.append(item) 17 | } 18 | } 19 | } 20 | } 21 | 22 | function addItem(obj) { 23 | append(obj) 24 | base64Json.value.push(obj) 25 | serialize() 26 | } 27 | 28 | function removeIndex(index) { 29 | remove(index) 30 | base64Json.value.splice(index, 1) 31 | serialize() 32 | } 33 | 34 | function setItemProperty(index, key, value) { 35 | setProperty(index, key, value) 36 | base64Json.value[index][key] = value 37 | serialize() 38 | } 39 | 40 | function serialize() { 41 | if (throttle > 0) { 42 | serializeTimer.restart() 43 | } else { 44 | base64Json.serialize() 45 | } 46 | } 47 | 48 | property alias throttle: serializeTimer.interval 49 | property Timer serializeTimer: Timer { 50 | id: serializeTimer 51 | interval: 200 52 | onTriggered: { 53 | base64Json.serialize() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ColorGrid.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | 5 | GroupBox { 6 | id: colorGrid 7 | Layout.fillWidth: true 8 | default property alias _contentChildren: content.data 9 | 10 | GridLayout { 11 | id: content 12 | anchors.left: parent.left 13 | anchors.right: parent.right 14 | columns: 2 15 | 16 | Component.onCompleted: { 17 | for (var i = 0; i < children.length; i++) { 18 | var child = children[i] 19 | if (typeof child.configKey !== "undefined") { 20 | child.horizontalAlignment = Text.AlignRight 21 | } 22 | } 23 | } 24 | 25 | // Workaround for crash when using default on a Layout. 26 | // https://bugreports.qt.io/browse/QTBUG-52490 27 | // Still affecting Qt 5.7.0 28 | Component.onDestruction: { 29 | while (children.length > 0) { 30 | children[children.length - 1].parent = colorGrid 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ColorUtil.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | // Version: 2 3 | 4 | // https://stackoverflow.com/questions/9733288/how-to-programmatically-calculate-the-contrast-ratio-between-two-colors 5 | // https://www.w3.org/TR/AERT/#color-contrast 6 | function brightness(c) { 7 | return (c.r*299 + c.g*587 + c.b*114) / 1000 8 | } 9 | 10 | // https://www.w3.org/TR/AERT/#color-contrast 11 | function contrast(c1, c2) { 12 | return Math.max(c1.r, c2.r) - Math.min(c1.r, c2.r) + Math.max(c1.g, c2.g) - Math.min(c1.g, c2.g) + Math.max(c1.b, c2.b) - Math.min(c1.b, c2.b) 13 | } 14 | 15 | // https://www.w3.org/TR/AERT/#color-contrast 16 | // w3 mentions 500 using rgb 255 values. QML rgba is 0..1 however, and 500/255=1.96 17 | function hasEnoughContrast(c1, c2) { 18 | return contrast(c1, c2) >= 1.96 19 | } 20 | 21 | function setAlpha(c, a) { 22 | return Qt.rgba(c.r, c.g, c.b, a) 23 | } 24 | 25 | function _interpolate(a, b, t) { 26 | return (a - b) * t + b 27 | } 28 | // Linear Interpolation from color1 to color2 by a ratio of t. 29 | function lerp(c1, c2, t) { 30 | var r = _interpolate(c1.r, c2.r, t) 31 | var g = _interpolate(c1.g, c2.g, t) 32 | var b = _interpolate(c1.b, c2.b, t) 33 | var a = _interpolate(c1.a, c2.a, t) 34 | return Qt.rgba(r, g, b, a) 35 | } 36 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigCheckBox.qml: -------------------------------------------------------------------------------- 1 | // Version 2 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | import ".." 8 | 9 | CheckBox { 10 | id: configCheckBox 11 | 12 | property string configKey: '' 13 | checked: plasmoid.configuration[configKey] 14 | onClicked: plasmoid.configuration[configKey] = !plasmoid.configuration[configKey] 15 | } 16 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigColor.qml: -------------------------------------------------------------------------------- 1 | // Version 5 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | import QtQuick.Dialogs 1.2 7 | import QtQuick.Window 2.2 8 | import org.kde.kirigami 2.0 as Kirigami 9 | 10 | import ".." 11 | 12 | RowLayout { 13 | id: configColor 14 | spacing: 2 15 | // Layout.fillWidth: true 16 | Layout.maximumWidth: 300 * Kirigami.Units.devicePixelRatio 17 | 18 | property alias label: label.text 19 | property alias labelColor: label.color 20 | property alias horizontalAlignment: label.horizontalAlignment 21 | property alias showAlphaChannel: dialog.showAlphaChannel 22 | property color buttonOutlineColor: { 23 | if (valueColor.r + valueColor.g + valueColor.b > 0.5) { 24 | return "#BB000000" // Black outline 25 | } else { 26 | return "#BBFFFFFF" // White outline 27 | } 28 | } 29 | 30 | property TextField textField: textField 31 | property ColorDialog dialog: dialog 32 | 33 | property string configKey: '' 34 | property string defaultColor: '' 35 | property string value: { 36 | if (configKey) { 37 | return plasmoid.configuration[configKey] 38 | } else { 39 | return "#000" 40 | } 41 | } 42 | 43 | readonly property color defaultColorValue: defaultColor 44 | readonly property color valueColor: { 45 | if (value == '' && defaultColor) { 46 | return defaultColor 47 | } else { 48 | return value 49 | } 50 | } 51 | 52 | onValueChanged: { 53 | if (!textField.activeFocus) { 54 | textField.text = configColor.value 55 | } 56 | if (configKey) { 57 | if (value == defaultColorValue) { 58 | plasmoid.configuration[configKey] = "" 59 | } else { 60 | plasmoid.configuration[configKey] = value 61 | } 62 | } 63 | } 64 | 65 | function setValue(newColor) { 66 | textField.text = newColor 67 | } 68 | 69 | Label { 70 | id: label 71 | text: "Label" 72 | Layout.fillWidth: horizontalAlignment == Text.AlignRight 73 | horizontalAlignment: Text.AlignLeft 74 | } 75 | 76 | MouseArea { 77 | id: mouseArea 78 | Layout.preferredWidth: textField.height 79 | Layout.preferredHeight: textField.height 80 | hoverEnabled: true 81 | 82 | onClicked: dialog.open() 83 | 84 | Rectangle { 85 | anchors.fill: parent 86 | color: configColor.valueColor 87 | border.width: 2 88 | border.color: parent.containsMouse ? Kirigami.Theme.highlightColor : buttonOutlineColor 89 | } 90 | } 91 | 92 | TextField { 93 | id: textField 94 | placeholderText: defaultColor ? defaultColor : "#AARRGGBB" 95 | Layout.fillWidth: label.horizontalAlignment == Text.AlignLeft 96 | onTextChanged: { 97 | // Make sure the text is: 98 | // Empty (use default) 99 | // or #123 or #112233 or #11223344 before applying the color. 100 | if (text.length === 0 101 | || (text.indexOf('#') === 0 && (text.length == 4 || text.length == 7 || text.length == 9)) 102 | ) { 103 | configColor.value = text 104 | } 105 | } 106 | } 107 | 108 | ColorDialog { 109 | id: dialog 110 | visible: false 111 | modality: Qt.WindowModal 112 | title: configColor.label 113 | showAlphaChannel: true 114 | color: configColor.valueColor 115 | onCurrentColorChanged: { 116 | if (visible && color != currentColor) { 117 | configColor.value = currentColor 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigComboBox.qml: -------------------------------------------------------------------------------- 1 | // Version 5 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | /* 8 | ** Example: 9 | ** 10 | ConfigComboBox { 11 | configKey: "appDescription" 12 | model: [ 13 | { value: "a", text: i18n("A") }, 14 | { value: "b", text: i18n("B") }, 15 | { value: "c", text: i18n("C") }, 16 | ] 17 | } 18 | ConfigComboBox { 19 | configKey: "appDescription" 20 | populated: false 21 | onPopulate: { 22 | model = [ 23 | { value: "a", text: i18n("A") }, 24 | { value: "b", text: i18n("B") }, 25 | { value: "c", text: i18n("C") }, 26 | ] 27 | } 28 | } 29 | */ 30 | RowLayout { 31 | id: configComboBox 32 | 33 | property string configKey: '' 34 | readonly property var currentItem: comboBox.model[comboBox.currentIndex] 35 | readonly property string value: currentItem ? currentItem[valueRole] : "" 36 | readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" 37 | onConfigValueChanged: { 38 | if (!comboBox.focus && value != configValue) { 39 | selectValue(configValue) 40 | } 41 | } 42 | 43 | property alias textRole: comboBox.textRole 44 | property alias valueRole: comboBox.valueRole 45 | property alias model: comboBox.model 46 | 47 | property alias before: labelBefore.text 48 | property alias after: labelAfter.text 49 | 50 | signal populate() 51 | property bool populated: true 52 | 53 | Component.onCompleted: { 54 | populate() 55 | selectValue(configValue) 56 | } 57 | 58 | Label { 59 | id: labelBefore 60 | text: "" 61 | visible: text 62 | } 63 | 64 | ComboBox { 65 | id: comboBox 66 | textRole: "text" // Doesn't autodeduce from model if we manually populate it 67 | property string valueRole: "value" 68 | 69 | model: [] 70 | 71 | onCurrentIndexChanged: { 72 | if (typeof model !== 'number' && 0 <= currentIndex && currentIndex < count) { 73 | var item = model[currentIndex] 74 | if (typeof item !== "undefined") { 75 | var val = item[valueRole] 76 | if (configKey && (typeof val !== "undefined") && populated) { 77 | plasmoid.configuration[configKey] = val 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | Label { 85 | id: labelAfter 86 | text: "" 87 | visible: text 88 | } 89 | 90 | function size() { 91 | if (typeof model === "number") { 92 | return model 93 | } else if (typeof model.count === "number") { 94 | return model.count 95 | } else if (typeof model.length === "number") { 96 | return model.length 97 | } else { 98 | return 0 99 | } 100 | } 101 | 102 | function findValue(val) { 103 | for (var i = 0; i < size(); i++) { 104 | if (model[i][valueRole] == val) { 105 | return i 106 | } 107 | } 108 | return -1 109 | } 110 | 111 | function selectValue(val) { 112 | var index = findValue(val) 113 | if (index >= 0) { 114 | comboBox.currentIndex = index 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigDimension.qml: -------------------------------------------------------------------------------- 1 | // Version 2 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | import org.kde.kirigami 2.0 as Kirigami 7 | 8 | GridLayout { 9 | id: configDimension 10 | columnSpacing: 0 11 | rowSpacing: 0 12 | 13 | property int orientation: Qt.Horizontal 14 | property color lineColor: "#000" 15 | property int lineThickness: 2 * Kirigami.Units.devicePixelRatio 16 | 17 | property alias configKey: configSpinBox.configKey 18 | property alias configValue: configSpinBox.configValue 19 | property alias horizontalAlignment: configSpinBox.horizontalAlignment 20 | property alias maximumValue: configSpinBox.maximumValue 21 | property alias minimumValue: configSpinBox.minimumValue 22 | property alias prefix: configSpinBox.prefix 23 | property alias stepSize: configSpinBox.stepSize 24 | property alias suffix: configSpinBox.suffix 25 | property alias value: configSpinBox.value 26 | 27 | property alias before: configSpinBox.before 28 | property alias after: configSpinBox.after 29 | 30 | states: [ 31 | State { 32 | name: "horizontal" 33 | when: orientation == Qt.Horizontal 34 | 35 | PropertyChanges { target: configDimension 36 | rows: 1 37 | } 38 | PropertyChanges { target: lineA 39 | implicitWidth: configDimension.lineThickness 40 | Layout.fillHeight: true 41 | } 42 | PropertyChanges { target: lineSpanA 43 | Layout.fillWidth: true 44 | Layout.alignment: Qt.AlignVCenter 45 | implicitHeight: configDimension.lineThickness 46 | } 47 | PropertyChanges { target: configSpinBox 48 | Layout.alignment: Qt.AlignVCenter 49 | } 50 | PropertyChanges { target: lineSpanB 51 | Layout.fillWidth: true 52 | Layout.alignment: Qt.AlignVCenter 53 | implicitHeight: configDimension.lineThickness 54 | } 55 | PropertyChanges { target: lineB 56 | implicitWidth: configDimension.lineThickness 57 | Layout.fillHeight: true 58 | } 59 | } 60 | , State { 61 | name: "vertical" 62 | when: orientation == Qt.Vertical 63 | 64 | PropertyChanges { target: configDimension 65 | columns: 1 66 | } 67 | PropertyChanges { target: lineA 68 | Layout.alignment: Qt.AlignHCenter 69 | implicitHeight: configDimension.lineThickness 70 | implicitWidth: configSpinBox.implicitHeight 71 | } 72 | PropertyChanges { target: lineSpanA 73 | Layout.fillHeight: true 74 | Layout.alignment: Qt.AlignHCenter 75 | implicitWidth: configDimension.lineThickness 76 | } 77 | PropertyChanges { target: configSpinBox 78 | Layout.alignment: Qt.AlignHCenter 79 | } 80 | PropertyChanges { target: lineSpanB 81 | Layout.fillHeight: true 82 | Layout.alignment: Qt.AlignHCenter 83 | implicitWidth: configDimension.lineThickness 84 | } 85 | PropertyChanges { target: lineB 86 | Layout.alignment: Qt.AlignHCenter 87 | implicitHeight: configDimension.lineThickness 88 | implicitWidth: configSpinBox.implicitHeight 89 | } 90 | } 91 | ] 92 | 93 | Rectangle { 94 | id: lineA 95 | color: configDimension.lineColor 96 | } 97 | Rectangle { 98 | id: lineSpanA 99 | color: configDimension.lineColor 100 | } 101 | ConfigSpinBox { 102 | id: configSpinBox 103 | } 104 | Rectangle { 105 | id: lineSpanB 106 | color: configDimension.lineColor 107 | } 108 | Rectangle { 109 | id: lineB 110 | color: configDimension.lineColor 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigFontFamily.qml: -------------------------------------------------------------------------------- 1 | // Version 2 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | ConfigComboBox { 8 | id: configFontFamily 9 | 10 | populated: false 11 | 12 | // Based on: org.kde.plasma.digitalclock 13 | onPopulate: { 14 | var arr = [] // Use temp array to avoid constant binding stuff 15 | arr.push({ text: i18nc("Use default font", "Default"), value: "" }) 16 | 17 | var fonts = Qt.fontFamilies() 18 | for (var i = 0; i < fonts.length; i++) { 19 | arr.push({ text: fonts[i], value: fonts[i] }) 20 | } 21 | model = arr 22 | populated = true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigNotification.qml: -------------------------------------------------------------------------------- 1 | // Version 4 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.1 5 | import QtQuick.Layouts 1.1 6 | 7 | ColumnLayout { 8 | id: configNotification 9 | property alias label: notificationEnabledCheckBox.text 10 | property alias notificationEnabledKey: notificationEnabledCheckBox.configKey 11 | 12 | property alias notificationEnabled: notificationEnabledCheckBox.checked 13 | 14 | property alias sfxLabel: configSound.label 15 | property alias sfxEnabledKey: configSound.sfxEnabledKey 16 | property alias sfxPathKey: configSound.sfxPathKey 17 | 18 | property alias sfxEnabled: configSound.sfxEnabled 19 | property alias sfxPathValue: configSound.sfxPathValue 20 | property alias sfxPathDefaultValue: configSound.sfxPathDefaultValue 21 | 22 | property int indentWidth: 24 * units.devicePixelRatio 23 | 24 | ConfigCheckBox { 25 | id: notificationEnabledCheckBox 26 | } 27 | 28 | RowLayout { 29 | spacing: 0 30 | Item { implicitWidth: indentWidth } // indent 31 | ConfigSound { 32 | id: configSound 33 | label: i18n("SFX:") 34 | enabled: notificationEnabled 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigPage.qml: -------------------------------------------------------------------------------- 1 | // Version 4 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Layouts 1.0 5 | 6 | Item { 7 | id: page 8 | Layout.fillWidth: true 9 | default property alias _contentChildren: content.data 10 | implicitHeight: content.implicitHeight 11 | 12 | ColumnLayout { 13 | id: content 14 | anchors.left: parent.left 15 | anchors.right: parent.right 16 | anchors.top: parent.top 17 | 18 | // Workaround for crash when using default on a Layout. 19 | // https://bugreports.qt.io/browse/QTBUG-52490 20 | // Still affecting Qt 5.7.0 21 | Component.onDestruction: { 22 | while (children.length > 0) { 23 | children[children.length - 1].parent = page 24 | } 25 | } 26 | } 27 | 28 | property alias showAppletVersion: appletVersionLoader.active 29 | Loader { 30 | id: appletVersionLoader 31 | active: false 32 | visible: active 33 | source: "AppletVersion.qml" 34 | anchors.right: parent.right 35 | anchors.bottom: parent.top 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigRadioButtonGroup.qml: -------------------------------------------------------------------------------- 1 | // Version 4 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | /* 8 | ** Example: 9 | ** 10 | ConfigRadioButtonGroup { 11 | configKey: "appDescription" 12 | model: [ 13 | { value: "a", text: i18n("A") }, 14 | { value: "b", text: i18n("B") }, 15 | { value: "c", text: i18n("C") }, 16 | ] 17 | } 18 | */ 19 | 20 | RowLayout { 21 | id: configRadioButtonGroup 22 | Layout.fillWidth: true 23 | default property alias _contentChildren: content.data 24 | property alias label: label.text 25 | 26 | property var exclusiveGroup: ExclusiveGroup { id: radioButtonGroup } 27 | 28 | property string configKey: '' 29 | readonly property var configValue: configKey ? plasmoid.configuration[configKey] : "" 30 | 31 | property alias model: buttonRepeater.model 32 | 33 | //--- 34 | Label { 35 | id: label 36 | visible: !!text 37 | Layout.alignment: Qt.AlignTop | Qt.AlignLeft 38 | } 39 | ColumnLayout { 40 | id: content 41 | 42 | Repeater { 43 | id: buttonRepeater 44 | RadioButton { 45 | visible: typeof modelData.visible !== "undefined" ? modelData.visible : true 46 | enabled: typeof modelData.enabled !== "undefined" ? modelData.enabled : true 47 | text: modelData.text 48 | checked: modelData.value === configValue 49 | exclusiveGroup: radioButtonGroup 50 | onClicked: { 51 | focus = true 52 | if (configKey) { 53 | plasmoid.configuration[configKey] = modelData.value 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigSection.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.0 3 | import QtQuick.Layouts 1.0 4 | 5 | GroupBox { 6 | id: configSection 7 | Layout.fillWidth: true 8 | default property alias _contentChildren: content.data 9 | 10 | ColumnLayout { 11 | id: content 12 | anchors.left: parent.left 13 | anchors.right: parent.right 14 | 15 | // Workaround for crash when using default on a Layout. 16 | // https://bugreports.qt.io/browse/QTBUG-52490 17 | // Still affecting Qt 5.7.0 18 | Component.onDestruction: { 19 | while (children.length > 0) { 20 | children[children.length - 1].parent = configSection 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigSlider.qml: -------------------------------------------------------------------------------- 1 | // Version 2 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | RowLayout { 8 | id: configSlider 9 | 10 | property string configKey: '' 11 | property alias maximumValue: slider.maximumValue 12 | property alias minimumValue: slider.minimumValue 13 | property alias stepSize: slider.stepSize 14 | property alias updateValueWhileDragging: slider.updateValueWhileDragging 15 | property alias value: slider.value 16 | 17 | property alias before: labelBefore.text 18 | property alias after: labelAfter.text 19 | 20 | Layout.fillWidth: true 21 | 22 | Label { 23 | id: labelBefore 24 | text: "" 25 | visible: text 26 | } 27 | 28 | Slider { 29 | id: slider 30 | Layout.fillWidth: configSlider.Layout.fillWidth 31 | 32 | value: plasmoid.configuration[configKey] 33 | // onValueChanged: plasmoid.configuration[configKey] = value 34 | onValueChanged: serializeTimer.start() 35 | maximumValue: 2147483647 36 | } 37 | 38 | Label { 39 | id: labelAfter 40 | text: "" 41 | visible: text 42 | } 43 | 44 | Timer { // throttle 45 | id: serializeTimer 46 | interval: 300 47 | onTriggered: plasmoid.configuration[configKey] = value 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigSound.qml: -------------------------------------------------------------------------------- 1 | // Version 5 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Dialogs 1.0 6 | import QtQuick.Layouts 1.0 7 | 8 | RowLayout { 9 | id: configSound 10 | property alias label: sfxEnabledCheckBox.text 11 | property alias sfxEnabledKey: sfxEnabledCheckBox.configKey 12 | property alias sfxPathKey: sfxPath.configKey 13 | 14 | property alias sfxEnabled: sfxEnabledCheckBox.checked 15 | property alias sfxPathValue: sfxPath.value 16 | property alias sfxPathDefaultValue: sfxPath.defaultValue 17 | 18 | // Importing QtMultimedia apparently segfaults both OpenSUSE and Kubuntu. 19 | // https://github.com/Zren/plasma-applet-eventcalendar/issues/84 20 | // https://github.com/Zren/plasma-applet-eventcalendar/issues/167 21 | // property var sfxTest: Qt.createQmlObject("import QtMultimedia 5.4; Audio {}", configSound) 22 | property var sfxTest: null 23 | 24 | spacing: 0 25 | ConfigCheckBox { 26 | id: sfxEnabledCheckBox 27 | } 28 | Button { 29 | iconName: "media-playback-start-symbolic" 30 | enabled: sfxEnabled && !!sfxTest 31 | onClicked: { 32 | sfxTest.source = sfxPath.value 33 | sfxTest.play() 34 | } 35 | } 36 | ConfigString { 37 | id: sfxPath 38 | enabled: sfxEnabled 39 | Layout.fillWidth: true 40 | } 41 | Button { 42 | iconName: "folder-symbolic" 43 | enabled: sfxEnabled 44 | onClicked: sfxPathDialog.visible = true 45 | 46 | FileDialog { 47 | id: sfxPathDialog 48 | title: i18n("Choose a sound effect") 49 | folder: '/usr/share/sounds' 50 | nameFilters: [ 51 | i18n("Sound files (%1)", "*.wav *.mp3 *.oga *.ogg"), 52 | i18n("All files (%1)", "*"), 53 | ] 54 | onAccepted: { 55 | sfxPathValue = fileUrl 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigSpinBox.qml: -------------------------------------------------------------------------------- 1 | // Version 3 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | RowLayout { 8 | id: configSpinBox 9 | 10 | property string configKey: '' 11 | readonly property var configValue: configKey ? plasmoid.configuration[configKey] : 0 12 | property alias decimals: spinBox.decimals 13 | property alias horizontalAlignment: spinBox.horizontalAlignment 14 | property alias maximumValue: spinBox.maximumValue 15 | property alias minimumValue: spinBox.minimumValue 16 | property alias prefix: spinBox.prefix 17 | property alias stepSize: spinBox.stepSize 18 | property alias suffix: spinBox.suffix 19 | property alias value: spinBox.value 20 | 21 | property alias before: labelBefore.text 22 | property alias after: labelAfter.text 23 | 24 | Label { 25 | id: labelBefore 26 | text: "" 27 | visible: text 28 | } 29 | 30 | SpinBox { 31 | id: spinBox 32 | 33 | value: configValue 34 | onValueChanged: serializeTimer.start() 35 | maximumValue: 2147483647 36 | } 37 | 38 | Label { 39 | id: labelAfter 40 | text: "" 41 | visible: text 42 | } 43 | 44 | Timer { // throttle 45 | id: serializeTimer 46 | interval: 300 47 | onTriggered: { 48 | if (configKey) { 49 | plasmoid.configuration[configKey] = value 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ConfigString.qml: -------------------------------------------------------------------------------- 1 | // Version 2 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.0 5 | import QtQuick.Layouts 1.0 6 | 7 | TextField { 8 | id: configString 9 | Layout.fillWidth: true 10 | 11 | property string configKey: '' 12 | property alias value: configString.text 13 | readonly property string configValue: configKey ? plasmoid.configuration[configKey] : "" 14 | onConfigValueChanged: { 15 | if (!configString.focus && value != configValue) { 16 | value = configValue 17 | } 18 | } 19 | property string defaultValue: "" 20 | 21 | text: configString.configValue 22 | onTextChanged: serializeTimer.restart() 23 | 24 | ToolButton { 25 | iconName: "edit-clear" 26 | onClicked: configString.value = defaultValue 27 | 28 | anchors.top: parent.top 29 | anchors.right: parent.right 30 | anchors.bottom: parent.bottom 31 | 32 | width: height 33 | } 34 | 35 | Timer { // throttle 36 | id: serializeTimer 37 | interval: 300 38 | onTriggered: { 39 | if (configKey) { 40 | plasmoid.configuration[configKey] = value 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ContextMenu.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.components 2.0 as PlasmaComponents 3 | 4 | PlasmaComponents.ContextMenu { 5 | id: contextMenu 6 | 7 | signal populate(var contextMenu) 8 | 9 | // Force loading of MenuItem.qml so dynamic creation *should* be synchronous. 10 | // It's a property since the default content property of PlasmaComponent.ContextMenu doesn't like it. 11 | property var menuItemComponent: Component { 12 | MenuItem {} 13 | } 14 | 15 | function newSeperator(parentMenu) { 16 | return newMenuItem(parentMenu, { 17 | separator: true, 18 | }) 19 | } 20 | 21 | function newMenuItem(parentMenu, properties) { 22 | // return menuItemComponent.createObject(parentMenu || contextMenu, properties || {}) // Warns: 'Created graphical object was not placed in the graphics scene' 23 | return menuItemComponent.createObject(parent, properties || {}) // So attach it to the parent of the ContextMenu (probably bad). 24 | } 25 | 26 | function newSubMenu(parentMenu, properties) { 27 | var subMenuItem = newMenuItem(parentMenu || contextMenu, properties) 28 | var subMenu = Qt.createComponent("ContextMenu.qml").createObject(parentMenu || contextMenu) 29 | subMenuItem.subMenu = subMenu 30 | subMenu.visualParent = subMenuItem.action 31 | return subMenuItem 32 | } 33 | 34 | function loadMenu() { 35 | contextMenu.clearMenuItems() 36 | populate(contextMenu) 37 | } 38 | 39 | function show(x, y) { 40 | loadMenu() 41 | if (content.length > 0) { 42 | open(x, y) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package/contents/ui/lib/ExecUtil.qml: -------------------------------------------------------------------------------- 1 | // Version 6 2 | 3 | import QtQuick 2.0 4 | import org.kde.plasma.core 2.0 as PlasmaCore 5 | 6 | PlasmaCore.DataSource { 7 | id: executable 8 | engine: "executable" 9 | connectedSources: [] 10 | onNewData: { 11 | var cmd = sourceName 12 | var exitCode = data["exit code"] 13 | var exitStatus = data["exit status"] 14 | var stdout = data["stdout"] 15 | var stderr = data["stderr"] 16 | var listener = listeners[cmd] 17 | if (listener) { 18 | listener(cmd, exitCode, exitStatus, stdout, stderr) 19 | } 20 | exited(cmd, exitCode, exitStatus, stdout, stderr) 21 | disconnectSource(sourceName) // cmd finished 22 | } 23 | 24 | signal exited(string cmd, int exitCode, int exitStatus, string stdout, string stderr) 25 | 26 | function trimOutput(stdout) { 27 | return stdout.replace(/\n/g, ' ').trim() 28 | } 29 | 30 | property var listeners: ({}) // Empty Map 31 | 32 | // Note that this has not gone under a security audit. 33 | // You probably shouldn't trust 3rd party input. 34 | function wrapToken(token) { 35 | token = "" + token 36 | // ' => '"'"' to escape the single quotes 37 | token = token.replace(/\'/g, "\'\"\'\"\'") 38 | token = "\'" + token + "\'" 39 | return token 40 | } 41 | 42 | // Note that this has not gone under a security audit. 43 | // You probably shouldn't trust 3rd party input. 44 | // Some of these might be unnecessary. 45 | function sanitizeString(str) { 46 | // Remove NULL (0x00), Ctrl+C (0x03), Ctrl+D (0x04) block of characters 47 | // Remove quotes ("" and '') 48 | // Remove DEL 49 | return str.replace(/[\x00-\x1F\'\"\x7F]/g, '') 50 | } 51 | 52 | function stripQuotes(str) { 53 | return str.replace(/[\'\"]/g, '') 54 | } 55 | 56 | function exec(cmd, callback) { 57 | if (Array.isArray(cmd)) { 58 | cmd = cmd.map(wrapToken) 59 | cmd = cmd.join(' ') 60 | } 61 | if (typeof callback === 'function') { 62 | if (listeners[cmd]) { // Our implementation only allows 1 callback per command. 63 | exited.disconnect(listeners[cmd]) 64 | delete listeners[cmd] 65 | } 66 | var listener = execCallback.bind(executable, callback) 67 | listeners[cmd] = listener 68 | } 69 | // console.log('cmd', cmd) 70 | connectSource(cmd) 71 | } 72 | 73 | function execCallback(callback, cmd, exitCode, exitStatus, stdout, stderr) { 74 | delete listeners[cmd] 75 | callback(cmd, exitCode, exitStatus, stdout, stderr) 76 | } 77 | 78 | //--- Tests 79 | function test() { 80 | exec(['notify-send', 'test', '$(notify-send escape1)']) 81 | exec(['notify-send', 'test', '`notify-send escape2`']) 82 | exec(['notify-send', 'test', '\'; notify-send escape3;\'']) 83 | exec(['notify-send', 'test', '\\\'; notify-send escape4;\\\'']) 84 | } 85 | // Component.onCompleted: test() 86 | } 87 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Logger.qml: -------------------------------------------------------------------------------- 1 | // Version 2 2 | 3 | import QtQuick 2.0 4 | 5 | Item { 6 | id: logger 7 | property string name: 'logger' 8 | property bool showDebug: false 9 | 10 | function prettifyArguments(rawArgs) { 11 | var args = Array.apply(null, rawArgs) 12 | for (var i = 0; i < args.length; i++) { 13 | if (typeof args[i] === "object" || args[i] instanceof Array) { 14 | args[i] = JSON.stringify(args[i], null, '\t') 15 | } 16 | } 17 | return args 18 | } 19 | 20 | function debug() { 21 | if (showDebug) { 22 | var args = Array.apply(null, arguments) 23 | args.unshift('[' + name + ':debug]') 24 | console.log.apply(console, args) 25 | } 26 | } 27 | 28 | function debugJSON() { 29 | if (showDebug) { 30 | var args = prettifyArguments(arguments) 31 | args.unshift('[' + name + ':debug]') 32 | console.log.apply(console, args) 33 | } 34 | } 35 | 36 | function log() { 37 | var args = Array.apply(null, arguments) 38 | args.unshift('[' + name + ']') 39 | console.log.apply(console, args) 40 | } 41 | 42 | function logJSON() { 43 | var args = prettifyArguments(arguments) 44 | args.unshift('[' + name + ']') 45 | console.log.apply(console, args) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package/contents/ui/lib/MenuItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import org.kde.plasma.components 2.0 as PlasmaComponents 3 | 4 | PlasmaComponents.MenuItem { 5 | property var subMenu: undefined 6 | } 7 | -------------------------------------------------------------------------------- /package/contents/ui/lib/MessageWidget.qml: -------------------------------------------------------------------------------- 1 | // Version 6 2 | 3 | import QtQuick 2.0 4 | import QtQuick.Controls 1.2 5 | import QtQuick.Layouts 1.0 6 | 7 | import org.kde.plasma.core 2.0 as PlasmaCore 8 | 9 | // Origionally from digitalclock's configTimeZones.qml 10 | // Recoloured with Bootstrap color scheme 11 | Rectangle { 12 | id: messageWidget 13 | 14 | Layout.fillWidth: true 15 | 16 | property alias text: label.text 17 | property alias wrapMode: label.wrapMode 18 | property alias closeButtonVisible: closeButton.visible 19 | property alias animate: visibleAnimation.enabled 20 | property int iconSize: units.iconSizes.large 21 | 22 | enum MessageType { 23 | Positive, 24 | Information, 25 | Warning, 26 | Error 27 | } 28 | property int messageType: MessageWidget.MessageType.Warning 29 | 30 | clip: true 31 | radius: 5 32 | border.width: 1 33 | 34 | property var icon: { 35 | if (messageType == MessageWidget.MessageType.Information) { 36 | return "dialog-information" 37 | } else if (messageType == MessageWidget.MessageType.Warning) { 38 | return "dialog-warning" 39 | } else if (messageType == MessageWidget.MessageType.Error) { 40 | return "dialog-error" 41 | } else { // positive 42 | return "dialog-ok" 43 | } 44 | } 45 | 46 | property color gradBaseColor: { 47 | if (messageType == MessageWidget.MessageType.Information) { 48 | // return theme.highlightColor 49 | return "#d9edf7" // Bootstrap 50 | } else if (messageType == MessageWidget.MessageType.Warning) { 51 | // return Qt.rgba(176/255, 128/255, 0, 1) // KMessageWidget 52 | // return "#EAC360" // DigitalClock 53 | return "#fcf8e3" // Bootstrap 54 | } else if (messageType == MessageWidget.MessageType.Error) { 55 | // return Qt.rgba(191/255, 3/255, 3/255, 1) 56 | return "#f2dede" // Bootstrap 57 | } else { // positive 58 | // return Qt.rgba(0, 110/255, 40/255, 1) 59 | return "#dff0d8" // Bootstrap 60 | } 61 | } 62 | 63 | border.color: { 64 | if (messageType == MessageWidget.MessageType.Information) { 65 | // return theme.highlightColor 66 | return "#bcdff1" // Bootstrap 67 | } else if (messageType == MessageWidget.MessageType.Warning) { 68 | // return "#79735B" // DigitalClock 69 | return "#faf2cc" // Bootstrap 70 | } else if (messageType == MessageWidget.MessageType.Error) { 71 | return "#ebcccc" // Bootstrap 72 | } else { // positive 73 | return "#d0e9c6" // Bootstrap 74 | } 75 | } 76 | 77 | property color labelColor: { 78 | // return PlasmaCore.ColorScope.textColor 79 | if (messageType == MessageWidget.MessageType.Information) { 80 | return "#31708f" // Bootstrap 81 | } else if (messageType == MessageWidget.MessageType.Warning) { 82 | return "#8a6d3b" // Bootstrap 83 | } else if (messageType == MessageWidget.MessageType.Error) { 84 | return "#a94442" // Bootstrap 85 | } else { // positive 86 | return "#3c763d" // Bootstrap 87 | } 88 | } 89 | 90 | function show(message, messageType) { 91 | if (typeof messageType !== "undefined") { 92 | messageWidget.messageType = messageType 93 | } 94 | text = message 95 | visible = true 96 | } 97 | 98 | function success(message) { 99 | show(message, MessageWidget.MessageType.Positive) 100 | } 101 | 102 | function info(message) { 103 | show(message, MessageWidget.MessageType.Information) 104 | } 105 | 106 | function warn(message) { 107 | show(message, MessageWidget.MessageType.Warning) 108 | } 109 | 110 | function err(message) { 111 | show(message, MessageWidget.MessageType.Error) 112 | } 113 | 114 | function close() { 115 | visible = false 116 | } 117 | 118 | gradient: Gradient { 119 | GradientStop { position: 0.0; color: Qt.lighter(messageWidget.gradBaseColor, 1.1) } 120 | GradientStop { position: 0.1; color: messageWidget.gradBaseColor } 121 | GradientStop { position: 1.0; color: Qt.darker(messageWidget.gradBaseColor, 1.1) } 122 | } 123 | 124 | readonly property int expandedHeight: layout.implicitHeight + (2 * layout.anchors.margins) 125 | 126 | visible: text 127 | opacity: visible ? 1.0 : 0 128 | implicitHeight: visible ? messageWidget.expandedHeight : 0 129 | 130 | Component.onCompleted: { 131 | // Remove bindings 132 | visible = visible 133 | opacity = opacity 134 | if (visible) { 135 | implicitHeight = Qt.binding(function(){ return messageWidget.expandedHeight }) 136 | } else { 137 | implicitHeight = 0 138 | } 139 | } 140 | 141 | Behavior on visible { 142 | id: visibleAnimation 143 | 144 | ParallelAnimation { 145 | PropertyAnimation { 146 | target: messageWidget 147 | property: "opacity" 148 | to: messageWidget.visible ? 0 : 1.0 149 | easing.type: Easing.Linear 150 | } 151 | PropertyAnimation { 152 | target: messageWidget 153 | property: "implicitHeight" 154 | to: messageWidget.visible ? 0 : messageWidget.expandedHeight 155 | easing.type: Easing.Linear 156 | } 157 | } 158 | } 159 | 160 | RowLayout { 161 | id: layout 162 | anchors.fill: parent 163 | anchors.margins: units.smallSpacing 164 | spacing: units.smallSpacing 165 | 166 | PlasmaCore.IconItem { 167 | id: iconItem 168 | Layout.alignment: Qt.AlignVCenter 169 | implicitHeight: messageWidget.iconSize 170 | implicitWidth: messageWidget.iconSize 171 | source: messageWidget.icon 172 | } 173 | 174 | Label { 175 | id: label 176 | Layout.alignment: Qt.AlignVCenter 177 | Layout.fillWidth: true 178 | verticalAlignment: Text.AlignVCenter 179 | wrapMode: Text.WordWrap 180 | color: messageWidget.labelColor 181 | } 182 | 183 | ToolButton { 184 | id: closeButton 185 | Layout.alignment: Qt.AlignVCenter 186 | iconName: "dialog-close" 187 | 188 | onClicked: { 189 | messageWidget.close() 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /package/contents/ui/lib/Requests.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | // Version 8 3 | 4 | function request(opt, callback) { 5 | if (typeof opt === 'string') { 6 | opt = { url: opt } 7 | } 8 | var req = new XMLHttpRequest() 9 | req.onerror = function() { 10 | // Network Error / No Connection 11 | console.log('XMLHttpRequest.onerror', req.status) 12 | var msg = "HTTP Error " + req.status 13 | callback(msg, null, req) 14 | } 15 | req.onreadystatechange = function() { 16 | if (req.readyState === XMLHttpRequest.DONE) { // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-done 17 | if (200 <= req.status && req.status < 400) { 18 | callback(null, req.responseText, req) 19 | } else { 20 | if (req.status === 0) { 21 | console.log('HTTP 0 Headers: \n' + req.getAllResponseHeaders()) 22 | } 23 | var msg = "HTTP Error " + req.status 24 | callback(msg, req.responseText, req) 25 | } 26 | } 27 | } 28 | req.open(opt.method || "GET", opt.url, true) 29 | if (opt.headers) { 30 | for (var key in opt.headers) { 31 | req.setRequestHeader(key, opt.headers[key]) 32 | } 33 | } 34 | req.send(opt.data) 35 | } 36 | 37 | function encodeParams(params) { 38 | var s = '' 39 | var i = 0 40 | for (var key in params) { 41 | if (i > 0) { 42 | s += '&' 43 | } 44 | var value = params[key] 45 | if (typeof value === "object") { 46 | // TODO: Flatten obj={list: [1, 2]} as 47 | // obj[list][0]=1 48 | // obj[list][1]=2 49 | } 50 | s += encodeURIComponent(key) + '=' + encodeURIComponent(value) 51 | i += 1 52 | } 53 | return s 54 | } 55 | 56 | function encodeFormData(opt) { 57 | opt.headers = opt.headers || {} 58 | opt.headers['Content-Type'] = 'application/x-www-form-urlencoded' 59 | if (opt.data) { 60 | opt.data = encodeParams(opt.data) 61 | } 62 | return opt 63 | } 64 | 65 | function post(opt, callback) { 66 | if (typeof opt === 'string') { 67 | opt = { url: opt } 68 | } 69 | opt.method = 'POST' 70 | encodeFormData(opt) 71 | request(opt, callback) 72 | } 73 | 74 | 75 | function getJSON(opt, callback) { 76 | if (typeof opt === 'string') { 77 | opt = { url: opt } 78 | } 79 | opt.headers = opt.headers || {} 80 | opt.headers['Accept'] = 'application/json' 81 | request(opt, function(err, data, req) { 82 | if (!err && data) { 83 | data = JSON.parse(data) 84 | } 85 | callback(err, data, req) 86 | }) 87 | } 88 | 89 | 90 | function postJSON(opt, callback) { 91 | if (typeof opt === 'string') { 92 | opt = { url: opt } 93 | } 94 | opt.method = opt.method || 'POST' 95 | opt.headers = opt.headers || {} 96 | opt.headers['Content-Type'] = 'application/json' 97 | if (opt.data) { 98 | opt.data = JSON.stringify(opt.data) 99 | } 100 | getJSON(opt, callback) 101 | } 102 | 103 | function getFile(url, callback) { 104 | var req = new XMLHttpRequest() 105 | req.onerror = function() { 106 | // Network Error / No Connection 107 | console.log('XMLHttpRequest.onerror', req.status) 108 | var msg = "HTTP Error " + req.status 109 | callback(msg, null, req) 110 | } 111 | req.onreadystatechange = function() { 112 | if (req.readyState === 4) { 113 | // Since the file is local, it will have HTTP 0 Unsent. 114 | callback(null, req.responseText, req) 115 | } 116 | } 117 | req.open("GET", url, true) 118 | req.send() 119 | } 120 | 121 | function parseMetadata(data) { 122 | var lines = data.split('\n') 123 | var d = {} 124 | for (var i = 0; i < lines.length; i++) { 125 | var line = lines[i] 126 | var delimeterIndex = line.indexOf('=') 127 | if (delimeterIndex >= 0) { 128 | var key = line.substr(0, delimeterIndex) 129 | var value = line.substr(delimeterIndex + 1) 130 | d[key] = value 131 | } 132 | } 133 | return d 134 | } 135 | 136 | function getAppletMetadata(callback) { 137 | var url = Qt.resolvedUrl('.') 138 | 139 | var s = '/share/plasma/plasmoids/' 140 | var index = url.indexOf(s) 141 | if (index >= 0) { 142 | var a = index + s.length 143 | var b = url.indexOf('/', a) 144 | // var packageName = url.substr(a, b-a) 145 | var metadataUrl = url.substr(0, b) + '/metadata.desktop' 146 | Requests.getFile(metadataUrl, function(err, data) { 147 | if (err) { 148 | return callback(err) 149 | } 150 | 151 | var metadata = parseMetadata(data) 152 | callback(null, metadata) 153 | }) 154 | } else { 155 | return callback('Could not parse version.') 156 | } 157 | } 158 | 159 | function getAppletVersion(callback) { 160 | getAppletMetadata(function(err, metadata) { 161 | if (err) return callback(err) 162 | 163 | callback(err, metadata['X-KDE-PluginInfo-Version']) 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /package/contents/ui/weather/OpenWeatherMap.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | .import "../lib/Requests.js" as Requests 4 | 5 | function weatherIsSetup(config) { 6 | return !!config.openWeatherMapCityId 7 | } 8 | 9 | function openOpenWeatherMapCityUrl(cityId) { 10 | var url = 'https://openweathermap.org/city/' 11 | url += cityId 12 | Qt.openUrlExternally(url) 13 | } 14 | 15 | function fetchHourlyWeatherForecast(args, callback) { 16 | if (!args.appId) return callback('OpenWeatherMap AppId not set') 17 | if (!args.cityId) return callback('OpenWeatherMap CityId not set') 18 | 19 | // return testRateLimitError(callback) 20 | 21 | var url = 'https://api.openweathermap.org/data/2.5/' 22 | url += 'forecast?id=' + args.cityId 23 | url += '&units=' + (args.units || 'metric') 24 | url += '&appid=' + args.appId 25 | Requests.getJSON(url, callback) 26 | } 27 | 28 | function fetchDailyWeatherForecast(args, callback) { 29 | if (!args.appId) return callback('OpenWeatherMap AppId not set') 30 | if (!args.cityId) return callback('OpenWeatherMap CityId not set') 31 | 32 | // return testRateLimitError(callback) 33 | 34 | var url = 'https://api.openweathermap.org/data/2.5/' 35 | url += 'forecast/daily?id=' + args.cityId 36 | url += '&units=' + (args.units || 'metric') 37 | url += '&appid=' + args.appId 38 | Requests.getJSON(url, callback) 39 | } 40 | 41 | // https://openweathermap.org/weather-conditions 42 | var weatherIconMap = { 43 | '01d': 'weather-clear', 44 | '02d': 'weather-few-clouds', 45 | '03d': 'weather-clouds', 46 | '04d': 'weather-overcast', 47 | '09d': 'weather-showers-scattered', 48 | '10d': 'weather-showers', 49 | '11d': 'weather-storm', 50 | '13d': 'weather-snow', 51 | '50d': 'weather-fog', 52 | '01n': 'weather-clear-night', 53 | '02n': 'weather-few-clouds-night', 54 | '03n': 'weather-clouds-night', 55 | '04n': 'weather-overcast', 56 | '09n': 'weather-showers-scattered-night', 57 | '10n': 'weather-showers-night', 58 | '11n': 'weather-storm-night', 59 | '13n': 'weather-snow', 60 | '50n': 'weather-fog', 61 | } 62 | 63 | function parseDailyData(weatherData) { 64 | for (var j = 0; j < weatherData.list.length; j++) { 65 | var forecastItem = weatherData.list[j] 66 | 67 | forecastItem.iconName = weatherIconMap[forecastItem.weather[0].icon] 68 | forecastItem.text = forecastItem.weather[0].main 69 | forecastItem.description = forecastItem.weather[0].description 70 | 71 | var lines = [] 72 | lines.push('Morning: ' + Math.round(forecastItem.temp.morn) + '°') 73 | lines.push('Day: ' + Math.round(forecastItem.temp.day) + '°') 74 | lines.push('Evening: ' + Math.round(forecastItem.temp.eve) + '°') 75 | lines.push('Night: ' + Math.round(forecastItem.temp.night) + '°') 76 | forecastItem.notes = lines.join('
') 77 | } 78 | 79 | return weatherData 80 | } 81 | 82 | function parseHourlyData(weatherData) { 83 | for (var j = 0; j < weatherData.list.length; j++) { 84 | var forecastItem = weatherData.list[j] 85 | 86 | forecastItem.temp = forecastItem.main.temp 87 | forecastItem.iconName = weatherIconMap[forecastItem.weather[0].icon] 88 | // forecastItem.text = forecastItem.weather[0].main 89 | forecastItem.description = forecastItem.weather[0].description 90 | 91 | var rain = forecastItem.rain && forecastItem.rain['3h'] || 0 92 | var snow = forecastItem.snow && forecastItem.snow['3h'] || 0 93 | var mm = rain + snow 94 | forecastItem.precipitation = mm 95 | } 96 | 97 | return weatherData 98 | } 99 | 100 | function handleError(funcName, callback, err, data, xhr) { 101 | console.error('[eventcalendar]', funcName + '.err', err, xhr && xhr.status, data) 102 | return callback(err, data, xhr) 103 | } 104 | 105 | function updateDailyWeather(config, callback) { 106 | // console.debug('OpenWeatherMap.fetchDailyWeatherForecast') 107 | fetchDailyWeatherForecast({ 108 | appId: config.openWeatherMapAppId, 109 | cityId: config.openWeatherMapCityId, 110 | units: config.weatherUnits, 111 | }, function(err, data, xhr) { 112 | if (err) return handleError('OpenWeatherMap.fetchDailyWeatherForecast', callback, err, data, xhr) 113 | // console.debug('OpenWeatherMap.fetchDailyWeatherForecast.response') 114 | // console.debug('OpenWeatherMap.fetchDailyWeatherForecast.response', data) 115 | 116 | data = parseDailyData(data) 117 | 118 | callback(err, data, xhr) 119 | }) 120 | } 121 | 122 | function updateHourlyWeather(config, callback) { 123 | // console.debug('OpenWeatherMap.fetchHourlyWeatherForecast') 124 | fetchHourlyWeatherForecast({ 125 | appId: config.openWeatherMapAppId, 126 | cityId: config.openWeatherMapCityId, 127 | units: config.weatherUnits, 128 | }, function(err, data, xhr) { 129 | if (err) return handleError('updateHourlyWeather', callback, err, data, xhr) 130 | // console.debug('OpenWeatherMap.fetchHourlyWeatherForecast.response') 131 | // console.debug('OpenWeatherMap.fetchHourlyWeatherForecast.response', data) 132 | 133 | data = parseHourlyData(data) 134 | 135 | callback(err, data, xhr) 136 | }) 137 | } 138 | 139 | //--- Tests 140 | function testRateLimitError(callback) { 141 | var err = 'HTTP Error 429: ' 142 | var data = { 143 | "cod":429, 144 | "message": "Your account is temporary blocked due to exceeding of requests limitation of your subscription type. Please choose the proper subscription http://openweathermap.org/price" 145 | } 146 | var xhr = { status: 429 } 147 | return callback(err, data, xhr) 148 | } 149 | -------------------------------------------------------------------------------- /package/contents/ui/weather/WeatherApi.js: -------------------------------------------------------------------------------- 1 | .pragma library 2 | 3 | .import "OpenWeatherMap.js" as OpenWeatherMap 4 | .import "WeatherCanada.js" as WeatherCanada 5 | 6 | /* How many hours each data point represents */ 7 | function getDataPointDuration(config) { 8 | var weatherService = config.weatherService 9 | if (weatherService == 'OpenWeatherMap') { 10 | return 3 11 | } else if (weatherService == 'WeatherCanada') { 12 | return 1 13 | } else { 14 | return 1 15 | } 16 | } 17 | 18 | /* Precipitation units ('mm' or '%') */ 19 | function getRainUnits(config) { 20 | var weatherService = config.weatherService 21 | if (weatherService == 'OpenWeatherMap') { 22 | return 'mm' 23 | } else if (weatherService == 'WeatherCanada') { 24 | return '%' 25 | } else { 26 | return 'mm' 27 | } 28 | } 29 | 30 | /* Open the city's webpage using Qt.openUrlExternally(url) */ 31 | function openCityUrl(config) { 32 | var weatherService = config.weatherService 33 | if (weatherService == 'OpenWeatherMap') { 34 | OpenWeatherMap.openOpenWeatherMapCityUrl(config.openWeatherMapCityId) 35 | } else if (weatherService == 'WeatherCanada') { 36 | Qt.openUrlExternally(WeatherCanada.getCityUrl(config.weatherCanadaCityId)) 37 | } 38 | } 39 | 40 | /* Update the weather shown in the agenda. */ 41 | /* @returns: callback(err, { 42 | list: [ 43 | { 44 | dt: 1474831800, // seconds 45 | temp: { 46 | min: 14.5, 47 | max: 14.5, 48 | morn: 14.5, // (Optional) 49 | day: 14.5, // (Optional) 50 | eve: 14.5, // (Optional) 51 | night: 14.5, // (Optional) 52 | }, 53 | iconName: 'weather-clear', 54 | text: 'Clear', // Word/Short description (the "Weather Text" shown in the agenda) 55 | description: 'clear sky', // Sentence (shown in the tooltip) 56 | notes: 'Morning: 13°\nEvening: 5°', // Tooltip subtext (Optional) 57 | }, 58 | ... 59 | ] 60 | }, xhr) 61 | */ 62 | function updateDailyWeather(config, callback) { 63 | if (!weatherIsSetup(config)) { 64 | return callback('Weather configuration not setup') 65 | } 66 | var weatherService = config.weatherService 67 | if (weatherService == 'OpenWeatherMap') { 68 | OpenWeatherMap.updateDailyWeather(config, callback) 69 | } else if (weatherService == 'WeatherCanada') { 70 | WeatherCanada.updateDailyWeather(config, callback) 71 | } 72 | } 73 | 74 | /* Update the meteogram dataset. Will not be called if the meteogram isn't enabled. */ 75 | /* @returns: callback(err, { 76 | list: [ 77 | { 78 | dt: 1474831800, // seconds 79 | temp: 14.5, 80 | iconName: 'weather-clear', 81 | description: 'clear sky', // Sentence (Tooltip) 82 | precipitation: 20, // Can represent 20mm or 20% 83 | }, 84 | ... 85 | ] 86 | }, xhr) 87 | */ 88 | function updateHourlyWeather(config, callback) { 89 | if (!weatherIsSetup(config)) { 90 | return callback('Weather configuration not setup') 91 | } 92 | var weatherService = config.weatherService 93 | if (weatherService == 'OpenWeatherMap') { 94 | OpenWeatherMap.updateHourlyWeather(config, callback) 95 | } else if (weatherService == 'WeatherCanada') { 96 | WeatherCanada.updateHourlyWeather(config, callback) 97 | } 98 | } 99 | 100 | /* Return true if all configuration has been setup. */ 101 | function weatherIsSetup(config) { 102 | var weatherService = config.weatherService 103 | if (weatherService == 'OpenWeatherMap') { 104 | return OpenWeatherMap.weatherIsSetup(config) 105 | } else if (weatherService == 'WeatherCanada') { 106 | return WeatherCanada.weatherIsSetup(config) 107 | } else { 108 | return false 109 | } 110 | } 111 | 112 | var weatherIconBySeverity = [ 113 | // Least Severe 114 | 'weather-clear-night', 115 | 'weather-clear', 116 | 'weather-few-clouds-night', 117 | 'weather-few-clouds', 118 | 'weather-clouds-night', 119 | 'weather-clouds', 120 | 'weather-overcast', 121 | 'weather-fog', 122 | 'weather-overcast', 123 | 'weather-showers-scattered-night', 124 | 'weather-showers-scattered', 125 | 'weather-snow-scattered-night', 126 | 'weather-snow-scattered-day', 127 | 'weather-snow', 128 | 'weather-snow-rain-night', 129 | 'weather-snow-rain', 130 | 'weather-showers-night', 131 | 'weather-showers', 132 | 'weather-storm-night', 133 | 'weather-storm', 134 | 'weather-severe-alert', 135 | // Most severe 136 | ] 137 | 138 | function getMostSevereIcon(weatherIconList) { 139 | var mostSevereIndex = weatherIconBySeverity.indexOf(weatherIconList[0]) 140 | for (var i = 1; i < weatherIconList.length; i++) { 141 | var index = weatherIconBySeverity.indexOf(weatherIconList[i]) 142 | mostSevereIndex = Math.max(mostSevereIndex, index) 143 | } 144 | if (mostSevereIndex === -1) { 145 | return weatherIconList[0] 146 | } 147 | return weatherIconBySeverity[mostSevereIndex] 148 | } 149 | -------------------------------------------------------------------------------- /package/metadata.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Event Calendar 3 | Comment=Plasmoid for a calendar+agenda with weather that syncs to Google Calendar. 4 | 5 | 6 | Icon=view-calendar 7 | Type=Service 8 | 9 | X-KDE-PluginInfo-Author=Chris Holland 10 | X-KDE-PluginInfo-Email=zrenfire@gmail.com 11 | X-KDE-PluginInfo-Category=Date and Time 12 | X-KDE-PluginInfo-License=GPL 13 | X-KDE-PluginInfo-Name=org.kde.plasma.eventcalendar 14 | X-KDE-PluginInfo-Website=https://github.com/Zren/plasma-applet-eventcalendar 15 | X-KDE-PluginInfo-Version=76 16 | X-KDE-ServiceTypes=Plasma/Applet 17 | X-Plasma-API=declarativeappletscript 18 | X-Plasma-MainScript=ui/main.qml 19 | X-Plasma-Provides=org.kde.plasma.time,org.kde.plasma.date 20 | 21 | Name[da]=Begivenhedskalender 22 | Name[de]=Ereigniskalender 23 | Name[el]=Ημερολόγιο Συμβάντων 24 | Name[es]=Calendario de Eventos 25 | Name[fi]=Tapahtumakalenteri 26 | Name[fr]=Calendrier des événements 27 | Name[he]=לוח שנה עם אירועים 28 | Name[it]=Calendario Eventi 29 | Name[ja]=イベントカレンダー 30 | Name[ko]=이벤트 달력 31 | Name[nl]=Afsprakenboek 32 | Name[pt_BR]=Calendário de eventos 33 | Name[pt_PT]=Calendário de eventos 34 | Name[ru]=Календарь событий 35 | Name[sl]=Koledar z dnevnim redom 36 | Name[tr]=Etkinlik Takvimi 37 | Name[uk]=Календар подій 38 | Comment[de]=Miniprogramm für einen Kalender mit Ereignisansicht und Wetter, der Ereignisse mit Google Kalender synchronisiert. 39 | Comment[es]=Plasmoid para calendario + agenda con información del clima. Sincroniza con Google Calendar. 40 | Comment[fi]=Plasmoidi kalenteri + agenda säätietoineen synkronoitu Google Calendarin kanssa. 41 | Comment[he]=יישומון לוח שנה וסדר יום שמסתנכרן עם Google Calendar ועם גישה ישירה לתחזית מזג האוויר. 42 | Comment[it]=Plasmoide per un calendario+agenda con meteo che si sincronizza con Google Calendar. 43 | Comment[ko]=Google 캘린더랑 동기화할 수 있는 달력, 일정 및 날씨 Plasmoid 위젯입니다. 44 | Comment[nl]=Een plasmoid met kalender, agenda en weer die synchroniseert met Google Agenda. 45 | Comment[pt_BR]=Plasmoide para um calendário e agenda com informações do clima que sincroniza com o Google Agenda. 46 | Comment[pt_PT]=Plasmoid para um calendário+agenda com Meteorologia que sincroniza com o Calendário Google. 47 | Comment[ru]=Календарь событий с погодой и синхронизацией с Google Calendar. 48 | Comment[sl]=Vtičnik za koledar z dnevnim redom in vremensko napovedjo, ki se usklajuje z Googlovim koledarjem. 49 | Comment[sv]=Plasmoid för kalender och agenda med väder som synkroniseras med Google Kalender. 50 | -------------------------------------------------------------------------------- /package/translate/ReadMe.md: -------------------------------------------------------------------------------- 1 | > Version 7 of Zren's i18n scripts. 2 | 3 | With KDE Frameworks v5.37 and above, translations are bundled with the `*.plasmoid` file downloaded from the store. 4 | 5 | ## Install Translations 6 | 7 | Go to `~/.local/share/plasma/plasmoids/org.kde.plasma.eventcalendar/translate/` and run `sh ./build --restartplasma`. 8 | 9 | ## New Translations 10 | 11 | 1. Fill out [`template.pot`](template.pot) with your translations then open a [new issue](https://github.com/Zren/plasma-applet-eventcalendar/issues/new), name the file `spanish.txt`, attach the txt file to the issue (drag and drop). 12 | 13 | Or if you know how to make a pull request 14 | 15 | 1. Copy the `template.pot` file and name it your locale's code (Eg: `en`/`de`/`fr`) with the extension `.po`. Then fill out all the `msgstr ""`. 16 | 17 | ## Scripts 18 | 19 | * `sh ./merge` will parse the `i18n()` calls in the `*.qml` files and write it to the `template.pot` file. Then it will merge any changes into the `*.po` language files. 20 | * `sh ./build` will convert the `*.po` files to it's binary `*.mo` version and move it to `contents/locale/...` which will bundle the translations in the `*.plasmoid` without needing the user to manually install them. 21 | * `sh ./plasmoidlocaletest` will run `./build` then `plasmoidviewer` (part of `plasma-sdk`). 22 | 23 | ## Links 24 | 25 | * https://zren.github.io/kde/docs/widget/#translations-i18n 26 | * https://techbase.kde.org/Development/Tutorials/Localization/i18n_Build_Systems 27 | * https://api.kde.org/frameworks/ki18n/html/prg_guide.html 28 | 29 | ## Examples 30 | 31 | * https://l10n.kde.org/stats/gui/trunk-kf5/team/fr/plasma-desktop/ 32 | * https://github.com/psifidotos/nowdock-plasmoid/tree/master/po 33 | * https://github.com/kotelnik/plasma-applet-redshift-control/tree/master/translations 34 | 35 | ## Status 36 | | Locale | Lines | % Done| 37 | |----------|---------|-------| 38 | | Template | 219 | | 39 | | da | 184/219 | 84% | 40 | | de | 215/219 | 98% | 41 | | el | 167/219 | 76% | 42 | | es | 218/219 | 99% | 43 | | fi | 215/219 | 98% | 44 | | fr | 186/219 | 84% | 45 | | he | 216/219 | 98% | 46 | | it | 215/219 | 98% | 47 | | ja | 183/219 | 83% | 48 | | ko | 215/219 | 98% | 49 | | nl | 219/219 | 100% | 50 | | pl | 157/219 | 71% | 51 | | pt_BR | 215/219 | 98% | 52 | | pt_PT | 214/219 | 97% | 53 | | ru | 215/219 | 98% | 54 | | sl | 193/219 | 88% | 55 | | sv | 179/219 | 81% | 56 | | tr | 183/219 | 83% | 57 | | uk | 157/219 | 71% | 58 | | zh_CN | 166/219 | 75% | 59 | -------------------------------------------------------------------------------- /package/translate/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Version: 7 3 | 4 | # This script will convert the *.po files to *.mo files, rebuilding the package/contents/locale folder. 5 | # Feature discussion: https://phabricator.kde.org/D5209 6 | # Eg: contents/locale/fr_CA/LC_MESSAGES/plasma_applet_org.kde.plasma.eventcalendar.mo 7 | 8 | DIR=`cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd` 9 | plasmoidName=`kreadconfig5 --file="$DIR/../metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Name"` 10 | website=`kreadconfig5 --file="$DIR/../metadata.desktop" --group="Desktop Entry" --key="X-KDE-PluginInfo-Website"` 11 | bugAddress="$website" 12 | packageRoot="${DIR}/.." # Root of translatable sources 13 | projectName="plasma_applet_${plasmoidName}" # project name 14 | 15 | ### Colors 16 | # https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux 17 | # https://stackoverflow.com/questions/911168/how-can-i-detect-if-my-shell-script-is-running-through-a-pipe 18 | TC_Red='\033[31m'; TC_Orange='\033[33m'; 19 | TC_LightGray='\033[90m'; TC_LightRed='\033[91m'; TC_LightGreen='\033[92m'; TC_Yellow='\033[93m'; TC_LightBlue='\033[94m'; 20 | TC_Reset='\033[0m'; TC_Bold='\033[1m'; 21 | if [ ! -t 1 ]; then 22 | TC_Red=''; TC_Orange=''; 23 | TC_LightGray=''; TC_LightRed=''; TC_LightGreen=''; TC_Yellow=''; TC_LightBlue=''; 24 | TC_Bold=''; TC_Reset=''; 25 | fi 26 | function echoTC() { 27 | text="$1" 28 | textColor="$2" 29 | echo -e "${textColor}${text}${TC_Reset}" 30 | } 31 | function echoGray { echoTC "$1" "$TC_LightGray"; } 32 | function echoRed { echoTC "$1" "$TC_Red"; } 33 | function echoGreen { echoTC "$1" "$TC_LightGreen"; } 34 | 35 | #--- 36 | if [ -z "$plasmoidName" ]; then 37 | echoRed "[translate/build] Error: Couldn't read plasmoidName." 38 | exit 39 | fi 40 | 41 | if [ -z "$(which msgfmt)" ]; then 42 | echoRed "[translate/build] Error: msgfmt command not found. Need to install gettext" 43 | echoRed "[translate/build] Running ${TC_Bold}'sudo apt install gettext'" 44 | sudo apt install gettext 45 | echoRed "[translate/build] gettext installation should be finished. Going back to installing translations." 46 | fi 47 | 48 | #--- 49 | echoGray "[translate/build] Compiling messages" 50 | 51 | function relativePath() { 52 | basePath=`realpath -- "$1"` 53 | longerPath=`realpath -- "$2"` 54 | echo "${longerPath#${basePath}*}" 55 | } 56 | catalogs=`find . -name '*.po' | sort` 57 | for cat in $catalogs; do 58 | catLocale=`basename ${cat%.*}` 59 | moFilename="${catLocale}.mo" 60 | installPath="${packageRoot}/contents/locale/${catLocale}/LC_MESSAGES/${projectName}.mo" 61 | installPath=`realpath -- "$installPath"` 62 | relativeInstallPath=`relativePath "${packageRoot}" "${installPath}"` 63 | relativeInstallPath="${relativeInstallPath#/*}" 64 | echoGray "[translate/build] Converting '${cat}' => '${relativeInstallPath}'" 65 | msgfmt -o "${moFilename}" "${cat}" 66 | mkdir -p "$(dirname "$installPath")" 67 | mv "${moFilename}" "${installPath}" 68 | done 69 | 70 | echoGreen "[translate/build] Done building messages" 71 | 72 | if [ "$1" = "--restartplasma" ]; then 73 | echo "[translate/build] ${TC_Bold}Restarting plasmashell${TC_Reset}" 74 | killall plasmashell 75 | kstart5 plasmashell 76 | echo "[translate/build] Done restarting plasmashell" 77 | else 78 | echo "[translate/build] (re)install the plasmoid and restart plasmashell to test translations." 79 | fi 80 | -------------------------------------------------------------------------------- /uninstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Version 3 3 | 4 | packageServiceType=`kreadconfig5 --file="$PWD/package/metadata.desktop" --group="Desktop Entry" --key="X-KDE-ServiceTypes"` 5 | 6 | # Eg: kpackagetool5 -t "Plasma/Applet" -r package 7 | kpackagetool5 -t "${packageServiceType}" -r package 8 | -------------------------------------------------------------------------------- /update: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git pull origin master 4 | source ./install 5 | --------------------------------------------------------------------------------