├── .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 | 
10 | 
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 |
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 |
--------------------------------------------------------------------------------