Compare commits

...

16 Commits

Author SHA1 Message Date
duffyduck 3e22a40e17 Release v0.0.0.15 2026-04-03 18:18:08 +02:00
duffyduck 36aca2c04d Add custom app icon: contact silhouette with sync arrows
Generated programmatically, no external ICO file needed.
Blue rounded square with white person silhouette and green/yellow
sync arrows. Used for main window title bar and system tray.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:17:26 +02:00
duffyduck 4f56f28ccb Fix multiple tray icons: create icon once, only update menu
SetupTrayIcon was called on every RefreshProfileList, creating
a new NotifyIcon each time. Split into SetupTrayIcon (once) and
UpdateTrayMenu (on refresh).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:15:44 +02:00
duffyduck 2b9ad5bf3c Release v0.0.0.14 2026-04-03 18:13:48 +02:00
duffyduck 8a316600b5 Add debug logging for Starface create/update failures
Log the full request body and response when POST /contacts fails
so we can see why new contacts are not being created.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:12:26 +02:00
duffyduck 0f61b4cd31 Release v0.0.0.13 2026-04-03 18:04:40 +02:00
duffyduck b9f5a26b27 Start progress bar only when sync begins, not on form open
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:03:07 +02:00
duffyduck 4359d3ad1a Add Sync Reset button to clear mappings for a profile
Deletes all contact mappings and resets LastSync timestamp.
Next sync will re-match all contacts fresh without creating
duplicates (thanks to the duplicate detection in sync engine).
No contacts are deleted on either side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:02:25 +02:00
duffyduck 39b30f9a80 Rewrite sync engine for robustness and duplicate prevention
Three-phase sync approach:
1. Process existing mappings (detect changes on both sides,
   handle conflicts with configurable winner)
2. Sync unmapped Outlook contacts to Starface with duplicate
   check (match by email, name+company, name, phone)
3. Sync unmapped Starface contacts to Outlook with duplicate check

Key improvements:
- Duplicate detection before creating: checks email, name+company,
  name, and phone number with normalization
- Matched duplicates get linked instead of re-created
- Conflict resolution when both sides changed
- Dead mappings (both sides deleted) get cleaned up
- Each contact logged individually with direction indicator
- Address book switch works: old mappings get cleaned, contacts
  re-matched against new book

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:00:41 +02:00
duffyduck f0bcfdfd30 Release v0.0.0.12 2026-04-03 12:55:53 +02:00
duffyduck 650d0def51 Fix Starface contact field mapping based on actual API response
Read mapping: Match by 'name' field instead of 'displayKey' because
many fields use USER_DEFINED as displayKey (street, city, state,
comment). Actual name fields: firstname, familyname, company, phone,
mobile, homephone, fax, e-mail, url, comment, street, city, postcode,
state, country.

