Compare commits

...

18 Commits

Author SHA1 Message Date
duffyduck 235960f450 Release v0.0.0.5 2026-04-03 11:53:31 +02:00
duffyduck 9298c3c287 Fix Outlook folder discovery to find all contact folders
- Scan all stores recursively instead of only the default folder
- Properly release COM objects to avoid leaks
- Better error handling with debug output per store/folder
- Show error message if Outlook is not reachable
- Fallback to default contacts folder if recursive scan finds nothing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:52:44 +02:00
duffyduck d0a2ffc9fd Release v0.0.0.4 2026-04-03 11:49:17 +02:00
duffyduck 7ac5ff24a2 Release v0.0.0.3 2026-04-03 11:48:36 +02:00
duffyduck cbe1a8f452 Release v0.0.0.3 2026-04-03 11:11:52 +02:00
duffyduck 6627212032 Release v0.0.0.3 2026-04-03 11:08:03 +02:00
duffyduck 1aa74ec014 Add Outlook folder browser and user settings
- Replace Outlook folder ComboBox with TextBox + Browse button
  that opens a TreeView-based folder browser dialog
- Add per-user settings stored in %AppData% (not HKLM)
  with option to start minimized (System Tray only)
- Add Settings button to main window
- Settings are per-user, independent of profiles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:04:57 +02:00
duffyduck 41c690f8c2 Release v0.0.0.2 2026-04-03 10:54:58 +02:00
duffyduck 50240ee0f3 Change autostart to HKLM so it applies to all users
Important for Terminal Server / multi-user environments where
the setup is run once by an admin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:54:05 +02:00
duffyduck d3b28c0dcc Release v0.0.0.1 2026-04-03 10:52:54 +02:00
duffyduck abd52b351a Fix Inno Setup compile error and release script sed pattern
- Declare ErrorCode variable in InitializeSetup function
- Remove orphan var block at end of Code section
- Fix sed pattern in release.sh to only match version inside quotes
- Restore AboutForm.cs variable name damaged by previous sed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:52:21 +02:00
duffyduck 8ac4c12b46 Add sudo to docker command in release script
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:48:12 +02:00
duffyduck 574c1923f9 Add release script documentation to README
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:45:15 +02:00
duffyduck b1658c8d3c Use basic auth instead of token for Gitea release upload
Prompts for username and password at script start instead
of requiring a pre-configured API token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:41:58 +02:00
duffyduck 6a9a73106d Add release script for automated build and Gitea upload
Handles version bump, build, Inno Setup via Docker, git tag,
push, Gitea release creation and setup.exe upload in one step.

