From 8cdd98b2ff8dc4d3878a35ef352f72ee19d99efd Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Sat, 20 Aug 2022 17:06:03 +0000 Subject: [PATCH] tvplanit: Export events to ical files (still buggy). git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8401 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- components/tvplanit/languages/vpsr.de.po | 20 +- components/tvplanit/languages/vpsr.en.po | 20 +- components/tvplanit/languages/vpsr.fi.po | 18 +- components/tvplanit/languages/vpsr.fr.po | 18 +- components/tvplanit/languages/vpsr.nl.po | 18 +- components/tvplanit/languages/vpsr.pl.po | 19 +- components/tvplanit/languages/vpsr.pot | 18 +- components/tvplanit/languages/vpsr.ru.po | 18 +- components/tvplanit/source/include/vpsr.inc | 4 +- components/tvplanit/source/vpbase.pas | 6 +- components/tvplanit/source/vpcontactgrid.pas | 1 - components/tvplanit/source/vpdata.pas | 107 +++++++++ components/tvplanit/source/vpdayview.pas | 52 +++- components/tvplanit/source/vpical.pas | 235 +++++++++++++++++-- components/tvplanit/source/vpvcard.pas | 2 +- components/tvplanit/source/vpweekview.pas | 2 +- 16 files changed, 485 insertions(+), 73 deletions(-) diff --git a/components/tvplanit/languages/vpsr.de.po b/components/tvplanit/languages/vpsr.de.po index b0a4abe24..019af4c94 100644 --- a/components/tvplanit/languages/vpsr.de.po +++ b/components/tvplanit/languages/vpsr.de.po @@ -1023,11 +1023,6 @@ msgctxt "vpsr.rspopupaddevent" msgid "Add event..." msgstr "Ereignis hinzufügen..." -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "Von iCalendar-Datei(en) importieren..." - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1048,6 +1043,16 @@ msgctxt "vpsr.rspopupeditevent" msgid "Edit event..." msgstr "Ereignis bearbeiten..." +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +#, fuzzy +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "Von iCalendar-Datei(en) importieren..." + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "Ereignisse überlagern" @@ -1206,6 +1211,10 @@ msgstr "Format in Datei speichern?" msgid "Save format to \"%s\"?" msgstr "Format speichern als \"%s\"?" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "Als vCard exportieren" @@ -1798,3 +1807,4 @@ msgstr "Unbekannte Achsen-Spezifikation: %s" #: vpsr.sxmldecnotatbeg msgid "The XML declaration must appear before the first element" msgstr "Die XML-Deklaration muss vor dem ersten Element erscheinen" + diff --git a/components/tvplanit/languages/vpsr.en.po b/components/tvplanit/languages/vpsr.en.po index 82580d339..9e49fa737 100644 --- a/components/tvplanit/languages/vpsr.en.po +++ b/components/tvplanit/languages/vpsr.en.po @@ -1014,11 +1014,6 @@ msgstr "Pixels" msgid "Add event..." msgstr "Add event..." -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "Import from iCalendar file(s)..." - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1036,6 +1031,16 @@ msgstr "&Delete event..." msgid "Edit event..." msgstr "Edit event..." +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +#, fuzzy +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "Import from iCalendar file(s)..." + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "Overlay events" @@ -1192,6 +1197,10 @@ msgstr "Save format to file?" msgid "Save format to \"%s\"?" msgstr "Save format to \"%s\"?" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "Export to vCard" @@ -1779,3 +1788,4 @@ msgstr "Unknown axis specifier: %s" #: vpsr.sxmldecnotatbeg msgid "The XML declaration must appear before the first element" msgstr "The XML declaration must appear before the first element" + diff --git a/components/tvplanit/languages/vpsr.fi.po b/components/tvplanit/languages/vpsr.fi.po index e4dd4b99e..e3dd9b1ab 100644 --- a/components/tvplanit/languages/vpsr.fi.po +++ b/components/tvplanit/languages/vpsr.fi.po @@ -1014,11 +1014,6 @@ msgctxt "vpsr.rspopupaddevent" msgid "Add event..." msgstr "" -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "" - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1039,6 +1034,15 @@ msgctxt "vpsr.rspopupeditevent" msgid "Edit event..." msgstr "" +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "" + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "" @@ -1197,6 +1201,10 @@ msgstr "" msgid "Save format to \"%s\"?" msgstr "" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "" diff --git a/components/tvplanit/languages/vpsr.fr.po b/components/tvplanit/languages/vpsr.fr.po index c724a0d28..8dfb59392 100644 --- a/components/tvplanit/languages/vpsr.fr.po +++ b/components/tvplanit/languages/vpsr.fr.po @@ -1029,11 +1029,6 @@ msgctxt "vpsr.rspopupaddevent" msgid "Add event..." msgstr "Ajouter un événement..." -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "" - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1054,6 +1049,15 @@ msgctxt "vpsr.rspopupeditevent" msgid "Edit event..." msgstr "Modifier un événement" +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "" + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "" @@ -1212,6 +1216,10 @@ msgstr "" msgid "Save format to \"%s\"?" msgstr "" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "" diff --git a/components/tvplanit/languages/vpsr.nl.po b/components/tvplanit/languages/vpsr.nl.po index d065cc15a..34d17d082 100644 --- a/components/tvplanit/languages/vpsr.nl.po +++ b/components/tvplanit/languages/vpsr.nl.po @@ -1023,11 +1023,6 @@ msgctxt "vpsr.rspopupaddevent" msgid "Add event..." msgstr "Gebeurtenis toevoegen..." -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "" - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1048,6 +1043,15 @@ msgctxt "vpsr.rspopupeditevent" msgid "Edit event..." msgstr "Gebeurtenis bewerken..." +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "" + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "" @@ -1206,6 +1210,10 @@ msgstr "" msgid "Save format to \"%s\"?" msgstr "" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "" diff --git a/components/tvplanit/languages/vpsr.pl.po b/components/tvplanit/languages/vpsr.pl.po index 93cae1e13..5ecdb720f 100644 --- a/components/tvplanit/languages/vpsr.pl.po +++ b/components/tvplanit/languages/vpsr.pl.po @@ -1022,11 +1022,6 @@ msgstr "Pixele" msgid "Add event..." msgstr "Dodaj wydarzenie..." -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "Import z pliku iCalendar" - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1047,6 +1042,16 @@ msgctxt "vpsr.rspopupeditevent" msgid "Edit event..." msgstr "Edytuj wydarzenie..." +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +#, fuzzy +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "Import z pliku iCalendar" + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "Nakładające się wydarzenia" @@ -1205,6 +1210,10 @@ msgstr "Zapisać dane do pliku?" msgid "Save format to \"%s\"?" msgstr "Zapisać format do \"%s\"?" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "" diff --git a/components/tvplanit/languages/vpsr.pot b/components/tvplanit/languages/vpsr.pot index dfc6a3abb..c0d9f0c4e 100644 --- a/components/tvplanit/languages/vpsr.pot +++ b/components/tvplanit/languages/vpsr.pot @@ -1004,11 +1004,6 @@ msgstr "" msgid "Add event..." msgstr "" -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "" - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1026,6 +1021,15 @@ msgstr "" msgid "Edit event..." msgstr "" +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "" + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "" @@ -1182,6 +1186,10 @@ msgstr "" msgid "Save format to \"%s\"?" msgstr "" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "" diff --git a/components/tvplanit/languages/vpsr.ru.po b/components/tvplanit/languages/vpsr.ru.po index 884ff0fd4..7555af341 100644 --- a/components/tvplanit/languages/vpsr.ru.po +++ b/components/tvplanit/languages/vpsr.ru.po @@ -1023,11 +1023,6 @@ msgctxt "vpsr.rspopupaddevent" msgid "Add event..." msgstr "Добавить событие..." -#: vpsr.rspopupaddeventfromical -msgctxt "vpsr.rspopupaddeventfromical" -msgid "Import from iCalendar file(s)..." -msgstr "" - #: vpsr.rspopupaddtaskfromical msgctxt "vpsr.rspopupaddtaskfromical" msgid "Import from iCalendar file(s)..." @@ -1048,6 +1043,15 @@ msgctxt "vpsr.rspopupeditevent" msgid "Edit event..." msgstr "Изменить событие..." +#: vpsr.rspopupexporteventtoical +msgid "Export to iCalendar file..." +msgstr "" + +#: vpsr.rspopupimporteventfromical +msgctxt "vpsr.rspopupimporteventfromical" +msgid "Import from iCalendar file(s)..." +msgstr "" + #: vpsr.rspopupresourcegroups msgid "Overlay events" msgstr "" @@ -1206,6 +1210,10 @@ msgstr "" msgid "Save format to \"%s\"?" msgstr "" +#: vpsr.rssaveicaltitle +msgid "Export to iCal file" +msgstr "" + #: vpsr.rssavevcardtitle msgid "Export to vCard" msgstr "" diff --git a/components/tvplanit/source/include/vpsr.inc b/components/tvplanit/source/include/vpsr.inc index b93b5cde5..d05e1b7ea 100644 --- a/components/tvplanit/source/include/vpsr.inc +++ b/components/tvplanit/source/include/vpsr.inc @@ -181,6 +181,7 @@ resourcestring RSStartEndTimesEqual = 'Start and end times cannot be equal.'; RSCannotEditOverlayedEvent= 'Cannot edit this overlayed event.'; RSLoadICalTitle = 'Import from iCal file(s)'; + RSSaveICalTitle = 'Export to iCal file'; RSNoOverlayedEvents = 'none'; RSOverlayedEvent = 'overlayed'; RSOverlayed = 'Overlayed'; @@ -207,7 +208,8 @@ resourcestring { Popup specific } RSPopupAddEvent = 'Add event...'; - RSPopupAddEventFromICal = 'Import from iCalendar file(s)...'; + RSPopupImportEventFromICal= 'Import from iCalendar file(s)...'; + RSPopupExportEventToICal = 'Export to iCalendar file...'; RSPopupEditEvent = 'Edit event...'; RSPopupDeleteEvent = '&Delete event...'; RSPopupChangeDate = 'Change date'; diff --git a/components/tvplanit/source/vpbase.pas b/components/tvplanit/source/vpbase.pas index 573349bc7..c1600ed82 100644 --- a/components/tvplanit/source/vpbase.pas +++ b/components/tvplanit/source/vpbase.pas @@ -81,7 +81,8 @@ type mikAddEvent, mikEditEvent, mikDeleteEvent, mikAddTask, mikEditTask, mikDeleteTask, mikAddContact, mikEditContact, mikDeleteContact, - mikImportEventFromICal, mikImportTaskFromICal, + mikImportEventFromICal, mikExportEventToICal, + mikImportTaskFromICal, mikImportContactFromVCards, mikExportContactToVCard, mikResourceGroups, mikNoOverlaidEvents, mikChangeDate, mikCustomDate, mikToday, mikYesterday, mikTomorrow, @@ -94,7 +95,8 @@ type RSPopupAddEvent, RSPopupEditEvent, RSPopupDeleteEvent, RSTaskPopupAdd, RSTaskPopupEdit, RSTaskPopupDelete, RSContactPopupAdd, RSContactPopupEdit, RSContactPopupDelete, - RSPopupAddEventFromICal, RSPopupAddTaskFromICal, + RSPopupImportEventFromICal, RSPopupExportEventToICal, + RSPopupAddTaskFromICal, RSContactPopupImportVCards, RSContactPopupExportVCard, RSPopupResourceGroups, RSNoOverlayedEvents, RSPopupChangeDate, RSCustomDate, RSToday, RSYesterday, RSTomorrow, diff --git a/components/tvplanit/source/vpcontactgrid.pas b/components/tvplanit/source/vpcontactgrid.pas index 48bc327b8..3ecbad041 100644 --- a/components/tvplanit/source/vpcontactgrid.pas +++ b/components/tvplanit/source/vpcontactgrid.pas @@ -1676,7 +1676,6 @@ end; procedure TVpContactGrid.PopupImportVCards(Sender: TObject); var dlg: TOpenDialog; - vcards: TVpVCards; i: Integer; fn: String; id: Integer; diff --git a/components/tvplanit/source/vpdata.pas b/components/tvplanit/source/vpdata.pas index 252037881..21854aa45 100644 --- a/components/tvplanit/source/vpdata.pas +++ b/components/tvplanit/source/vpdata.pas @@ -241,6 +241,7 @@ type procedure DeleteEvent(Event: TVpEvent); function EventCountByDay(Value: TDateTime): Integer; procedure EventsByDate(Date: TDateTime; EventList: TList); + procedure ExportICalFile(const AFileName: String; const AEvents: TVpEventArr); function GetEvent(Index: Integer): TVpEvent; function ImportICalFile(const AFileName: String; APreview: Boolean = false; ADefaultCategory: Integer = -1): TVpEventArr; @@ -312,6 +313,7 @@ type destructor Destroy; override; function CanEdit: Boolean; function CopyToSchedule(ASchedule: TVpSchedule): TVpEvent; + function CreateICalEvent(ACalendar: TVpICalendar): TVpICalEvent; class procedure GetAlarmParams(ATrigger: TDateTime; out AdvTime: Integer; out AdvTimeUnits: TVpAlarmAdvType); function GetResource: TVpResource; @@ -1343,6 +1345,94 @@ begin Result.UserField8 := FUserField9; end; +{ Warning: Recurrence and Alarm poorly tested... } +function TVpEvent.CreateICalEvent(ACalendar: TVpICalendar): TVpICalEvent; +var + datastore: TVpCustomDatastore; +begin + Result := TVpICalEvent.Create(ACalendar); + Result.Summary := FDescription; + Result.Description := FNotes; + Result.Location := FLocation; + + Result.StartTime[false] := FStartTime; + Result.EndTime[false] := FEndTime + OneSecond; + + datastore := TVpCustomDatastore(GetResource.Owner.Owner); + Result.Categories.Text := datastore.CategoryColorMap.GetCategoryName(FCategory); + + Result.RecurrenceFrequency := ''; + Result.RecurrenceInterval := 0; + Result.RecurrenceCount := 0; + Result.RecurrenceByXXX := ''; + case FRepeatCode of + rtDaily: + begin + Result.RecurrenceFrequency := 'DAILY'; + Result.RecurrenceInterval := 1; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)); + end; + rtWeekly: + begin + Result.RecurrenceFrequency := 'WEEKLY'; + Result.RecurrenceInterval := 1; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)) div 7; + end; + rtMonthlyByDay: + begin + Result.RecurrenceFrequency := 'MONTHLY'; + Result.RecurrenceInterval := 1; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)) div 30; + end; + rtMonthlyByDate: + begin + Result.RecurrenceFrequency := 'MONTHLY'; + Result.RecurrenceInterval := 1; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)) div 30; + end; + rtYearlyByDay: + begin + Result.RecurrenceFrequency := 'YEARLY'; + Result.RecurrenceInterval := 1; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)) div 365; + end; + rtYearlyByDate: + begin + Result.RecurrenceFrequency := 'YEARLY'; + Result.RecurrenceInterval := 1; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)) div 365; + end; + rtCustom: + begin + Result.RecurrenceFrequency := 'DAILY'; + Result.RecurrenceInterval := FCustInterval; + if FRepeatRangeEnd <> FOREVER_DATE then + Result.RecurrenceCount := DaysBetween(DayOf(FRepeatRangeEnd), DayOf(FStartTime)); + end; + else + ; + end; + + if AlarmSet then + begin + Result.UseAlarm(true); + Result.Alarm.Audio := (FDingPath <> '') and FileExists(FDingPath); + Result.Alarm.AudioSrc := FDingPath; + case FAlarmAdvType of + atMinutes: Result.Alarm.Trigger := FAlarmAdv * OneMinute; + atHours: Result.Alarm.Trigger := FAlarmAdv * OneHour; + atDays: Result.Alarm.Trigger := FAlarmAdv; + end; + Result.Alarm.RepeatCount := 1; + end; +end; + { Returns the resource to which the event belongs. } function TVpEvent.GetResource: TVpResource; begin @@ -1748,6 +1838,23 @@ begin result := TVpEvent(FEventList[Index]); end; +procedure TVpSchedule.ExportICalFile(const AFileName: String; + const AEvents: TVpEventArr); +var + cal: TVpICalendar; + lEvent: TVpEvent; +begin + cal := TVpICalendar.Create; + try + for lEvent in AEvents do + if lEvent <> nil then + cal.Add(lEvent.CreateICalEvent(cal)); + cal.SaveToFile(AFileName); + finally + cal.Free; + end; +end; + function TVpSchedule.ImportICalFile(const AFileName: String; APreview: Boolean = false; ADefaultCategory: Integer = -1): TVpEventArr; const diff --git a/components/tvplanit/source/vpdayview.pas b/components/tvplanit/source/vpdayview.pas index 521c286e3..9f85917e3 100644 --- a/components/tvplanit/source/vpdayview.pas +++ b/components/tvplanit/source/vpdayview.pas @@ -371,7 +371,8 @@ type { Popup } function GetPopupMenu: TPopupMenu; override; procedure PopupAddEvent(Sender: TObject); - procedure PopupAddFromICalFile(Sender: TObject); + procedure PopupImportICalFile(Sender: TObject); + procedure PopupExportICalFile(Sender: TObject); procedure PopupDeleteEvent(Sender: TObject); procedure PopupEditEvent(Sender: TObject); procedure PopupToday(Sender: TObject); @@ -451,7 +452,8 @@ type function BuildEventString(AEvent: TVpEvent; UseAsHint: Boolean): String; procedure DeleteActiveEvent(Verify: Boolean); procedure DragDrop(Source: TObject; X, Y: Integer); override; - function ImportICalFile(const AFileName: String; APreview: Boolean = false; + procedure ExportICalFile(const AFileName: String; const AEvents: TVpEventArr); + function ImportICalFile(const AFileName: String; APreview: Boolean = false; ADefaultCategory: Integer = -1): TVpEventArr; procedure Invalidate; override; function IsHoliday(ADate: TDate; out AHolidayName: String): Boolean; @@ -1197,14 +1199,22 @@ begin NewItem.Kind := mikSeparator; FDefaultPopup.Items.Add(NewItem); - if RSPopupAddEventFromICal <> '' then begin + if RSPopupImportEventFromICal <> '' then begin NewItem := TVpMenuItem.Create(Self); // Import from iCal NewItem.Kind := mikImportEventFromICal; - NewItem.OnClick := PopupAddFromICalFile; + NewItem.OnClick := PopupImportICalFile; NewItem.Tag := 0; FDefaultPopup.Items.Add(NewItem); end; + if RSPopupExportEventToICal <> '' then begin // Export ical + NewItem := TVpMenuItem.Create(Self); + NewItem.Kind := mikExportEventToICal; + NewItem.OnClick := PopupExportICalFile; + NewItem.Tag := 1; + FDefaultPopup.Items.Add(NewItem); + end; + NewItem := TVpMenuItem.Create(Self); // ---- NewItem.Kind := mikSeparator; FDefaultPopup.Items.Add(NewItem); @@ -1363,7 +1373,29 @@ begin dvSpawnEventEditDialog(True); end; -procedure TVpDayView.PopupAddFromICalFile(Sender: TObject); +procedure TVpDayView.PopupExportICalFile(Sender: TObject); +var + dlg: TSaveDialog; +begin + if (not Assigned(Datastore)) or (not Assigned(Datastore.Resource)) or + (FActiveEvent = nil) + then + exit; + + dlg := TSaveDialog.Create(nil); + try + dlg.Title := RSSaveICalTitle; + dlg.Filter := RSICalFilter; + dlg.FileName := ''; + dlg.Options := dlg.Options - [ofAllowMultiSelect] + [ofOverwritePrompt]; + if dlg.Execute then + ExportICalFile(dlg.FileName, [FActiveEvent]); + finally + dlg.Free; + end; +end; + +procedure TVpDayView.PopupImportICalFile(Sender: TObject); var dlg: TOpenDialog; fn: String; @@ -2353,7 +2385,6 @@ begin Invalidate; dvInPlaceEditor.SetFocus; end; -{=====} procedure TVpDayView.EndEdit(Sender: TObject); begin @@ -2377,6 +2408,15 @@ begin end; end; +procedure TVpDayview.ExportICalFile(const AFileName: String; + const AEvents: TVpEventArr); +begin + if (not Assigned(Datastore)) or (not Assigned(Datastore.Resource)) then + exit; + + Datastore.Resource.Schedule.ExportICalFile(AFileName, AEvents); +end; + { Reads the events listed in the specified ical file and adds them to the day view control. All events imported are collected in the Result array. ADefaultCategory is the category to which the event is assigned if no fitting diff --git a/components/tvplanit/source/vpical.pas b/components/tvplanit/source/vpical.pas index 3c5b44bf4..53ee6846d 100644 --- a/components/tvplanit/source/vpical.pas +++ b/components/tvplanit/source/vpical.pas @@ -19,6 +19,8 @@ type private FCalendar: TVpICalendar; FChecked: Boolean; + protected + procedure SaveToStrings(const AList: TStrings); virtual; public constructor Create(ACalendar: TVpICalendar); virtual; function FindItem(AKey: String): TVpICalItem; @@ -39,13 +41,15 @@ type FTrigger: Double; // "AlarmAdvance" FAudio: Boolean; FAudioSrc: String; + protected + procedure SaveToStrings(const AList: TStrings); override; public procedure Analyze; override; - property Duration: Double read FDuration; - property RepeatCount: Integer read FRepeat; - property Trigger: Double read FTrigger; - property Audio: Boolean read FAudio; - property AudioSrc: String read FAudioSrc; + property Duration: Double read FDuration write FDuration; + property RepeatCount: Integer read FRepeat write FRepeat; + property Trigger: Double read FTrigger write FTrigger; + property Audio: Boolean read FAudio write FAudio; + property AudioSrc: String read FAudioSrc write FAudioSrc; end; { TVpICalEvent } @@ -72,26 +76,29 @@ type function GetCategoryCount: Integer; function GetEndTime(UTC: Boolean): TDateTime; function GetStartTime(UTC: Boolean): TDateTime; + procedure SetEndTime(UTC: Boolean; const AValue: TDateTime); + procedure SetStartTime(UTC: Boolean; const AValue: TDateTime); public constructor Create(ACalendar: TVpICalendar); override; destructor Destroy; override; procedure Analyze; override; function Categories: TStrings; function IsAllDayEvent: Boolean; - procedure UseAlarm; - property Summary: String read FSummary; // is "Description" of tvp - property Description: String read FDescription; // is "Notes" of tvp - property Location: String read FLocation; - property StartTime[UTC: Boolean]: TDateTime read GetStartTime; - property EndTime[UTC: Boolean]: TDateTime read GetEndTime; + procedure SaveToStrings(const AList: TStrings); override; + procedure UseAlarm(AEnable: Boolean); + property Summary: String read FSummary write FSummary; // is "Description" of tvp + property Description: String read FDescription write FDescription; // is "Notes" of tvp + property Location: String read FLocation write FLocation; + property StartTime[UTC: Boolean]: TDateTime read GetStartTime write SetStartTime; + property EndTime[UTC: Boolean]: TDateTime read GetEndTime write SetEndTime; property Category[AIndex: Integer]: String read GetCategory; property CategoryCount: Integer read GetCategoryCount; property Alarm: TVpICalAlarm read FAlarm; - property RecurrenceFrequency: String read FRecurrenceFreq; - property RecurrenceInterval: Integer read FRecurrenceInterval; - property RecurrenceEndDate: TDateTime read FRecurrenceEndDate; - property RecurrenceCount: Integer read FRecurrenceCount; - property RecurrenceByXXX: String read FRecurrenceByXXX; + property RecurrenceFrequency: String read FRecurrenceFreq write FRecurrenceFreq; + property RecurrenceInterval: Integer read FRecurrenceInterval write FRecurrenceInterval; + property RecurrenceEndDate: TDateTime read FRecurrenceEndDate write FRecurrenceEndDate; + property RecurrenceCount: Integer read FRecurrenceCount write FRecurrenceCount; + property RecurrenceByXXX: String read FRecurrenceByXXX write FRecurrenceByXXX; property PickedCategory: Integer read FPickedCategory write FPickedCategory; end; @@ -120,6 +127,7 @@ type destructor Destroy; override; procedure Analyze; override; function Categories: TStrings; + procedure SaveToStrings(const AList: TStrings); override; property Summary: String read FSummary; property Comment: String read FComment; property StartTime[UTC: Boolean]: TDateTime read GetStartTime; @@ -143,6 +151,8 @@ type protected // Reading procedure LoadFromStrings(const AStrings: TStrings); + // Writing + procedure SaveToStrings(const AList: TStrings); // Time conversion function ConvertTime(ADateTime: TDateTime; ATimeZoneID: String; ToUTC: Boolean): TDateTime; function LocalTimeToUTC(ADateTime: TDateTime; ATimeZoneID: String): TDateTime; @@ -150,13 +160,17 @@ type public constructor Create; destructor Destroy; override; + procedure Add(AEntry: TVpICalEntry); procedure Clear; procedure LoadFromFile(const AFileName: String); procedure LoadFromStream(const AStream: TStream); + procedure SaveToFile(const AFileName: String); + procedure SaveToStream(const AStream: TStream); property Count: Integer read GetCount; property EventCount: Integer read GetEventCount; property TodoCount: Integer read GetToDoCount; property Entry[AIndex: Integer]: TVpICalEntry read GetEntry; default; + property Version: String read FVersion write FVersion; end; @@ -165,6 +179,9 @@ implementation uses VpConst, VpBase; +const + TIME_FORMAT = 'yyyymmdd"T"hhnnss'; + TIME_FORMAT_UTC = TIME_FORMAT + '"Z"'; // Examples: 19970702T160000, or T123000, or 20120101 function iCalDateTime(AText: String; out IsUTC: Boolean): TDateTime; @@ -208,6 +225,34 @@ begin IsUTC := AText[Length(AText)] = 'Z'; end; +function IsInteger(d, Epsilon: Double): Boolean; +begin + Result := abs(d - round(d)) < Epsilon; +end; + +// Converts a duration (in day units) to a string according to ical specs +function Duration2iCalStr(AValue: double): String; +var + isNeg: Boolean = false; +begin + if AValue < 0 then + begin + isNeg := true; + AValue := -AValue; + end; + if IsInteger(AValue, 1.0 / SecondsInDay) then + Result :=Format('P%dS', [round(AValue * SecondsInDay)]) + else if IsInteger(AValue, 1.0/MinutesInDay) then + Result := Format('P%dM', [round(AValue * MinutesInDay)]) + else if IsInteger(AValue, 1.0/HoursInday) then + Result := Format('P%dH', [round(AValue * HoursInDay)]) + else if IsInteger(AValue, 1.0) then + Result := Format('P%dD', [round(AValue)]) + else + Result := Format('P%D%s', [trunc(AValue), FormatDateTime('h"H"n"M"s"S"', AValue)]); + if isNeg then Result := '-' + Result; +end; + // Example: PT0H20M0S, or -PT15M, or -P2D function iCalDuration(AText: String): Double; var @@ -292,6 +337,9 @@ begin Result := TVpICalItem(inherited FindItem(AKey, '')); end; +procedure TVpICalEntry.SaveToStrings(const AList: TStrings); +begin +end; {==============================================================================} { TVpICalAlarm } @@ -320,6 +368,22 @@ begin end; end; +procedure TVpICalAlarm.SaveToStrings(const AList: TStrings); +begin + AList.Add('BEGIN:VALARM'); + if Audio then + AList.Add('ACTION:AUDIO'); + if AudioSrc <> '' then + AList.Add('ATTACH;FMTTYPE=AUDIO:' + AudioSrc); + if RepeatCount > 0 then + AList.Add('REPEAT:' + IntToStr(RepeatCount)); + if Trigger <> 0 then + AList.Add('TRIGGER;VALUE=DURATION:' + Duration2iCalStr(Trigger)); + if Duration <> 0 then + AList.Add('DURATION;VALUE=DURATION:' + Duration2iCalStr(Duration)); + AList.Add('END:VALARM'); +end; + {==============================================================================} { TVpICalEvent } @@ -471,10 +535,80 @@ begin Result := false; end; -procedure TVpICalEvent.UseAlarm; +procedure TVpICalEvent.SaveToStrings(const AList: TStrings); +var + lKey: String = ''; + lValue: String = ''; begin - FAlarm.Free; - FAlarm := TVpICalAlarm.Create(FCalendar); + AList.Add('BEGIN:VEVENT'); + + if FSummary <> '' then + AList.Add('SUMMARY:' + FSummary); + if FDescription <> '' then + AList.Add('DESCRIPTION:' + FDescription); + if FLocation <> '' then + AList.Add('LOCATION:' + FLocation); + if FCategories.Count > 0 then + AList.Add('CATEGORIES:' + FCategories.CommaText); + + // todo: check time zones! + if FStartTimeTZ <> '' then + lKey := 'DTSTART;TZID=' + FStartTimeTZ + ':' + else + lKey := 'DTSTART:'; + AList.Add(lKey + FormatDateTime(TIME_FORMAT_UTC, StartTime[true])); + if FEndTimeTZ <> '' then + lKey := 'DTEND;TZID=' + FEndTimeTZ + ':' + else + lKey := 'DTEND:'; + AList.Add(lKey + FormatDateTime(TIME_FORMAT_UTC, EndTime[true])); + + if RecurrenceFrequency <> '' then + begin + lKey := 'RRULE:'; + lValue := 'FREQ=' + RecurrenceFrequency; + if RecurrenceInterval > 0 then + lValue := lValue + ';INTERVAL=' + IntToStr(RecurrenceInterval); + if RecurrenceEndDate <> 0 then + lValue := lValue + ';UNTIL=' + FormatDateTime(TIME_FORMAT, RecurrenceEndDate); + if RecurrenceCount > 0 then + lValue := lValue + ';COUNT=' + IntToStr(RecurrenceCount); + if RecurrenceByXXX <> '' then + lValue := lValue + ';' + RecurrenceByXXX; + AList.Add(lKey + lValue); + end; + + if Alarm <> nil then + Alarm.SaveToStrings(AList); +end; + +procedure TVpICalEvent.SetEndTime(UTC: Boolean; const AValue: TDateTime); +begin + if AValue = NO_DATE then + FEndTime := NO_DATE + else + if UTC then + FEndTime := AValue + else + FEndTime := FCalendar.LocalTimeToUTC(AValue, FEndTimeTZ); +end; + +procedure TVpICalEvent.SetStartTime(UTC: Boolean; const AValue: TDateTime); +begin + if AValue = NO_DATE then + FStartTime := NO_DATE + else + if UTC then + FStartTime := AValue + else + FStartTime := FCalendar.LocalTimeToUTC(AValue, FStartTimeTZ); +end; + +procedure TVpICalEvent.UseAlarm(AEnable: Boolean); +begin + FreeAndNil(FAlarm); + if AEnable then + FAlarm := TVpICalAlarm.Create(FCalendar); end; @@ -589,6 +723,10 @@ begin Result := FCalendar.LocalTimeToUTC(FStartTime, FStartTimeTZ); end; +procedure TVpICalToDo.SaveToStrings(const AList: TStrings); +begin + // to do... +end; {==============================================================================} @@ -598,15 +736,25 @@ end; constructor TVpICalendar.Create; begin inherited; + FVersion := '2.0'; SetLength(FEntries, 0); end; destructor TVpICalendar.Destroy; begin - SetLength(FEntries, 0); + Clear; inherited; end; +procedure TVpICalendar.Add(AEntry: TVpICalEntry); +var + n: Integer; +begin + n := Length(FEntries); + SetLength(FEntries, n+1); + FEntries[n] := AEntry; +end; + procedure TVpICalendar.Clear; var j: Integer; @@ -724,7 +872,7 @@ begin 'VALARM': if currEntry is TVpICalEvent then begin oldEntry := currEntry; - TVpICalEvent(currEntry).UseAlarm; + TVpICalEvent(currEntry).UseAlarm(true); currEntry := TVpICalEvent(currEntry).Alarm; end; else @@ -756,6 +904,51 @@ begin SetLength(FEntries, n); end; +procedure TVpICalendar.SaveToFile(const AFileName: String); +var + L: TStrings; +begin + L := TStringList.Create; + try + SaveToStrings(L); + L.SaveToFile(AFileName); + finally + L.Free; + end; +end; + +procedure TVpICalendar.SaveToStream(const AStream: TStream); +var + L: TStrings; +begin + L := TStringList.Create; + try + SaveToStrings(L); + L.SaveToStream(AStream); + finally + L.Free; + end; +end; + +procedure TVpICalendar.SaveToStrings(const AList: TStrings); +var + i: Integer; +begin + AList.Clear; + AList.Add('BEGIN:VCALENDAR'); + if FVersion <> '' then + AList.Add('VERSION:' + FVersion); + + AList.Add('BEGIN:VTIMEZONE'); + // to do: complete here TIMEZONE section with DAYLIGHT and STANDARD sections + AList.Add('END:VTIMEZONE'); + + for i := 0 to Count-1 do + Entry[i].SaveToStrings(AList); + + AList.Add('END:VCALENDAR'); +end; + function TVpICalendar.ConvertTime(ADateTime: TDateTime; ATimeZoneID: String; ToUTC: Boolean): TDateTime; var diff --git a/components/tvplanit/source/vpvcard.pas b/components/tvplanit/source/vpvcard.pas index 4ef71e838..f0ca17636 100644 --- a/components/tvplanit/source/vpvcard.pas +++ b/components/tvplanit/source/vpvcard.pas @@ -595,7 +595,7 @@ procedure TVpVCards.SaveToStrings(AList: TStrings); var i: Integer; begin - AList.clear; + AList.Clear; for i := 0 to Count-1 do FCards[i].SaveToStrings(AList); end; diff --git a/components/tvplanit/source/vpweekview.pas b/components/tvplanit/source/vpweekview.pas index 03026684f..8dcf7711b 100644 --- a/components/tvplanit/source/vpweekview.pas +++ b/components/tvplanit/source/vpweekview.pas @@ -1227,7 +1227,7 @@ begin NewItem.Kind := mikSeparator; FDefaultPopup.Items.Add(NewItem); - if RSPopupAddEventFromICal <> '' then begin + if RSPopupImportEventFromICal <> '' then begin NewItem := TVpMenuItem.Create(Self); NewItem.Kind := mikImportEventFromICal; // Import from iCal NewItem.OnClick := PopupAddFromICalFile;