Write mapping: Use correct 4-block structure (contact, address,
telephone, email) with proper resourceKey values matching the
Starface internal format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:54:47 +02:00
duffyduck f57518a1d5 Release v0.0.0.11 2026-04-03 12:51:14 +02:00
duffyduck 1be0a94b51 Fetch full contact details from Starface instead of summary
The contacts list endpoint only returns summaryValues/phoneNumbers.
Now fetch each contact individually via GET /contacts/{id} to get
all fields (blocks/attributes). Also log the detail JSON structure
so we can verify the field mapping is correct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:50:16 +02:00
duffyduck 3c38a1a6cc Release v0.0.0.10 2026-04-03 12:17:30 +02:00
duffyduck ba0b79de64 Release v0.0.0.9 2026-04-03 12:14:42 +02:00
duffyduck 60bd3163a9 Add debug logging for raw Starface API contact data
Logs the first contact from Starface API response into the sync
log so we can see the actual JSON structure and fix field mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:13:43 +02:00
8 changed files with 552 additions and 180 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php) ; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync" #define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.8" #define MyAppVersion "0.0.0.15"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation" #define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de" #define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe" #define MyAppExeName "StarfaceOutlookSync.exe"
@@ -155,11 +155,14 @@ namespace StarfaceOutlookSync.Services
return books; return books;
} }
public event Action<string> OnDebug;
public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book) public async Task<List<UnifiedContact>> GetContactsAsync(StarfaceAddressBook book)
{ {
var contacts = new List<UnifiedContact>(); var contacts = new List<UnifiedContact>();
int page = 0; int page = 0;
const int pageSize = 200; const int pageSize = 200;
bool firstPage = true;
while (true) while (true)
{ {
@@ -206,8 +209,34 @@ namespace StarfaceOutlookSync.Services
if (array.Count == 0) break; if (array.Count == 0) break;
OnDebug?.Invoke($"Seite {page}: {array.Count} Kontakte in Liste");
// Die Listen-API gibt nur Summary zurueck.
// Jeden Kontakt einzeln abrufen fuer alle Felder.
foreach (var item in array) foreach (var item in array)
contacts.Add(MapFromStarface(item)); {
var id = item["id"]?.ToString();
if (string.IsNullOrEmpty(id)) continue;
try
{
var detailResp = await _http.GetAsync($"{_baseUrl}/contacts/{id}");
if (detailResp.IsSuccessStatusCode)
{
var detailBody = await detailResp.Content.ReadAsStringAsync();
var detailObj = JObject.Parse(detailBody);
if (firstPage)
{
OnDebug?.Invoke($"Starface Kontakt-Detail (1. Kontakt):\n{detailObj.ToString(Formatting.Indented)}");
firstPage = false;
}
contacts.Add(MapFromStarface(detailObj));
}
}
catch { }
}
if (array.Count < pageSize) break; if (array.Count < pageSize) break;
page++; page++;
@@ -223,11 +252,20 @@ namespace StarfaceOutlookSync.Services
if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId)) if (book.Type == "user" && !string.IsNullOrEmpty(book.UserId))
query = $"?userId={book.UserId}"; query = $"?userId={book.UserId}";
var content = new StringContent(sfContact.ToString(), Encoding.UTF8, "application/json"); var body = sfContact.ToString();
var resp = await _http.PostAsync($"{_baseUrl}/contacts{query}", content); OnDebug?.Invoke($"POST /contacts{query} Body:\n{body}");
if (!resp.IsSuccessStatusCode) return null;
var created = JObject.Parse(await resp.Content.ReadAsStringAsync()); var content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _http.PostAsync($"{_baseUrl}/contacts{query}", content);
var respBody = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
{
OnDebug?.Invoke($"POST /contacts fehlgeschlagen: {(int)resp.StatusCode} {resp.StatusCode}\n{respBody}");
return null;
}
var created = JObject.Parse(respBody);
return MapFromStarface(created); return MapFromStarface(created);
} }
@@ -255,7 +293,10 @@ namespace StarfaceOutlookSync.Services
var contact = new UnifiedContact(); var contact = new UnifiedContact();
contact.StarfaceId = item["id"]?.ToString() ?? ""; contact.StarfaceId = item["id"]?.ToString() ?? "";
var attrs = new Dictionary<string, string>(); // Attribute per "name"-Feld mappen (zuverlaessiger als displayKey,
// weil viele Felder USER_DEFINED als displayKey haben)
var byName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var byDisplayKey = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var blocks = item["blocks"] as JArray; var blocks = item["blocks"] as JArray;
if (blocks != null) if (blocks != null)
{ {
@@ -265,79 +306,123 @@ namespace StarfaceOutlookSync.Services
if (blockAttrs == null) continue; if (blockAttrs == null) continue;
foreach (var attr in blockAttrs) foreach (var attr in blockAttrs)
{ {
var key = attr["displayKey"]?.ToString() ?? ""; var name = attr["name"]?.ToString() ?? "";
var displayKey = attr["displayKey"]?.ToString() ?? "";
var val = attr["value"]?.ToString() ?? ""; var val = attr["value"]?.ToString() ?? "";
if (!string.IsNullOrEmpty(val)) if (!string.IsNullOrEmpty(val))
attrs[key] = val; {
if (!string.IsNullOrEmpty(name))
byName[name] = val;
// displayKey nur als Fallback (viele sind USER_DEFINED)
if (!string.IsNullOrEmpty(displayKey) && displayKey != "USER_DEFINED")
byDisplayKey[displayKey] = val;
}
} }
} }
} }
contact.FirstName = attrs.GetValueOrDefault("NAME", ""); // Primaer nach name-Feld mappen, Fallback auf displayKey
contact.LastName = attrs.GetValueOrDefault("SURNAME", ""); string Get(string name, string displayKey = null)
contact.Company = attrs.GetValueOrDefault("COMPANY", ""); {
contact.JobTitle = attrs.GetValueOrDefault("JOB_TITLE", ""); if (byName.TryGetValue(name, out var v)) return v;
contact.Email = attrs.GetValueOrDefault("EMAIL", ""); if (displayKey != null && byDisplayKey.TryGetValue(displayKey, out v)) return v;
contact.PhoneWork = attrs.GetValueOrDefault("OFFICE_PHONE_NUMBER", ""); return "";
contact.PhoneMobile = attrs.GetValueOrDefault("MOBILE_PHONE_NUMBER", ""); }
contact.PhoneHome = attrs.GetValueOrDefault("PRIVATE_PHONE_NUMBER", "");
contact.Fax = attrs.GetValueOrDefault("FAX_NUMBER", ""); contact.FirstName = Get("firstname", "NAME");
contact.Street = attrs.GetValueOrDefault("STREET", ""); contact.LastName = Get("familyname", "SURNAME");
contact.City = attrs.GetValueOrDefault("CITY", ""); contact.Company = Get("company", "COMPANY");
contact.PostalCode = attrs.GetValueOrDefault("POSTAL_CODE", ""); contact.JobTitle = Get("jobtitle", "JOB_TITLE");
contact.State = attrs.GetValueOrDefault("STATE", ""); contact.Email = Get("e-mail", "EMAIL");
contact.Country = attrs.GetValueOrDefault("COUNTRY", ""); contact.PhoneWork = Get("phone", "PHONE_NUMBER");
contact.Website = attrs.GetValueOrDefault("URL", ""); contact.PhoneMobile = Get("mobile", "MOBILE_PHONE_NUMBER");
contact.Notes = attrs.GetValueOrDefault("NOTE", ""); contact.PhoneHome = Get("homephone", "PRIVATE_PHONE_NUMBER");
contact.Salutation = attrs.GetValueOrDefault("SALUTATION", ""); contact.Fax = Get("fax", "FAX_NUMBER");
contact.Title = attrs.GetValueOrDefault("TITLE", ""); contact.Street = Get("street", "STREET");
contact.Birthday = attrs.GetValueOrDefault("BIRTHDAY", ""); contact.City = Get("city", "CITY");
contact.PostalCode = Get("postcode", "POSTAL_CODE");
contact.State = Get("state", "STATE");
contact.Country = Get("country", "COUNTRY");
contact.Website = Get("url", "URL");
contact.Notes = Get("comment", "NOTE");
contact.Salutation = Get("salutation", "SALUTATION");
contact.Title = Get("title", "TITLE");
contact.Birthday = Get("birthday", "BIRTHDAY");
return contact; return contact;
} }
private JObject MapToStarface(UnifiedContact contact) private JObject MapToStarface(UnifiedContact contact)
{ {
var attrs = new JArray(); JArray MakeAttrs(params (string displayKey, string name, string value)[] fields)
void AddAttr(string displayKey, string name, string value)
{ {
if (!string.IsNullOrEmpty(value)) var arr = new JArray();
attrs.Add(new JObject { ["displayKey"] = displayKey, ["name"] = name, ["value"] = value }); foreach (var (dk, n, v) in fields)
{
if (!string.IsNullOrEmpty(v))
arr.Add(new JObject { ["displayKey"] = dk, ["name"] = n, ["value"] = v });
}
return arr;
} }
AddAttr("NAME", "firstName", contact.FirstName); // Block-Struktur wie von der Starface erwartet
AddAttr("SURNAME", "lastName", contact.LastName); var contactBlock = MakeAttrs(
AddAttr("COMPANY", "company", contact.Company); ("NAME", "firstname", contact.FirstName),
AddAttr("JOB_TITLE", "jobTitle", contact.JobTitle); ("SURNAME", "familyname", contact.LastName),
AddAttr("EMAIL", "email", contact.Email); ("COMPANY", "company", contact.Company)
AddAttr("OFFICE_PHONE_NUMBER", "businessPhone", contact.PhoneWork); );
AddAttr("MOBILE_PHONE_NUMBER", "mobilePhone", contact.PhoneMobile);
AddAttr("PRIVATE_PHONE_NUMBER", "homePhone", contact.PhoneHome); var addressBlock = MakeAttrs(
AddAttr("FAX_NUMBER", "fax", contact.Fax); ("USER_DEFINED", "street", contact.Street),
AddAttr("STREET", "street", contact.Street); ("POSTAL_CODE", "postcode", contact.PostalCode),
AddAttr("CITY", "city", contact.City); ("USER_DEFINED", "city", contact.City),
AddAttr("POSTAL_CODE", "postalCode", contact.PostalCode); ("USER_DEFINED", "state", contact.State),
AddAttr("STATE", "state", contact.State); ("USER_DEFINED", "country", contact.Country)
AddAttr("COUNTRY", "country", contact.Country); );
AddAttr("URL", "website", contact.Website);
AddAttr("NOTE", "notes", contact.Notes); var phoneBlock = MakeAttrs(
AddAttr("SALUTATION", "salutation", contact.Salutation); ("PHONE_NUMBER", "phone", contact.PhoneWork),
AddAttr("TITLE", "title", contact.Title); ("PRIVATE_PHONE_NUMBER", "homephone", contact.PhoneHome),
AddAttr("BIRTHDAY", "birthday", contact.Birthday); ("MOBILE_PHONE_NUMBER", "mobile", contact.PhoneMobile),
("FAX_NUMBER", "fax", contact.Fax)
);
var emailBlock = MakeAttrs(
("EMAIL", "e-mail", contact.Email),
("URL", "url", contact.Website),
("USER_DEFINED", "comment", contact.Notes)
);
var blocks = new JArray();
blocks.Add(new JObject
{
["name"] = "contact",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_contact",
["attributes"] = contactBlock
});
blocks.Add(new JObject
{
["name"] = "address",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_address",
["attributes"] = addressBlock
});
blocks.Add(new JObject
{
["name"] = "telephone",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_telephone",
["attributes"] = phoneBlock
});
blocks.Add(new JObject
{
["name"] = "email",
["resourceKey"] = "de.vertico.starface.addressbook.block.label_email",
["attributes"] = emailBlock
});
return new JObject return new JObject
{ {
["id"] = contact.StarfaceId ?? "", ["id"] = contact.StarfaceId ?? "",
["blocks"] = new JArray ["blocks"] = blocks
{
new JObject
{
["name"] = "contact",
["resourceKey"] = "contact",
["attributes"] = attrs
}
}
}; };
} }
+236 -98
View File
@@ -15,9 +15,15 @@ namespace StarfaceOutlookSync.Services
private void Log(string message) => OnProgress?.Invoke(message); private void Log(string message) => OnProgress?.Invoke(message);
/// <summary>
/// Findet einen passenden Kontakt in der Kandidatenliste.
/// Matching-Reihenfolge: E-Mail, dann Vorname+Nachname+Firma, dann Vorname+Nachname.
/// </summary>
private static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates) private static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
{ {
// Erst E-Mail-Match if (candidates == null || candidates.Count == 0) return null;
// 1. Exakte E-Mail
if (!string.IsNullOrEmpty(contact.Email)) if (!string.IsNullOrEmpty(contact.Email))
{ {
var byEmail = candidates.FirstOrDefault(c => var byEmail = candidates.FirstOrDefault(c =>
@@ -26,7 +32,18 @@ namespace StarfaceOutlookSync.Services
if (byEmail != null) return byEmail; if (byEmail != null) return byEmail;
} }
// Dann Name-Match // 2. Vorname + Nachname + Firma (staerkstes Match ohne E-Mail)
if ((!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName))
&& !string.IsNullOrEmpty(contact.Company))
{
var byNameCompany = candidates.FirstOrDefault(c =>
c.FirstName.Equals(contact.FirstName, StringComparison.OrdinalIgnoreCase) &&
c.LastName.Equals(contact.LastName, StringComparison.OrdinalIgnoreCase) &&
c.Company.Equals(contact.Company, StringComparison.OrdinalIgnoreCase));
if (byNameCompany != null) return byNameCompany;
}
// 3. Vorname + Nachname (ohne Firma)
if (!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName)) if (!string.IsNullOrEmpty(contact.FirstName) || !string.IsNullOrEmpty(contact.LastName))
{ {
var byName = candidates.FirstOrDefault(c => var byName = candidates.FirstOrDefault(c =>
@@ -36,9 +53,32 @@ namespace StarfaceOutlookSync.Services
if (byName != null) return byName; if (byName != null) return byName;
} }
// 4. Telefonnummer (Buero oder Mobil)
if (!string.IsNullOrEmpty(contact.PhoneWork))
{
var byPhone = candidates.FirstOrDefault(c =>
!string.IsNullOrEmpty(c.PhoneWork) &&
NormalizePhone(c.PhoneWork) == NormalizePhone(contact.PhoneWork));
if (byPhone != null) return byPhone;
}
if (!string.IsNullOrEmpty(contact.PhoneMobile))
{
var byMobile = candidates.FirstOrDefault(c =>
!string.IsNullOrEmpty(c.PhoneMobile) &&
NormalizePhone(c.PhoneMobile) == NormalizePhone(contact.PhoneMobile));
if (byMobile != null) return byMobile;
}
return null; return null;
} }
private static string NormalizePhone(string phone)
{
if (string.IsNullOrEmpty(phone)) return "";
// Nur Ziffern und + behalten
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
}
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile) public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
{ {
var result = new SyncResult var result = new SyncResult
@@ -49,10 +89,10 @@ namespace StarfaceOutlookSync.Services
try try
{ {
// Starface verbinden
Log("Verbinde mit Starface..."); Log("Verbinde mit Starface...");
using (var starface = new StarfaceApiClient(profile.StarfaceConnection)) using (var starface = new StarfaceApiClient(profile.StarfaceConnection))
{ {
starface.OnDebug += (msg) => Log(msg);
var loginOk = await starface.LoginAsync(); var loginOk = await starface.LoginAsync();
if (!loginOk) if (!loginOk)
{ {
@@ -61,10 +101,6 @@ namespace StarfaceOutlookSync.Services
return result; return result;
} }
var mappings = _profileManager.GetMappings(profile.Id);
var mappingByOutlook = mappings.ToDictionary(m => m.OutlookEntryId, m => m);
var mappingByStarface = mappings.ToDictionary(m => m.StarfaceId, m => m);
// Kontakte laden // Kontakte laden
Log("Lade Outlook-Kontakte..."); Log("Lade Outlook-Kontakte...");
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath); var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
@@ -74,144 +110,246 @@ namespace StarfaceOutlookSync.Services
var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook); var starfaceContacts = await starface.GetContactsAsync(profile.StarfaceAddressBook);
Log($"{starfaceContacts.Count} Starface-Kontakte geladen"); Log($"{starfaceContacts.Count} Starface-Kontakte geladen");
// Outlook -> Starface // Bestehende Mappings laden
if (profile.SyncDirection == SyncDirection.Both || var mappings = _profileManager.GetMappings(profile.Id);
profile.SyncDirection == SyncDirection.OutlookToStarface)
{
Log("Synchronisiere Outlook -> Starface...");
foreach (var oc in outlookContacts)
{
try
{
SyncMapping existing = null;
if (!string.IsNullOrEmpty(oc.OutlookEntryId))
mappingByOutlook.TryGetValue(oc.OutlookEntryId, out existing);
if (existing != null) // Sets fuer schnellen Lookup
var mappingByOutlook = new Dictionary<string, SyncMapping>();
var mappingByStarface = new Dictionary<string, SyncMapping>();
foreach (var m in mappings)
{
if (!string.IsNullOrEmpty(m.OutlookEntryId))
mappingByOutlook[m.OutlookEntryId] = m;
if (!string.IsNullOrEmpty(m.StarfaceId))
mappingByStarface[m.StarfaceId] = m;
}
// Tracking: welche Kontakte wurden bereits verarbeitet
var processedStarfaceIds = new HashSet<string>();
var processedOutlookIds = new HashSet<string>();
var newMappings = new List<SyncMapping>();
// ============================================
// Phase 1: Bestehende Mappings abgleichen
// ============================================
Log("Gleiche bestehende Zuordnungen ab...");
foreach (var mapping in mappings.ToList())
{
var oc = outlookContacts.FirstOrDefault(c => c.OutlookEntryId == mapping.OutlookEntryId);
var sc = starfaceContacts.FirstOrDefault(c => c.StarfaceId == mapping.StarfaceId);
if (oc != null) processedOutlookIds.Add(oc.OutlookEntryId);
if (sc != null) processedStarfaceIds.Add(sc.StarfaceId);
if (oc == null && sc == null)
{
// Beide Seiten geloescht -> Mapping entfernen
continue;
}
if (oc != null && sc != null)
{
// Beide vorhanden -> auf Aenderungen pruefen
var olHash = oc.GetHash();
var sfHash = sc.GetHash();
bool olChanged = olHash != mapping.LastSyncHash;
bool sfChanged = sfHash != mapping.LastSyncHash;
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
{
// Outlook hat sich geaendert -> Starface updaten
if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook))
{ {
var hash = oc.GetHash(); mapping.LastSyncHash = olHash;
if (hash != existing.LastSyncHash) result.Updated++;
Log($" Aktualisiert (OL->SF): {oc.DisplayName}");
}
}
else if (sfChanged && !olChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook))
{
// Starface hat sich geaendert -> Outlook updaten
if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc))
{
mapping.LastSyncHash = sfHash;
result.Updated++;
Log($" Aktualisiert (SF->OL): {sc.DisplayName}");
}
}
else if (olChanged && sfChanged)
{
// Beide geaendert -> Konflikt, neuere gewinnt (Outlook bevorzugt)
if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
{
if (await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook))
{ {
if (await starface.UpdateContactAsync(existing.StarfaceId, oc, profile.StarfaceAddressBook)) mapping.LastSyncHash = olHash;
{ result.Updated++;
existing.LastSyncHash = hash; Log($" Konflikt (OL gewinnt): {oc.DisplayName}");
result.Updated++;
}
} }
} }
else else
{ {
var match = FindMatch(oc, starfaceContacts); if (_outlookService.UpdateContact(mapping.OutlookEntryId, sc))
if (match != null && !string.IsNullOrEmpty(match.StarfaceId))
{ {
if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook)) mapping.LastSyncHash = sfHash;
result.Updated++;
Log($" Konflikt (SF gewinnt): {sc.DisplayName}");
}
}
}
// Beide unveraendert -> nichts tun
}
newMappings.Add(mapping);
}
// ============================================
// Phase 2: Neue Outlook-Kontakte (ohne Mapping)
// ============================================
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface)
{
var unmappedOutlook = outlookContacts
.Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId))
.ToList();
if (unmappedOutlook.Count > 0)
Log($"Neue Outlook-Kontakte: {unmappedOutlook.Count}");
// Starface-Kontakte die noch kein Mapping haben (fuer Duplikat-Check)
var unmappedStarface = starfaceContacts
.Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId))
.ToList();
foreach (var oc in unmappedOutlook)
{
try
{
// Duplikat-Check: existiert der Kontakt schon in der Starface?
var match = FindMatch(oc, unmappedStarface);
if (match != null)
{
// Existiert schon -> verknuepfen und updaten
if (await starface.UpdateContactAsync(match.StarfaceId, oc, profile.StarfaceAddressBook))
{
newMappings.Add(new SyncMapping
{ {
_profileManager.AddOrUpdateMapping(new SyncMapping ProfileId = profile.Id,
{ OutlookEntryId = oc.OutlookEntryId,
ProfileId = profile.Id, StarfaceId = match.StarfaceId,
OutlookEntryId = oc.OutlookEntryId, LastSyncHash = oc.GetHash()
StarfaceId = match.StarfaceId, });
LastSyncHash = oc.GetHash() processedStarfaceIds.Add(match.StarfaceId);
}); unmappedStarface.Remove(match);
result.Updated++; result.Updated++;
} Log($" Verknuepft (OL->SF): {oc.DisplayName}");
}
}
else
{
// Neu -> in Starface erstellen
Log($" Erstelle in Starface: {oc.DisplayName}");
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook);
if (created != null && !string.IsNullOrEmpty(created.StarfaceId))
{
newMappings.Add(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId,
StarfaceId = created.StarfaceId,
LastSyncHash = oc.GetHash()
});
result.Created++;
Log($" Erstellt (OL->SF): {oc.DisplayName}");
} }
else else
{ {
var created = await starface.CreateContactAsync(oc, profile.StarfaceAddressBook); Log($" FEHLER: Kontakt konnte nicht erstellt werden: {oc.DisplayName}");
if (created != null && !string.IsNullOrEmpty(created.StarfaceId)) result.Errors++;
{
_profileManager.AddOrUpdateMapping(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = oc.OutlookEntryId,
StarfaceId = created.StarfaceId,
LastSyncHash = oc.GetHash()
});
result.Created++;
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Errors++; result.Errors++;
result.ErrorMessages.Add($"{oc.DisplayName}: {ex.Message}"); result.ErrorMessages.Add($"OL->SF {oc.DisplayName}: {ex.Message}");
} }
} }
} }
// Starface -> Outlook // ============================================
if (profile.SyncDirection == SyncDirection.Both || // Phase 3: Neue Starface-Kontakte (ohne Mapping)
profile.SyncDirection == SyncDirection.StarfaceToOutlook) // ============================================
if (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.StarfaceToOutlook)
{ {
Log("Synchronisiere Starface -> Outlook..."); var unmappedStarface = starfaceContacts
foreach (var sc in starfaceContacts) .Where(c => !string.IsNullOrEmpty(c.StarfaceId) && !processedStarfaceIds.Contains(c.StarfaceId))
.ToList();
if (unmappedStarface.Count > 0)
Log($"Neue Starface-Kontakte: {unmappedStarface.Count}");
// Outlook-Kontakte die noch kein Mapping haben (fuer Duplikat-Check)
var unmappedOutlook = outlookContacts
.Where(c => !string.IsNullOrEmpty(c.OutlookEntryId) && !processedOutlookIds.Contains(c.OutlookEntryId))
.ToList();
foreach (var sc in unmappedStarface)
{ {
try try
{ {
SyncMapping existing = null; // Duplikat-Check: existiert der Kontakt schon in Outlook?
if (!string.IsNullOrEmpty(sc.StarfaceId)) var match = FindMatch(sc, unmappedOutlook);
mappingByStarface.TryGetValue(sc.StarfaceId, out existing); if (match != null)
if (existing != null)
{ {
var hash = sc.GetHash(); // Existiert schon -> verknuepfen und updaten
if (hash != existing.LastSyncHash) if (_outlookService.UpdateContact(match.OutlookEntryId, sc))
{ {
if (_outlookService.UpdateContact(existing.OutlookEntryId, sc)) newMappings.Add(new SyncMapping
{ {
existing.LastSyncHash = hash; ProfileId = profile.Id,
result.Updated++; OutlookEntryId = match.OutlookEntryId,
} StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash()
});
processedOutlookIds.Add(match.OutlookEntryId);
unmappedOutlook.Remove(match);
result.Updated++;
Log($" Verknuepft (SF->OL): {sc.DisplayName}");
} }
} }
else else
{ {
var match = FindMatch(sc, outlookContacts); // Neu -> in Outlook erstellen
if (match != null && !string.IsNullOrEmpty(match.OutlookEntryId)) var created = _outlookService.CreateContact(sc, profile.OutlookFolderPath);
if (created != null && !string.IsNullOrEmpty(created.OutlookEntryId))
{ {
if (_outlookService.UpdateContact(match.OutlookEntryId, sc)) newMappings.Add(new SyncMapping
{ {
_profileManager.AddOrUpdateMapping(new SyncMapping ProfileId = profile.Id,
{ OutlookEntryId = created.OutlookEntryId,
ProfileId = profile.Id, StarfaceId = sc.StarfaceId,
OutlookEntryId = match.OutlookEntryId, LastSyncHash = sc.GetHash()
StarfaceId = sc.StarfaceId, });
LastSyncHash = sc.GetHash() result.Created++;
}); Log($" Erstellt (SF->OL): {sc.DisplayName}");
result.Updated++;
}
}
else
{
var created = _outlookService.CreateContact(sc, profile.OutlookFolderPath);
if (created != null && !string.IsNullOrEmpty(created.OutlookEntryId))
{
_profileManager.AddOrUpdateMapping(new SyncMapping
{
ProfileId = profile.Id,
OutlookEntryId = created.OutlookEntryId,
StarfaceId = sc.StarfaceId,
LastSyncHash = sc.GetHash()
});
result.Created++;
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
result.Errors++; result.Errors++;
result.ErrorMessages.Add($"{sc.DisplayName}: {ex.Message}"); result.ErrorMessages.Add($"SF->OL {sc.DisplayName}: {ex.Message}");
} }
} }
} }
// Mappings speichern
_profileManager.SaveMappings(profile.Id, newMappings);
_profileManager.UpdateLastSync(profile.Id); _profileManager.UpdateLastSync(profile.Id);
_profileManager.SaveMappings(profile.Id, mappings);
await starface.LogoutAsync(); await starface.LogoutAsync();
Log("Synchronisation abgeschlossen!");
Log($"Fertig: {result.Created} erstellt, {result.Updated} aktualisiert, {result.Errors} Fehler");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle> <AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company> <Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product> <Product>Starface Outlook Sync</Product>
<Version>0.0.0.8</Version> <Version>0.0.0.15</Version>
<AssemblyVersion>0.0.0.8</AssemblyVersion> <AssemblyVersion>0.0.0.15</AssemblyVersion>
<FileVersion>0.0.0.8</FileVersion> <FileVersion>0.0.0.15</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description> <Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright> <Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label var lblVersion = new Label
{ {
Text = "Version 0.0.0.8", Text = "Version 0.0.0.15",
Left = 0, Top = 56, Width = 340, Height = 20, Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter, TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray ForeColor = Color.Gray
+108
View File
@@ -0,0 +1,108 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
namespace StarfaceOutlookSync.UI
{
/// <summary>
/// Generiert das App-Icon programmatisch (Kontakt-Symbol mit Sync-Pfeilen).
/// Kein externes ICO-File noetig.
/// </summary>
public static class AppIcon
{
private static Icon _icon;
public static Icon GetIcon()
{
if (_icon != null) return _icon;
_icon = CreateIcon(32);
return _icon;
}
public static Icon GetSmallIcon()
{
return CreateIcon(16);
}
private static Icon CreateIcon(int size)
{
using (var bmp = new Bitmap(size, size))
using (var g = Graphics.FromImage(bmp))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Transparent);
if (size >= 32)
DrawIcon32(g);
else
DrawIcon16(g);
return Icon.FromHandle(bmp.GetHicon());
}
}
private static void DrawIcon32(Graphics g)
{
// Hintergrund: abgerundetes Quadrat
using (var bgBrush = new SolidBrush(Color.FromArgb(0, 120, 212)))
{
FillRoundedRect(g, bgBrush, 0, 0, 31, 31, 6);
}
// Person (Kopf)
using (var whiteBrush = new SolidBrush(Color.White))
{
g.FillEllipse(whiteBrush, 11, 4, 10, 10);
// Person (Koerper)
g.FillPie(whiteBrush, 6, 14, 20, 18, 180, 180);
}
// Sync-Pfeile unten rechts
using (var arrowPen = new Pen(Color.FromArgb(120, 255, 120), 2f))
{
arrowPen.StartCap = LineCap.Round;
arrowPen.EndCap = LineCap.ArrowAnchor;
// Pfeil rechts
g.DrawArc(arrowPen, 20, 22, 10, 8, 200, 140);
arrowPen.Color = Color.FromArgb(255, 200, 80);
// Pfeil links
g.DrawArc(arrowPen, 20, 22, 10, 8, 20, 140);
}
}
private static void DrawIcon16(Graphics g)
{
// Hintergrund
using (var bgBrush = new SolidBrush(Color.FromArgb(0, 120, 212)))
{
FillRoundedRect(g, bgBrush, 0, 0, 15, 15, 3);
}
// Person (vereinfacht)
using (var whiteBrush = new SolidBrush(Color.White))
{
g.FillEllipse(whiteBrush, 4, 1, 7, 7);
g.FillPie(whiteBrush, 2, 8, 12, 10, 180, 180);
}
// Kleiner Sync-Indikator
using (var dotBrush = new SolidBrush(Color.FromArgb(120, 255, 120)))
{
g.FillEllipse(dotBrush, 11, 11, 4, 4);
}
}
private static void FillRoundedRect(Graphics g, Brush brush, int x, int y, int w, int h, int r)
{
using (var path = new GraphicsPath())
{
path.AddArc(x, y, r * 2, r * 2, 180, 90);
path.AddArc(x + w - r * 2, y, r * 2, r * 2, 270, 90);
path.AddArc(x + w - r * 2, y + h - r * 2, r * 2, r * 2, 0, 90);
path.AddArc(x, y + h - r * 2, r * 2, r * 2, 90, 90);
path.CloseFigure();
g.FillPath(brush, path);
}
}
}
}
+56 -16
View File
@@ -18,7 +18,7 @@ namespace StarfaceOutlookSync.UI
private NotifyIcon _trayIcon; private NotifyIcon _trayIcon;
private ContextMenuStrip _trayMenu; private ContextMenuStrip _trayMenu;
private ListView _profileList; private ListView _profileList;
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnSettings, _btnInfo; private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnInfo;
private StatusStrip _statusBar; private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel; private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer; private Timer _autoSyncTimer;
@@ -62,6 +62,7 @@ namespace StarfaceOutlookSync.UI
MinimumSize = new Size(500, 350); MinimumSize = new Size(500, 350);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9); Font = new Font("Segoe UI", 9);
Icon = AppIcon.GetIcon();
// Profil-Liste // Profil-Liste
_profileList = new ListView _profileList = new ListView
@@ -97,16 +98,19 @@ namespace StarfaceOutlookSync.UI
_btnDelete = new Button { Text = "Loeschen", Width = 80, Height = 30 }; _btnDelete = new Button { Text = "Loeschen", Width = 80, Height = 30 };
_btnDelete.Click += (s, e) => DeleteProfile(); _btnDelete.Click += (s, e) => DeleteProfile();
_btnSync = new Button { Text = "Jetzt synchronisieren", Width = 150, Height = 30 }; _btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 };
_btnSync.Click += async (s, e) => await SyncSelectedProfile(); _btnSync.Click += async (s, e) => await SyncSelectedProfile();
_btnSettings = new Button { Text = "Einstellungen", Width = 100, Height = 30 }; _btnReset = new Button { Text = "Sync Reset", Width = 80, Height = 30 };
_btnReset.Click += (s, e) => ResetSync();
_btnSettings = new Button { Text = "Einstellungen", Width = 95, Height = 30 };
_btnSettings.Click += (s, e) => ShowSettings(); _btnSettings.Click += (s, e) => ShowSettings();
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 }; _btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
_btnInfo.Click += (s, e) => ShowAbout(); _btnInfo.Click += (s, e) => ShowAbout();
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnSettings, _btnInfo }); buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnInfo });
// Statusbar // Statusbar
_statusBar = new StatusStrip(); _statusBar = new StatusStrip();
@@ -121,10 +125,26 @@ namespace StarfaceOutlookSync.UI
private void SetupTrayIcon() private void SetupTrayIcon()
{ {
_trayMenu = new ContextMenuStrip(); _trayMenu = new ContextMenuStrip();
_trayIcon = new NotifyIcon
{
Text = "Starface Kontakt-Sync",
Icon = AppIcon.GetSmallIcon(),
ContextMenuStrip = _trayMenu,
Visible = true
};
_trayIcon.DoubleClick += (s, e) => ShowMainWindow();
UpdateTrayMenu();
}
private void UpdateTrayMenu()
{
_trayMenu.Items.Clear();
_trayMenu.Items.Add("Oeffnen", null, (s, e) => ShowMainWindow()); _trayMenu.Items.Add("Oeffnen", null, (s, e) => ShowMainWindow());
_trayMenu.Items.Add("-"); _trayMenu.Items.Add("-");
// Schnell-Sync fuer jedes Profil
var profiles = _profileManager.GetProfiles(); var profiles = _profileManager.GetProfiles();
foreach (var p in profiles.Where(p => p.Enabled)) foreach (var p in profiles.Where(p => p.Enabled))
{ {
@@ -139,16 +159,6 @@ namespace StarfaceOutlookSync.UI
_trayMenu.Items.Add("-"); _trayMenu.Items.Add("-");
_trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication()); _trayMenu.Items.Add("Beenden", null, (s, e) => ExitApplication());
_trayIcon = new NotifyIcon
{
Text = "Starface Kontakt-Sync",
Icon = SystemIcons.Application, // Placeholder, wird durch eigenes Icon ersetzt
ContextMenuStrip = _trayMenu,
Visible = true
};
_trayIcon.DoubleClick += (s, e) => ShowMainWindow();
} }
private void SetupAutoSync() private void SetupAutoSync()
@@ -216,7 +226,7 @@ namespace StarfaceOutlookSync.UI
} }
// Tray-Menu aktualisieren // Tray-Menu aktualisieren
SetupTrayIcon(); UpdateTrayMenu();
} }
private void NewProfile() private void NewProfile()
@@ -255,6 +265,36 @@ namespace StarfaceOutlookSync.UI
} }
} }
private void ResetSync()
{
if (_profileList.SelectedItems.Count == 0)
{
MessageBox.Show("Bitte ein Profil auswaehlen.", "Sync Reset",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var profile = _profileList.SelectedItems[0].Tag as SyncProfile;
if (profile == null) return;
var msg = $"Sync-Zuordnungen fuer '{profile.Name}' zuruecksetzen?\n\n" +
"Alle Kontakt-Verknuepfungen werden geloescht.\n" +
"Beim naechsten Sync werden die Kontakte neu abgeglichen.\n" +
"Es werden keine Kontakte geloescht.";
if (MessageBox.Show(msg, "Sync Reset",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
{
_profileManager.SaveMappings(profile.Id, new List<SyncMapping>());
// LastSync auch zuruecksetzen
profile.LastSync = "";
_profileManager.UpdateProfile(profile);
RefreshProfileList();
MessageBox.Show("Sync-Zuordnungen wurden zurueckgesetzt.",
"Sync Reset", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private Task SyncSelectedProfile() private Task SyncSelectedProfile()
{ {
if (_profileList.SelectedItems.Count == 0) if (_profileList.SelectedItems.Count == 0)
@@ -41,7 +41,8 @@ namespace StarfaceOutlookSync.UI
_progressBar = new ProgressBar _progressBar = new ProgressBar
{ {
Left = 12, Top = 38, Width = 460, Height = 22, Left = 12, Top = 38, Width = 460, Height = 22,
Style = ProgressBarStyle.Marquee Style = ProgressBarStyle.Blocks,
Value = 0
}; };
_txtLog = new TextBox _txtLog = new TextBox