Usage: ./release.sh 0.1.0.0 "Release description"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:40:32 +02:00
duffyduck ec51dc8fc6 Restructure README: Docker as preferred installer build method
Reorder installer section with Docker first (recommended),
Windows second, Wine as fallback. Add one-liner build command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:37:48 +02:00
duffyduck a831ec6f81 Update README with Linux build instructions
Add .NET SDK install, build and Inno Setup instructions for
both Windows and Linux (Docker or Wine).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:34:52 +02:00
duffyduck ad649ad319 Switch to .NET 8 for cross-platform build support
- Target net8.0-windows instead of net4.8
- EnableWindowsTargeting for Linux build
- Replace Marshal.GetActiveObject with P/Invoke (not in .NET 8)
- Use NuGet package for Outlook Interop instead of local DLL ref
- Update Inno Setup script for .NET 8 runtime check
- Builds successfully on Linux, runs on Windows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:33:21 +02:00
11 changed files with 748 additions and 97 deletions
+107 -5
View File
@@ -19,7 +19,7 @@ Windows-Anwendung zur bidirektionalen Synchronisation von Kontakten zwischen Mic
## Voraussetzungen
- Windows 10/11
- .NET Framework 4.8 (in Windows 10/11 bereits enthalten)
- .NET 8 Desktop Runtime (Setup prueft und verlinkt die Download-Seite falls noetig)
- Microsoft Outlook (Classic oder Neu, beliebige Version)
- Starface Telefonanlage ab Version 6.7 (REST-API)
@@ -49,19 +49,121 @@ Benutzerdaten (Profile, Mappings) werden mit entfernt.
## Entwicklung
### Voraussetzungen (Build)
- .NET 8 SDK (laeuft auf Windows, Linux und macOS)
- Fuer den Installer: Inno Setup 6 (Windows) oder Docker (Linux)
### .NET SDK installieren
**Windows:**
```
winget install Microsoft.DotNet.SDK.8
```
**Debian/Ubuntu:**
```bash
# Ueber das offizielle Install-Script:
wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 8.0
export PATH="$HOME/.dotnet:$PATH"
```
### Bauen
Das Projekt kann sowohl unter Windows als auch unter Linux gebaut werden.
Die fertige EXE laeuft nur auf Windows (WinForms + COM Interop).
```bash
# In Visual Studio oeffnen und bauen, oder:
# Release-Build
dotnet build src/StarfaceOutlookSync/StarfaceOutlookSync.csproj -c Release
# Oder publish (mit allen Abhaengigkeiten):
dotnet publish src/StarfaceOutlookSync/StarfaceOutlookSync.csproj -c Release
```
Build-Ausgabe: `src/StarfaceOutlookSync/bin/Release/net8.0-windows/win-x64/`
### Installer erstellen
Die Setup-EXE wird mit Inno Setup 6 erstellt.
**Linux (Docker - empfohlen):**
Einfachster Weg unter Linux. Baut und packt alles in einem Schritt:
```bash
# 1. Release bauen
dotnet build src/StarfaceOutlookSync/StarfaceOutlookSync.csproj -c Release
# 2. Installer erstellen
docker run --rm -v "$PWD:/work" amake/innosetup installer/setup.iss
```
Die fertige Setup-EXE liegt danach in `dist/`.
Komplett als Einzeiler (Build + Installer):
```bash
dotnet build src/StarfaceOutlookSync/StarfaceOutlookSync.csproj -c Release && docker run --rm -v "$PWD:/work" amake/innosetup installer/setup.iss
```
**Windows:**
1. [Inno Setup 6](https://jrsoftware.org/isinfo.php) installieren
2. Release-Build erstellen
3. `installer/setup.iss` in Inno Setup oeffnen und kompilieren
4. Setup-EXE wird in `dist/` erstellt
2. `installer/setup.iss` oeffnen und kompilieren
3. Setup-EXE wird in `dist/` erstellt
Oder per Kommandozeile:
```cmd
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\setup.iss
```
**Linux (Wine - Alternative falls kein Docker vorhanden):**
```bash
# Einmalig: Inno Setup in Wine installieren
sudo apt install wine
wine ~/Downloads/innosetup-6.x.exe
# Kompilieren
wine "$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe" installer/setup.iss
```
### Release erstellen
Das Release-Script automatisiert den kompletten Release-Prozess:
Versionsnummer aktualisieren, bauen, Installer erstellen, Git Tag + Push
und Gitea Release mit Setup-EXE als Download hochladen.
```bash
./release.sh <version> ["beschreibung"]
```
Beispiele:
```bash
# Release mit Standard-Beschreibung
./release.sh 0.1.0.0
# Release mit eigener Beschreibung
./release.sh 0.2.0.0 "Neues Feature: Auto-Sync"
```
Das Script fragt beim Start nach Gitea-Benutzername und Kennwort
und fuehrt dann folgende Schritte automatisch aus:
1. Prueft Voraussetzungen (dotnet, docker, curl, sauberes git)
2. Aktualisiert Versionsnummer in `.csproj`, `AboutForm.cs` und `setup.iss`
3. Baut das Projekt (`dotnet build -c Release`)
4. Erstellt den Installer via Docker (`amake/innosetup`)
5. Git Commit + Tag (`vX.X.X.X`)
6. Push zu Gitea (main + tag)
7. Erstellt Gitea Release mit Setup-EXE als Download-Anhang
Voraussetzungen:
- .NET 8 SDK
- Docker
- curl
- Gitea-Account mit Push-Rechten auf das Repository
## Projektstruktur
+22 -14
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.0.1"
#define MyAppVersion "0.0.0.5"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe"
@@ -36,7 +36,7 @@ Name: "autostart"; Description: "Bei Windows-Anmeldung automatisch starten"; Gro
[Files]
; Hauptanwendung - Pfad anpassen nach Build
Source: "..\src\StarfaceOutlookSync\bin\Release\net4.8\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\src\StarfaceOutlookSync\bin\Release\net8.0-windows\win-x64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
@@ -44,8 +44,8 @@ Name: "{group}\{#MyAppName} deinstallieren"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Registry]
; Autostart
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "StarfaceOutlookSync"; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart
; Autostart fuer alle Benutzer (HKLM)
Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "StarfaceOutlookSync"; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{#MyAppName} jetzt starten"; Flags: nowait postinstall skipifsilent
@@ -57,26 +57,34 @@ Filename: "taskkill"; Parameters: "/F /IM {#MyAppExeName}"; Flags: runhidden; Ru
Type: filesandordirs; Name: "{userappdata}\StarfaceOutlookSync"
[Code]
// Pruefe ob .NET Framework 4.8 installiert ist
function IsDotNetInstalled(): Boolean;
// Pruefe ob .NET 8 Desktop Runtime installiert ist
function IsDotNet8Installed(): Boolean;
var
Version: Cardinal;
ResultCode: Integer;
begin
Result := RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Version);
// dotnet --list-runtimes enthaelt "Microsoft.WindowsDesktop.App 8.x"
Result := Exec('dotnet', '--list-runtimes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
if Result then
Result := Version >= 528040; // .NET 4.8
Result := ResultCode = 0;
// Einfacher Check: dotnet.exe muss existieren
Result := FileExists(ExpandConstant('{commonpf}\dotnet\dotnet.exe')) or
FileExists(ExpandConstant('{commonpf64}\dotnet\dotnet.exe'));
end;
function InitializeSetup(): Boolean;
var
ErrorCode: Integer;
begin
Result := True;
if not IsDotNetInstalled() then
if not IsDotNet8Installed() then
begin
MsgBox('.NET Framework 4.8 oder hoeher wird benoetigt.' + #13#10 +
'Bitte installieren Sie es von:' + #13#10 +
'https://dotnet.microsoft.com/download/dotnet-framework/net48',
mbError, MB_OK);
if MsgBox('.NET 8 Desktop Runtime wird benoetigt.' + #13#10 + #13#10 +
'Soll die Download-Seite geoeffnet werden?',
mbConfirmation, MB_YESNO) = IDYES then
begin
ShellExec('open', 'https://dotnet.microsoft.com/download/dotnet/8.0/runtime', '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode);
end;
Result := False;
end;
end;
Executable
+244
View File
@@ -0,0 +1,244 @@
#!/bin/bash
#
# Release-Script fuer Starface Outlook Sync
#
# Aktualisiert Versionsnummer, baut die App, erstellt den Installer
# und laedt das Release auf Gitea hoch.
#
# Voraussetzungen:
# - .NET 8 SDK (dotnet)
# - Docker (fuer Inno Setup)
# - curl (fuer Gitea API)
# - Gitea-Zugangsdaten (werden beim Start abgefragt)
#
# Verwendung:
# ./release.sh 0.1.0.0
# ./release.sh 0.1.0.0 "Erster Beta-Release"
set -e
# ============================================================
# Konfiguration
# ============================================================
GITEA_URL="https://git.hacker-net.de"
REPO_OWNER="Hacker-Software"
REPO_NAME="starface-outlook-sync-addin"
DOTNET_PATH="${DOTNET_PATH:-$HOME/.dotnet}"
PROJECT="src/StarfaceOutlookSync/StarfaceOutlookSync.csproj"
ABOUT_FORM="src/StarfaceOutlookSync/UI/AboutForm.cs"
INNO_SCRIPT="installer/setup.iss"
DIST_DIR="dist"
# ============================================================
# Argumente pruefen
# ============================================================
VERSION="${1}"
RELEASE_NOTE="${2:-Release v${VERSION}}"
if [ -z "$VERSION" ]; then
echo "Verwendung: $0 <version> [release-beschreibung]"
echo "Beispiel: $0 0.1.0.0 \"Erster Beta-Release\""
echo ""
echo "Aktuelle Version in .csproj:"
grep '<Version>' "$PROJECT" | head -1 | sed 's/.*<Version>/ /;s/<.*//'
exit 1
fi
# Versionsformat pruefen (x.x.x.x)
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Fehler: Version muss im Format x.x.x.x sein (z.B. 0.1.0.0)"
exit 1
fi
# ============================================================
# Gitea-Zugangsdaten abfragen
# ============================================================
read -p "Gitea Benutzername: " GITEA_USER
read -s -p "Gitea Kennwort: " GITEA_PASS
echo ""
if [ -z "$GITEA_USER" ] || [ -z "$GITEA_PASS" ]; then
echo "Fehler: Benutzername und Kennwort erforderlich."
exit 1
fi
GITEA_AUTH="${GITEA_USER}:${GITEA_PASS}"
# ============================================================
# Tools pruefen
# ============================================================
export PATH="$DOTNET_PATH:$PATH"
echo "=== Starface Outlook Sync - Release v${VERSION} ==="
echo ""
echo "[1/7] Pruefe Voraussetzungen..."
if ! command -v dotnet &> /dev/null; then
echo " Fehler: dotnet nicht gefunden."
echo " Install: wget https://dot.net/v1/dotnet-install.sh -O- | bash /dev/stdin --channel 8.0"
exit 1
fi
echo " dotnet $(dotnet --version)"
if ! command -v docker &> /dev/null; then
echo " Fehler: docker nicht gefunden."
echo " Install: https://docs.docker.com/engine/install/debian/"
exit 1
fi
echo " docker $(docker --version | cut -d' ' -f3 | tr -d ',')"
if ! command -v curl &> /dev/null; then
echo " Fehler: curl nicht gefunden."
exit 1
fi
# Pruefen ob Git sauber ist
if [ -n "$(git status --porcelain)" ]; then
echo ""
echo " Warnung: Uncommitted changes vorhanden!"
git status --short
echo ""
read -p " Trotzdem fortfahren? (j/n) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[jJyY]$ ]]; then
exit 1
fi
fi
# ============================================================
# Versionsnummern aktualisieren
# ============================================================
echo ""
echo "[2/7] Aktualisiere Versionsnummer auf ${VERSION}..."
# .csproj
sed -i "s|<Version>.*</Version>|<Version>${VERSION}</Version>|g" "$PROJECT"
sed -i "s|<AssemblyVersion>.*</AssemblyVersion>|<AssemblyVersion>${VERSION}</AssemblyVersion>|g" "$PROJECT"
sed -i "s|<FileVersion>.*</FileVersion>|<FileVersion>${VERSION}</FileVersion>|g" "$PROJECT"
# AboutForm.cs - nur den Text im String-Literal ersetzen, nicht Variablennamen
sed -i "s|\"Version [0-9.]*\"|\"Version ${VERSION}\"|g" "$ABOUT_FORM"
# Inno Setup
sed -i "s|#define MyAppVersion \".*\"|#define MyAppVersion \"${VERSION}\"|g" "$INNO_SCRIPT"
echo " .csproj, AboutForm.cs, setup.iss aktualisiert."
# ============================================================
# Bauen
# ============================================================
echo ""
echo "[3/7] Baue Release..."
dotnet build "$PROJECT" -c Release --nologo -v quiet
echo " Build erfolgreich."
# ============================================================
# Installer erstellen
# ============================================================
echo ""
echo "[4/7] Erstelle Installer mit Docker..."
mkdir -p "$DIST_DIR"
sudo docker run --rm -v "$PWD:/work" amake/innosetup "$INNO_SCRIPT"
SETUP_FILE="${DIST_DIR}/StarfaceOutlookSync_Setup_${VERSION}.exe"
if [ ! -f "$SETUP_FILE" ]; then
echo " Fehler: Setup-Datei nicht gefunden: $SETUP_FILE"
exit 1
fi
SETUP_SIZE=$(du -h "$SETUP_FILE" | cut -f1)
echo " Installer erstellt: $SETUP_FILE ($SETUP_SIZE)"
# ============================================================
# Git Commit + Tag
# ============================================================
echo ""
echo "[5/7] Git Commit und Tag..."
git add "$PROJECT" "$ABOUT_FORM" "$INNO_SCRIPT"
git commit -m "Release v${VERSION}" --allow-empty
git tag -a "v${VERSION}" -m "Release v${VERSION}"
echo " Commit und Tag v${VERSION} erstellt."
# ============================================================
# Git Push
# ============================================================
echo ""
echo "[6/7] Push zu Gitea..."
git push origin main
git push origin "v${VERSION}"
echo " Push erfolgreich."
# ============================================================
# Gitea Release erstellen
# ============================================================
echo ""
echo "[7/7] Erstelle Gitea Release..."
API_URL="${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}"
# Release erstellen
RELEASE_RESPONSE=$(curl -s -X POST \
-u "${GITEA_AUTH}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"v${VERSION}\",
\"name\": \"v${VERSION}\",
\"body\": \"${RELEASE_NOTE}\",
\"draft\": false,
\"prerelease\": false
}" \
"${API_URL}/releases")
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
if [ -z "$RELEASE_ID" ]; then
echo " Fehler beim Erstellen des Release:"
echo " $RELEASE_RESPONSE"
exit 1
fi
echo " Release erstellt (ID: $RELEASE_ID)"
# Setup-EXE als Attachment hochladen
echo " Lade Installer hoch..."
UPLOAD_RESPONSE=$(curl -s -X POST \
-u "${GITEA_AUTH}" \
-F "attachment=@${SETUP_FILE}" \
"${API_URL}/releases/${RELEASE_ID}/assets?name=$(basename $SETUP_FILE)")
ASSET_URL=$(echo "$UPLOAD_RESPONSE" | grep -o '"browser_download_url":"[^"]*"' | cut -d'"' -f4)
if [ -z "$ASSET_URL" ]; then
echo " Warnung: Upload-Antwort:"
echo " $UPLOAD_RESPONSE"
else
echo " Upload erfolgreich: $ASSET_URL"
fi
# ============================================================
# Zusammenfassung
# ============================================================
echo ""
echo "=== Release v${VERSION} abgeschlossen ==="
echo ""
echo " Release: ${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/releases/tag/v${VERSION}"
echo " Installer: $SETUP_FILE ($SETUP_SIZE)"
echo " Tag: v${VERSION}"
echo ""
@@ -0,0 +1,37 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace StarfaceOutlookSync.Models
{
public class UserSettings
{
public bool StartMinimized { get; set; } = false;
private static readonly string SettingsFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"StarfaceOutlookSync", "settings.json");
public static UserSettings Load()
{
try
{
if (File.Exists(SettingsFile))
return JsonConvert.DeserializeObject<UserSettings>(File.ReadAllText(SettingsFile))
?? new UserSettings();
}
catch { }
return new UserSettings();
}
public void Save()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(SettingsFile));
File.WriteAllText(SettingsFile, JsonConvert.SerializeObject(this, Formatting.Indented));
}
catch { }
}
}
}
@@ -11,6 +11,17 @@ namespace StarfaceOutlookSync.Services
private Outlook.Application _outlookApp;
private bool _weStartedOutlook;
// Marshal.GetActiveObject existiert nicht in .NET 8, daher P/Invoke
[DllImport("oleaut32.dll", PreserveSig = false)]
private static extern void GetActiveObject([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
private static object GetActiveComObject(string progId)
{
var clsid = Type.GetTypeFromProgID(progId, true).GUID;
GetActiveObject(clsid, IntPtr.Zero, out var obj);
return obj;
}
private Outlook.Application GetOutlookApp()
{
if (_outlookApp != null) return _outlookApp;
@@ -18,7 +29,7 @@ namespace StarfaceOutlookSync.Services
try
{
// Versuche laufende Outlook-Instanz zu finden
_outlookApp = (Outlook.Application)Marshal.GetActiveObject("Outlook.Application");
_outlookApp = (Outlook.Application)GetActiveComObject("Outlook.Application");
_weStartedOutlook = false;
}
catch
@@ -39,20 +50,29 @@ namespace StarfaceOutlookSync.Services
var app = GetOutlookApp();
var ns = app.GetNamespace("MAPI");
// Standard-Kontaktordner
var defaultFolder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
folders.Add(defaultFolder.FolderPath);
// Unterordner
AddSubFolders(defaultFolder, folders);
// Weitere Kontaktordner in anderen Stores
// Alle Stores durchgehen (jedes Konto, jede PST-Datei etc.)
foreach (Outlook.Store store in ns.Stores)
{
try
{
var rootFolder = store.GetRootFolder();
FindContactFolders(rootFolder, folders);
FindContactFoldersRecursive(rootFolder, folders);
Marshal.ReleaseComObject(rootFolder);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error scanning store '{store.DisplayName}': {ex.Message}");
}
}
// Falls nichts gefunden, Standard-Kontaktordner als Fallback
if (folders.Count == 0)
{
try
{
var defaultFolder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
folders.Add(defaultFolder.FolderPath);
Marshal.ReleaseComObject(defaultFolder);
}
catch { }
}
@@ -67,30 +87,34 @@ namespace StarfaceOutlookSync.Services
return folders;
}
private void AddSubFolders(Outlook.MAPIFolder folder, List<string> paths)
private void FindContactFoldersRecursive(Outlook.MAPIFolder folder, List<string> paths)
{
foreach (Outlook.MAPIFolder sub in folder.Folders)
try
{
if (sub.DefaultItemType == Outlook.OlItemType.olContactItem)
// Kontaktordner erkennen: DefaultItemType ODER Ordnername enthaelt "Kontakt"/"Contact"
if (folder.DefaultItemType == Outlook.OlItemType.olContactItem)
{
if (!paths.Contains(sub.FolderPath))
paths.Add(sub.FolderPath);
AddSubFolders(sub, paths);
if (!paths.Contains(folder.FolderPath))
paths.Add(folder.FolderPath);
}
// Alle Unterordner durchsuchen
foreach (Outlook.MAPIFolder sub in folder.Folders)
{
try
{
FindContactFoldersRecursive(sub, paths);
}
catch { }
finally
{
Marshal.ReleaseComObject(sub);
}
}
}
}
private void FindContactFolders(Outlook.MAPIFolder folder, List<string> paths)
{
if (folder.DefaultItemType == Outlook.OlItemType.olContactItem)
catch (Exception ex)
{
if (!paths.Contains(folder.FolderPath))
paths.Add(folder.FolderPath);
}
foreach (Outlook.MAPIFolder sub in folder.Folders)
{
FindContactFolders(sub, paths);
System.Diagnostics.Debug.WriteLine($"Error scanning folder '{folder.Name}': {ex.Message}");
}
}
@@ -2,32 +2,24 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net4.8</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ApplicationIcon>Resources\app.ico</ApplicationIcon>
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product>
<Version>0.0.0.1</Version>
<AssemblyVersion>0.0.0.1</AssemblyVersion>
<FileVersion>0.0.0.1</FileVersion>
<Version>0.0.0.5</Version>
<AssemblyVersion>0.0.0.5</AssemblyVersion>
<FileVersion>0.0.0.5</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Office.Interop.Outlook">
<HintPath>$(ProgramFiles)\Microsoft Office\root\Office16\ADDINS\Microsoft.Office.Interop.Outlook.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\app.ico" />
<PackageReference Include="Microsoft.Office.Interop.Outlook" Version="15.0.4797.1004" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label
{
Text = "Version 0.0.0.1",
Text = "Version 0.0.0.5",
Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray
+41 -5
View File
@@ -18,7 +18,7 @@ namespace StarfaceOutlookSync.UI
private NotifyIcon _trayIcon;
private ContextMenuStrip _trayMenu;
private ListView _profileList;
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnInfo;
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnSettings, _btnInfo;
private StatusStrip _statusBar;
private ToolStripStatusLabel _statusLabel;
private Timer _autoSyncTimer;
@@ -29,6 +29,30 @@ namespace StarfaceOutlookSync.UI
SetupTrayIcon();
SetupAutoSync();
RefreshProfileList();
// Minimiert starten falls in Einstellungen aktiviert
var settings = UserSettings.Load();
if (settings.StartMinimized)
{
WindowState = FormWindowState.Minimized;
ShowInTaskbar = false;
Visible = false;
}
}
protected override void SetVisibleCore(bool value)
{
// Beim ersten Anzeigen pruefen ob minimiert gestartet werden soll
if (!IsHandleCreated)
{
var settings = UserSettings.Load();
if (settings.StartMinimized)
{
CreateHandle();
value = false;
}
}
base.SetVisibleCore(value);
}
private void InitializeComponent()
@@ -76,10 +100,13 @@ namespace StarfaceOutlookSync.UI
_btnSync = new Button { Text = "Jetzt synchronisieren", Width = 150, Height = 30 };
_btnSync.Click += async (s, e) => await SyncSelectedProfile();
_btnSettings = new Button { Text = "Einstellungen", Width = 100, Height = 30 };
_btnSettings.Click += (s, e) => ShowSettings();
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
_btnInfo.Click += (s, e) => ShowAbout();
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnInfo });
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnSettings, _btnInfo });
// Statusbar
_statusBar = new StatusStrip();
@@ -228,17 +255,17 @@ namespace StarfaceOutlookSync.UI
}
}
private async Task SyncSelectedProfile()
private Task SyncSelectedProfile()
{
if (_profileList.SelectedItems.Count == 0)
{
MessageBox.Show("Bitte ein Profil auswaehlen.", "Sync",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
return Task.CompletedTask;
}
var profile = _profileList.SelectedItems[0].Tag as SyncProfile;
if (profile == null) return;
if (profile == null) return Task.CompletedTask;
using (var syncForm = new SyncProgressForm(profile))
{
@@ -246,6 +273,7 @@ namespace StarfaceOutlookSync.UI
}
RefreshProfileList();
return Task.CompletedTask;
}
private async Task RunSync(SyncProfile profile)
@@ -282,6 +310,14 @@ namespace StarfaceOutlookSync.UI
_statusLabel.Text = text;
}
private void ShowSettings()
{
using (var settings = new SettingsForm())
{
settings.ShowDialog(this);
}
}
private void ShowAbout()
{
using (var about = new AboutForm())
@@ -0,0 +1,125 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;
namespace StarfaceOutlookSync.UI
{
public class OutlookFolderBrowserForm : Form
{
private TreeView _tree;
private Button _btnOk, _btnCancel;
public string SelectedFolderPath { get; private set; } = "";
public string SelectedFolderName { get; private set; } = "";
public OutlookFolderBrowserForm(List<string> folderPaths, string currentSelection)
{
InitializeComponent();
BuildTree(folderPaths, currentSelection);
}
private void InitializeComponent()
{
Text = "Outlook-Ordner waehlen";
Size = new Size(400, 450);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9);
var lblInfo = new Label
{
Text = "Bitte waehlen Sie den Outlook-Kontaktordner fuer die Synchronisation:",
Left = 12, Top = 12, Width = 360, Height = 36
};
_tree = new TreeView
{
Left = 12, Top = 52, Width = 360, Height = 310,
HideSelection = false,
ShowLines = true,
ShowPlusMinus = true,
ShowRootLines = true
};
_tree.DoubleClick += (s, e) => { if (_tree.SelectedNode?.Tag != null) SelectAndClose(); };
_btnOk = new Button
{
Text = "OK", Left = 210, Top = 372, Width = 75, Height = 28,
DialogResult = DialogResult.None
};
_btnOk.Click += (s, e) => SelectAndClose();
_btnCancel = new Button
{
Text = "Abbrechen", Left = 292, Top = 372, Width = 80, Height = 28,
DialogResult = DialogResult.Cancel
};
Controls.AddRange(new Control[] { lblInfo, _tree, _btnOk, _btnCancel });
AcceptButton = _btnOk;
CancelButton = _btnCancel;
}
private void BuildTree(List<string> folderPaths, string currentSelection)
{
_tree.Nodes.Clear();
var nodeMap = new Dictionary<string, TreeNode>();
foreach (var path in folderPaths)
{
// Pfad: \\Store\Kontakte\Unterordner
var parts = path.TrimStart('\\').Split('\\');
string runningPath = "";
TreeNode parent = null;
for (int i = 0; i < parts.Length; i++)
{
runningPath += "\\" + parts[i];
if (!nodeMap.ContainsKey(runningPath))
{
var node = new TreeNode(parts[i]);
// Nur Blatt-Knoten oder bekannte Pfade sind waehlbar
if (i == parts.Length - 1)
node.Tag = path; // Vollstaendiger Pfad
if (parent == null)
_tree.Nodes.Add(node);
else
parent.Nodes.Add(node);
nodeMap[runningPath] = node;
// Aktuellen Ordner vorauswaehlen
if (path == currentSelection)
{
_tree.SelectedNode = node;
node.EnsureVisible();
}
}
parent = nodeMap[runningPath];
}
}
_tree.ExpandAll();
}
private void SelectAndClose()
{
if (_tree.SelectedNode?.Tag == null)
{
MessageBox.Show("Bitte einen Kontaktordner waehlen.", "Hinweis",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
SelectedFolderPath = _tree.SelectedNode.Tag.ToString();
SelectedFolderName = _tree.SelectedNode.Text;
DialogResult = DialogResult.OK;
Close();
}
}
}
+49 -28
View File
@@ -15,14 +15,18 @@ namespace StarfaceOutlookSync.UI
private readonly bool _isNew;
// Controls
private TextBox _txtName, _txtHost, _txtPort, _txtLoginId, _txtPassword;
private TextBox _txtName, _txtHost, _txtPort, _txtLoginId, _txtPassword, _txtOutlookFolder;
private CheckBox _chkSsl, _chkEnabled;
private ComboBox _cmbAddressBook, _cmbOutlookFolder, _cmbDirection;
private ComboBox _cmbAddressBook, _cmbDirection;
private Button _btnBrowseFolder;
private NumericUpDown _numAutoSync;
private Button _btnTest, _btnLoadBooks, _btnSave, _btnCancel;
private Label _lblTestResult;
private List<StarfaceAddressBook> _addressBooks = new List<StarfaceAddressBook>();
private List<string> _outlookFolderPaths = new List<string>();
private string _selectedOutlookPath = "";
private string _selectedOutlookName = "";
public ProfileEditorForm(SyncProfile profile)
{
@@ -87,8 +91,11 @@ namespace StarfaceOutlookSync.UI
panel.Controls.Add(MakeSectionLabel("Outlook-Einstellungen", 12, y)); y += 26;
panel.Controls.Add(MakeLabel("Kontakte-Ordner:", 12, y)); y += 22;
_cmbOutlookFolder = new ComboBox { Left = 12, Top = y, Width = 420, DropDownStyle = ComboBoxStyle.DropDownList };
panel.Controls.Add(_cmbOutlookFolder); y += 32;
_txtOutlookFolder = new TextBox { Left = 12, Top = y, Width = 330, ReadOnly = true, BackColor = SystemColors.Window };
_btnBrowseFolder = new Button { Text = "Durchsuchen...", Left = 348, Top = y - 1, Width = 84, Height = 24 };
_btnBrowseFolder.Click += (s, e) => BrowseOutlookFolder();
panel.Controls.Add(_txtOutlookFolder);
panel.Controls.Add(_btnBrowseFolder); y += 32;
panel.Controls.Add(MakeLabel("Sync-Richtung:", 12, y)); y += 22;
_cmbDirection = new ComboBox { Left = 12, Top = y, Width = 250, DropDownStyle = ComboBoxStyle.DropDownList };
@@ -132,18 +139,15 @@ namespace StarfaceOutlookSync.UI
{
using (var outlook = new OutlookContactsService())
{
var folders = outlook.GetContactFolderPaths();
_cmbOutlookFolder.Items.Clear();
foreach (var f in folders)
_cmbOutlookFolder.Items.Add(f);
if (folders.Count > 0)
_cmbOutlookFolder.SelectedIndex = 0;
_outlookFolderPaths = outlook.GetContactFolderPaths();
}
}
catch
catch (Exception ex)
{
_cmbOutlookFolder.Items.Add("\\\\Kontakte");
_cmbOutlookFolder.SelectedIndex = 0;
MessageBox.Show(
$"Outlook-Kontaktordner konnten nicht geladen werden:\n{ex.Message}\n\nIst Outlook gestartet?",
"Outlook-Verbindung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_outlookFolderPaths = new List<string>();
}
// Bestehende Werte laden
@@ -160,15 +164,10 @@ namespace StarfaceOutlookSync.UI
_cmbDirection.SelectedIndex = (int)_existingProfile.SyncDirection;
// Outlook-Ordner auswaehlen
for (int i = 0; i < _cmbOutlookFolder.Items.Count; i++)
{
if (_cmbOutlookFolder.Items[i].ToString() == _existingProfile.OutlookFolderPath)
{
_cmbOutlookFolder.SelectedIndex = i;
break;
}
}
// Outlook-Ordner
_selectedOutlookPath = _existingProfile.OutlookFolderPath;
_selectedOutlookName = _existingProfile.OutlookFolderName;
_txtOutlookFolder.Text = _selectedOutlookPath;
// Adressbuch
if (_existingProfile.StarfaceAddressBook != null)
@@ -178,6 +177,26 @@ namespace StarfaceOutlookSync.UI
_cmbAddressBook.SelectedIndex = 0;
}
}
else if (_outlookFolderPaths.Count > 0)
{
// Standard-Ordner vorauswaehlen
_selectedOutlookPath = _outlookFolderPaths[0];
_selectedOutlookName = _selectedOutlookPath.Substring(_selectedOutlookPath.LastIndexOf('\\') + 1);
_txtOutlookFolder.Text = _selectedOutlookPath;
}
}
private void BrowseOutlookFolder()
{
using (var browser = new OutlookFolderBrowserForm(_outlookFolderPaths, _selectedOutlookPath))
{
if (browser.ShowDialog(this) == DialogResult.OK)
{
_selectedOutlookPath = browser.SelectedFolderPath;
_selectedOutlookName = browser.SelectedFolderName;
_txtOutlookFolder.Text = _selectedOutlookPath;
}
}
}
private StarfaceConnection GetConnection()
@@ -292,10 +311,12 @@ namespace StarfaceOutlookSync.UI
return;
}
var selectedFolder = _cmbOutlookFolder.SelectedItem?.ToString() ?? "";
var folderName = selectedFolder;
if (folderName.Contains("\\"))
folderName = folderName.Substring(folderName.LastIndexOf('\\') + 1);
if (string.IsNullOrEmpty(_selectedOutlookPath))
{
MessageBox.Show("Bitte einen Outlook-Kontaktordner waehlen.",
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var profile = new SyncProfile
{
@@ -303,8 +324,8 @@ namespace StarfaceOutlookSync.UI
Name = _txtName.Text.Trim(),
StarfaceConnection = GetConnection(),
StarfaceAddressBook = _addressBooks[_cmbAddressBook.SelectedIndex],
OutlookFolderPath = selectedFolder,
OutlookFolderName = folderName,
OutlookFolderPath = _selectedOutlookPath,
OutlookFolderName = _selectedOutlookName,
SyncDirection = (SyncDirection)_cmbDirection.SelectedIndex,
Enabled = _chkEnabled.Checked,
AutoSyncIntervalMinutes = (int)_numAutoSync.Value,
@@ -0,0 +1,62 @@
using System.Drawing;
using System.Windows.Forms;
using StarfaceOutlookSync.Models;
namespace StarfaceOutlookSync.UI
{
public class SettingsForm : Form
{
private CheckBox _chkStartMinimized;
private Button _btnSave, _btnCancel;
private readonly UserSettings _settings;
public SettingsForm()
{
_settings = UserSettings.Load();
InitializeComponent();
}
private void InitializeComponent()
{
Text = "Einstellungen";
Size = new Size(350, 180);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9);
_chkStartMinimized = new CheckBox
{
Text = "Minimiert starten (nur im System Tray)",
Left = 20, Top = 24, AutoSize = true,
Checked = _settings.StartMinimized
};
_btnSave = new Button
{
Text = "Speichern", Left = 80, Top = 100, Width = 85, Height = 28,
DialogResult = DialogResult.None
};
_btnSave.Click += (s, e) => Save();
_btnCancel = new Button
{
Text = "Abbrechen", Left = 174, Top = 100, Width = 85, Height = 28,
DialogResult = DialogResult.Cancel
};
Controls.AddRange(new Control[] { _chkStartMinimized, _btnSave, _btnCancel });
AcceptButton = _btnSave;
CancelButton = _btnCancel;
}
private void Save()
{
_settings.StartMinimized = _chkStartMinimized.Checked;
_settings.Save();
DialogResult = DialogResult.OK;
Close();
}
}
}